Skip to content

Commit fa03e61

Browse files
committed
Removed explicit catch all logic and generalized around JSRUntimeInvocationHandler. Added ability to query BunitJSInterop for registered handler.
1 parent 930a95b commit fa03e61

6 files changed

Lines changed: 159 additions & 154 deletions

File tree

src/bunit.web/JSInterop/BunitJSInterop.cs

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.ComponentModel;
34
using System.Linq;
45
using System.Threading;
56
using System.Threading.Tasks;
@@ -14,7 +15,6 @@ public class BunitJSInterop
1415
{
1516
private readonly Dictionary<string, List<JSRuntimeInvocation>> _invocations = new Dictionary<string, List<JSRuntimeInvocation>>();
1617
private readonly Dictionary<string, List<object>> _handlers = new Dictionary<string, List<object>>();
17-
private readonly Dictionary<Type, object> _catchAllHandlers = new Dictionary<Type, object>();
1818

1919
/// <summary>
2020
/// Gets a dictionary of all <see cref="List{JSRuntimeInvocation}"/> this mock has observed.
@@ -48,12 +48,12 @@ public BunitJSInterop()
4848
/// <see cref="IJSRuntime.InvokeAsync{TValue}(string, object[])"/>.
4949
/// </summary>
5050
/// <typeparam name="TResult">The result type of the invocation.</typeparam>
51-
/// <returns>A <see cref="JSRuntimeCatchAllInvocationHandler{TResult}"/>.</returns>
52-
public JSRuntimeCatchAllInvocationHandler<TResult> Setup<TResult>()
51+
/// <returns>A <see cref="JSRuntimeInvocationHandler{TResult}"/>.</returns>
52+
public JSRuntimeInvocationHandler<TResult> Setup<TResult>()
5353
{
54-
var result = new JSRuntimeCatchAllInvocationHandler<TResult>();
54+
var result = new JSRuntimeInvocationHandler<TResult>(JSRuntimeInvocationHandler<object>.CatchAllIdentifier, _ => true);
5555

56-
_catchAllHandlers[typeof(TResult)] = result;
56+
AddHandler(result);
5757

5858
return result;
5959
}
@@ -118,14 +118,33 @@ public JSRuntimeInvocationHandler SetupVoid(string identifier, params object[] a
118118
/// <summary>
119119
/// Configure a catch all JSInterop invocation handler, that should not receive any result.
120120
/// </summary>
121-
/// <returns>A <see cref="JSRuntimeCatchAllInvocationHandler"/>.</returns>
122-
public JSRuntimeCatchAllInvocationHandler SetupVoid()
121+
/// <returns>A <see cref="JSRuntimeInvocationHandler"/>.</returns>
122+
public JSRuntimeInvocationHandler SetupVoid()
123123
{
124-
var result = new JSRuntimeCatchAllInvocationHandler();
125-
_catchAllHandlers[typeof(object)] = result;
124+
var result = new JSRuntimeInvocationHandler(JSRuntimeInvocationHandler<object>.CatchAllIdentifier, _ => true);
125+
126+
AddHandler(result);
127+
126128
return result;
127129
}
128130

131+
/// <summary>
132+
/// Looks through the registered handlers and returns the latest registered that can handle
133+
/// the provided <paramref name="identifier"/> and <paramref name="args"/>, and that
134+
/// will return <typeparamref name="TResult"/>.
135+
/// </summary>
136+
/// <returns>Returns the <see cref="JSRuntimeInvocationHandler{TResult}"/> or null if no one is found.</returns>
137+
public JSRuntimeInvocationHandler<TResult>? TryGetInvokeHandler<TResult>(string identifier, object?[]? args = null)
138+
=> TryGetHandlerFor<TResult>(new JSRuntimeInvocation(identifier, default, args));
139+
140+
/// <summary>
141+
/// Looks through the registered handlers and returns the latest registered that can handle
142+
/// the provided <paramref name="identifier"/> and <paramref name="args"/>, and that returns a "void" result.
143+
/// </summary>
144+
/// <returns>Returns the <see cref="JSRuntimeInvocationHandler"/> or null if no one is found.</returns>
145+
public JSRuntimeInvocationHandler? TryGetInvokeVoidHandler(string identifier, object?[]? args = null)
146+
=> TryGetHandlerFor<object>(new JSRuntimeInvocation(identifier, default, args), x => x.IsVoidResultHandler) as JSRuntimeInvocationHandler;
147+
129148
private void AddHandler<TResult>(JSRuntimeInvocationHandler<TResult> handler)
130149
{
131150
if (!_handlers.ContainsKey(handler.Identifier))
@@ -144,6 +163,23 @@ private void RegisterInvocation(JSRuntimeInvocation invocation)
144163
_invocations[invocation.Identifier].Add(invocation);
145164
}
146165

166+
private JSRuntimeInvocationHandler<TResult>? TryGetHandlerFor<TResult>(JSRuntimeInvocation invocation, Predicate<JSRuntimeInvocationHandler<TResult>>? handlerPredicate = null)
167+
{
168+
handlerPredicate ??= _ => true;
169+
JSRuntimeInvocationHandler<TResult>? result = default;
170+
if (_handlers.TryGetValue(invocation.Identifier, out var plannedInvocations))
171+
{
172+
result = plannedInvocations.OfType<JSRuntimeInvocationHandler<TResult>>()
173+
.Where(x => handlerPredicate(x) && x.Matches(invocation)).LastOrDefault();
174+
}
175+
176+
if (result is null && _handlers.TryGetValue(JSRuntimeInvocationHandler<TResult>.CatchAllIdentifier, out var catchAllHandlers))
177+
{
178+
result = catchAllHandlers.OfType<JSRuntimeInvocationHandler<TResult>>().Where(x => handlerPredicate(x)).LastOrDefault();
179+
}
180+
return result;
181+
}
182+
147183
private class BUnitJSRuntime : IJSRuntime
148184
{
149185
private readonly BunitJSInterop _jsInterop;
@@ -161,37 +197,19 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
161197
var invocation = new JSRuntimeInvocation(identifier, cancellationToken, args);
162198
_jsInterop.RegisterInvocation(invocation);
163199

164-
return TryHandlePlannedInvocation<TValue>(identifier, invocation)
165-
?? new ValueTask<TValue>(default(TValue)!);
200+
return TryHandlePlannedInvocation<TValue>(invocation) ?? new ValueTask<TValue>(default(TValue)!);
166201
}
167202

168-
private ValueTask<TValue>? TryHandlePlannedInvocation<TValue>(string identifier, JSRuntimeInvocation invocation)
203+
private ValueTask<TValue>? TryHandlePlannedInvocation<TValue>(JSRuntimeInvocation invocation)
169204
{
170205
ValueTask<TValue>? result = default;
171-
if (_jsInterop._handlers.TryGetValue(identifier, out var plannedInvocations))
172-
{
173-
var handler = plannedInvocations.OfType<JSRuntimeInvocationHandlerBase<TValue>>()
174-
.Where(x => x.Matches(invocation)).LastOrDefault();
175-
176-
if (handler is not null)
177-
{
178-
var task = handler.RegisterInvocation(invocation);
179-
return new ValueTask<TValue>(task);
180-
}
181-
}
182206

183-
if (_jsInterop._catchAllHandlers.TryGetValue(typeof(TValue), out var catchAllInvocation))
207+
if (_jsInterop.TryGetHandlerFor<TValue>(invocation) is JSRuntimeInvocationHandler<TValue> handler)
184208
{
185-
var planned = catchAllInvocation as JSRuntimeInvocationHandlerBase<TValue>;
186-
187-
if (planned is not null)
188-
{
189-
var task = ((JSRuntimeInvocationHandlerBase<TValue>)catchAllInvocation).RegisterInvocation(invocation);
190-
return new ValueTask<TValue>(task);
191-
}
209+
var task = handler.RegisterInvocation(invocation);
210+
result = new ValueTask<TValue>(task);
192211
}
193-
194-
if (result is null && _jsInterop.Mode == JSRuntimeMode.Strict)
212+
else if (_jsInterop.Mode == JSRuntimeMode.Strict)
195213
{
196214
throw new JSRuntimeUnhandledInvocationException(invocation);
197215
}

src/bunit.web/JSInterop/JSRuntimeCatchAllInvocationHandler.cs

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/bunit.web/JSInterop/JSRuntimeInvocation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Bunit
88
/// <summary>
99
/// Represents an invocation of JavaScript via the JSRuntime Mock
1010
/// </summary>
11-
[SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification = "Following Blazors design")]
11+
[SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification = "Following Blazor's API design")]
1212
public readonly struct JSRuntimeInvocation : IEquatable<JSRuntimeInvocation>
1313
{
1414
/// <summary>

src/bunit.web/JSInterop/JSRuntimeInvocationHandler.cs

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Threading.Tasks;
34

45
namespace Bunit
56
{
@@ -8,28 +9,90 @@ namespace Bunit
89
/// and returns <typeparamref name="TResult"/>.
910
/// </summary>
1011
/// <typeparam name="TResult">The expect result type.</typeparam>
11-
public class JSRuntimeInvocationHandler<TResult> : JSRuntimeInvocationHandlerBase<TResult>
12+
public class JSRuntimeInvocationHandler<TResult>
1213
{
1314
private readonly Func<IReadOnlyList<object?>, bool> _invocationMatcher;
15+
internal const string CatchAllIdentifier = "*";
16+
17+
private readonly List<JSRuntimeInvocation> _invocations;
18+
19+
private TaskCompletionSource<TResult> _completionSource;
20+
21+
/// <summary>
22+
/// Gets whether this handler is set up to handle calls to <c>InvokeVoidAsync=(string, object[])</c>.
23+
/// </summary>
24+
public virtual bool IsVoidResultHandler { get; } = false;
25+
26+
/// <summary>
27+
/// Gets whether this handler will match any invocations that expect a <typeparamref name="TResult"/> as the return type.
28+
/// </summary>
29+
public bool IsCatchAllHandler { get; }
1430

1531
/// <summary>
1632
/// The expected identifier for the function to invoke.
1733
/// </summary>
1834
public string Identifier { get; }
1935

36+
/// <summary>
37+
/// Gets the invocations that this <see cref="JSRuntimeInvocationHandler{TResult}"/> has matched with.
38+
/// </summary>
39+
public IReadOnlyList<JSRuntimeInvocation> Invocations => _invocations.AsReadOnly();
40+
41+
/// <summary>
42+
/// Creates an instance of a <see cref="JSRuntimeInvocationHandler{TResult}"/>.
43+
/// </summary>
2044
internal JSRuntimeInvocationHandler(string identifier, Func<IReadOnlyList<object?>, bool> matcher)
2145
{
2246
Identifier = identifier;
47+
IsCatchAllHandler = identifier == CatchAllIdentifier;
2348
_invocationMatcher = matcher;
49+
_invocations = new List<JSRuntimeInvocation>();
50+
_completionSource = new TaskCompletionSource<TResult>();
2451
}
2552

2653
/// <summary>
2754
/// Sets the <typeparamref name="TResult"/> result that invocations will receive.
2855
/// </summary>
2956
/// <param name="result"></param>
30-
public void SetResult(TResult result) => SetResultBase(result);
57+
public void SetResult(TResult result)
58+
{
59+
if (_completionSource.Task.IsCompleted)
60+
_completionSource = new TaskCompletionSource<TResult>();
61+
62+
_completionSource.SetResult(result);
63+
}
3164

32-
internal override bool Matches(JSRuntimeInvocation invocation)
65+
/// <summary>
66+
/// Sets the <typeparamref name="TException"/> exception that invocations will receive.
67+
/// </summary>
68+
/// <param name="exception"></param>
69+
public void SetException<TException>(TException exception)
70+
where TException : Exception
71+
{
72+
if (_completionSource.Task.IsCompleted)
73+
_completionSource = new TaskCompletionSource<TResult>();
74+
75+
_completionSource.SetException(exception);
76+
}
77+
78+
/// <summary>
79+
/// Marks the <see cref="Task{TResult}"/> that invocations will receive as canceled.
80+
/// </summary>
81+
public void SetCanceled()
82+
{
83+
if (_completionSource.Task.IsCompleted)
84+
_completionSource = new TaskCompletionSource<TResult>();
85+
86+
_completionSource.SetCanceled();
87+
}
88+
89+
internal Task<TResult> RegisterInvocation(JSRuntimeInvocation invocation)
90+
{
91+
_invocations.Add(invocation);
92+
return _completionSource.Task;
93+
}
94+
95+
internal bool Matches(JSRuntimeInvocation invocation)
3396
{
3497
return Identifier.Equals(invocation.Identifier, StringComparison.Ordinal)
3598
&& _invocationMatcher(invocation.Arguments);
@@ -41,12 +104,15 @@ internal override bool Matches(JSRuntimeInvocation invocation)
41104
/// </summary>
42105
public class JSRuntimeInvocationHandler : JSRuntimeInvocationHandler<object>
43106
{
107+
/// <inheritdoc/>
108+
public override bool IsVoidResultHandler { get; } = true;
109+
44110
internal JSRuntimeInvocationHandler(string identifier, Func<IReadOnlyList<object?>, bool> matcher) : base(identifier, matcher)
45111
{ }
46112

47113
/// <summary>
48114
/// Completes the current awaiting void invocation requests.
49115
/// </summary>
50-
public void SetVoidResult() => SetResultBase(default!);
116+
public void SetVoidResult() => SetResult(default!);
51117
}
52118
}

src/bunit.web/JSInterop/JSRuntimeInvocationHandlerBase.cs

Lines changed: 0 additions & 75 deletions
This file was deleted.

0 commit comments

Comments
 (0)