Skip to content

Commit 816aa72

Browse files
committed
bUnit-dev#361: Allow untyped JSInterop.Setup methods, that matches with any type
1 parent 49c73ec commit 816aa72

File tree

4 files changed

+180
-0
lines changed

4 files changed

+180
-0
lines changed

Diff for: src/bunit.web/JSInterop/BunitJSInteropSetupExtensions.cs

+34
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,40 @@ public static JSRuntimeInvocationHandler<TResult> Setup<TResult>(this BunitJSInt
6767
public static JSRuntimeInvocationHandler<TResult> Setup<TResult>(this BunitJSInterop jsInterop)
6868
=> Setup<TResult>(jsInterop, _ => true, isCatchAllHandler: true);
6969

70+
/// <summary>
71+
/// Configure an untyped JSInterop invocation handler passing the <paramref name="invocationMatcher"/> test.
72+
/// </summary>
73+
/// <param name="jsInterop">The bUnit JSInterop to setup the invocation handling with.</param>
74+
/// <param name="invocationMatcher">A matcher that is passed an <see cref="JSRuntimeInvocation"/>. If it returns true the invocation is matched.</param>
75+
/// <returns>A <see cref="UntypedJSRuntimeInvocationHandler"/>.</returns>
76+
public static UntypedJSRuntimeInvocationHandler Setup(this BunitJSInterop jsInterop, InvocationMatcher invocationMatcher)
77+
{
78+
if (jsInterop is null)
79+
throw new ArgumentNullException(nameof(jsInterop));
80+
return new UntypedJSRuntimeInvocationHandler(jsInterop, invocationMatcher);
81+
}
82+
83+
/// <summary>
84+
/// Configure an untyped JSInterop invocation handler with the <paramref name="identifier"/> and arguments
85+
/// passing the <paramref name="invocationMatcher"/> test.
86+
/// </summary>
87+
/// <param name="jsInterop">The bUnit JSInterop to setup the invocation handling with.</param>
88+
/// <param name="identifier">The identifier to setup a response for.</param>
89+
/// <param name="invocationMatcher">A matcher that is passed an <see cref="JSRuntimeInvocation"/> associated with the<paramref name="identifier"/>. If it returns true the invocation is matched.</param>
90+
/// <returns>A <see cref="UntypedJSRuntimeInvocationHandler"/>.</returns>
91+
public static UntypedJSRuntimeInvocationHandler Setup(this BunitJSInterop jsInterop, string identifier, InvocationMatcher invocationMatcher)
92+
=> Setup(jsInterop, inv => identifier.Equals(inv.Identifier, StringComparison.Ordinal) && invocationMatcher(inv));
93+
94+
/// <summary>
95+
/// Configure an untyped JSInterop invocation handler with the <paramref name="identifier"/> and <paramref name="arguments"/>.
96+
/// </summary>
97+
/// <param name="jsInterop">The bUnit JSInterop to setup the invocation handling with.</param>
98+
/// <param name="identifier">The identifier to setup a response for.</param>
99+
/// <param name="arguments">The arguments that an invocation to <paramref name="identifier"/> should match.</param>
100+
/// <returns>A <see cref="UntypedJSRuntimeInvocationHandler"/>.</returns>
101+
public static UntypedJSRuntimeInvocationHandler Setup(this BunitJSInterop jsInterop, string identifier, params object?[]? arguments)
102+
=> Setup(jsInterop, identifier, invocation => invocation.Arguments.SequenceEqual(arguments ?? Array.Empty<object?>()));
103+
70104
/// <summary>
71105
/// Configure a JSInterop invocation handler for an <c>InvokeVoidAsync</c> call with arguments
72106
/// passing the <paramref name="invocationMatcher"/> test, that should not receive any result.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace Bunit.JSInterop.InvocationHandlers
5+
{
6+
internal class JSRuntimeInvocationHandlerFactory<TResult, TException> : JSRuntimeInvocationHandler<TResult>
7+
where TException : Exception
8+
{
9+
private readonly Func<JSRuntimeInvocation, TResult>? resultFactory;
10+
private readonly Func<JSRuntimeInvocation, TException>? exceptionFactory;
11+
12+
public JSRuntimeInvocationHandlerFactory(Func<JSRuntimeInvocation, TResult>? rFactory, Func<JSRuntimeInvocation, TException>? eFactory, InvocationMatcher matcher, bool isCatchAllHandler) : base(matcher, isCatchAllHandler)
13+
{
14+
resultFactory = rFactory;
15+
exceptionFactory = eFactory;
16+
}
17+
18+
protected override internal Task<TResult> HandleAsync(JSRuntimeInvocation invocation)
19+
{
20+
if (exceptionFactory != null && exceptionFactory.Invoke(invocation) is TException t)
21+
{
22+
base.SetException<TException>(t);
23+
}
24+
else if (resultFactory != null && resultFactory.Invoke(invocation) is TResult res)
25+
{
26+
base.SetResult(res);
27+
}
28+
return base.HandleAsync(invocation);
29+
}
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace Bunit.JSInterop.InvocationHandlers
5+
{
6+
/// <summary>
7+
/// Represents a handler for an invocation of a JavaScript function with specific arguments
8+
/// that can return any type, depending on the SetResult/SetCancelled/SetException invocations.
9+
/// </summary>
10+
public class UntypedJSRuntimeInvocationHandler : JSRuntimeInvocationHandler
11+
{
12+
private readonly InvocationMatcher invocationMatcher;
13+
14+
private readonly BunitJSInterop jsInterop;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="UntypedJSRuntimeInvocationHandler"/> class.
18+
/// </summary>
19+
/// <param name="interop">The bUnit JSInterop to setup the invocation handling with.</param>
20+
/// <param name="matcher">An invocation matcher used to determine if the handler should handle an invocation.</param>
21+
/// <param name="isCatchAllHandler">Set to true if this handler is a catch all handler, that should only be used if there are no other non-catch all handlers available.</param>
22+
public UntypedJSRuntimeInvocationHandler(BunitJSInterop interop, InvocationMatcher matcher, bool isCatchAllHandler = true)
23+
: base(matcher, isCatchAllHandler)
24+
{
25+
jsInterop = interop;
26+
invocationMatcher = matcher ?? throw new ArgumentNullException(nameof(matcher));
27+
}
28+
29+
/// <summary>
30+
/// Sets the result factory function, that when invoked sets the <typeparamref name="TReturnType"/> result that invocations will receive.
31+
/// </summary>
32+
/// <param name="resultFactory">The result factory function that creates the handler result.</param>
33+
/// <returns>This handler to allow calls to be chained.</returns>
34+
public JSRuntimeInvocationHandler<TReturnType> SetResult<TReturnType>(Func<JSRuntimeInvocation, TReturnType> resultFactory)
35+
{
36+
var handler = new JSRuntimeInvocationHandlerFactory<TReturnType, Exception>(resultFactory, null, invocationMatcher, IsCatchAllHandler);
37+
38+
jsInterop.AddInvocationHandler<TReturnType>(handler);
39+
40+
return handler;
41+
}
42+
43+
/// <summary>
44+
/// Marks the <see cref="Task"/> that invocations will receive as canceled.
45+
/// </summary>
46+
/// <returns>This handler to allow calls to be chained.</returns>
47+
public JSRuntimeInvocationHandler<TReturnType> SetCanceled<TReturnType>()
48+
{
49+
var handler = new JSRuntimeInvocationHandler<TReturnType>(invocationMatcher, IsCatchAllHandler);
50+
handler.SetCanceled();
51+
52+
jsInterop.AddInvocationHandler<TReturnType>(handler);
53+
54+
return handler;
55+
}
56+
57+
/// <summary>
58+
/// Sets the exception factory, that when invoked determines the <typeparamref name="TException"/> exception that invocations will receive.
59+
/// </summary>
60+
/// <param name="exceptionFactory">The exception function factory to set.</param>
61+
/// <returns>This handler to allow calls to be chained.</returns>
62+
public JSRuntimeInvocationHandler<TReturnType> SetException<TReturnType, TException>(Func<JSRuntimeInvocation, TException> exceptionFactory) where TException : Exception
63+
{
64+
var handler = new JSRuntimeInvocationHandlerFactory<TReturnType, TException>(null, exceptionFactory, invocationMatcher, IsCatchAllHandler);
65+
66+
jsInterop.AddInvocationHandler<TReturnType>(handler);
67+
68+
return handler;
69+
}
70+
}
71+
}

Diff for: tests/bunit.web.tests/JSInterop/BunitJSInteropTest.cs

+44
Original file line numberDiff line numberDiff line change
@@ -511,5 +511,49 @@ public void Test046()
511511
exception.Invocation.Identifier.ShouldBe(identifier);
512512
exception.Invocation.Arguments.ShouldBe(args);
513513
}
514+
515+
[Fact(DisplayName = "Untyped setup can return a typed result from a factory function")]
516+
public async Task Test059()
517+
{
518+
var sut = CreateSut(JSRuntimeMode.Strict);
519+
var identifier = "func";
520+
521+
var jsRuntime = sut.JSRuntime;
522+
523+
var handler = sut.Setup(i => i.Identifier == identifier);
524+
handler.SetResult(_ => false);
525+
526+
var i1 = await jsRuntime.InvokeAsync<bool>(identifier);
527+
i1.ShouldBe(false);
528+
}
529+
530+
[Fact(DisplayName = "Untyped setup can throw an exception from a factory function")]
531+
public void Test060()
532+
{
533+
var sut = CreateSut(JSRuntimeMode.Strict);
534+
var identifier = "func";
535+
536+
var jsRuntime = sut.JSRuntime;
537+
538+
var handler = sut.Setup(i => i.Identifier == identifier);
539+
handler.SetException<bool, NotImplementedException>(_ => throw new NotImplementedException());
540+
541+
Should.Throw<NotImplementedException>(async () => await jsRuntime.InvokeAsync<bool>(identifier));
542+
}
543+
544+
[Fact(DisplayName = "An untyped invocation handler can be canceled")]
545+
public void Test061()
546+
{
547+
var sut = CreateSut(JSRuntimeMode.Strict);
548+
var identifier = "func";
549+
550+
var jsRuntime = sut.JSRuntime;
551+
552+
var handler = sut.Setup(i => i.Identifier == identifier);
553+
handler.SetCanceled<bool>();
554+
555+
var res = jsRuntime.InvokeAsync<bool>(identifier);
556+
res.IsCanceled.ShouldBeTrue();
557+
}
514558
}
515559
}

0 commit comments

Comments
 (0)