diff --git a/src/Components/test/E2ETest/Tests/InteropTest.cs b/src/Components/test/E2ETest/Tests/InteropTest.cs index b98fb69d72f1..17cefe6d94e3 100644 --- a/src/Components/test/E2ETest/Tests/InteropTest.cs +++ b/src/Components/test/E2ETest/Tests/InteropTest.cs @@ -108,7 +108,8 @@ public void CanInvokeInteropMethods() ["invokeNewWithClassConstructorAsync.function"] = "6", ["invokeNewWithNonConstructorAsync"] = "Success", // Function reference tests - ["changeFunctionViaObjectReferenceAsync"] = "42" + ["changeFunctionViaObjectReferenceAsync"] = "42", + ["invokeDelegateFromAsAsyncFunction"] = "42" }; var expectedSyncValues = new Dictionary diff --git a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor index 92d845d42571..781dd84d1286 100644 --- a/src/Components/test/testassets/BasicTestApp/InteropComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/InteropComponent.razor @@ -614,6 +614,9 @@ var testClassRef = await JSRuntime.InvokeNewAsync("jsInteropTests.TestClass", "abraka"); await testClassRef.SetValueAsync("getTextLength", funcRef); ReturnValues["changeFunctionViaObjectReferenceAsync"] = (await testClassRef.InvokeAsync("getTextLength")).ToString(); + + var funcDelegate = funcRef.AsAsyncFunction>>(); + ReturnValues["invokeDelegateFromAsAsyncFunction"] = (await funcDelegate()).ToString(); } private void FunctionReferenceTests() diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index fdd7a4ed65ec..cc26b2dcd0a2 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -562,7 +562,17 @@ export module DotNet { const targetInstance = cachedJSObjectsById[targetInstanceId]; if (targetInstance) { - return targetInstance.resolveInvocationHandler(identifier, callType ?? JSCallType.FunctionCall); + if (identifier) { + return targetInstance.resolveInvocationHandler(identifier, callType ?? JSCallType.FunctionCall); + } else { + const wrappedObject = targetInstance.getWrappedObject(); + + if (wrappedObject instanceof Function) { + return wrappedObject; + } else { + throw new Error(`JS object instance with ID ${targetInstanceId} is not a function.`); + } + } } throw new Error(`JS object instance with ID ${targetInstanceId} does not exist (has it been disposed?).`); @@ -626,7 +636,10 @@ export module DotNet { if (!isReadableProperty(parent, memberName)) { throw new Error(`The property '${identifier}' is not defined or is not readable.`); } - return () => parent[memberName]; + + return parent[memberName] instanceof Function + ? () => parent[memberName].bind(parent) + : () => parent[memberName]; case JSCallType.SetValue: if (!isWritableProperty(parent, memberName)) { throw new Error(`The property '${identifier}' is not writable.`); diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs new file mode 100644 index 000000000000..d8d2b7b52a65 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSFunctionReference.cs @@ -0,0 +1,212 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using static Microsoft.AspNetCore.Internal.LinkerFlags; + +namespace Microsoft.JSInterop.Infrastructure; + +/// +/// Helper for constructing a Func delegate that wraps interop call to a JavaScript function referenced via . +/// +internal readonly struct JSFunctionReference +{ + /// + /// Caches previously constructed MethodInfo instances for various delegate types. + /// + private static readonly ConcurrentDictionary _methodInfoCache = new(); + + private readonly IJSObjectReference _jsObjectReference; + + public JSFunctionReference(IJSObjectReference jsObjectReference) + { + _jsObjectReference = jsObjectReference; + } + + public static T CreateInvocationDelegate(IJSObjectReference jsObjectReference) where T : Delegate + { + Type delegateType = typeof(T); + + if (_methodInfoCache.TryGetValue(delegateType, out var wrapperMethod)) + { + var wrapper = new JSFunctionReference(jsObjectReference); + return (T)Delegate.CreateDelegate(delegateType, wrapper, wrapperMethod); + } + + if (!delegateType.IsGenericType) + { + throw CreateInvalidTypeParameterException(delegateType); + } + + var returnTypeCandidate = delegateType.GenericTypeArguments[^1]; + + if (returnTypeCandidate == typeof(ValueTask)) + { + var methodName = GetVoidMethodName(delegateType); + return CreateVoidDelegate(delegateType, jsObjectReference, methodName); + } + else if (returnTypeCandidate == typeof(Task)) + { + var methodName = GetVoidTaskMethodName(delegateType); + return CreateVoidDelegate(delegateType, jsObjectReference, methodName); + } + else if (returnTypeCandidate.IsGenericType) + { + var returnTypeGenericTypeDefinition = returnTypeCandidate.GetGenericTypeDefinition(); + + if (returnTypeGenericTypeDefinition == typeof(ValueTask<>)) + { + var methodName = GetMethodName(delegateType); + var innerReturnType = returnTypeCandidate.GenericTypeArguments[0]; + return CreateDelegate(delegateType, innerReturnType, jsObjectReference, methodName); + } + + else if (returnTypeGenericTypeDefinition == typeof(Task<>)) + { + var methodName = GetTaskMethodName(delegateType); + var innerReturnType = returnTypeCandidate.GenericTypeArguments[0]; + return CreateDelegate(delegateType, innerReturnType, jsObjectReference, methodName); + } + } + + throw CreateInvalidTypeParameterException(delegateType); + } + + private static T CreateDelegate(Type delegateType, Type returnType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate + { + var wrapperMethod = typeof(JSFunctionReference).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!; + Type[] genericArguments = [.. delegateType.GenericTypeArguments[..^1], returnType]; + +#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. + var concreteWrapperMethod = wrapperMethod.MakeGenericMethod(genericArguments); +#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. + + _methodInfoCache.TryAdd(delegateType, concreteWrapperMethod); + + var wrapper = new JSFunctionReference(jsObjectReference); + return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod); + } + + private static T CreateVoidDelegate(Type delegateType, IJSObjectReference jsObjectReference, string methodName) where T : Delegate + { + var wrapperMethod = typeof(JSFunctionReference).GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!; + Type[] genericArguments = delegateType.GenericTypeArguments[..^1]; + +#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. + var concreteWrapperMethod = wrapperMethod.MakeGenericMethod(genericArguments); +#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. + + _methodInfoCache.TryAdd(delegateType, concreteWrapperMethod); + + var wrapper = new JSFunctionReference(jsObjectReference); + return (T)Delegate.CreateDelegate(delegateType, wrapper, concreteWrapperMethod); + } + + private static InvalidOperationException CreateInvalidTypeParameterException(Type delegateType) + { + return new InvalidOperationException( + $"The type {delegateType} is not supported as the type parameter of '{nameof(JSObjectReferenceExtensions.AsAsyncFunction)}'. 'T' must be Func with the return type Task or ValueTask."); + } + + private static string GetMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch + { + var gd when gd == typeof(Func<>) => nameof(Invoke0), + var gd when gd == typeof(Func<,>) => nameof(Invoke1), + var gd when gd == typeof(Func<,,>) => nameof(Invoke2), + var gd when gd == typeof(Func<,,,>) => nameof(Invoke3), + var gd when gd == typeof(Func<,,,,>) => nameof(Invoke4), + var gd when gd == typeof(Func<,,,,,>) => nameof(Invoke5), + var gd when gd == typeof(Func<,,,,,,>) => nameof(Invoke6), + var gd when gd == typeof(Func<,,,,,,,>) => nameof(Invoke7), + var gd when gd == typeof(Func<,,,,,,,,>) => nameof(Invoke8), + _ => throw CreateInvalidTypeParameterException(delegateType) + }; + + private static string GetTaskMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch + { + var gd when gd == typeof(Func<>) => nameof(InvokeTask0), + var gd when gd == typeof(Func<,>) => nameof(InvokeTask1), + var gd when gd == typeof(Func<,,>) => nameof(InvokeTask2), + var gd when gd == typeof(Func<,,,>) => nameof(InvokeTask3), + var gd when gd == typeof(Func<,,,,>) => nameof(InvokeTask4), + var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeTask5), + var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeTask6), + var gd when gd == typeof(Func<,,,,,,,>) => nameof(InvokeTask7), + var gd when gd == typeof(Func<,,,,,,,,>) => nameof(InvokeTask8), + _ => throw CreateInvalidTypeParameterException(delegateType) + }; + + private static string GetVoidMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch + { + var gd when gd == typeof(Func<>) => nameof(InvokeVoid0), + var gd when gd == typeof(Func<,>) => nameof(InvokeVoid1), + var gd when gd == typeof(Func<,,>) => nameof(InvokeVoid2), + var gd when gd == typeof(Func<,,,>) => nameof(InvokeVoid3), + var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoid4), + var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoid5), + var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoid6), + var gd when gd == typeof(Func<,,,,,,,>) => nameof(InvokeVoid7), + var gd when gd == typeof(Func<,,,,,,,,>) => nameof(InvokeVoid8), + _ => throw CreateInvalidTypeParameterException(delegateType) + }; + + private static string GetVoidTaskMethodName(Type delegateType) => delegateType.GetGenericTypeDefinition() switch + { + var gd when gd == typeof(Func<>) => nameof(InvokeVoidTask0), + var gd when gd == typeof(Func<,>) => nameof(InvokeVoidTask1), + var gd when gd == typeof(Func<,,>) => nameof(InvokeVoidTask2), + var gd when gd == typeof(Func<,,,>) => nameof(InvokeVoidTask3), + var gd when gd == typeof(Func<,,,,>) => nameof(InvokeVoidTask4), + var gd when gd == typeof(Func<,,,,,>) => nameof(InvokeVoidTask5), + var gd when gd == typeof(Func<,,,,,,>) => nameof(InvokeVoidTask6), + var gd when gd == typeof(Func<,,,,,,,>) => nameof(InvokeVoidTask7), + var gd when gd == typeof(Func<,,,,,,,,>) => nameof(InvokeVoidTask8), + _ => throw CreateInvalidTypeParameterException(delegateType) + }; + + // Variants returning ValueTask using InvokeAsync + public ValueTask Invoke0<[DynamicallyAccessedMembers(JsonSerialized)] TResult>() => _jsObjectReference.InvokeAsync(string.Empty, []); + public ValueTask Invoke1(T1 arg1) => _jsObjectReference.InvokeAsync(string.Empty, [arg1]); + public ValueTask Invoke2(T1 arg1, T2 arg2) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2]); + public ValueTask Invoke3(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3]); + public ValueTask Invoke4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4]); + public ValueTask Invoke5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]); + public ValueTask Invoke6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]); + public ValueTask Invoke7(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7]); + public ValueTask Invoke8(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]); + + // Variants returning ValueTask using InvokeVoidAsync + public ValueTask InvokeVoid0() => _jsObjectReference.InvokeVoidAsync(string.Empty); + public ValueTask InvokeVoid1(T1 arg1) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]); + public ValueTask InvokeVoid2(T1 arg1, T2 arg2) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]); + public ValueTask InvokeVoid3(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]); + public ValueTask InvokeVoid4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4]); + public ValueTask InvokeVoid5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]); + public ValueTask InvokeVoid6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]); + public ValueTask InvokeVoid7(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7]); + public ValueTask InvokeVoid8(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]); + + // Variants returning Task using InvokeAsync + public Task InvokeTask0<[DynamicallyAccessedMembers(JsonSerialized)] TResult>() => _jsObjectReference.InvokeAsync(string.Empty, []).AsTask(); + public Task InvokeTask1(T1 arg1) => _jsObjectReference.InvokeAsync(string.Empty, [arg1]).AsTask(); + public Task InvokeTask2(T1 arg1, T2 arg2) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2]).AsTask(); + public Task InvokeTask3(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3]).AsTask(); + public Task InvokeTask4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4]).AsTask(); + public Task InvokeTask5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]).AsTask(); + public Task InvokeTask6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]).AsTask(); + public Task InvokeTask7(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7]).AsTask(); + public Task InvokeTask8(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => _jsObjectReference.InvokeAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]).AsTask(); + + // Variants returning Task using InvokeVoidAsync + public Task InvokeVoidTask0() => _jsObjectReference.InvokeVoidAsync(string.Empty).AsTask(); + public Task InvokeVoidTask1(T1 arg1) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1]).AsTask(); + public Task InvokeVoidTask2(T1 arg1, T2 arg2) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2]).AsTask(); + public Task InvokeVoidTask3(T1 arg1, T2 arg2, T3 arg3) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3]).AsTask(); + public Task InvokeVoidTask4(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4]).AsTask(); + public Task InvokeVoidTask5(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5]).AsTask(); + public Task InvokeVoidTask6(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6]).AsTask(); + public Task InvokeVoidTask7(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7]).AsTask(); + public Task InvokeVoidTask8(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => _jsObjectReference.InvokeVoidAsync(string.Empty, [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8]).AsTask(); +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs index 4ddcc358ef2f..823b0e2f2743 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSObjectReferenceExtensions.cs @@ -167,4 +167,19 @@ public static ValueTask InvokeNewAsync(this IJSObjectReferen return jsObjectReference.InvokeNewAsync(identifier, cancellationToken, args); } + + /// + /// Converts a JavaScript function reference into a .NET delegate of the specified type. + /// + /// The type of the delegate to create. Must be a Func with the result type , , , or . + /// The JavaScript object reference that represents the function to be invoked. + /// A Func delegate of type that can be used to invoke the JavaScript function. + /// Thrown when is null. + /// Thrown when is not a valid Func type. + public static T AsAsyncFunction(this IJSObjectReference jsObjectReference) where T : Delegate + { + ArgumentNullException.ThrowIfNull(jsObjectReference); + + return JSFunctionReference.CreateInvocationDelegate(jsObjectReference); + } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index 3270d74b01a0..cab579e8a24e 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -56,6 +56,7 @@ Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, object?[]? args Microsoft.JSInterop.JSRuntime.InvokeNewAsync(string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.JSRuntime.SetValueAsync(string! identifier, TValue value, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +static Microsoft.JSInterop.JSObjectReferenceExtensions.AsAsyncFunction(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference) -> T! static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.Threading.CancellationToken cancellationToken, object?[]? args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSObjectReferenceExtensions.InvokeNewAsync(this Microsoft.JSInterop.IJSObjectReference! jsObjectReference, string! identifier, System.TimeSpan timeout, object?[]? args) -> System.Threading.Tasks.ValueTask diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs new file mode 100644 index 000000000000..a879dc54b6c0 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/JSObjectReferenceExtensionsTest.cs @@ -0,0 +1,192 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.JSInterop.Implementation; +using Microsoft.JSInterop.Infrastructure; + +namespace Microsoft.JSInterop.Tests; + +public class JSObjectReferenceExtensionsTest +{ + [Fact] + public void AsAsyncFunction_WithVoidValueTaskFunc_ReturnsFunc() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act + var func = jsObjectReference.AsAsyncFunction>(); + + // Assert + Assert.NotNull(func); + Assert.IsType>(func); + } + + [Fact] + public void AsAsyncFunction_WithVoidTaskFunc_ReturnsFunc() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act + var func = jsObjectReference.AsAsyncFunction>(); + + // Assert + Assert.NotNull(func); + Assert.IsType>(func); + } + + [Fact] + public void AsAsyncFunction_WithValueTaskFunc_ReturnsFunc() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act + var func = jsObjectReference.AsAsyncFunction>>(); + + // Assert + Assert.NotNull(func); + Assert.IsType>>(func); + } + + [Fact] + public void AsAsyncFunction_WithTaskFunc_ReturnsFunc() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act + var func = jsObjectReference.AsAsyncFunction>>(); + + // Assert + Assert.NotNull(func); + Assert.IsType>>(func); + } + + [Fact] + public void AsAsyncFunction_WithValueTaskFunc_ReturnsFunc_ThatInvokesInterop() + { + // Arrange + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(42)); + var reader = new Utf8JsonReader(bytes); + + // Act + var func = jsObjectReference.AsAsyncFunction>>(); + ValueTask task = func(1); + + jsRuntime.EndInvokeJS( + jsRuntime.InvokeCalls[0].AsyncHandle, + /* succeeded: */ true, + ref reader); + + // Assert + Assert.True(task.IsCompleted); +#pragma warning disable xUnit1031 // Do not use blocking task operations in test method + Assert.Equal(42, task.Result); +#pragma warning restore xUnit1031 // Do not use blocking task operations in test method + } + + [Fact] + public void AsAsyncFunction_WithTaskFunc_ReturnsFunc_ThatInvokesInterop() + { + // Arrange + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(42)); + var reader = new Utf8JsonReader(bytes); + + // Act + var func = jsObjectReference.AsAsyncFunction>>(); + Task task = func(1); + + jsRuntime.EndInvokeJS( + jsRuntime.InvokeCalls[0].AsyncHandle, + /* succeeded: */ true, + ref reader); + + // Assert + Assert.True(task.IsCompleted); +#pragma warning disable xUnit1031 // Do not use blocking task operations in test method + Assert.Equal(42, task.Result); +#pragma warning restore xUnit1031 // Do not use blocking task operations in test method + } + + [Fact] + public void AsAsyncFunction_WithEventHandlerDelegate_Throws() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act/Assert + Assert.Throws(jsObjectReference.AsAsyncFunction); + } + + [Fact] + public void AsAsyncFunction_WithActionDelegate_Throws() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act/Assert + Assert.Throws(jsObjectReference.AsAsyncFunction>); + } + + [Fact] + public void AsAsyncFunction_WithFuncWithInvalidReturnType_Throws() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act/Assert + Assert.Throws(jsObjectReference.AsAsyncFunction>); + } + + [Fact] + public void AsAsyncFunction_WithFuncWithTooManyParams_Throws() + { + var jsRuntime = new TestJSRuntime(); + var jsObjectReference = new JSObjectReference(jsRuntime, 1); + + // Act/Assert + Assert.Throws(jsObjectReference.AsAsyncFunction>); + } + + class TestJSRuntime : JSInProcessRuntime + { + public List InvokeCalls { get; set; } = []; + + public string? NextResultJson { get; set; } + + protected override string? InvokeJS(string identifier, string? argsJson, JSCallResultType resultType, long targetInstanceId) + { + throw new NotImplementedException(); + } + + protected override string? InvokeJS(in JSInvocationInfo invocationInfo) + { + InvokeCalls.Add(invocationInfo); + return NextResultJson; + } + + protected override void BeginInvokeJS(long taskId, string identifier, [StringSyntax("Json")] string? argsJson, JSCallResultType resultType, long targetInstanceId) + => throw new NotImplementedException("This test only covers sync calls"); + + protected override void BeginInvokeJS(in JSInvocationInfo invocationInfo) + { + InvokeCalls.Add(invocationInfo); + } + + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) + => throw new NotImplementedException("This test only covers sync calls"); + } +}