Skip to content

Commit 64954a3

Browse files
committed
Refactored bUnit JSInterop to make it more easily extendable
1 parent 31cb91d commit 64954a3

16 files changed

Lines changed: 227 additions & 143 deletions

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ The following section list all changes in 1.0.0 preview 01.
1111
### Added
1212
List of new features.
1313

14-
- Added support for triggering `@ontoggle` event handlers through a dedicated `Toggle()` method. By [@egil](https://github.com/egil) in [#248](https://github.com/egil/bUnit/pull/248).
14+
- Added support for triggering `@ontoggle` event handlers through a dedicated `Toggle()` method. By [@egil](https://github.com/egil) in [#256](https://github.com/egil/bUnit/pull/256).
15+
16+
- Added out of the box support for `<Virtualize>` component. When a `<Virtualize>` component is used in a component under test, it's JavaScript interop-calls are faked by bUnits JSInterop, and it should result in all items being rendered immediately.
1517

1618
### Changed
1719
List of changes in existing functionality.
@@ -24,6 +26,13 @@ List of changes in existing functionality.
2426

2527
Learn more [issue #237](https://github.com/egil/bUnit/issues/237). By [@egil](https://github.com/egil) in [#247](https://github.com/egil/bUnit/pull/247).
2628

29+
- The `Setup<TResult>(string identifier, Func<IReadOnlyList<object?>, bool> argumentsMatcher)` and `SetupVoid(string identifier, Func<IReadOnlyList<object?>, bool> argumentsMatcher)` methods in bUnits JSInterop/MockJSRuntime has a new second parameter, an `InvocationMatcher`.
30+
31+
The `InvocationMatcher` type is a delegate that receives a `JSRuntimeInvoation` and returns true. The `JSRuntimeInvoation` type contains the arguments of the invocation and the identifier for the invocation. This means old code using the `Setup` and `SetupVoid` methods should be updated to use the arguments list in `JSRuntimeInvoation`, e.g., change the following call:
32+
33+
`ctx.JSInterop.Setup<string>("foo", args => args.Count == 2)` to this:
34+
`ctx.JSInterop.Setup<string>("foo", invocation => invocation.Arguments.Count == 2)`.
35+
2736
### Removed
2837
List of now removed features.
2938

src/bunit.core/Rendering/TestRenderer.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,15 @@ private void AssertNoUnhandledExceptions()
270270
{
271271
_unhandledException = null;
272272
LogUnhandledException(unhandled);
273-
ExceptionDispatchInfo.Capture(unhandled).Throw();
273+
274+
if (unhandled is AggregateException aggregateException && aggregateException.InnerExceptions.Count == 1)
275+
{
276+
ExceptionDispatchInfo.Capture(aggregateException.InnerExceptions[0]).Throw();
277+
}
278+
else
279+
{
280+
ExceptionDispatchInfo.Capture(unhandled).Throw();
281+
}
274282
}
275283
}
276284

src/bunit.web/JSInterop/BunitJSInterop.cs

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,22 @@ public BunitJSInterop()
5252
public JSRuntimeInvocationHandler<TResult> Setup<TResult>()
5353
{
5454
var result = new JSRuntimeInvocationHandler<TResult>(JSRuntimeInvocationHandler<object>.CatchAllIdentifier, _ => true);
55-
56-
AddHandler(result);
57-
55+
AddInvocationHandler(result);
5856
return result;
5957
}
6058

6159
/// <summary>
6260
/// Configure a JSInterop invocation handler with the <paramref name="identifier"/> and arguments
63-
/// passing the <paramref name="argumentsMatcher"/> test.
61+
/// passing the <paramref name="invocationMatcher"/> test.
6462
/// </summary>
6563
/// <typeparam name="TResult">The result type of the invocation.</typeparam>
6664
/// <param name="identifier">The identifier to setup a response for.</param>
67-
/// <param name="argumentsMatcher">A matcher that is passed arguments received in invocations to <paramref name="identifier"/>. If it returns true the invocation is matched.</param>
65+
/// <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>
6866
/// <returns>A <see cref="JSRuntimeInvocationHandler{TResult}"/>.</returns>
69-
public JSRuntimeInvocationHandler<TResult> Setup<TResult>(string identifier, Func<IReadOnlyList<object?>, bool> argumentsMatcher)
67+
public JSRuntimeInvocationHandler<TResult> Setup<TResult>(string identifier, InvocationMatcher invocationMatcher)
7068
{
71-
var result = new JSRuntimeInvocationHandler<TResult>(identifier, argumentsMatcher);
72-
73-
AddHandler(result);
74-
69+
var result = new JSRuntimeInvocationHandler<TResult>(identifier, invocationMatcher);
70+
AddInvocationHandler(result);
7571
return result;
7672
}
7773

@@ -84,22 +80,20 @@ public JSRuntimeInvocationHandler<TResult> Setup<TResult>(string identifier, Fun
8480
/// <returns>A <see cref="JSRuntimeInvocationHandler{TResult}"/>.</returns>
8581
public JSRuntimeInvocationHandler<TResult> Setup<TResult>(string identifier, params object[] arguments)
8682
{
87-
return Setup<TResult>(identifier, args => args.SequenceEqual(arguments));
83+
return Setup<TResult>(identifier, invocation => invocation.Arguments.SequenceEqual(arguments));
8884
}
8985

9086
/// <summary>
9187
/// Configure a JSInterop invocation handler with the <paramref name="identifier"/> and arguments
92-
/// passing the <paramref name="argumentsMatcher"/> test, that should not receive any result.
88+
/// passing the <paramref name="invocationMatcher"/> test, that should not receive any result.
9389
/// </summary>
9490
/// <param name="identifier">The identifier to setup a response for.</param>
95-
/// <param name="argumentsMatcher">A matcher that is passed arguments received in invocations to <paramref name="identifier"/>. If it returns true the invocation is matched.</param>
91+
/// <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>
9692
/// <returns>A <see cref="JSRuntimeInvocationHandler"/>.</returns>
97-
public JSRuntimeInvocationHandler SetupVoid(string identifier, Func<IReadOnlyList<object?>, bool> argumentsMatcher)
93+
public JSRuntimeInvocationHandler SetupVoid(string identifier, InvocationMatcher invocationMatcher)
9894
{
99-
var result = new JSRuntimeInvocationHandler(identifier, argumentsMatcher);
100-
101-
AddHandler(result);
102-
95+
var result = new JSRuntimeInvocationHandler(identifier, invocationMatcher);
96+
AddInvocationHandler(result);
10397
return result;
10498
}
10599

@@ -112,7 +106,7 @@ public JSRuntimeInvocationHandler SetupVoid(string identifier, Func<IReadOnlyLis
112106
/// <returns>A <see cref="JSRuntimeInvocationHandler"/>.</returns>
113107
public JSRuntimeInvocationHandler SetupVoid(string identifier, params object[] arguments)
114108
{
115-
return SetupVoid(identifier, args => args.SequenceEqual(arguments));
109+
return SetupVoid(identifier, invocation => invocation.Arguments.SequenceEqual(arguments));
116110
}
117111

118112
/// <summary>
@@ -122,9 +116,7 @@ public JSRuntimeInvocationHandler SetupVoid(string identifier, params object[] a
122116
public JSRuntimeInvocationHandler SetupVoid()
123117
{
124118
var result = new JSRuntimeInvocationHandler(JSRuntimeInvocationHandler<object>.CatchAllIdentifier, _ => true);
125-
126-
AddHandler(result);
127-
119+
AddInvocationHandler(result);
128120
return result;
129121
}
130122

@@ -135,7 +127,7 @@ public JSRuntimeInvocationHandler SetupVoid()
135127
/// </summary>
136128
/// <returns>Returns the <see cref="JSRuntimeInvocationHandler{TResult}"/> or null if no one is found.</returns>
137129
public JSRuntimeInvocationHandler<TResult>? TryGetInvokeHandler<TResult>(string identifier, object?[]? args = null)
138-
=> TryGetHandlerFor<TResult>(new JSRuntimeInvocation(identifier, default, args));
130+
=> TryGetHandlerFor<TResult>(new JSRuntimeInvocation(identifier, default, args)) as JSRuntimeInvocationHandler<TResult>;
139131

140132
/// <summary>
141133
/// Looks through the registered handlers and returns the latest registered that can handle
@@ -145,7 +137,11 @@ public JSRuntimeInvocationHandler SetupVoid()
145137
public JSRuntimeInvocationHandler? TryGetInvokeVoidHandler(string identifier, object?[]? args = null)
146138
=> TryGetHandlerFor<object>(new JSRuntimeInvocation(identifier, default, args), x => x.IsVoidResultHandler) as JSRuntimeInvocationHandler;
147139

148-
private void AddHandler<TResult>(JSRuntimeInvocationHandler<TResult> handler)
140+
/// <summary>
141+
/// Adds an invocation handler to bUnit's JSInterop. Can be used to register
142+
/// custom invocation handlers.
143+
/// </summary>
144+
public void AddInvocationHandler<TResult>(JSRuntimeInvocationHandlerBase<TResult> handler)
149145
{
150146
if (!_handlers.ContainsKey(handler.Identifier))
151147
{
@@ -163,20 +159,23 @@ private void RegisterInvocation(JSRuntimeInvocation invocation)
163159
_invocations[invocation.Identifier].Add(invocation);
164160
}
165161

166-
private JSRuntimeInvocationHandler<TResult>? TryGetHandlerFor<TResult>(JSRuntimeInvocation invocation, Predicate<JSRuntimeInvocationHandler<TResult>>? handlerPredicate = null)
162+
private JSRuntimeInvocationHandlerBase<TResult>? TryGetHandlerFor<TResult>(JSRuntimeInvocation invocation, Predicate<JSRuntimeInvocationHandlerBase<TResult>>? handlerPredicate = null)
167163
{
168164
handlerPredicate ??= _ => true;
169-
JSRuntimeInvocationHandler<TResult>? result = default;
165+
JSRuntimeInvocationHandlerBase<TResult>? result = default;
166+
170167
if (_handlers.TryGetValue(invocation.Identifier, out var plannedInvocations))
171168
{
172-
result = plannedInvocations.OfType<JSRuntimeInvocationHandler<TResult>>()
173-
.Where(x => handlerPredicate(x) && x.Matches(invocation)).LastOrDefault();
169+
result = plannedInvocations.OfType<JSRuntimeInvocationHandlerBase<TResult>>()
170+
.LastOrDefault(x => handlerPredicate(x) && x.Matches(invocation));
174171
}
175172

176-
if (result is null && _handlers.TryGetValue(JSRuntimeInvocationHandler<TResult>.CatchAllIdentifier, out var catchAllHandlers))
173+
if (result is null && _handlers.TryGetValue(JSRuntimeInvocationHandler.CatchAllIdentifier, out var catchAllHandlers))
177174
{
178-
result = catchAllHandlers.OfType<JSRuntimeInvocationHandler<TResult>>().Where(x => handlerPredicate(x)).LastOrDefault();
175+
result = catchAllHandlers.OfType<JSRuntimeInvocationHandlerBase<TResult>>()
176+
.LastOrDefault(x => handlerPredicate(x) && x.Matches(invocation));
179177
}
178+
180179
return result;
181180
}
182181

@@ -204,7 +203,7 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
204203
{
205204
ValueTask<TValue>? result = default;
206205

207-
if (_jsInterop.TryGetHandlerFor<TValue>(invocation) is JSRuntimeInvocationHandler<TValue> handler)
206+
if (_jsInterop.TryGetHandlerFor<TValue>(invocation) is JSRuntimeInvocationHandlerBase<TValue> handler)
208207
{
209208
var task = handler.RegisterInvocation(invocation);
210209
result = new ValueTask<TValue>(task);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace Bunit.JSInterop.ComponentSupport
2+
{
3+
/// <summary>
4+
/// Helper methods for registering handlers on the <see cref="BunitJSInterop"/>.
5+
/// </summary>
6+
public static class BunitJSInteropExtensions
7+
{
8+
/// <summary>
9+
/// Adds the built-in JSRuntime invocation handlers to the <paramref name="jSInterop"/>.
10+
/// </summary>
11+
public static BunitJSInterop AddBuiltInJSRuntimeInvocationHandlers(this BunitJSInterop jSInterop)
12+
{
13+
#if NET5_0
14+
//jSInterop.AddInvocationHandler(new Virtualize.VirtualizeJSRuntimeInvocationHandler());
15+
#endif
16+
return jSInterop;
17+
}
18+
19+
}
20+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Bunit
2+
{
3+
/// <summary>
4+
/// Represents a invocation matcher / predicate, that is used to determine
5+
/// if a <see cref="JSRuntimeInvocationHandlerBase{TResult}"/> matches a specific
6+
/// <see cref="JSRuntimeInvocation"/>.
7+
/// </summary>
8+
/// <param name="invocation">The invocation to match against.</param>
9+
/// <returns>True if the <see cref="JSRuntimeInvocationHandlerBase{TResult}"/> can handle the invocation, false otherwise.</returns>
10+
public delegate bool InvocationMatcher(JSRuntimeInvocation invocation);
11+
}
Lines changed: 11 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Threading.Tasks;
43

54
namespace Bunit
65
{
@@ -9,110 +8,36 @@ namespace Bunit
98
/// and returns <typeparamref name="TResult"/>.
109
/// </summary>
1110
/// <typeparam name="TResult">The expect result type.</typeparam>
12-
public class JSRuntimeInvocationHandler<TResult>
11+
public class JSRuntimeInvocationHandler<TResult> : JSRuntimeInvocationHandlerBase<TResult>
1312
{
14-
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; }
30-
31-
/// <summary>
32-
/// The expected identifier for the function to invoke.
33-
/// </summary>
34-
public string Identifier { get; }
35-
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-
4113
/// <summary>
42-
/// Creates an instance of a <see cref="JSRuntimeInvocationHandler{TResult}"/>.
14+
/// Creates an instance of a <see cref="JSRuntimeInvocationHandler{TResult}"/> type.
4315
/// </summary>
44-
internal JSRuntimeInvocationHandler(string identifier, Func<IReadOnlyList<object?>, bool> matcher)
45-
{
46-
Identifier = identifier;
47-
IsCatchAllHandler = identifier == CatchAllIdentifier;
48-
_invocationMatcher = matcher;
49-
_invocations = new List<JSRuntimeInvocation>();
50-
_completionSource = new TaskCompletionSource<TResult>();
51-
}
16+
protected internal JSRuntimeInvocationHandler(string identifier, InvocationMatcher matcher) : base(identifier, matcher) { }
5217

5318
/// <summary>
5419
/// Sets the <typeparamref name="TResult"/> result that invocations will receive.
5520
/// </summary>
5621
/// <param name="result"></param>
57-
public void SetResult(TResult result)
58-
{
59-
if (_completionSource.Task.IsCompleted)
60-
_completionSource = new TaskCompletionSource<TResult>();
61-
62-
_completionSource.SetResult(result);
63-
}
64-
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)
96-
{
97-
return Identifier.Equals(invocation.Identifier, StringComparison.Ordinal)
98-
&& _invocationMatcher(invocation.Arguments);
99-
}
22+
public void SetResult(TResult result) => SetResultBase(result);
10023
}
10124

10225
/// <summary>
10326
/// Represents a handler for an invocation of a JavaScript function which returns nothing, with specific arguments.
10427
/// </summary>
105-
public class JSRuntimeInvocationHandler : JSRuntimeInvocationHandler<object>
28+
public class JSRuntimeInvocationHandler : JSRuntimeInvocationHandlerBase<object>
10629
{
10730
/// <inheritdoc/>
108-
public override bool IsVoidResultHandler { get; } = true;
31+
public override sealed bool IsVoidResultHandler { get; } = true;
10932

110-
internal JSRuntimeInvocationHandler(string identifier, Func<IReadOnlyList<object?>, bool> matcher) : base(identifier, matcher)
111-
{ }
33+
/// <summary>
34+
/// Creates an instance of a <see cref="JSRuntimeInvocationHandler"/> type.
35+
/// </summary>
36+
protected internal JSRuntimeInvocationHandler(string identifier, InvocationMatcher matcher) : base(identifier, matcher) { }
11237

11338
/// <summary>
11439
/// Completes the current awaiting void invocation requests.
11540
/// </summary>
116-
public void SetVoidResult() => SetResult(default!);
41+
public void SetVoidResult() => SetResultBase(default!);
11742
}
11843
}

0 commit comments

Comments
 (0)