Skip to content

Commit 4f37ca4

Browse files
committed
Added additional bUnit JSInterop Setup methods, that makes it possible to get complete control of invocation matching for the created handler
1 parent 19352b8 commit 4f37ca4

13 files changed

Lines changed: 161 additions & 204 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ List of new features.
6969

7070
By [@egil](https://github.com/egil) in [#345](https://github.com/egil/bUnit/pull/345).
7171

72+
- Added additional bUnit JSInterop `Setup` methods, that makes it possible to get complete control of invocation matching for the created handler. By [@egil](https://github.com/egil).
73+
7274
### Changed
7375
List of changes in existing functionality.
7476

src/bunit.web/JSInterop/BunitJSInterop.cs

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
43
using System.Threading.Tasks;
54
using Bunit.JSInterop;
65
using Bunit.JSInterop.InvocationHandlers;
@@ -16,7 +15,7 @@ namespace Bunit
1615
/// </summary>
1716
public class BunitJSInterop
1817
{
19-
private readonly Dictionary<string, List<object>> handlers = new(StringComparer.Ordinal);
18+
private readonly Dictionary<Type, List<object>> handlers = new();
2019
private JSRuntimeMode mode;
2120

2221
/// <summary>
@@ -56,10 +55,12 @@ public void AddInvocationHandler<TResult>(JSRuntimeInvocationHandlerBase<TResult
5655
if (handler is null)
5756
throw new ArgumentNullException(nameof(handler));
5857

59-
if (!handlers.ContainsKey(handler.Identifier))
60-
handlers.Add(handler.Identifier, new List<object>());
58+
var resultType = typeof(TResult);
6159

62-
handlers[handler.Identifier].Add(handler);
60+
if (!handlers.ContainsKey(resultType))
61+
handlers.Add(resultType, new List<object>());
62+
63+
handlers[resultType].Add(handler);
6364
}
6465

6566
internal ValueTask<TValue> HandleInvocation<TValue>(JSRuntimeInvocation invocation)
@@ -93,22 +94,40 @@ internal virtual void RegisterInvocation(JSRuntimeInvocation invocation)
9394

9495
internal JSRuntimeInvocationHandlerBase<TResult>? TryGetHandlerFor<TResult>(JSRuntimeInvocation invocation, Predicate<JSRuntimeInvocationHandlerBase<TResult>>? handlerPredicate = null)
9596
{
97+
var resultType = typeof(TResult);
9698
handlerPredicate ??= _ => true;
97-
JSRuntimeInvocationHandlerBase<TResult>? result = default;
9899

99-
if (handlers.TryGetValue(invocation.Identifier, out var plannedInvocations))
100+
if (!handlers.TryGetValue(resultType, out var plannedInvocations))
100101
{
101-
result = plannedInvocations.OfType<JSRuntimeInvocationHandlerBase<TResult>>()
102-
.LastOrDefault(x => handlerPredicate(x) && x.CanHandle(invocation));
102+
return default;
103103
}
104104

105-
if (result is null && handlers.TryGetValue(JSRuntimeInvocationHandler.CatchAllIdentifier, out var catchAllHandlers))
105+
// Search from the latest added handler for a result type
106+
// and find the first handler that can handle an invocation
107+
// and is not a catch all handler.
108+
for (int i = plannedInvocations.Count - 1; i >= 0; i--)
106109
{
107-
result = catchAllHandlers.OfType<JSRuntimeInvocationHandlerBase<TResult>>()
108-
.LastOrDefault(x => handlerPredicate(x) && x.CanHandle(invocation));
110+
var candidate = (JSRuntimeInvocationHandlerBase<TResult>)plannedInvocations[i];
111+
112+
if (!candidate.IsCatchAllHandler && handlerPredicate(candidate) && candidate.CanHandle(invocation))
113+
{
114+
return candidate;
115+
}
109116
}
110117

111-
return result;
118+
// If none of the non catch all handlers can handle,
119+
// search for the latest added handler catch all handler that can, if any.
120+
for (int i = plannedInvocations.Count - 1; i >= 0; i--)
121+
{
122+
var candidate = (JSRuntimeInvocationHandlerBase<TResult>)plannedInvocations[i];
123+
124+
if (candidate.IsCatchAllHandler && handlerPredicate(candidate) && candidate.CanHandle(invocation))
125+
{
126+
return candidate;
127+
}
128+
}
129+
130+
return default;
112131
}
113132

114133
#if NET5_0

src/bunit.web/JSInterop/BunitJSInteropSetupExtensions.cs

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ namespace Bunit
1111
public static partial class BunitJSInteropSetupExtensions
1212
{
1313
/// <summary>
14-
/// Configure a JSInterop invocation handler with the <paramref name="identifier"/> and arguments
14+
/// Configure a JSInterop invocation handler for an <c>InvokeAsync&lt;TResult&gt;</c> call with arguments
1515
/// passing the <paramref name="invocationMatcher"/> test.
1616
/// </summary>
1717
/// <typeparam name="TResult">The result type of the invocation.</typeparam>
1818
/// <param name="jsInterop">The bUnit JSInterop to setup the invocation handling with.</param>
19-
/// <param name="identifier">The identifier to setup a response for.</param>
20-
/// <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>
19+
/// <param name="invocationMatcher">A matcher that is passed an <see cref="JSRuntimeInvocation"/>. If it returns true the invocation is matched.</param>
20+
/// <param name="isCatchAllHandler">Set to true if the created handler is a catch all handler, that should only be used if there are no other non-catch all handlers available.</param>
2121
/// <returns>A <see cref="JSRuntimeInvocationHandler{TResult}"/>.</returns>
22-
public static JSRuntimeInvocationHandler<TResult> Setup<TResult>(this BunitJSInterop jsInterop, string identifier, InvocationMatcher invocationMatcher)
22+
public static JSRuntimeInvocationHandler<TResult> Setup<TResult>(this BunitJSInterop jsInterop, InvocationMatcher invocationMatcher, bool isCatchAllHandler = false)
2323
{
2424
if (jsInterop is null)
2525
throw new ArgumentNullException(nameof(jsInterop));
@@ -28,11 +28,23 @@ public static JSRuntimeInvocationHandler<TResult> Setup<TResult>(this BunitJSInt
2828
EnsureResultNotIJSObjectReference<TResult>();
2929
#endif
3030

31-
var result = new JSRuntimeInvocationHandler<TResult>(identifier, invocationMatcher);
31+
var result = new JSRuntimeInvocationHandler<TResult>(invocationMatcher, isCatchAllHandler);
3232
jsInterop.AddInvocationHandler(result);
3333
return result;
3434
}
3535

36+
/// <summary>
37+
/// Configure a JSInterop invocation handler with the <paramref name="identifier"/> and arguments
38+
/// passing the <paramref name="invocationMatcher"/> test.
39+
/// </summary>
40+
/// <typeparam name="TResult">The result type of the invocation.</typeparam>
41+
/// <param name="jsInterop">The bUnit JSInterop to setup the invocation handling with.</param>
42+
/// <param name="identifier">The identifier to setup a response for.</param>
43+
/// <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>
44+
/// <returns>A <see cref="JSRuntimeInvocationHandler{TResult}"/>.</returns>
45+
public static JSRuntimeInvocationHandler<TResult> Setup<TResult>(this BunitJSInterop jsInterop, string identifier, InvocationMatcher invocationMatcher)
46+
=> Setup<TResult>(jsInterop, inv => identifier.Equals(inv.Identifier, StringComparison.Ordinal) && invocationMatcher(inv));
47+
3648
/// <summary>
3749
/// Configure a JSInterop invocation handler with the <paramref name="identifier"/> and <paramref name="arguments"/>.
3850
/// </summary>
@@ -53,26 +65,37 @@ public static JSRuntimeInvocationHandler<TResult> Setup<TResult>(this BunitJSInt
5365
/// <param name="jsInterop">The bUnit JSInterop to setup the invocation handling with.</param>
5466
/// <returns>A <see cref="JSRuntimeInvocationHandler{TResult}"/>.</returns>
5567
public static JSRuntimeInvocationHandler<TResult> Setup<TResult>(this BunitJSInterop jsInterop)
56-
=> Setup<TResult>(jsInterop, JSRuntimeInvocationHandler<object>.CatchAllIdentifier, _ => true);
68+
=> Setup<TResult>(jsInterop, _ => true, isCatchAllHandler: true);
5769

5870
/// <summary>
59-
/// Configure a JSInterop invocation handler with the <paramref name="identifier"/> and arguments
71+
/// Configure a JSInterop invocation handler for an <c>InvokeVoidAsync</c> call with arguments
6072
/// passing the <paramref name="invocationMatcher"/> test, that should not receive any result.
6173
/// </summary>
6274
/// <param name="jsInterop">The bUnit JSInterop to setup the invocation handling with.</param>
63-
/// <param name="identifier">The identifier to setup a response for.</param>
64-
/// <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>
75+
/// <param name="invocationMatcher">A matcher that is passed an <see cref="JSRuntimeInvocation"/>. If it returns true the invocation is matched.</param>
76+
/// <param name="isCatchAllHandler">Set to true if the created handler is a catch all handler, that should only be used if there are no other non-catch all handlers available.</param>
6577
/// <returns>A <see cref="JSRuntimeInvocationHandler"/>.</returns>
66-
public static JSRuntimeInvocationHandler SetupVoid(this BunitJSInterop jsInterop, string identifier, InvocationMatcher invocationMatcher)
78+
public static JSRuntimeInvocationHandler SetupVoid(this BunitJSInterop jsInterop, InvocationMatcher invocationMatcher, bool isCatchAllHandler = false)
6779
{
6880
if (jsInterop is null)
6981
throw new ArgumentNullException(nameof(jsInterop));
7082

71-
var result = new JSRuntimeInvocationHandler(identifier, invocationMatcher);
83+
var result = new JSRuntimeInvocationHandler(invocationMatcher, isCatchAllHandler);
7284
jsInterop.AddInvocationHandler(result);
7385
return result;
7486
}
7587

88+
/// <summary>
89+
/// Configure a JSInterop invocation handler with the <paramref name="identifier"/> and arguments
90+
/// passing the <paramref name="invocationMatcher"/> test, that should not receive any result.
91+
/// </summary>
92+
/// <param name="jsInterop">The bUnit JSInterop to setup the invocation handling with.</param>
93+
/// <param name="identifier">The identifier to setup a response for.</param>
94+
/// <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>
95+
/// <returns>A <see cref="JSRuntimeInvocationHandler"/>.</returns>
96+
public static JSRuntimeInvocationHandler SetupVoid(this BunitJSInterop jsInterop, string identifier, InvocationMatcher invocationMatcher)
97+
=> SetupVoid(jsInterop, inv => identifier.Equals(inv.Identifier, StringComparison.Ordinal) && invocationMatcher(inv));
98+
7699
/// <summary>
77100
/// Configure a JSInterop invocation handler with the <paramref name="identifier"/>
78101
/// and <paramref name="arguments"/>, that should not receive any result.
@@ -90,7 +113,7 @@ public static JSRuntimeInvocationHandler SetupVoid(this BunitJSInterop jsInterop
90113
/// <param name="jsInterop">The bUnit JSInterop to setup the invocation handling with.</param>
91114
/// <returns>A <see cref="JSRuntimeInvocationHandler"/>.</returns>
92115
public static JSRuntimeInvocationHandler SetupVoid(this BunitJSInterop jsInterop)
93-
=> SetupVoid(jsInterop, JSRuntimeInvocationHandler<object>.CatchAllIdentifier, _ => true);
116+
=> SetupVoid(jsInterop, _ => true, isCatchAllHandler: true);
94117

95118
/// <summary>
96119
/// Looks through the registered handlers and returns the latest registered that can handle

0 commit comments

Comments
 (0)