DotNetDispatcherTest.cs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. // Copyright (c) .NET Foundation. All rights reserved.
  2. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  3. #nullable disable
  4. using System;
  5. using System.Linq;
  6. using System.Text.Json;
  7. using System.Threading.Tasks;
  8. using Xunit;
  9. namespace Microsoft.JSInterop.Infrastructure
  10. {
  11. public class DotNetDispatcherTest
  12. {
  13. private readonly static string thisAssemblyName = typeof(DotNetDispatcherTest).Assembly.GetName().Name;
  14. [Fact]
  15. public void CannotInvokeWithEmptyAssemblyName()
  16. {
  17. var ex = Assert.Throws<ArgumentException>(() =>
  18. {
  19. DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(" ", "SomeMethod", default, default), "[]");
  20. });
  21. Assert.StartsWith("Property 'AssemblyName' cannot be null, empty, or whitespace.", ex.Message);
  22. Assert.Equal("assemblyKey", ex.ParamName);
  23. }
  24. [Fact]
  25. public void CannotInvokeWithEmptyMethodIdentifier()
  26. {
  27. var ex = Assert.Throws<ArgumentException>(() =>
  28. {
  29. DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo("SomeAssembly", " ", default, default), "[]");
  30. });
  31. Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message);
  32. Assert.Equal("methodIdentifier", ex.ParamName);
  33. }
  34. [Fact]
  35. public void CannotInvokeMethodsOnUnloadedAssembly()
  36. {
  37. var assemblyName = "Some.Fake.Assembly";
  38. var ex = Assert.Throws<ArgumentException>(() =>
  39. {
  40. DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(assemblyName, "SomeMethod", default, default), null);
  41. });
  42. Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message);
  43. }
  44. // Note: Currently it's also not possible to invoke generic methods.
  45. // That's not something determined by DotNetDispatcher, but rather by the fact that we
  46. // don't close over the generics in the reflection code.
  47. // Not defining this behavior through unit tests because the default outcome is
  48. // fine (an exception stating what info is missing).
  49. [Theory]
  50. [InlineData("MethodOnInternalType")]
  51. [InlineData("PrivateMethod")]
  52. [InlineData("ProtectedMethod")]
  53. [InlineData("StaticMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
  54. [InlineData("InstanceMethodWithoutAttribute")] // That's not really its identifier; just making the point that there's no way to invoke it
  55. public void CannotInvokeUnsuitableMethods(string methodIdentifier)
  56. {
  57. var ex = Assert.Throws<ArgumentException>(() =>
  58. {
  59. DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(thisAssemblyName, methodIdentifier, default, default), null);
  60. });
  61. Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public invokable method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message);
  62. }
  63. [Fact]
  64. public void CanInvokeStaticVoidMethod()
  65. {
  66. // Arrange/Act
  67. var jsRuntime = new TestJSRuntime();
  68. SomePublicType.DidInvokeMyInvocableStaticVoid = false;
  69. var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticVoid", default, default), null);
  70. // Assert
  71. Assert.Null(resultJson);
  72. Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid);
  73. }
  74. [Fact]
  75. public void CanInvokeStaticNonVoidMethod()
  76. {
  77. // Arrange/Act
  78. var jsRuntime = new TestJSRuntime();
  79. var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticNonVoid", default, default), null);
  80. var result = JsonSerializer.Deserialize<TestDTO>(resultJson, jsRuntime.JsonSerializerOptions);
  81. // Assert
  82. Assert.Equal("Test", result.StringVal);
  83. Assert.Equal(123, result.IntVal);
  84. }
  85. [Fact]
  86. public void CanInvokeStaticNonVoidMethodWithoutCustomIdentifier()
  87. {
  88. // Arrange/Act
  89. var jsRuntime = new TestJSRuntime();
  90. var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, default), null);
  91. var result = JsonSerializer.Deserialize<TestDTO>(resultJson, jsRuntime.JsonSerializerOptions);
  92. // Assert
  93. Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal);
  94. Assert.Equal(456, result.IntVal);
  95. }
  96. [Fact]
  97. public void CanInvokeStaticWithParams()
  98. {
  99. // Arrange: Track a .NET object to use as an arg
  100. var jsRuntime = new TestJSRuntime();
  101. var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
  102. var objectRef = DotNetObjectReference.Create(arg3);
  103. jsRuntime.Invoke<object>("unimportant", objectRef);
  104. // Arrange: Remaining args
  105. var argsJson = JsonSerializer.Serialize(new object[]
  106. {
  107. new TestDTO { StringVal = "Another string", IntVal = 456 },
  108. new[] { 100, 200 },
  109. objectRef
  110. }, jsRuntime.JsonSerializerOptions);
  111. // Act
  112. var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson);
  113. var result = JsonDocument.Parse(resultJson);
  114. var root = result.RootElement;
  115. // Assert: First result value marshalled via JSON
  116. var resultDto1 = JsonSerializer.Deserialize<TestDTO>(root[0].GetRawText(), jsRuntime.JsonSerializerOptions);
  117. Assert.Equal("ANOTHER STRING", resultDto1.StringVal);
  118. Assert.Equal(756, resultDto1.IntVal);
  119. // Assert: Second result value marshalled by ref
  120. var resultDto2Ref = root[1];
  121. Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.StringVal), out _));
  122. Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _));
  123. Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey.EncodedUtf8Bytes, out var property));
  124. var resultDto2 = Assert.IsType<DotNetObjectReference<TestDTO>>(jsRuntime.GetObjectReference(property.GetInt64())).Value;
  125. Assert.Equal("MY STRING", resultDto2.StringVal);
  126. Assert.Equal(1299, resultDto2.IntVal);
  127. }
  128. [Fact]
  129. public void InvokingWithIncorrectUseOfDotNetObjectRefThrows()
  130. {
  131. // Arrange
  132. var jsRuntime = new TestJSRuntime();
  133. var method = nameof(SomePublicType.IncorrectDotNetObjectRefUsage);
  134. var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" };
  135. var objectRef = DotNetObjectReference.Create(arg3);
  136. jsRuntime.Invoke<object>("unimportant", objectRef);
  137. // Arrange: Remaining args
  138. var argsJson = JsonSerializer.Serialize(new object[]
  139. {
  140. new TestDTO { StringVal = "Another string", IntVal = 456 },
  141. new[] { 100, 200 },
  142. objectRef
  143. }, jsRuntime.JsonSerializerOptions);
  144. // Act & Assert
  145. var ex = Assert.Throws<InvalidOperationException>(() =>
  146. DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, method, default, default), argsJson));
  147. 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);
  148. }
  149. [Fact]
  150. public void CanInvokeInstanceVoidMethod()
  151. {
  152. // Arrange: Track some instance
  153. var jsRuntime = new TestJSRuntime();
  154. var targetInstance = new SomePublicType();
  155. var objectRef = DotNetObjectReference.Create(targetInstance);
  156. jsRuntime.Invoke<object>("unimportant", objectRef);
  157. // Act
  158. var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null);
  159. // Assert
  160. Assert.Null(resultJson);
  161. Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid);
  162. }
  163. [Fact]
  164. public void CanInvokeBaseInstanceVoidMethod()
  165. {
  166. // Arrange: Track some instance
  167. var jsRuntime = new TestJSRuntime();
  168. var targetInstance = new DerivedClass();
  169. var objectRef = DotNetObjectReference.Create(targetInstance);
  170. jsRuntime.Invoke<object>("unimportant", objectRef);
  171. // Act
  172. var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "BaseClassInvokableInstanceVoid", 1, default), null);
  173. // Assert
  174. Assert.Null(resultJson);
  175. Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid);
  176. }
  177. [Fact]
  178. public void DotNetObjectReferencesCanBeDisposed()
  179. {
  180. // Arrange
  181. var jsRuntime = new TestJSRuntime();
  182. var targetInstance = new SomePublicType();
  183. var objectRef = DotNetObjectReference.Create(targetInstance);
  184. jsRuntime.Invoke<object>("unimportant", objectRef);
  185. // Act
  186. DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "__Dispose", objectRef.ObjectId, default), null);
  187. // Assert
  188. Assert.True(objectRef.Disposed);
  189. }
  190. [Fact]
  191. public void CannotUseDotNetObjectRefAfterDisposal()
  192. {
  193. // This test addresses the case where the developer calls objectRef.Dispose()
  194. // from .NET code, as opposed to .dispose() from JS code
  195. // Arrange: Track some instance, then dispose it
  196. var jsRuntime = new TestJSRuntime();
  197. var targetInstance = new SomePublicType();
  198. var objectRef = DotNetObjectReference.Create(targetInstance);
  199. jsRuntime.Invoke<object>("unimportant", objectRef);
  200. objectRef.Dispose();
  201. // Act/Assert
  202. var ex = Assert.Throws<ArgumentException>(
  203. () => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null));
  204. Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
  205. }
  206. [Fact]
  207. public void CannotUseDotNetObjectRefAfterReleaseDotNetObject()
  208. {
  209. // This test addresses the case where the developer calls .dispose()
  210. // from JS code, as opposed to objectRef.Dispose() from .NET code
  211. // Arrange: Track some instance, then dispose it
  212. var jsRuntime = new TestJSRuntime();
  213. var targetInstance = new SomePublicType();
  214. var objectRef = DotNetObjectReference.Create(targetInstance);
  215. jsRuntime.Invoke<object>("unimportant", objectRef);
  216. objectRef.Dispose();
  217. // Act/Assert
  218. var ex = Assert.Throws<ArgumentException>(
  219. () => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null));
  220. Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
  221. }
  222. [Fact]
  223. public void EndInvoke_WithSuccessValue()
  224. {
  225. // Arrange
  226. var jsRuntime = new TestJSRuntime();
  227. var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 };
  228. var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
  229. var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, jsRuntime.JsonSerializerOptions);
  230. // Act
  231. DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
  232. // Assert
  233. Assert.True(task.IsCompletedSuccessfully);
  234. var result = task.Result;
  235. Assert.Equal(testDTO.StringVal, result.StringVal);
  236. Assert.Equal(testDTO.IntVal, result.IntVal);
  237. }
  238. [Fact]
  239. public async Task EndInvoke_WithErrorString()
  240. {
  241. // Arrange
  242. var jsRuntime = new TestJSRuntime();
  243. var expected = "Some error";
  244. var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
  245. var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, expected }, jsRuntime.JsonSerializerOptions);
  246. // Act
  247. DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
  248. // Assert
  249. var ex = await Assert.ThrowsAsync<JSException>(async () => await task);
  250. Assert.Equal(expected, ex.Message);
  251. }
  252. [Fact]
  253. public async Task EndInvoke_WithNullError()
  254. {
  255. // Arrange
  256. var jsRuntime = new TestJSRuntime();
  257. var task = jsRuntime.InvokeAsync<TestDTO>("unimportant");
  258. var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, null }, jsRuntime.JsonSerializerOptions);
  259. // Act
  260. DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson);
  261. // Assert
  262. var ex = await Assert.ThrowsAsync<JSException>(async () => await task);
  263. Assert.Empty(ex.Message);
  264. }
  265. [Fact]
  266. public void CanInvokeInstanceMethodWithParams()
  267. {
  268. // Arrange: Track some instance plus another object we'll pass as a param
  269. var jsRuntime = new TestJSRuntime();
  270. var targetInstance = new SomePublicType();
  271. var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
  272. jsRuntime.Invoke<object>("unimportant",
  273. DotNetObjectReference.Create(targetInstance),
  274. DotNetObjectReference.Create(arg2));
  275. var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]";
  276. // Act
  277. var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceMethod", 1, default), argsJson);
  278. // Assert
  279. Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson);
  280. var resultDto = ((DotNetObjectReference<TestDTO>)jsRuntime.GetObjectReference(3)).Value;
  281. Assert.Equal(1235, resultDto.IntVal);
  282. Assert.Equal("MY STRING", resultDto.StringVal);
  283. }
  284. [Fact]
  285. public void CanInvokeNonGenericInstanceMethodOnGenericType()
  286. {
  287. var jsRuntime = new TestJSRuntime();
  288. var targetInstance = new GenericType<int>();
  289. jsRuntime.Invoke<object>("_setup",
  290. DotNetObjectReference.Create(targetInstance));
  291. var argsJson = "[\"hello world\"]";
  292. // Act
  293. var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, nameof(GenericType<int>.EchoStringParameter), 1, default), argsJson);
  294. // Assert
  295. Assert.Equal("\"hello world\"", resultJson);
  296. }
  297. [Fact]
  298. public void CanInvokeMethodsThatAcceptGenericParametersOnGenericTypes()
  299. {
  300. var jsRuntime = new TestJSRuntime();
  301. var targetInstance = new GenericType<string>();
  302. jsRuntime.Invoke<object>("_setup",
  303. DotNetObjectReference.Create(targetInstance));
  304. var argsJson = "[\"hello world\"]";
  305. // Act
  306. var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, nameof(GenericType<string>.EchoParameter), 1, default), argsJson);
  307. // Assert
  308. Assert.Equal("\"hello world\"", resultJson);
  309. }
  310. [Fact]
  311. public void CannotInvokeStaticOpenGenericMethods()
  312. {
  313. var methodIdentifier = "StaticGenericMethod";
  314. var jsRuntime = new TestJSRuntime();
  315. // Act
  316. var ex = Assert.Throws<ArgumentException>(() => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, methodIdentifier, 0, default), "[7]"));
  317. Assert.Contains($"The assembly '{thisAssemblyName}' does not contain a public invokable method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].", ex.Message);
  318. }
  319. [Fact]
  320. public void CannotInvokeInstanceOpenGenericMethods()
  321. {
  322. var methodIdentifier = "InstanceGenericMethod";
  323. var targetInstance = new GenericType<int>();
  324. var jsRuntime = new TestJSRuntime();
  325. jsRuntime.Invoke<object>("_setup",
  326. DotNetObjectReference.Create(targetInstance));
  327. var argsJson = "[\"hello world\"]";
  328. // Act
  329. var ex = Assert.Throws<ArgumentException>(() => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, methodIdentifier, 1, default), argsJson));
  330. Assert.Contains($"The type 'GenericType`1' does not contain a public invokable method with [{nameof(JSInvokableAttribute)}(\"{methodIdentifier}\")].", ex.Message);
  331. }
  332. [Fact]
  333. public void CannotInvokeMethodsWithGenericParameters_IfTypesDoNotMatch()
  334. {
  335. var jsRuntime = new TestJSRuntime();
  336. var targetInstance = new GenericType<int>();
  337. jsRuntime.Invoke<object>("_setup",
  338. DotNetObjectReference.Create(targetInstance));
  339. var argsJson = "[\"hello world\"]";
  340. // Act & Assert
  341. Assert.Throws<JsonException>(() =>
  342. DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, nameof(GenericType<int>.EchoParameter), 1, default), argsJson));
  343. }
  344. [Fact]
  345. public void CannotInvokeWithFewerNumberOfParameters()
  346. {
  347. // Arrange
  348. var jsRuntime = new TestJSRuntime();
  349. var argsJson = JsonSerializer.Serialize(new object[]
  350. {
  351. new TestDTO { StringVal = "Another string", IntVal = 456 },
  352. new[] { 100, 200 },
  353. }, jsRuntime.JsonSerializerOptions);
  354. // Act/Assert
  355. var ex = Assert.Throws<ArgumentException>(() =>
  356. {
  357. DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson);
  358. });
  359. Assert.Equal("The call to 'InvocableStaticWithParams' expects '3' parameters, but received '2'.", ex.Message);
  360. }
  361. [Fact]
  362. public void CannotInvokeWithMoreParameters()
  363. {
  364. // Arrange
  365. var jsRuntime = new TestJSRuntime();
  366. var objectRef = DotNetObjectReference.Create(new TestDTO { IntVal = 4 });
  367. var argsJson = JsonSerializer.Serialize(new object[]
  368. {
  369. new TestDTO { StringVal = "Another string", IntVal = 456 },
  370. new[] { 100, 200 },
  371. objectRef,
  372. 7,
  373. }, jsRuntime.JsonSerializerOptions);
  374. // Act/Assert
  375. var ex = Assert.Throws<JsonException>(() =>
  376. {
  377. DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson);
  378. });
  379. Assert.Equal("Unexpected JSON token Number. Ensure that the call to `InvocableStaticWithParams' is supplied with exactly '3' parameters.", ex.Message);
  380. }
  381. [Fact]
  382. public async Task CanInvokeAsyncMethod()
  383. {
  384. // Arrange: Track some instance plus another object we'll pass as a param
  385. var jsRuntime = new TestJSRuntime();
  386. var targetInstance = new SomePublicType();
  387. var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" };
  388. var arg1Ref = DotNetObjectReference.Create(targetInstance);
  389. var arg2Ref = DotNetObjectReference.Create(arg2);
  390. jsRuntime.Invoke<object>("unimportant", arg1Ref, arg2Ref);
  391. // Arrange: all args
  392. var argsJson = JsonSerializer.Serialize(new object[]
  393. {
  394. new TestDTO { IntVal = 1000, StringVal = "String via JSON" },
  395. arg2Ref,
  396. }, jsRuntime.JsonSerializerOptions);
  397. // Act
  398. var callId = "123";
  399. var resultTask = jsRuntime.NextInvocationTask;
  400. DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "InvokableAsyncMethod", 1, callId), argsJson);
  401. await resultTask;
  402. // Assert: Correct completion information
  403. Assert.Equal(callId, jsRuntime.LastCompletionCallId);
  404. Assert.True(jsRuntime.LastCompletionResult.Success);
  405. var resultJson = Assert.IsType<string>(jsRuntime.LastCompletionResult.ResultJson);
  406. var result = JsonSerializer.Deserialize<SomePublicType.InvokableAsyncMethodResult>(resultJson, jsRuntime.JsonSerializerOptions);
  407. Assert.Equal("STRING VIA JSON", result.SomeDTO.StringVal);
  408. Assert.Equal(2000, result.SomeDTO.IntVal);
  409. // Assert: Second result value marshalled by ref
  410. var resultDto2 = result.SomeDTORef.Value;
  411. Assert.Equal("MY STRING", resultDto2.StringVal);
  412. Assert.Equal(2468, resultDto2.IntVal);
  413. }
  414. [Fact]
  415. public async Task CanInvokeSyncThrowingMethod()
  416. {
  417. // Arrange
  418. var jsRuntime = new TestJSRuntime();
  419. // Act
  420. var callId = "123";
  421. var resultTask = jsRuntime.NextInvocationTask;
  422. DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, callId), default);
  423. await resultTask; // This won't throw, it sets properties on the jsRuntime.
  424. // Assert
  425. Assert.Equal(callId, jsRuntime.LastCompletionCallId);
  426. Assert.False(jsRuntime.LastCompletionResult.Success); // Fails
  427. // Make sure the method that threw the exception shows up in the call stack
  428. // https://github.com/dotnet/aspnetcore/issues/8612
  429. Assert.Contains(nameof(ThrowingClass.ThrowingMethod), jsRuntime.LastCompletionResult.Exception.ToString());
  430. }
  431. [Fact]
  432. public async Task CanInvokeAsyncThrowingMethod()
  433. {
  434. // Arrange
  435. var jsRuntime = new TestJSRuntime();
  436. // Act
  437. var callId = "123";
  438. var resultTask = jsRuntime.NextInvocationTask;
  439. DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, callId), default);
  440. await resultTask; // This won't throw, it sets properties on the jsRuntime.
  441. // Assert
  442. Assert.Equal(callId, jsRuntime.LastCompletionCallId);
  443. Assert.False(jsRuntime.LastCompletionResult.Success); // Fails
  444. // Make sure the method that threw the exception shows up in the call stack
  445. // https://github.com/dotnet/aspnetcore/issues/8612
  446. Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), jsRuntime.LastCompletionResult.Exception.ToString());
  447. }
  448. [Fact]
  449. public async Task BeginInvoke_ThrowsWithInvalidArgsJson_WithCallId()
  450. {
  451. // Arrange
  452. var jsRuntime = new TestJSRuntime();
  453. var callId = "123";
  454. var resultTask = jsRuntime.NextInvocationTask;
  455. DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, callId), "<xml>not json</xml>");
  456. await resultTask; // This won't throw, it sets properties on the jsRuntime.
  457. // Assert
  458. Assert.Equal(callId, jsRuntime.LastCompletionCallId);
  459. Assert.False(jsRuntime.LastCompletionResult.Success); // Fails
  460. var exception = jsRuntime.LastCompletionResult.Exception;
  461. Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", exception.ToString());
  462. }
  463. [Fact]
  464. public void BeginInvoke_ThrowsWithInvalid_DotNetObjectRef()
  465. {
  466. // Arrange
  467. var jsRuntime = new TestJSRuntime();
  468. var callId = "123";
  469. var resultTask = jsRuntime.NextInvocationTask;
  470. DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, callId), null);
  471. // Assert
  472. Assert.Equal(callId, jsRuntime.LastCompletionCallId);
  473. Assert.False(jsRuntime.LastCompletionResult.Success); // Fails
  474. var exception = jsRuntime.LastCompletionResult.Exception;
  475. Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectReference instance was already disposed.", exception.ToString());
  476. }
  477. [Theory]
  478. [InlineData("")]
  479. [InlineData("<xml>")]
  480. public void ParseArguments_ThrowsIfJsonIsInvalid(string arguments)
  481. {
  482. Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) }));
  483. }
  484. [Theory]
  485. [InlineData("{\"key\":\"value\"}")]
  486. [InlineData("\"Test\"")]
  487. public void ParseArguments_ThrowsIfTheArgsJsonIsNotArray(string arguments)
  488. {
  489. // Act & Assert
  490. Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) }));
  491. }
  492. [Theory]
  493. [InlineData("[\"hello\"")]
  494. [InlineData("[\"hello\",")]
  495. public void ParseArguments_ThrowsIfTheArgsJsonIsInvalidArray(string arguments)
  496. {
  497. // Act & Assert
  498. Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) }));
  499. }
  500. [Fact]
  501. public void ParseArguments_Works()
  502. {
  503. // Arrange
  504. var arguments = "[\"Hello\", 2]";
  505. // Act
  506. var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string), typeof(int), });
  507. // Assert
  508. Assert.Equal(new object[] { "Hello", 2 }, result);
  509. }
  510. [Fact]
  511. public void ParseArguments_SingleArgument()
  512. {
  513. // Arrange
  514. var arguments = "[{\"IntVal\": 7}]";
  515. // Act
  516. var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(TestDTO), });
  517. // Assert
  518. var value = Assert.IsType<TestDTO>(Assert.Single(result));
  519. Assert.Equal(7, value.IntVal);
  520. Assert.Null(value.StringVal);
  521. }
  522. [Fact]
  523. public void ParseArguments_NullArgument()
  524. {
  525. // Arrange
  526. var arguments = "[4, null]";
  527. // Act
  528. var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(int), typeof(TestDTO), });
  529. // Assert
  530. Assert.Collection(
  531. result,
  532. v => Assert.Equal(4, v),
  533. v => Assert.Null(v));
  534. }
  535. [Fact]
  536. public void ParseArguments_Throws_WithIncorrectDotNetObjectRefUsage()
  537. {
  538. // Arrange
  539. var method = "SomeMethod";
  540. var arguments = "[4, {\"__dotNetObject\": 7}]";
  541. // Act
  542. var ex = Assert.Throws<InvalidOperationException>(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), method, arguments, new[] { typeof(int), typeof(TestDTO), }));
  543. // Assert
  544. 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);
  545. }
  546. [Fact]
  547. public void EndInvokeJS_ThrowsIfJsonIsEmptyString()
  548. {
  549. Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), ""));
  550. }
  551. [Fact]
  552. public void EndInvokeJS_ThrowsIfJsonIsNotArray()
  553. {
  554. Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "{\"key\": \"value\"}"));
  555. }
  556. [Fact]
  557. public void EndInvokeJS_ThrowsIfJsonArrayIsInComplete()
  558. {
  559. Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false"));
  560. }
  561. [Fact]
  562. public void EndInvokeJS_ThrowsIfJsonArrayHasMoreThan3Arguments()
  563. {
  564. Assert.ThrowsAny<JsonException>(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false, \"Hello\", 5]"));
  565. }
  566. [Fact]
  567. public void EndInvokeJS_Works()
  568. {
  569. var jsRuntime = new TestJSRuntime();
  570. var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
  571. DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]");
  572. Assert.True(task.IsCompletedSuccessfully);
  573. Assert.Equal(7, task.Result.IntVal);
  574. }
  575. [Fact]
  576. public void EndInvokeJS_WithArrayValue()
  577. {
  578. var jsRuntime = new TestJSRuntime();
  579. var task = jsRuntime.InvokeAsync<int[]>("somemethod");
  580. DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]");
  581. Assert.True(task.IsCompletedSuccessfully);
  582. Assert.Equal(new[] { 1, 2, 3 }, task.Result);
  583. }
  584. [Fact]
  585. public void EndInvokeJS_WithNullValue()
  586. {
  587. var jsRuntime = new TestJSRuntime();
  588. var task = jsRuntime.InvokeAsync<TestDTO>("somemethod");
  589. DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]");
  590. Assert.True(task.IsCompletedSuccessfully);
  591. Assert.Null(task.Result);
  592. }
  593. [Fact]
  594. public void ReceiveByteArray_Works()
  595. {
  596. // Arrange
  597. var jsRuntime = new TestJSRuntime();
  598. var byteArray = new byte[] { 1, 5, 7 };
  599. // Act
  600. DotNetDispatcher.ReceiveByteArray(jsRuntime, 0, byteArray);
  601. // Assert
  602. Assert.Equal(1, jsRuntime.ByteArraysToBeRevived.Count);
  603. Assert.Equal(byteArray, jsRuntime.ByteArraysToBeRevived.Buffer[0]);
  604. }
  605. internal class SomeInteralType
  606. {
  607. [JSInvokable("MethodOnInternalType")] public void MyMethod() { }
  608. }
  609. public class SomePublicType
  610. {
  611. public static bool DidInvokeMyInvocableStaticVoid;
  612. public bool DidInvokeMyInvocableInstanceVoid;
  613. [JSInvokable("PrivateMethod")] private static void MyPrivateMethod() { }
  614. [JSInvokable("ProtectedMethod")] protected static void MyProtectedMethod() { }
  615. protected static void StaticMethodWithoutAttribute() { }
  616. protected static void InstanceMethodWithoutAttribute() { }
  617. [JSInvokable("InvocableStaticVoid")]
  618. public static void MyInvocableVoid()
  619. {
  620. DidInvokeMyInvocableStaticVoid = true;
  621. }
  622. [JSInvokable("InvocableStaticNonVoid")]
  623. public static object MyInvocableNonVoid()
  624. => new TestDTO { StringVal = "Test", IntVal = 123 };
  625. [JSInvokable("InvocableStaticWithParams")]
  626. public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, DotNetObjectReference<TestDTO> dtoByRef)
  627. => new object[]
  628. {
  629. new TestDTO // Return via JSON marshalling
  630. {
  631. StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
  632. IntVal = dtoViaJson.IntVal + incrementAmounts.Sum()
  633. },
  634. DotNetObjectReference.Create(new TestDTO // Return by ref
  635. {
  636. StringVal = dtoByRef.Value.StringVal.ToUpperInvariant(),
  637. IntVal = dtoByRef.Value.IntVal + incrementAmounts.Sum()
  638. })
  639. };
  640. [JSInvokable(nameof(IncorrectDotNetObjectRefUsage))]
  641. public static object[] IncorrectDotNetObjectRefUsage(TestDTO dtoViaJson, int[] incrementAmounts, TestDTO dtoByRef)
  642. => throw new InvalidOperationException("Shouldn't be called");
  643. [JSInvokable]
  644. public static TestDTO InvokableMethodWithoutCustomIdentifier()
  645. => new TestDTO { StringVal = "InvokableMethodWithoutCustomIdentifier", IntVal = 456 };
  646. [JSInvokable]
  647. public void InvokableInstanceVoid()
  648. {
  649. DidInvokeMyInvocableInstanceVoid = true;
  650. }
  651. [JSInvokable]
  652. public object[] InvokableInstanceMethod(string someString, DotNetObjectReference<TestDTO> someDTORef)
  653. {
  654. var someDTO = someDTORef.Value;
  655. // Returning an array to make the point that object references
  656. // can be embedded anywhere in the result
  657. return new object[]
  658. {
  659. $"You passed {someString}",
  660. DotNetObjectReference.Create(new TestDTO
  661. {
  662. IntVal = someDTO.IntVal + 1,
  663. StringVal = someDTO.StringVal.ToUpperInvariant()
  664. })
  665. };
  666. }
  667. [JSInvokable]
  668. public async Task<InvokableAsyncMethodResult> InvokableAsyncMethod(TestDTO dtoViaJson, DotNetObjectReference<TestDTO> dtoByRefWrapper)
  669. {
  670. await Task.Delay(50);
  671. var dtoByRef = dtoByRefWrapper.Value;
  672. return new InvokableAsyncMethodResult
  673. {
  674. SomeDTO = new TestDTO // Return via JSON
  675. {
  676. StringVal = dtoViaJson.StringVal.ToUpperInvariant(),
  677. IntVal = dtoViaJson.IntVal * 2,
  678. },
  679. SomeDTORef = DotNetObjectReference.Create(new TestDTO // Return by ref
  680. {
  681. StringVal = dtoByRef.StringVal.ToUpperInvariant(),
  682. IntVal = dtoByRef.IntVal * 2,
  683. })
  684. };
  685. }
  686. public class InvokableAsyncMethodResult
  687. {
  688. public TestDTO SomeDTO { get; set; }
  689. public DotNetObjectReference<TestDTO> SomeDTORef { get; set; }
  690. }
  691. }
  692. public class BaseClass
  693. {
  694. public bool DidInvokeMyBaseClassInvocableInstanceVoid;
  695. [JSInvokable]
  696. public void BaseClassInvokableInstanceVoid()
  697. {
  698. DidInvokeMyBaseClassInvocableInstanceVoid = true;
  699. }
  700. }
  701. public class DerivedClass : BaseClass
  702. {
  703. }
  704. public class TestDTO
  705. {
  706. public string StringVal { get; set; }
  707. public int IntVal { get; set; }
  708. }
  709. public class ThrowingClass
  710. {
  711. [JSInvokable]
  712. public static string ThrowingMethod()
  713. {
  714. throw new InvalidTimeZoneException();
  715. }
  716. [JSInvokable]
  717. public static async Task<string> AsyncThrowingMethod()
  718. {
  719. await Task.Yield();
  720. throw new InvalidTimeZoneException();
  721. }
  722. }
  723. public class GenericType<TValue>
  724. {
  725. [JSInvokable] public string EchoStringParameter(string input) => input;
  726. [JSInvokable] public TValue EchoParameter(TValue input) => input;
  727. }
  728. public class GenericMethodClass
  729. {
  730. [JSInvokable("StaticGenericMethod")] public static string StaticGenericMethod<TValue>(TValue input) => input.ToString();
  731. [JSInvokable("InstanceGenericMethod")] public string GenericMethod<TValue>(TValue input) => input.ToString();
  732. }
  733. public class TestJSRuntime : JSInProcessRuntime
  734. {
  735. private TaskCompletionSource<object> _nextInvocationTcs = new TaskCompletionSource<object>();
  736. public Task NextInvocationTask => _nextInvocationTcs.Task;
  737. public long LastInvocationAsyncHandle { get; private set; }
  738. public string LastInvocationIdentifier { get; private set; }
  739. public string LastInvocationArgsJson { get; private set; }
  740. public string LastCompletionCallId { get; private set; }
  741. public DotNetInvocationResult LastCompletionResult { get; private set; }
  742. protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
  743. {
  744. LastInvocationAsyncHandle = asyncHandle;
  745. LastInvocationIdentifier = identifier;
  746. LastInvocationArgsJson = argsJson;
  747. _nextInvocationTcs.SetResult(null);
  748. _nextInvocationTcs = new TaskCompletionSource<object>();
  749. }
  750. protected override string InvokeJS(string identifier, string argsJson, JSCallResultType resultType, long targetInstanceId)
  751. {
  752. LastInvocationAsyncHandle = default;
  753. LastInvocationIdentifier = identifier;
  754. LastInvocationArgsJson = argsJson;
  755. _nextInvocationTcs.SetResult(null);
  756. _nextInvocationTcs = new TaskCompletionSource<object>();
  757. return null;
  758. }
  759. protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
  760. {
  761. LastCompletionCallId = invocationInfo.CallId;
  762. LastCompletionResult = invocationResult;
  763. _nextInvocationTcs.SetResult(null);
  764. _nextInvocationTcs = new TaskCompletionSource<object>();
  765. }
  766. }
  767. }
  768. }