Skip to content

Commit c78be10

Browse files
authored
Add SetupVoid and SetVoidResult capabilities to JsRuntime mock (#35)
* Moved MockJsRuntime to its own namespace * Add SetupVoid() and SetVoidResult() to PlannedInvocation and Mock * Updates to template and sample
1 parent 31c4a4f commit c78be10

File tree

9 files changed

+305
-29
lines changed

9 files changed

+305
-29
lines changed

sample/tests/Tests/Components/FocussingInputTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text;
55
using System.Threading.Tasks;
66
using Egil.RazorComponents.Testing.Asserting;
7+
using Egil.RazorComponents.Testing.Mocking.JSInterop;
78
using Egil.RazorComponents.Testing.SampleApp.Components;
89
using Egil.RazorComponents.Testing.Mocking.JSInterop;
910
using Xunit;
@@ -25,6 +26,7 @@ public void Test001()
2526
// Assert
2627
// that there is a single call to document.body.focus.call
2728
var invocation = jsRtMock.VerifyInvoke("document.body.focus.call");
29+
2830
// Assert that the invocation received a single argument
2931
// and that it was a reference to the input element.
3032
var expectedReferencedElement = cut.Find("input");

sample/tests/Tests/Components/TodoListTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Shouldly;
22
using AngleSharp.Dom;
33
using Egil.RazorComponents.Testing.Asserting;
4+
using Egil.RazorComponents.Testing.Mocking.JSInterop;
45
using Egil.RazorComponents.Testing.EventDispatchExtensions;
56
using Egil.RazorComponents.Testing.Mocking.JSInterop;
67
using Egil.RazorComponents.Testing.SampleApp.Components;

sample/tests/_Imports.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
@using Egil.RazorComponents.Testing.EventDispatchExtensions
55
@using Egil.RazorComponents.Testing.Mocking.JSInterop
66
@using Egil.RazorComponents.Testing.Asserting
7+
@using Egil.RazorComponents.Testing.Mocking.JSInterop
78

89
@using Egil.RazorComponents.Testing.SampleApp
910
@using Egil.RazorComponents.Testing.SampleApp.Data

src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,55 @@
55

66
namespace Egil.RazorComponents.Testing.Mocking.JSInterop
77
{
8+
/// <summary>
9+
/// Represents a planned invocation of a JavaScript function which returns nothing, with specific arguments.
10+
/// </summary>
11+
public class JsRuntimePlannedInvocation : JsRuntimePlannedInvocationBase<object>
12+
{
13+
internal JsRuntimePlannedInvocation(string identifier, Func<IReadOnlyList<object>, bool> matcher) : base(identifier, matcher)
14+
{
15+
}
16+
17+
/// <summary>
18+
/// Completes the current awaiting void invocation requests.
19+
/// </summary>
20+
public void SetVoidResult()
21+
{
22+
base.SetResultBase(default!);
23+
}
24+
}
25+
26+
/// <summary>
27+
/// Represents a planned invocation of a JavaScript function with specific arguments.
28+
/// </summary>
29+
/// <typeparam name="TResult"></typeparam>
30+
public class JsRuntimePlannedInvocation<TResult> : JsRuntimePlannedInvocationBase<TResult>
31+
{
32+
internal JsRuntimePlannedInvocation(string identifier, Func<IReadOnlyList<object>, bool> matcher) : base(identifier, matcher)
33+
{
34+
}
35+
36+
/// <summary>
37+
/// Sets the <typeparamref name="TResult"/> result that invocations will receive.
38+
/// </summary>
39+
/// <param name="result"></param>
40+
public void SetResult(TResult result)
41+
{
42+
base.SetResultBase(result);
43+
}
44+
}
45+
846
/// <summary>
947
/// Represents a planned invocation of a JavaScript function with specific arguments.
1048
/// </summary>
1149
/// <typeparam name="TResult"></typeparam>
12-
[SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "<Pending>")]
13-
public readonly struct JsRuntimePlannedInvocation<TResult>
50+
public abstract class JsRuntimePlannedInvocationBase<TResult>
1451
{
1552
private readonly List<JsRuntimeInvocation> _invocations;
53+
1654
private Func<IReadOnlyList<object>, bool> InvocationMatcher { get; }
17-
internal TaskCompletionSource<TResult> CompletionSource { get; }
55+
56+
private TaskCompletionSource<TResult> _completionSource;
1857

1958
/// <summary>
2059
/// The expected identifier for the function to invoke.
@@ -26,36 +65,52 @@ public readonly struct JsRuntimePlannedInvocation<TResult>
2665
/// </summary>
2766
public IReadOnlyList<JsRuntimeInvocation> Invocations => _invocations.AsReadOnly();
2867

29-
internal JsRuntimePlannedInvocation(string identifier, Func<IReadOnlyList<object>, bool> matcher)
68+
/// <summary>
69+
/// Creates an instance of a <see cref="JsRuntimePlannedInvocationBase{TResult}"/>.
70+
/// </summary>
71+
protected JsRuntimePlannedInvocationBase(string identifier, Func<IReadOnlyList<object>, bool> matcher)
3072
{
3173
Identifier = identifier;
3274
_invocations = new List<JsRuntimeInvocation>();
3375
InvocationMatcher = matcher;
34-
CompletionSource = new TaskCompletionSource<TResult>();
76+
_completionSource = new TaskCompletionSource<TResult>();
3577
}
3678

3779
/// <summary>
3880
/// Sets the <typeparamref name="TResult"/> result that invocations will receive.
3981
/// </summary>
4082
/// <param name="result"></param>
41-
public void SetResult(TResult result) => CompletionSource.SetResult(result);
83+
protected void SetResultBase(TResult result)
84+
{
85+
_completionSource.SetResult(result);
86+
}
4287

4388
/// <summary>
4489
/// Sets the <typeparamref name="TException"/> exception that invocations will receive.
4590
/// </summary>
4691
/// <param name="exception"></param>
47-
public void SetException<TException>(TException exception)
92+
public void SetException<TException>(TException exception)
4893
where TException : Exception
49-
=> CompletionSource.SetException(exception);
94+
{
95+
_completionSource.SetException(exception);
96+
}
5097

5198
/// <summary>
5299
/// Marks the <see cref="Task{TResult}"/> that invocations will receive as canceled.
53100
/// </summary>
54-
public void SetCanceled() => CompletionSource.SetCanceled();
101+
public void SetCanceled()
102+
{
103+
_completionSource.SetCanceled();
104+
}
55105

56-
internal void AddInvocation(JsRuntimeInvocation invocation)
106+
internal Task<TResult> RegisterInvocation(JsRuntimeInvocation invocation)
57107
{
108+
if (_completionSource.Task.IsCompleted)
109+
_completionSource = new TaskCompletionSource<TResult>();
110+
58111
_invocations.Add(invocation);
112+
113+
return _completionSource.Task;
59114
}
60115

61116
internal bool Matches(JsRuntimeInvocation invocation)

src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public IJSRuntime ToJsRuntime()
5252
/// <typeparam name="TResult">The result type of the invocation</typeparam>
5353
/// <param name="identifier">The identifier to setup a response for</param>
5454
/// <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>
55-
/// <returns>A <see cref="TaskCompletionSource{TResult}"/> whose <see cref="Task"/> is returned when the <paramref name="identifier"/> is invoked.</returns>
55+
/// <returns>A <see cref="JsRuntimePlannedInvocation{TResult}"/>.</returns>
5656
public JsRuntimePlannedInvocation<TResult> Setup<TResult>(string identifier, Func<IReadOnlyList<object>, bool> argumentsMatcher)
5757
{
5858
var result = new JsRuntimePlannedInvocation<TResult>(identifier, argumentsMatcher);
@@ -68,13 +68,41 @@ public JsRuntimePlannedInvocation<TResult> Setup<TResult>(string identifier, Fun
6868
/// <typeparam name="TResult"></typeparam>
6969
/// <param name="identifier">The identifier to setup a response for</param>
7070
/// <param name="arguments">The arguments that an invocation to <paramref name="identifier"/> should match.</param>
71-
/// <returns>A <see cref="TaskCompletionSource{TResult}"/> whose <see cref="Task"/> is returned when the <paramref name="identifier"/> is invoked.</returns>
71+
/// <returns>A <see cref="JsRuntimePlannedInvocation{TResult}"/>.</returns>
7272
public JsRuntimePlannedInvocation<TResult> Setup<TResult>(string identifier, params object[] arguments)
7373
{
7474
return Setup<TResult>(identifier, args => Enumerable.SequenceEqual(args, arguments));
7575
}
7676

77-
private void AddPlannedInvocation<TResult>(JsRuntimePlannedInvocation<TResult> planned)
77+
/// <summary>
78+
/// Configure a planned JSInterop invocation with the <paramref name="identifier"/> and arguments
79+
/// passing the <paramref name="argumentsMatcher"/> test, that should not receive any result.
80+
/// </summary>
81+
/// <param name="identifier">The identifier to setup a response for</param>
82+
/// <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>
83+
/// <returns>A <see cref="JsRuntimePlannedInvocation"/>.</returns>
84+
public JsRuntimePlannedInvocation SetupVoid(string identifier, Func<IReadOnlyList<object>, bool> argumentsMatcher)
85+
{
86+
var result = new JsRuntimePlannedInvocation(identifier, argumentsMatcher);
87+
88+
AddPlannedInvocation(result);
89+
90+
return result;
91+
}
92+
93+
/// <summary>
94+
/// Configure a planned JSInterop invocation with the <paramref name="identifier"/>
95+
/// and <paramref name="arguments"/>, that should not receive any result.
96+
/// </summary>
97+
/// <param name="identifier">The identifier to setup a response for</param>
98+
/// <param name="arguments">The arguments that an invocation to <paramref name="identifier"/> should match.</param>
99+
/// <returns>A <see cref="JsRuntimePlannedInvocation"/>.</returns>
100+
public JsRuntimePlannedInvocation SetupVoid(string identifier, params object[] arguments)
101+
{
102+
return SetupVoid(identifier, args => Enumerable.SequenceEqual(args, arguments));
103+
}
104+
105+
private void AddPlannedInvocation<TResult>(JsRuntimePlannedInvocationBase<TResult> planned)
78106
{
79107
if (!_plannedInvocations.ContainsKey(planned.Identifier))
80108
{
@@ -119,15 +147,13 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
119147

120148
if (_handlers._plannedInvocations.TryGetValue(identifier, out var plannedInvocations))
121149
{
122-
var planned = plannedInvocations.OfType<JsRuntimePlannedInvocation<TValue>>()
150+
var planned = plannedInvocations.OfType<JsRuntimePlannedInvocationBase<TValue>>()
123151
.SingleOrDefault(x => x.Matches(invocation));
124152

125-
// TODO: Should we check the CancellationToken at this point and automatically call
126-
// TrySetCanceled(CancellationToken) on the TaskCompletionSource? (https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.trysetcanceled?view=netcore-3.0#System_Threading_Tasks_TaskCompletionSource_1_TrySetCanceled_System_Threading_CancellationToken_)
127153
if (planned is { })
128154
{
129-
planned.AddInvocation(invocation);
130-
result = new ValueTask<TValue>(planned.CompletionSource.Task);
155+
var task = planned.RegisterInvocation(invocation);
156+
result = new ValueTask<TValue>(task);
131157
}
132158
}
133159

src/Mocking/JSInterop/UnplannedJsInvocationException.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,25 @@ public class UnplannedJsInvocationException : Exception
2525
/// </summary>
2626
/// <param name="invocation">The unplanned invocation.</param>
2727
public UnplannedJsInvocationException(JsRuntimeInvocation invocation)
28-
: base($"The invocation of '{invocation.Identifier} with arguments '[{PrintArguments(invocation.Arguments)}]")
28+
: base($"The invocation of '{invocation.Identifier}' {PrintArguments(invocation.Arguments)} was not expected.")
2929
{
3030
Invocation = invocation;
3131
}
3232

3333
private static string PrintArguments(IReadOnlyList<object> arguments)
3434
{
35-
return string.Join(", ", arguments.Select(x => x.ToString()));
35+
if (arguments.Count == 0)
36+
{
37+
return "without arguments";
38+
}
39+
else if (arguments.Count == 1)
40+
{
41+
return $"with the argument [{arguments[0].ToString()}]";
42+
}
43+
else
44+
{
45+
return $"with arguments [{string.Join(", ", arguments.Select(x => x.ToString()))}]";
46+
}
3647
}
3748
}
3849
}

template/template/Component1Test.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Egil.RazorComponents.Testing;
44
using Egil.RazorComponents.Testing.Diffing;
55
using Egil.RazorComponents.Testing.Asserting;
6+
using Egil.RazorComponents.Testing.Mocking.JSInterop;
67
using Egil.RazorComponents.Testing.EventDispatchExtensions;
78

89
namespace Company.RazorTests1

template/template/_Imports.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
@using Egil.RazorComponents.Testing.Diffing
44
@using Egil.RazorComponents.Testing.Asserting
55
@using Egil.RazorComponents.Testing.EventDispatchExtensions
6+
@using Egil.RazorComponents.Testing.Mocking.JSInterop
67
@using Xunit

0 commit comments

Comments
 (0)