| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917 |
- // Copyright (c) .NET Foundation. All rights reserved.
- // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
- #nullable disable
- using System;
- using System.Linq;
- using System.Text.Json;
- using System.Threading.Tasks;
- using Xunit;
- namespace Microsoft.JSInterop.Infrastructure
- {
- public class DotNetDispatcherTest
- {
- private readonly static string thisAssemblyName = typeof(DotNetDispatcherTest).Assembly.GetName().Name;
- [Fact]
- public void CannotInvokeWithEmptyAssemblyName()
- {
- var ex = Assert.Throws<ArgumentException>(() =>
- {
- DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(" ", "SomeMethod", default, default), "[]");
- });
- Assert.StartsWith("Property 'AssemblyName' cannot be null, empty, or whitespace.", ex.Message);
- Assert.Equal("assemblyKey", ex.ParamName);
- }
- [Fact]
- public void CannotInvokeWithEmptyMethodIdentifier()
- {
- var ex = Assert.Throws<ArgumentException>(() =>
- {
- DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo("SomeAssembly", " ", default, default), "[]");
- });
- Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
- Assert.Equal("methodIdentifier", ex.ParamName);
- }
- [Fact]
- public void CannotInvokeMethodsOnUnloadedAssembly()
- {
- var assemblyName = "Some.Fake.Assembly";
- var ex = Assert.Throws<ArgumentException>(() =>
- {
- DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(assemblyName, "SomeMethod", default, default), null);
- });
- Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message);
- }
- // Note: Currently it's also not possible to invoke generic methods.
- // That's not something determined by DotNetDispatcher, but rather by the fact that we
- // don't close over the generics in the reflection code.
- // Not defining this behavior through unit tests because the default outcome is
- // fine (an exception stating what info is missing).
- [Theory]
- [InlineData("MethodOnInternalType")]
- [InlineData("PrivateMethod")]
- [InlineData("ProtectedMethod")]
- [InlineData("StaticMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
- [InlineData("InstanceMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
- public void CannotInvokeUnsuitableMethods(string methodIdentifier)
- {
- var ex = Assert.Throws<ArgumentException>(() =>
- {
- DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(thisAssemblyName, methodIdentifier, default, default), null);
- });
- Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public invokable method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message);
- }
- [Fact]
- public void CanInvokeStaticVoidMethod()
- {
- // Arrange/Act
- var jsRuntime = new TestJSRuntime();
- SomePublicType.DidInvokeMyInvocableStaticVoid = false;
- var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticVoid", default, default), null);
- // Assert
- Assert.Null(resultJson);
- Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid);
- }
- [Fact]
- public void CanInvokeStaticNonVoidMethod()
- {
- // Arrange/Act
- var jsRuntime = new TestJSRuntime();
- var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticNonVoid", default, default), null);
- var result = JsonSerializer.Deserialize<TestDTO>(resultJson, jsRuntime.JsonSerializerOptions);
- // Assert
- Assert.Equal("Test", result.StringVal);
- Assert.Equal(123, result.IntVal);
- }
- [Fact]
- public void CanInvokeStaticNonVoidMethodWithoutCustomIdentifier()
- {
- // Arrange/Act
- var jsRuntime = new TestJSRuntime();
- var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, default), null);
- var result = JsonSerializer.Deserialize<TestDTO>(resultJson, jsRuntime.JsonSerializerOptions);
- // Assert
- Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal);
- Assert.Equal(456, result.IntVal);
- }
- [Fact]
- public void CanInvokeStaticWithParams()
- {
- // Arrange: Track a .NET object to use as an arg
- var jsRuntime = new TestJSRuntime();
- var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
- var objectRef = DotNetObjectReference.Create(arg3);
- jsRuntime.Invoke<object>("unimportant", objectRef);
- // Arrange: Remaining args
- var argsJson = JsonSerializer.Serialize(new object[]
- {
- new TestDTO { StringVal = "Another string", IntVal = 456 },
- new[] { 100, 200 },
- objectRef
- }, jsRuntime.JsonSerializerOptions);
- // Act
- var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson);
- var result = JsonDocument.Parse(resultJson);
- var root = result.RootElement;
- // Assert: First result value marshalled via JSON
- var resultDto1 = JsonSerializer.Deserialize<TestDTO>(root[0].GetRawText(), jsRuntime.JsonSerializerOptions);
- Assert.Equal("ANOTHER STRING", resultDto1.StringVal);
- Assert.Equal(756, resultDto1.IntVal);
- // Assert: Second result value marshalled by ref
- var resultDto2Ref = root[1];
- Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.StringVal), out _));
- Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _));
- Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey.EncodedUtf8Bytes, out var property));
- var resultDto2 = Assert.IsType<DotNetObjectReference<TestDTO>>(jsRuntime.GetObjectReference(property.GetInt64())).Value;
- Assert.Equal("MY STRING", resultDto2.StringVal);
- Assert.Equal(1299, resultDto2.IntVal);
- }
- [Fact]
- public void InvokingWithIncorrectUseOfDotNetObjectRefThrows()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- var method = nameof(SomePublicType.IncorrectDotNetObjectRefUsage);
- var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
- var objectRef = DotNetObjectReference.Create(arg3);
- jsRuntime.Invoke<object>("unimportant", objectRef);
- // Arrange: Remaining args
- var argsJson = JsonSerializer.Serialize(new object[]
- {
- new TestDTO { StringVal = "Another string", IntVal = 456 },
- new[] { 100, 200 },
- objectRef
- }, jsRuntime.JsonSerializerOptions);
- // Act & Assert
- var ex = Assert.Throws<InvalidOperationException>(() =>
- DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, method, default, default), argsJson));
- Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 3 must be declared as type 'DotNetObjectRef<TestDTO>' to receive the incoming value.", ex.Message);
- }
- [Fact]
- public void CanInvokeInstanceVoidMethod()
- {
- // Arrange: Track some instance
- var jsRuntime = new TestJSRuntime();
- var targetInstance = new SomePublicType();
- var objectRef = DotNetObjectReference.Create(targetInstance);
- jsRuntime.Invoke<object>("unimportant", objectRef);
- // Act
- var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null);
- // Assert
- Assert.Null(resultJson);
- Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid);
- }
- [Fact]
- public void CanInvokeBaseInstanceVoidMethod()
- {
- // Arrange: Track some instance
- var jsRuntime = new TestJSRuntime();
- var targetInstance = new DerivedClass();
- var objectRef = DotNetObjectReference.Create(targetInstance);
- jsRuntime.Invoke<object>("unimportant", objectRef);
- // Act
- var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "BaseClassInvokableInstanceVoid", 1, default), null);
- // Assert
- Assert.Null(resultJson);
- Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid);
- }
- [Fact]
- public void DotNetObjectReferencesCanBeDisposed()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- var targetInstance = new SomePublicType();
- var objectRef = DotNetObjectReference.Create(targetInstance);
- jsRuntime.Invoke<object>("unimportant", objectRef);
- // Act
- DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "__Dispose", objectRef.ObjectId, default), null);
- // Assert
- Assert.True(objectRef.Disposed);
- }
- [Fact]
- public void CannotUseDotNetObjectRefAfterDisposal()
- {
- // This test addresses the case where the developer calls objectRef.Dispose()
- // from .NET code, as opposed to .dispose() from JS code
- // Arrange: Track some instance, then dispose it
- var jsRuntime = new TestJSRuntime();
- var targetInstance = new SomePublicType();
- var objectRef = DotNetObjectReference.Create(targetInstance);
- jsRuntime.Invoke<object>("unimportant", objectRef);
- objectRef.Dispose();
- // Act/Assert
- var ex = Assert.Throws<ArgumentException>(
- () => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null));
- Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
- }
- [Fact]
- public void CannotUseDotNetObjectRefAfterReleaseDotNetObject()
- {
- // This test addresses the case where the developer calls .dispose()
- // from JS code, as opposed to objectRef.Dispose() from .NET code
- // Arrange: Track some instance, then dispose it
- var jsRuntime = new TestJSRuntime();
- var targetInstance = new SomePublicType();
- var objectRef = DotNetObjectReference.Create(targetInstance);
- jsRuntime.Invoke<object>("unimportant", objectRef);
- objectRef.Dispose();
- // Act/Assert
- var ex = Assert.Throws<ArgumentException>(
- () => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null));
- Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
- }
- [Fact]
- public void EndInvoke_WithSuccessValue()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 };
- var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
- var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, jsRuntime.JsonSerializerOptions);
- // Act
- DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
- // Assert
- Assert.True(task.IsCompletedSuccessfully);
- var result = task.Result;
- Assert.Equal(testDTO.StringVal, result.StringVal);
- Assert.Equal(testDTO.IntVal, result.IntVal);
- }
- [Fact]
- public async Task EndInvoke_WithErrorString()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- var expected = "Some error";
- var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
- var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, expected }, jsRuntime.JsonSerializerOptions);
- // Act
- DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
- // Assert
- var ex = await Assert.ThrowsAsync<JSException>(async () => await task);
- Assert.Equal(expected, ex.Message);
- }
- [Fact]
- public async Task EndInvoke_WithNullError()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
- var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, null }, jsRuntime.JsonSerializerOptions);
- // Act
- DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
- // Assert
- var ex = await Assert.ThrowsAsync<JSException>(async () => await task);
- Assert.Empty(ex.Message);
- }
- [Fact]
- public void CanInvokeInstanceMethodWithParams()
- {
- // Arrange: Track some instance plus another object we'll pass as a param
- var jsRuntime = new TestJSRuntime();
- var targetInstance = new SomePublicType();
- var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
- jsRuntime.Invoke<object>("unimportant",
- DotNetObjectReference.Create(targetInstance),
- DotNetObjectReference.Create(arg2));
- var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]";
- // Act
- var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceMethod", 1, default), argsJson);
- // Assert
- Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson);
- var resultDto = ((DotNetObjectReference<TestDTO>)jsRuntime.GetObjectReference(3)).Value;
- Assert.Equal(1235, resultDto.IntVal);
- Assert.Equal("MY STRING", resultDto.StringVal);
- }
- [Fact]
- public void CanInvokeNonGenericInstanceMethodOnGenericType()
- {
- var jsRuntime = new TestJSRuntime();
- var targetInstance = new GenericType<int>();
- jsRuntime.Invoke<object>("_setup",
- DotNetObjectReference.Create(targetInstance));
- var argsJson = "[\"hello world\"]";
- // Act
- var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, nameof(GenericType<int>.EchoStringParameter), 1, default), argsJson);
- // Assert
- Assert.Equal("\"hello world\"", resultJson);
- }
- [Fact]
- public void CanInvokeMethodsThatAcceptGenericParametersOnGenericTypes()
- {
- var jsRuntime = new TestJSRuntime();
- var targetInstance = new GenericType<string>();
- jsRuntime.Invoke<object>("_setup",
- DotNetObjectReference.Create(targetInstance));
- var argsJson = "[\"hello world\"]";
- // Act
- var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, nameof(GenericType<string>.EchoParameter), 1, default), argsJson);
- // Assert
- Assert.Equal("\"hello world\"", resultJson);
- }
- [Fact]
- public void CannotInvokeStaticOpenGenericMethods()
- {
- var methodIdentifier = "StaticGenericMethod";
- var jsRuntime = new TestJSRuntime();
- // Act
- var ex = Assert.Throws<ArgumentException>(() => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, methodIdentifier, 0, default), "[7]"));
- Assert.Contains($"The assembly '{thisAssemblyName}' does not contain a public invokable method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].", ex.Message);
- }
- [Fact]
- public void CannotInvokeInstanceOpenGenericMethods()
- {
- var methodIdentifier = "InstanceGenericMethod";
- var targetInstance = new GenericType<int>();
- var jsRuntime = new TestJSRuntime();
- jsRuntime.Invoke<object>("_setup",
- DotNetObjectReference.Create(targetInstance));
- var argsJson = "[\"hello world\"]";
- // Act
- var ex = Assert.Throws<ArgumentException>(() => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, methodIdentifier, 1, default), argsJson));
- Assert.Contains($"The type 'GenericType`1' does not contain a public invokable method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].", ex.Message);
- }
- [Fact]
- public void CannotInvokeMethodsWithGenericParameters_IfTypesDoNotMatch()
- {
- var jsRuntime = new TestJSRuntime();
- var targetInstance = new GenericType<int>();
- jsRuntime.Invoke<object>("_setup",
- DotNetObjectReference.Create(targetInstance));
- var argsJson = "[\"hello world\"]";
- // Act & Assert
- Assert.Throws<JsonException>(() =>
- DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, nameof(GenericType<int>.EchoParameter), 1, default), argsJson));
- }
- [Fact]
- public void CannotInvokeWithFewerNumberOfParameters()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- var argsJson = JsonSerializer.Serialize(new object[]
- {
- new TestDTO { StringVal = "Another string", IntVal = 456 },
- new[] { 100, 200 },
- }, jsRuntime.JsonSerializerOptions);
- // Act/Assert
- var ex = Assert.Throws<ArgumentException>(() =>
- {
- DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson);
- });
- Assert.Equal("The call to 'InvocableStaticWithParams' expects '3' parameters, but received '2'.", ex.Message);
- }
- [Fact]
- public void CannotInvokeWithMoreParameters()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- var objectRef = DotNetObjectReference.Create(new TestDTO { IntVal = 4 });
- var argsJson = JsonSerializer.Serialize(new object[]
- {
- new TestDTO { StringVal = "Another string", IntVal = 456 },
- new[] { 100, 200 },
- objectRef,
- 7,
- }, jsRuntime.JsonSerializerOptions);
- // Act/Assert
- var ex = Assert.Throws<JsonException>(() =>
- {
- DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson);
- });
- Assert.Equal("Unexpected JSON token Number. Ensure that the call to `InvocableStaticWithParams' is supplied with exactly '3' parameters.", ex.Message);
- }
- [Fact]
- public async Task CanInvokeAsyncMethod()
- {
- // Arrange: Track some instance plus another object we'll pass as a param
- var jsRuntime = new TestJSRuntime();
- var targetInstance = new SomePublicType();
- var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
- var arg1Ref = DotNetObjectReference.Create(targetInstance);
- var arg2Ref = DotNetObjectReference.Create(arg2);
- jsRuntime.Invoke<object>("unimportant", arg1Ref, arg2Ref);
- // Arrange: all args
- var argsJson = JsonSerializer.Serialize(new object[]
- {
- new TestDTO { IntVal = 1000, StringVal = "String via JSON" },
- arg2Ref,
- }, jsRuntime.JsonSerializerOptions);
- // Act
- var callId = "123";
- var resultTask = jsRuntime.NextInvocationTask;
- DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "InvokableAsyncMethod", 1, callId), argsJson);
- await resultTask;
- // Assert: Correct completion information
- Assert.Equal(callId, jsRuntime.LastCompletionCallId);
- Assert.True(jsRuntime.LastCompletionResult.Success);
- var resultJson = Assert.IsType<string>(jsRuntime.LastCompletionResult.ResultJson);
- var result = JsonSerializer.Deserialize<SomePublicType.InvokableAsyncMethodResult>(resultJson, jsRuntime.JsonSerializerOptions);
- Assert.Equal("STRING VIA JSON", result.SomeDTO.StringVal);
- Assert.Equal(2000, result.SomeDTO.IntVal);
- // Assert: Second result value marshalled by ref
- var resultDto2 = result.SomeDTORef.Value;
- Assert.Equal("MY STRING", resultDto2.StringVal);
- Assert.Equal(2468, resultDto2.IntVal);
- }
- [Fact]
- public async Task CanInvokeSyncThrowingMethod()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- // Act
- var callId = "123";
- var resultTask = jsRuntime.NextInvocationTask;
- DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, callId), default);
- await resultTask; // This won't throw, it sets properties on the jsRuntime.
- // Assert
- Assert.Equal(callId, jsRuntime.LastCompletionCallId);
- Assert.False(jsRuntime.LastCompletionResult.Success); // Fails
- // Make sure the method that threw the exception shows up in the call stack
- // https://github.com/dotnet/aspnetcore/issues/8612
- Assert.Contains(nameof(ThrowingClass.ThrowingMethod), jsRuntime.LastCompletionResult.Exception.ToString());
- }
- [Fact]
- public async Task CanInvokeAsyncThrowingMethod()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- // Act
- var callId = "123";
- var resultTask = jsRuntime.NextInvocationTask;
- DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, callId), default);
- await resultTask; // This won't throw, it sets properties on the jsRuntime.
- // Assert
- Assert.Equal(callId, jsRuntime.LastCompletionCallId);
- Assert.False(jsRuntime.LastCompletionResult.Success); // Fails
- // Make sure the method that threw the exception shows up in the call stack
- // https://github.com/dotnet/aspnetcore/issues/8612
- Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), jsRuntime.LastCompletionResult.Exception.ToString());
- }
- [Fact]
- public async Task BeginInvoke_ThrowsWithInvalidArgsJson_WithCallId()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- var callId = "123";
- var resultTask = jsRuntime.NextInvocationTask;
- DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, callId), "<xml>not json</xml>");
- await resultTask; // This won't throw, it sets properties on the jsRuntime.
- // Assert
- Assert.Equal(callId, jsRuntime.LastCompletionCallId);
- Assert.False(jsRuntime.LastCompletionResult.Success); // Fails
- var exception = jsRuntime.LastCompletionResult.Exception;
- Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", exception.ToString());
- }
- [Fact]
- public void BeginInvoke_ThrowsWithInvalid_DotNetObjectRef()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- var callId = "123";
- var resultTask = jsRuntime.NextInvocationTask;
- DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, callId), null);
- // Assert
- Assert.Equal(callId, jsRuntime.LastCompletionCallId);
- Assert.False(jsRuntime.LastCompletionResult.Success); // Fails
- var exception = jsRuntime.LastCompletionResult.Exception;
- Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectReference instance was already disposed.", exception.ToString());
- }
- [Theory]
- [InlineData("")]
- [InlineData("<xml>")]
- public void ParseArguments_ThrowsIfJsonIsInvalid(string arguments)
- {
- Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) }));
- }
- [Theory]
- [InlineData("{\"key\":\"value\"}")]
- [InlineData("\"Test\"")]
- public void ParseArguments_ThrowsIfTheArgsJsonIsNotArray(string arguments)
- {
- // Act & Assert
- Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) }));
- }
- [Theory]
- [InlineData("[\"hello\"")]
- [InlineData("[\"hello\",")]
- public void ParseArguments_ThrowsIfTheArgsJsonIsInvalidArray(string arguments)
- {
- // Act & Assert
- Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) }));
- }
- [Fact]
- public void ParseArguments_Works()
- {
- // Arrange
- var arguments = "[\"Hello\", 2]";
- // Act
- var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string), typeof(int), });
- // Assert
- Assert.Equal(new object[] { "Hello", 2 }, result);
- }
- [Fact]
- public void ParseArguments_SingleArgument()
- {
- // Arrange
- var arguments = "[{\"IntVal\": 7}]";
- // Act
- var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(TestDTO), });
- // Assert
- var value = Assert.IsType<TestDTO>(Assert.Single(result));
- Assert.Equal(7, value.IntVal);
- Assert.Null(value.StringVal);
- }
- [Fact]
- public void ParseArguments_NullArgument()
- {
- // Arrange
- var arguments = "[4, null]";
- // Act
- var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(int), typeof(TestDTO), });
- // Assert
- Assert.Collection(
- result,
- v => Assert.Equal(4, v),
- v => Assert.Null(v));
- }
- [Fact]
- public void ParseArguments_Throws_WithIncorrectDotNetObjectRefUsage()
- {
- // Arrange
- var method = "SomeMethod";
- var arguments = "[4, {\"__dotNetObject\": 7}]";
- // Act
- var ex = Assert.Throws<InvalidOperationException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), method, arguments, new[] { typeof(int), typeof(TestDTO), }));
- // Assert
- Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 2 must be declared as type 'DotNetObjectRef<TestDTO>' to receive the incoming value.", ex.Message);
- }
- [Fact]
- public void EndInvokeJS_ThrowsIfJsonIsEmptyString()
- {
- Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), ""));
- }
- [Fact]
- public void EndInvokeJS_ThrowsIfJsonIsNotArray()
- {
- Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "{\"key\": \"value\"}"));
- }
- [Fact]
- public void EndInvokeJS_ThrowsIfJsonArrayIsInComplete()
- {
- Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false"));
- }
- [Fact]
- public void EndInvokeJS_ThrowsIfJsonArrayHasMoreThan3Arguments()
- {
- Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false, \"Hello\", 5]"));
- }
- [Fact]
- public void EndInvokeJS_Works()
- {
- var jsRuntime = new TestJSRuntime();
- var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
- DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]");
- Assert.True(task.IsCompletedSuccessfully);
- Assert.Equal(7, task.Result.IntVal);
- }
- [Fact]
- public void EndInvokeJS_WithArrayValue()
- {
- var jsRuntime = new TestJSRuntime();
- var task = jsRuntime.InvokeAsync<int[]>("somemethod");
- DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]");
- Assert.True(task.IsCompletedSuccessfully);
- Assert.Equal(new[] { 1, 2, 3 }, task.Result);
- }
- [Fact]
- public void EndInvokeJS_WithNullValue()
- {
- var jsRuntime = new TestJSRuntime();
- var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
- DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]");
- Assert.True(task.IsCompletedSuccessfully);
- Assert.Null(task.Result);
- }
- [Fact]
- public void ReceiveByteArray_Works()
- {
- // Arrange
- var jsRuntime = new TestJSRuntime();
- var byteArray = new byte[] { 1, 5, 7 };
- // Act
- DotNetDispatcher.ReceiveByteArray(jsRuntime, 0, byteArray);
- // Assert
- Assert.Equal(1, jsRuntime.ByteArraysToBeRevived.Count);
- Assert.Equal(byteArray, jsRuntime.ByteArraysToBeRevived.Buffer[0]);
- }
- internal class SomeInteralType
- {
- [JSInvokable("MethodOnInternalType")] public void MyMethod() { }
- }
- public class SomePublicType
- {
- public static bool DidInvokeMyInvocableStaticVoid;
- public bool DidInvokeMyInvocableInstanceVoid;
- [JSInvokable("PrivateMethod")] private static void MyPrivateMethod() { }
- [JSInvokable("ProtectedMethod")] protected static void MyProtectedMethod() { }
- protected static void StaticMethodWithoutAttribute() { }
- protected static void InstanceMethodWithoutAttribute() { }
- [JSInvokable("InvocableStaticVoid")]
- public static void MyInvocableVoid()
- {
- DidInvokeMyInvocableStaticVoid = true;
- }
- [JSInvokable("InvocableStaticNonVoid")]
- public static object MyInvocableNonVoid()
- => new TestDTO { StringVal = "Test", IntVal = 123 };
- [JSInvokable("InvocableStaticWithParams")]
- public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, DotNetObjectReference<TestDTO> dtoByRef)
- => new object[]
- {
- new TestDTO // Return via JSON marshalling
- {
- StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
- IntVal = dtoViaJson.IntVal + incrementAmounts.Sum()
- },
- DotNetObjectReference.Create(new TestDTO // Return by ref
- {
- StringVal = dtoByRef.Value.StringVal.ToUpperInvariant(),
- IntVal = dtoByRef.Value.IntVal + incrementAmounts.Sum()
- })
- };
- [JSInvokable(nameof(IncorrectDotNetObjectRefUsage))]
- public static object[] IncorrectDotNetObjectRefUsage(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef)
- => throw new InvalidOperationException("Shouldn't be called");
- [JSInvokable]
- public static TestDTO InvokableMethodWithoutCustomIdentifier()
- => new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 };
- [JSInvokable]
- public void InvokableInstanceVoid()
- {
- DidInvokeMyInvocableInstanceVoid = true;
- }
- [JSInvokable]
- public object[] InvokableInstanceMethod(string someString, DotNetObjectReference<TestDTO> someDTORef)
- {
- var someDTO = someDTORef.Value;
- // Returning an array to make the point that object references
- // can be embedded anywhere in the result
- return new object[]
- {
- $"You passed {someString}",
- DotNetObjectReference.Create(new TestDTO
- {
- IntVal = someDTO.IntVal + 1,
- StringVal = someDTO.StringVal.ToUpperInvariant()
- })
- };
- }
- [JSInvokable]
- public async Task<InvokableAsyncMethodResult> InvokableAsyncMethod(TestDTO dtoViaJson, DotNetObjectReference<TestDTO> dtoByRefWrapper)
- {
- await Task.Delay(50);
- var dtoByRef = dtoByRefWrapper.Value;
- return new InvokableAsyncMethodResult
- {
- SomeDTO = new TestDTO // Return via JSON
- {
- StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
- IntVal = dtoViaJson.IntVal * 2,
- },
- SomeDTORef = DotNetObjectReference.Create(new TestDTO // Return by ref
- {
- StringVal = dtoByRef.StringVal.ToUpperInvariant(),
- IntVal = dtoByRef.IntVal * 2,
- })
- };
- }
- public class InvokableAsyncMethodResult
- {
- public TestDTO SomeDTO { get; set; }
- public DotNetObjectReference<TestDTO> SomeDTORef { get; set; }
- }
- }
- public class BaseClass
- {
- public bool DidInvokeMyBaseClassInvocableInstanceVoid;
- [JSInvokable]
- public void BaseClassInvokableInstanceVoid()
- {
- DidInvokeMyBaseClassInvocableInstanceVoid = true;
- }
- }
- public class DerivedClass : BaseClass
- {
- }
- public class TestDTO
- {
- public string StringVal { get; set; }
- public int IntVal { get; set; }
- }
- public class ThrowingClass
- {
- [JSInvokable]
- public static string ThrowingMethod()
- {
- throw new InvalidTimeZoneException();
- }
- [JSInvokable]
- public static async Task<string> AsyncThrowingMethod()
- {
- await Task.Yield();
- throw new InvalidTimeZoneException();
- }
- }
- public class GenericType<TValue>
- {
- [JSInvokable] public string EchoStringParameter(string input) => input;
- [JSInvokable] public TValue EchoParameter(TValue input) => input;
- }
- public class GenericMethodClass
- {
- [JSInvokable("StaticGenericMethod")] public static string StaticGenericMethod<TValue>(TValue input) => input.ToString();
- [JSInvokable("InstanceGenericMethod")] public string GenericMethod<TValue>(TValue input) => input.ToString();
- }
- public class TestJSRuntime : JSInProcessRuntime
- {
- private TaskCompletionSource<object> _nextInvocationTcs = new TaskCompletionSource<object>();
- public Task NextInvocationTask => _nextInvocationTcs.Task;
- public long LastInvocationAsyncHandle { get; private set; }
- public string LastInvocationIdentifier { get; private set; }
- public string LastInvocationArgsJson { get; private set; }
- public string LastCompletionCallId { get; private set; }
- public DotNetInvocationResult LastCompletionResult { get; private set; }
- protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
- {
- LastInvocationAsyncHandle = asyncHandle;
- LastInvocationIdentifier = identifier;
- LastInvocationArgsJson = argsJson;
- _nextInvocationTcs.SetResult(null);
- _nextInvocationTcs = new TaskCompletionSource<object>();
- }
- protected override string InvokeJS(string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
- {
- LastInvocationAsyncHandle = default;
- LastInvocationIdentifier = identifier;
- LastInvocationArgsJson = argsJson;
- _nextInvocationTcs.SetResult(null);
- _nextInvocationTcs = new TaskCompletionSource<object>();
- return null;
- }
- protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
- {
- LastCompletionCallId = invocationInfo.CallId;
- LastCompletionResult = invocationResult;
- _nextInvocationTcs.SetResult(null);
- _nextInvocationTcs = new TaskCompletionSource<object>();
- }
- }
- }
- }
|