Skip to content

Commit 04f62b6

Browse files
committed
Add unit tests, improve error handling
1 parent 1066bc4 commit 04f62b6

File tree

4 files changed

+224
-30
lines changed

4 files changed

+224
-30
lines changed

src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs

+30-28
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,12 @@ namespace Microsoft.JSInterop.Infrastructure;
1313
/// </summary>
1414
internal readonly struct JSFunctionReference
1515
{
16-
private static readonly ConcurrentDictionary<Type, MethodInfo> _methodInfoCache = new();
17-
18-
private readonly IJSObjectReference _jsObjectReference;
19-
2016
/// <summary>
2117
/// Caches previously constructed MethodInfo instances for various delegate types.
2218
/// </summary>
23-
public static ConcurrentDictionary<Type, MethodInfo> MethodInfoCache => _methodInfoCache;
19+
private static readonly ConcurrentDictionary<Type, MethodInfo> _methodInfoCache = new();
20+
21+
private readonly IJSObjectReference _jsObjectReference;
2422

2523
public JSFunctionReference(IJSObjectReference jsObjectReference)
2624
{
@@ -34,84 +32,88 @@ public static T CreateInvocationDelegate<T>(IJSObjectReference jsObjectReference
3432
{
3533
Type delegateType = typeof(T);
3634

37-
if (MethodInfoCache.TryGetValue(delegateType, out var wrapperMethod))
35+
if (_methodInfoCache.TryGetValue(delegateType, out var wrapperMethod))
3836
{
3937
var wrapper = new JSFunctionReference(jsObjectReference);
4038
return (T)Delegate.CreateDelegate(delegateType, wrapper, wrapperMethod);
4139
}
4240

4341
if (!delegateType.IsGenericType)
4442
{
45-
throw new ArgumentException("The delegate type must be a Func.");
43+
throw CreateInvalidTypeParameterException(delegateType);
4644
}
4745

4846
var returnTypeCandidate = delegateType.GenericTypeArguments[^1];
4947

5048
if (returnTypeCandidate == typeof(ValueTask))
5149
{
52-
var methodName = GetVoidMethodName(delegateType.GetGenericTypeDefinition());
50+
var methodName = GetVoidMethodName(delegateType);
5351
return CreateVoidDelegate<T>(delegateType, jsObjectReference, methodName);
5452
}
5553
else if (returnTypeCandidate == typeof(Task))
5654
{
57-
var methodName = GetVoidTaskMethodName(delegateType.GetGenericTypeDefinition());
55+
var methodName = GetVoidTaskMethodName(delegateType);
5856
return CreateVoidDelegate<T>(delegateType, jsObjectReference, methodName);
5957
}
60-
else
58+
else if (returnTypeCandidate.IsGenericType)
6159
{
6260
var returnTypeGenericTypeDefinition = returnTypeCandidate.GetGenericTypeDefinition();
6361

6462
if (returnTypeGenericTypeDefinition == typeof(ValueTask<>))
6563
{
66-
var methodName = GetMethodName(delegateType.GetGenericTypeDefinition());
64+
var methodName = GetMethodName(delegateType);
6765
var innerReturnType = returnTypeCandidate.GenericTypeArguments[0];
6866
return CreateDelegate<T>(delegateType, innerReturnType, jsObjectReference, methodName);
6967
}
7068

7169
else if (returnTypeGenericTypeDefinition == typeof(Task<>))
7270
{
73-
var methodName = GetTaskMethodName(delegateType.GetGenericTypeDefinition());
71+
var methodName = GetTaskMethodName(delegateType);
7472
var innerReturnType = returnTypeCandidate.GenericTypeArguments[0];
7573
return CreateDelegate<T>(delegateType, innerReturnType, jsObjectReference, methodName);
7674
}
77-
else
78-
{
79-
throw new ArgumentException("The delegate return type must be Task<TResult> or ValueTask<TResult>.");
80-
}
8175
}
76+
77+
throw CreateInvalidTypeParameterException(delegateType);
8278
}
8379

8480
private static T CreateDelegate<T>(Type delegateType, Type returnType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate
8581
{
86-
var wrapper = new JSFunctionReference(jsObjectReference);
8782
var wrapperMethod = typeof(JSFunctionReference).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!;
8883
Type[] genericArguments = [.. delegateType.GenericTypeArguments[..^1], returnType];
8984

9085
#pragma warning disable IL2060 // Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method.
9186
var concreteWrapperMethod = wrapperMethod.MakeGenericMethod(genericArguments);
9287
#pragma warning restore IL2060 // Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method.
9388

94-
MethodInfoCache.TryAdd(delegateType, concreteWrapperMethod);
89+
_methodInfoCache.TryAdd(delegateType, concreteWrapperMethod);
9590

91+
var wrapper = new JSFunctionReference(jsObjectReference);
9692
return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod);
9793
}
9894

9995
private static T CreateVoidDelegate<T>(Type delegateType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate
10096
{
101-
var wrapper = new JSFunctionReference(jsObjectReference);
10297
var wrapperMethod = typeof(JSFunctionReference).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!;
10398
Type[] genericArguments = delegateType.GenericTypeArguments[..^1];
10499

105100
#pragma warning disable IL2060 // Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method.
106101
var concreteWrapperMethod = wrapperMethod.MakeGenericMethod(genericArguments);
107102
#pragma warning restore IL2060 // Call to 'System.Reflection.MethodInfo.MakeGenericMethod' can not be statically analyzed. It's not possible to guarantee the availability of requirements of the generic method.
108103

109-
MethodInfoCache.TryAdd(delegateType, concreteWrapperMethod);
104+
_methodInfoCache.TryAdd(delegateType, concreteWrapperMethod);
110105

106+
var wrapper = new JSFunctionReference(jsObjectReference);
111107
return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod);
112108
}
113109

114-
private static string GetMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch
110+
private static InvalidOperationException CreateInvalidTypeParameterException(Type delegateType)
111+
{
112+
return new InvalidOperationException(
113+
$"The type {delegateType} is not supported as the type parameter of '{nameof(JSObjectReferenceExtensions.AsAsyncFunction)}'. 'T' must be Func with the return type Task<TResult> or ValueTask<TResult>.");
114+
}
115+
116+
private static string GetMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch
115117
{
116118
var gd when gd == typeof(Func<>) => nameof(Invoke0),
117119
var gd when gd == typeof(Func<,>) => nameof(Invoke1),
@@ -120,10 +122,10 @@ private static T CreateVoidDelegate<T>(Type delegateType, IJSObjectReference jsO
120122
var gd when gd == typeof(Func<,,,,>) => nameof(Invoke4),
121123
var gd when gd == typeof(Func<,,,,,>) => nameof(Invoke5),
122124
var gd when gd == typeof(Func<,,,,,,>) => nameof(Invoke6),
123-
_ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.")
125+
_ => throw CreateInvalidTypeParameterException(delegateType)
124126
};
125127

126-
private static string GetTaskMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch
128+
private static string GetTaskMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch
127129
{
128130
var gd when gd == typeof(Func<>) => nameof(InvokeTask0),
129131
var gd when gd == typeof(Func<,>) => nameof(InvokeTask1),
@@ -132,10 +134,10 @@ private static T CreateVoidDelegate<T>(Type delegateType, IJSObjectReference jsO
132134
var gd when gd == typeof(Func<,,,,>) => nameof(InvokeTask4),
133135
var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeTask5),
134136
var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeTask6),
135-
_ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.")
137+
_ => throw CreateInvalidTypeParameterException(delegateType)
136138
};
137139

138-
private static string GetVoidMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch
140+
private static string GetVoidMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch
139141
{
140142
var gd when gd == typeof(Func<>) => nameof(InvokeVoid0),
141143
var gd when gd == typeof(Func<,>) => nameof(InvokeVoid1),
@@ -144,10 +146,10 @@ private static T CreateVoidDelegate<T>(Type delegateType, IJSObjectReference jsO
144146
var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoid4),
145147
var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoid5),
146148
var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoid6),
147-
_ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.")
149+
_ => throw CreateInvalidTypeParameterException(delegateType)
148150
};
149151

150-
private static string GetVoidTaskMethodName(Type genericDelegateTypeDefiniton) => genericDelegateTypeDefiniton switch
152+
private static string GetVoidTaskMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch
151153
{
152154
var gd when gd == typeof(Func<>) => nameof(InvokeVoidTask0),
153155
var gd when gd == typeof(Func<,>) => nameof(InvokeVoidTask1),
@@ -156,7 +158,7 @@ private static T CreateVoidDelegate<T>(Type delegateType, IJSObjectReference jsO
156158
var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoidTask4),
157159
var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoidTask5),
158160
var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoidTask6),
159-
_ => throw new NotSupportedException($"The type {genericDelegateTypeDefiniton} is not supported.")
161+
_ => throw CreateInvalidTypeParameterException(delegateType)
160162
};
161163

162164
// Variants returning ValueTask<T> using InvokeAsync

src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public static ValueTask<IJSObjectReference> InvokeNewAsync(this IJSObjectReferen
177177
/// <param name="jsObjectReference"></param>
178178
/// <returns></returns>
179179
/// <exception cref="ArgumentException"></exception>
180-
public static T AsFunction<T>(this IJSObjectReference jsObjectReference) where T : Delegate
180+
public static T AsAsyncFunction<T>(this IJSObjectReference jsObjectReference) where T : Delegate
181181
{
182182
ArgumentNullException.ThrowIfNull(jsObjectReference);
183183

src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, object?[]? args
5656
Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
5757
Microsoft.JSInterop.JSRuntime.SetValueAsync<TValue>(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask
5858
Microsoft.JSInterop.JSRuntime.SetValueAsync<TValue>(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask
59-
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsFunction<T>(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> T!
59+
static Microsoft.JSInterop.JSObjectReferenceExtensions.AsAsyncFunction<T>(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> T!
6060
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
6161
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
6262
static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask<Microsoft.JSInterop.IJSObjectReference!>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Reflection.PortableExecutable;
6+
using System.Text;
7+
using System.Text.Json;
8+
using System.Threading.Tasks;
9+
using Microsoft.JSInterop.Implementation;
10+
using Microsoft.JSInterop.Infrastructure;
11+
12+
namespace Microsoft.JSInterop.Tests;
13+
14+
public class JSObjectReferenceExtensionsTest
15+
{
16+
[Fact]
17+
public void AsAsyncFunction_WithVoidValueTaskFunc_ReturnsFunc()
18+
{
19+
var jsRuntime = new TestJSRuntime();
20+
var jsObjectReference = new JSObjectReference(jsRuntime, 1);
21+
22+
// Act
23+
var func = jsObjectReference.AsAsyncFunction<Func<int, ValueTask>>();
24+
25+
// Assert
26+
Assert.NotNull(func);
27+
Assert.IsType<Func<int, ValueTask>>(func);
28+
}
29+
30+
[Fact]
31+
public void AsAsyncFunction_WithVoidTaskFunc_ReturnsFunc()
32+
{
33+
var jsRuntime = new TestJSRuntime();
34+
var jsObjectReference = new JSObjectReference(jsRuntime, 1);
35+
36+
// Act
37+
var func = jsObjectReference.AsAsyncFunction<Func<int, Task>>();
38+
39+
// Assert
40+
Assert.NotNull(func);
41+
Assert.IsType<Func<int, Task>>(func);
42+
}
43+
44+
[Fact]
45+
public void AsAsyncFunction_WithValueTaskFunc_ReturnsFunc()
46+
{
47+
var jsRuntime = new TestJSRuntime();
48+
var jsObjectReference = new JSObjectReference(jsRuntime, 1);
49+
50+
// Act
51+
var func = jsObjectReference.AsAsyncFunction<Func<int, ValueTask<int>>>();
52+
53+
// Assert
54+
Assert.NotNull(func);
55+
Assert.IsType<Func<int, ValueTask<int>>>(func);
56+
}
57+
58+
[Fact]
59+
public void AsAsyncFunction_WithTaskFunc_ReturnsFunc()
60+
{
61+
var jsRuntime = new TestJSRuntime();
62+
var jsObjectReference = new JSObjectReference(jsRuntime, 1);
63+
64+
// Act
65+
var func = jsObjectReference.AsAsyncFunction<Func<int, Task<int>>>();
66+
67+
// Assert
68+
Assert.NotNull(func);
69+
Assert.IsType<Func<int, Task<int>>>(func);
70+
}
71+
72+
[Fact]
73+
public void AsAsyncFunction_WithValueTaskFunc_ReturnsFunc_ThatInvokesInterop()
74+
{
75+
// Arrange
76+
var jsRuntime = new TestJSRuntime();
77+
var jsObjectReference = new JSObjectReference(jsRuntime, 1);
78+
79+
var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(42));
80+
var reader = new Utf8JsonReader(bytes);
81+
82+
// Act
83+
var func = jsObjectReference.AsAsyncFunction<Func<int, ValueTask<int>>>();
84+
ValueTask<int> task = func(1);
85+
86+
jsRuntime.EndInvokeJS(
87+
jsRuntime.InvokeCalls[0].AsyncHandle,
88+
/* succeeded: */ true,
89+
ref reader);
90+
91+
// Assert
92+
Assert.True(task.IsCompleted);
93+
#pragma warning disable xUnit1031 // Do not use blocking task operations in test method
94+
Assert.Equal(42, task.Result);
95+
#pragma warning restore xUnit1031 // Do not use blocking task operations in test method
96+
}
97+
98+
[Fact]
99+
public void AsAsyncFunction_WithTaskFunc_ReturnsFunc_ThatInvokesInterop()
100+
{
101+
// Arrange
102+
var jsRuntime = new TestJSRuntime();
103+
var jsObjectReference = new JSObjectReference(jsRuntime, 1);
104+
105+
var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(42));
106+
var reader = new Utf8JsonReader(bytes);
107+
108+
// Act
109+
var func = jsObjectReference.AsAsyncFunction<Func<int, Task<int>>>();
110+
Task<int> task = func(1);
111+
112+
jsRuntime.EndInvokeJS(
113+
jsRuntime.InvokeCalls[0].AsyncHandle,
114+
/* succeeded: */ true,
115+
ref reader);
116+
117+
// Assert
118+
Assert.True(task.IsCompleted);
119+
#pragma warning disable xUnit1031 // Do not use blocking task operations in test method
120+
Assert.Equal(42, task.Result);
121+
#pragma warning restore xUnit1031 // Do not use blocking task operations in test method
122+
}
123+
124+
[Fact]
125+
public void AsAsyncFunction_WithEventHandlerDelegate_Throws()
126+
{
127+
var jsRuntime = new TestJSRuntime();
128+
var jsObjectReference = new JSObjectReference(jsRuntime, 1);
129+
130+
// Act/Assert
131+
Assert.Throws<InvalidOperationException>(jsObjectReference.AsAsyncFunction<EventHandler>);
132+
}
133+
134+
[Fact]
135+
public void AsAsyncFunction_WithActionDelegate_Throws()
136+
{
137+
var jsRuntime = new TestJSRuntime();
138+
var jsObjectReference = new JSObjectReference(jsRuntime, 1);
139+
140+
// Act/Assert
141+
Assert.Throws<InvalidOperationException>(jsObjectReference.AsAsyncFunction<Action<int>>);
142+
}
143+
144+
[Fact]
145+
public void AsAsyncFunction_WithFuncWithInvalidReturnType_Throws()
146+
{
147+
var jsRuntime = new TestJSRuntime();
148+
var jsObjectReference = new JSObjectReference(jsRuntime, 1);
149+
150+
// Act/Assert
151+
Assert.Throws<InvalidOperationException>(jsObjectReference.AsAsyncFunction<Func<int>>);
152+
}
153+
154+
[Fact]
155+
public void AsAsyncFunction_WithFuncWithTooManyParams_Throws()
156+
{
157+
var jsRuntime = new TestJSRuntime();
158+
var jsObjectReference = new JSObjectReference(jsRuntime, 1);
159+
160+
// Act/Assert
161+
Assert.Throws<InvalidOperationException>(jsObjectReference.AsAsyncFunction<Func<int, int, int, int, int, int, int, int, int>>);
162+
}
163+
164+
class TestJSRuntime : JSInProcessRuntime
165+
{
166+
public List<JSInvocationInfo> InvokeCalls { get; set; } = [];
167+
168+
public string? NextResultJson { get; set; }
169+
170+
protected override string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId)
171+
{
172+
throw new NotImplementedException();
173+
}
174+
175+
protected override string? InvokeJS(in JSInvocationInfo invocationInfo)
176+
{
177+
InvokeCalls.Add(invocationInfo);
178+
return NextResultJson;
179+
}
180+
181+
protected override void BeginInvokeJS(long taskId, string identifier, [StringSyntax("Json")] string? argsJson, JSCallResultType resultType, long targetInstanceId)
182+
=> throw new NotImplementedException("This test only covers sync calls");
183+
184+
protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo)
185+
{
186+
InvokeCalls.Add(invocationInfo);
187+
}
188+
189+
protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult)
190+
=> throw new NotImplementedException("This test only covers sync calls");
191+
}
192+
}

0 commit comments

Comments
 (0)