Skip to content

Commit 928fda7

Browse files
nemesvegil
andauthored
Add a Setup<X>() method to MockJSRuntime (#234)
* Add a Setup<X>() method without parameters and identifier to match all invocations with type X * Introduce a new abstraction for planned invocations with identifier * Add a SetupVoid() method without parameters and identifier to match all void invocations * Removed unneeded type in planned invocation class hierarchy, updated changelog * Add documentation of the Setup and SetupVoid methods Co-authored-by: Egil Hansen <[email protected]>
1 parent 3528e90 commit 928fda7

File tree

6 files changed

+246
-34
lines changed

6 files changed

+246
-34
lines changed

CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,18 @@ List of new features.
3333

3434
By [@duracellko](https://github.com/duracellko) in [#101](https://github.com/egil/bUnit/issues/101).
3535

36-
- Added support for registering/adding "layout" components to a test context, which components should be rendered inside. This allows you to simplify the "arrange" step of a test when a component under test requires a certain render tree as its parent, e.g. a cascading value.
36+
- Added support for registering/adding components to a test context root render tree, which components under test is rendered inside. This allows you to simplify the "arrange" step of a test when a component under test requires a certain render tree as its parent, e.g. a cascading value.
3737

3838
For example, to pass a cascading string value `foo` to all components rendered with the test context, do the following:
3939

4040
```csharp
41-
ctx.AddLayoutComponent<CascadingValue<string>>(parameters => parameters.Add(p => p.Value, "foo"));
41+
ctx.RenderTree<CascadingValue<string>>(parameters => parameters.Add(p => p.Value, "foo"));
4242
var cut = ctx.RenderComponent<ComponentReceivingFoo>();
4343
```
4444

45-
By [@duracellko](https://github.com/duracellko) in [#101](https://github.com/egil/bUnit/issues/101).
45+
By [@egil](https://github.com/egil) in [#236](https://github.com/egil/bUnit/pull/236).
46+
47+
- Added "catch-all" `Setup` method to bUnit's mock JS runtime, that allows you to specify only the type when setting up a planned invocation. By [@nemesv](https://github.com/nemesv) in [#234](https://github.com/egil/bUnit/issues/234).
4648

4749
### Changed
4850
List of changes in existing functionality.

docs/site/docs/test-doubles/mocking-ijsruntime.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@ var mockJS = ctx.Services.AddMockJSRuntime(JSRuntimeMockMode.Strict);
4040

4141
## Setting up invocations
4242

43-
Use the `Setup<TResult>(...)` and `SetupVoid(...)` methods to configure the mock to handle calls from the matching `InvokeAsync<TResult>(...)` and `InvokeVoidAsync(...)` methods on `IJSRuntime`.
43+
Use the `Setup<TResult>(...)` and `SetupVoid(...)` methods to configure the mock to handle calls from the **matching** `InvokeAsync<TResult>(...)` and `InvokeVoidAsync(...)` methods on `IJSRuntime`.
4444

45-
When an invocation is set up through the `Setup<TResult>(...)` and `SetupVoid(...)` methods, a `JSRuntimePlannedInvocation<TResult>` object is returned. This can be used to set a result or an exception, to emulate what can happen during a JavaScript interop call in Blazor.
45+
Use the parameterless `Setup<TResult>()` method to mock any call to `InvokeAsync<TResult>(...)` with a given return type `TResult` and use the parameterless `SetupVoid()` to mock any call to `InvokeVoidAsync(...)`.
46+
47+
When an invocation is set up through of the `Setup<TResult>(...)` and `SetupVoid(...)` methods, a `JSRuntimePlannedInvocation<TResult>` object is returned. This can be used to set a result or an exception, to emulate what can happen during a JavaScript interop call in Blazor.
48+
49+
Similarly when the parameterless `Setup<TResult>()` and `SetupVoid()` methods are used a `JSRuntimeCatchAllPlannedInvocation<TResult>` object is returned which can be used to set the result of invocation.
4650

4751
Here are two examples:
4852

src/bunit.web/TestDoubles/JSInterop/JSRuntimePlannedInvocation.cs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,84 @@
44
namespace Bunit.TestDoubles
55
{
66
/// <summary>
7-
/// Represents a planned invocation of a JavaScript function which returns nothing, with specific arguments.
7+
/// Represents a planned invocation of a JavaScript function with specific arguments.
88
/// </summary>
9-
public class JSRuntimePlannedInvocation : JSRuntimePlannedInvocationBase<object>
9+
/// <typeparam name="TResult">The expect result type.</typeparam>
10+
public class JSRuntimePlannedInvocation<TResult> : JSRuntimePlannedInvocationBase<TResult>
1011
{
11-
internal JSRuntimePlannedInvocation(string identifier, Func<IReadOnlyList<object?>, bool> matcher) : base(identifier, matcher)
12+
private readonly Func<IReadOnlyList<object?>, bool> _invocationMatcher;
13+
14+
/// <summary>
15+
/// The expected identifier for the function to invoke.
16+
/// </summary>
17+
public string Identifier { get; }
18+
19+
internal JSRuntimePlannedInvocation(string identifier, Func<IReadOnlyList<object?>, bool> matcher)
1220
{
21+
Identifier = identifier;
22+
_invocationMatcher = matcher;
1323
}
1424

1525
/// <summary>
16-
/// Completes the current awaiting void invocation requests.
26+
/// Sets the <typeparamref name="TResult"/> result that invocations will receive.
1727
/// </summary>
18-
public void SetVoidResult()
28+
/// <param name="result"></param>
29+
public void SetResult(TResult result) => SetResultBase(result);
30+
31+
internal override bool Matches(JSRuntimeInvocation invocation)
1932
{
20-
SetResultBase(default!);
33+
return Identifier.Equals(invocation.Identifier, StringComparison.Ordinal)
34+
&& _invocationMatcher(invocation.Arguments);
2135
}
2236
}
2337

2438
/// <summary>
25-
/// Represents a planned invocation of a JavaScript function with specific arguments.
39+
/// Represents a planned invocation of a JavaScript function which returns nothing, with specific arguments.
2640
/// </summary>
27-
/// <typeparam name="TResult"></typeparam>
28-
public class JSRuntimePlannedInvocation<TResult> : JSRuntimePlannedInvocationBase<TResult>
41+
public class JSRuntimePlannedInvocation : JSRuntimePlannedInvocation<object>
2942
{
3043
internal JSRuntimePlannedInvocation(string identifier, Func<IReadOnlyList<object?>, bool> matcher) : base(identifier, matcher)
31-
{
32-
}
44+
{ }
45+
46+
/// <summary>
47+
/// Completes the current awaiting void invocation requests.
48+
/// </summary>
49+
public void SetVoidResult() => SetResultBase(default!);
50+
}
51+
52+
/// <summary>
53+
/// Represents any planned invocation of a JavaScript function with a specific return type.
54+
/// </summary>
55+
/// <typeparam name="TResult"></typeparam>
56+
public class JSRuntimeCatchAllPlannedInvocation<TResult> : JSRuntimePlannedInvocationBase<TResult>
57+
{
58+
internal JSRuntimeCatchAllPlannedInvocation()
59+
{ }
60+
61+
internal override bool Matches(JSRuntimeInvocation invocation) => true;
3362

3463
/// <summary>
3564
/// Sets the <typeparamref name="TResult"/> result that invocations will receive.
3665
/// </summary>
3766
/// <param name="result"></param>
3867
public void SetResult(TResult result) => SetResultBase(result);
3968
}
69+
70+
/// <summary>
71+
/// Represents any planned invocation of a JavaScript function which returns nothing.
72+
/// </summary>
73+
public class JSRuntimeCatchAllPlannedInvocation : JSRuntimePlannedInvocationBase<object>
74+
{
75+
internal JSRuntimeCatchAllPlannedInvocation() { }
76+
77+
internal override bool Matches(JSRuntimeInvocation invocation)
78+
{
79+
return true;
80+
}
81+
82+
/// <summary>
83+
/// Completes the current awaiting void invocation request.
84+
/// </summary>
85+
public void SetVoid() => SetResultBase(default!);
86+
}
4087
}

src/bunit.web/TestDoubles/JSInterop/JSRuntimePlannedInvocationBase.cs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,8 @@ public abstract class JSRuntimePlannedInvocationBase<TResult>
1212
{
1313
private readonly List<JSRuntimeInvocation> _invocations;
1414

15-
private Func<IReadOnlyList<object?>, bool> InvocationMatcher { get; }
16-
1715
private TaskCompletionSource<TResult> _completionSource;
1816

19-
/// <summary>
20-
/// The expected identifier for the function to invoke.
21-
/// </summary>
22-
public string Identifier { get; }
23-
2417
/// <summary>
2518
/// Gets the invocations that this <see cref="JSRuntimePlannedInvocation{TResult}"/> has matched with.
2619
/// </summary>
@@ -29,11 +22,9 @@ public abstract class JSRuntimePlannedInvocationBase<TResult>
2922
/// <summary>
3023
/// Creates an instance of a <see cref="JSRuntimePlannedInvocationBase{TResult}"/>.
3124
/// </summary>
32-
protected JSRuntimePlannedInvocationBase(string identifier, Func<IReadOnlyList<object?>, bool> matcher)
25+
protected JSRuntimePlannedInvocationBase()
3326
{
34-
Identifier = identifier;
3527
_invocations = new List<JSRuntimeInvocation>();
36-
InvocationMatcher = matcher;
3728
_completionSource = new TaskCompletionSource<TResult>();
3829
}
3930

@@ -79,10 +70,6 @@ internal Task<TResult> RegisterInvocation(JSRuntimeInvocation invocation)
7970
return _completionSource.Task;
8071
}
8172

82-
internal bool Matches(JSRuntimeInvocation invocation)
83-
{
84-
return Identifier.Equals(invocation.Identifier, StringComparison.Ordinal)
85-
&& InvocationMatcher(invocation.Arguments);
86-
}
73+
internal abstract bool Matches(JSRuntimeInvocation invocation);
8774
}
8875
}

src/bunit.web/TestDoubles/JSInterop/MockJSRuntimeInvokeHandler.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class MockJSRuntimeInvokeHandler
1414
{
1515
private readonly Dictionary<string, List<JSRuntimeInvocation>> _invocations = new Dictionary<string, List<JSRuntimeInvocation>>();
1616
private readonly Dictionary<string, List<object>> _plannedInvocations = new Dictionary<string, List<object>>();
17+
private readonly Dictionary<Type, object> _catchAllInvocations = new Dictionary<Type, object>();
1718

1819
/// <summary>
1920
/// Gets a dictionary of all <see cref="List{JSRuntimeInvocation}"/> this mock has observed.
@@ -44,6 +45,20 @@ public IJSRuntime ToJSRuntime()
4445
return new MockJSRuntime(this);
4546
}
4647

48+
/// <summary>
49+
/// Configure a catch all JSInterop invocation for a specific return type.
50+
/// </summary>
51+
/// <typeparam name="TResult">The result type of the invocation</typeparam>
52+
/// <returns>A <see cref="JSRuntimeCatchAllPlannedInvocation{TResult}"/>.</returns>
53+
public JSRuntimeCatchAllPlannedInvocation<TResult> Setup<TResult>()
54+
{
55+
var result = new JSRuntimeCatchAllPlannedInvocation<TResult>();
56+
57+
_catchAllInvocations[typeof(TResult)] = result;
58+
59+
return result;
60+
}
61+
4762
/// <summary>
4863
/// Configure a planned JSInterop invocation with the <paramref name="identifier"/> and arguments
4964
/// passing the <paramref name="argumentsMatcher"/> test.
@@ -101,7 +116,20 @@ public JSRuntimePlannedInvocation SetupVoid(string identifier, params object[] a
101116
return SetupVoid(identifier, args => args.SequenceEqual(arguments));
102117
}
103118

104-
private void AddPlannedInvocation<TResult>(JSRuntimePlannedInvocationBase<TResult> planned)
119+
/// <summary>
120+
/// Configure a catch all JSInterop invocation, that should not receive any result.
121+
/// </summary>
122+
/// <returns>A <see cref="JSRuntimeCatchAllPlannedInvocation"/>.</returns>
123+
public JSRuntimeCatchAllPlannedInvocation SetupVoid()
124+
{
125+
var result = new JSRuntimeCatchAllPlannedInvocation();
126+
127+
_catchAllInvocations[typeof(object)] = result;
128+
129+
return result;
130+
}
131+
132+
private void AddPlannedInvocation<TResult>(JSRuntimePlannedInvocation<TResult> planned)
105133
{
106134
if (!_plannedInvocations.ContainsKey(planned.Identifier))
107135
{
@@ -143,7 +171,6 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
143171
private ValueTask<TValue>? TryHandlePlannedInvocation<TValue>(string identifier, JSRuntimeInvocation invocation)
144172
{
145173
ValueTask<TValue>? result = default;
146-
147174
if (_handlers._plannedInvocations.TryGetValue(identifier, out var plannedInvocations))
148175
{
149176
var planned = plannedInvocations.OfType<JSRuntimePlannedInvocationBase<TValue>>()
@@ -152,7 +179,18 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
152179
if (planned is not null)
153180
{
154181
var task = planned.RegisterInvocation(invocation);
155-
result = new ValueTask<TValue>(task);
182+
return new ValueTask<TValue>(task);
183+
}
184+
}
185+
186+
if (_handlers._catchAllInvocations.TryGetValue(typeof(TValue), out var catchAllInvocation))
187+
{
188+
var planned = catchAllInvocation as JSRuntimePlannedInvocationBase<TValue>;
189+
190+
if (planned is not null)
191+
{
192+
var task = ((JSRuntimePlannedInvocationBase<TValue>)catchAllInvocation).RegisterInvocation(invocation);
193+
return new ValueTask<TValue>(task);
156194
}
157195
}
158196

tests/bunit.web.tests/TestDoubles/JSInterop/MockJSRuntimeInvokeHandlerTest.cs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,5 +246,139 @@ await Should.ThrowAsync<UnplannedJSInvocationException>(
246246
invocation.Arguments[0].ShouldBe("bar");
247247
invocation.Arguments[1].ShouldBe(42);
248248
}
249+
250+
[Fact(DisplayName = "Empty Setup returns the same result for all matching return type invocation")]
251+
public async Task Test015()
252+
{
253+
var sut = new MockJSRuntimeInvokeHandler(JSRuntimeMockMode.Strict);
254+
var plannedInvoke = sut.Setup<Guid>();
255+
var jsRuntime = sut.ToJSRuntime();
256+
257+
var expectedResult1 = Guid.NewGuid();
258+
plannedInvoke.SetResult(expectedResult1);
259+
var i1 = jsRuntime.InvokeAsync<Guid>("someFunc");
260+
261+
var i2 = jsRuntime.InvokeAsync<Guid>("otherFunc");
262+
263+
(await i1).ShouldBe(expectedResult1);
264+
(await i2).ShouldBe(expectedResult1);
265+
}
266+
267+
[Fact(DisplayName = "Empty Setup only matches the configured return type")]
268+
public void Test016()
269+
{
270+
var sut = new MockJSRuntimeInvokeHandler(JSRuntimeMockMode.Strict);
271+
var planned = sut.Setup<Guid>();
272+
273+
Should.Throw<UnplannedJSInvocationException>(() => { var _ = sut.ToJSRuntime().InvokeAsync<string>("foo"); });
274+
275+
planned.Invocations.Count.ShouldBe(0);
276+
}
277+
278+
[Fact(DisplayName = "Empty Setup allows to return different results by return types")]
279+
public async Task Test017()
280+
{
281+
var sut = new MockJSRuntimeInvokeHandler(JSRuntimeMockMode.Strict);
282+
var plannedInvoke1 = sut.Setup<Guid>();
283+
var plannedInvoke2 = sut.Setup<string>();
284+
var jsRuntime = sut.ToJSRuntime();
285+
286+
var expectedResult1 = Guid.NewGuid();
287+
plannedInvoke1.SetResult(expectedResult1);
288+
var i1 = jsRuntime.InvokeAsync<Guid>("someFunc");
289+
290+
var expectedResult2 = "somestring";
291+
plannedInvoke2.SetResult(expectedResult2);
292+
var i2 = jsRuntime.InvokeAsync<string>("otherFunc");
293+
294+
(await i1).ShouldBe(expectedResult1);
295+
(await i2).ShouldBe(expectedResult2);
296+
}
297+
298+
[Fact(DisplayName = "Empty Setup is only used when there is no handler exist for the invocation identifier")]
299+
public async Task Test018()
300+
{
301+
var sut = new MockJSRuntimeInvokeHandler(JSRuntimeMockMode.Strict);
302+
var catchAllplannedInvoke = sut.Setup<Guid>();
303+
var jsRuntime = sut.ToJSRuntime();
304+
305+
var catchAllexpectedResult = Guid.NewGuid();
306+
catchAllplannedInvoke.SetResult(catchAllexpectedResult);
307+
308+
var expectedResult = Guid.NewGuid();
309+
var plannedInvoke = sut.Setup<Guid>("func");
310+
plannedInvoke.SetResult(expectedResult);
311+
312+
var i1 = jsRuntime.InvokeAsync<Guid>("someFunc");
313+
314+
var i2 = jsRuntime.InvokeAsync<Guid>("func");
315+
316+
(await i1).ShouldBe(catchAllexpectedResult);
317+
(await i2).ShouldBe(expectedResult);
318+
}
319+
320+
[Fact(DisplayName = "Empty Setup uses the last set result")]
321+
public async Task Test019()
322+
{
323+
var sut = new MockJSRuntimeInvokeHandler(JSRuntimeMockMode.Strict);
324+
var plannedInvoke1 = sut.Setup<Guid>();
325+
var plannedInvoke2 = sut.Setup<Guid>();
326+
var jsRuntime = sut.ToJSRuntime();
327+
328+
var expectedResult1 = Guid.NewGuid();
329+
var expectedResult2 = Guid.NewGuid();
330+
331+
plannedInvoke1.SetResult(expectedResult1);
332+
plannedInvoke2.SetResult(expectedResult2);
333+
334+
var i1 = jsRuntime.InvokeAsync<Guid>("someFunc");
335+
336+
(await i1).ShouldBe(expectedResult2);
337+
}
338+
339+
[Fact(DisplayName = "SetupVoid matches all void invocations")]
340+
public async Task Test020()
341+
{
342+
var identifier = "someFunc";
343+
var sut = new MockJSRuntimeInvokeHandler(JSRuntimeMockMode.Strict);
344+
var plannedInvoke = sut.SetupVoid();
345+
346+
Should.Throw<UnplannedJSInvocationException>(() => { var _ = sut.ToJSRuntime().InvokeAsync<string>(identifier); });
347+
348+
var invocation = sut.ToJSRuntime().InvokeVoidAsync(identifier);
349+
plannedInvoke.SetVoid();
350+
351+
await invocation;
352+
353+
invocation.IsCompletedSuccessfully.ShouldBeTrue();
354+
plannedInvoke.Invocations.Count.ShouldBe(1);
355+
}
356+
357+
[Fact(DisplayName = "Empty Setup is not used for invocation with void return types")]
358+
public async Task Test021()
359+
{
360+
var sut = new MockJSRuntimeInvokeHandler(JSRuntimeMockMode.Strict);
361+
var plannedInvoke = sut.Setup<Guid>();
362+
363+
await Should.ThrowAsync<UnplannedJSInvocationException>(sut.ToJSRuntime().InvokeVoidAsync("someFunc").AsTask());
364+
}
365+
366+
[Fact(DisplayName = "SetupVoid is only used when there is no void handler")]
367+
public async Task Test022()
368+
{
369+
var identifier = "someFunc";
370+
var sut = new MockJSRuntimeInvokeHandler(JSRuntimeMockMode.Strict);
371+
var plannedInvoke = sut.SetupVoid(identifier);
372+
var plannedCatchall = sut.SetupVoid();
373+
374+
var invocation = sut.ToJSRuntime().InvokeVoidAsync(identifier);
375+
plannedInvoke.SetVoidResult();
376+
377+
await invocation;
378+
379+
invocation.IsCompletedSuccessfully.ShouldBeTrue();
380+
plannedInvoke.Invocations.Count.ShouldBe(1);
381+
plannedCatchall.Invocations.Count.ShouldBe(0);
382+
}
249383
}
250384
}

0 commit comments

Comments
 (0)