Skip to content

Commit 57c6bfa

Browse files
committed
feat: Error when no setup is made for JS
1 parent 47f9c22 commit 57c6bfa

File tree

3 files changed

+153
-1
lines changed

3 files changed

+153
-1
lines changed

src/bunit/JSInterop/InvocationHandlers/JSRuntimeInvocationHandlerBase{TResult}.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,14 @@ protected void SetResultBase(TResult result)
8282
protected internal virtual Task<TResult> HandleAsync(JSRuntimeInvocation invocation)
8383
{
8484
Invocations.RegisterInvocation(invocation);
85-
return completionSource.Task;
85+
86+
var task = completionSource.Task;
87+
if (task is { IsCanceled: false, IsFaulted: false, IsCompletedSuccessfully: false })
88+
{
89+
throw new JSRuntimeInvocationNotSetException(invocation);
90+
}
91+
92+
return task;
8693
}
8794

8895
/// <summary>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using System.Text;
2+
3+
namespace Bunit;
4+
5+
/// <summary>
6+
/// Exception used to indicate that an invocation was received by a JSRuntime invocation handler,
7+
/// but the handler was not configured with a result (via SetResult, SetVoidResult, SetCanceled, or SetException).
8+
/// This causes the invocation to hang indefinitely.
9+
/// </summary>
10+
public sealed class JSRuntimeInvocationNotSetException : Exception
11+
{
12+
/// <summary>
13+
/// Gets the invocation that was not handled with a result.
14+
/// </summary>
15+
public JSRuntimeInvocation Invocation { get; }
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="JSRuntimeInvocationNotSetException"/> class
19+
/// with the provided <see cref="Invocation"/> attached.
20+
/// </summary>
21+
/// <param name="invocation">The invocation that was not provided with a result.</param>
22+
public JSRuntimeInvocationNotSetException(JSRuntimeInvocation invocation)
23+
: base(CreateErrorMessage(invocation))
24+
{
25+
Invocation = invocation;
26+
}
27+
28+
[SuppressMessage("Minor Code Smell", "S6618:\"string.Create\" should be used instead of \"FormattableString\"", Justification = "string.Create not supported in all TFs")]
29+
private static string CreateErrorMessage(JSRuntimeInvocation invocation)
30+
{
31+
var sb = new StringBuilder();
32+
sb.AppendLine("bUnit's JSInterop invocation handler was setup to handle the call:");
33+
sb.AppendLine();
34+
35+
if (invocation.IsVoidResultInvocation)
36+
{
37+
sb.AppendLine(FormattableString.Invariant($" {invocation.InvocationMethodName}({GetArguments(invocation)})"));
38+
}
39+
else
40+
{
41+
sb.AppendLine(FormattableString.Invariant($" {invocation.InvocationMethodName}<{GetGenericInvocationArguments(invocation)}>({GetArguments(invocation)})"));
42+
}
43+
44+
sb.AppendLine();
45+
sb.AppendLine("However, the invocation handler was not configured to return a result,");
46+
sb.AppendLine("causing the invocation to hang indefinitely.");
47+
sb.AppendLine();
48+
sb.AppendLine("To fix this, configure the handler to return a result using one of the following methods:");
49+
sb.AppendLine();
50+
51+
if (invocation.IsVoidResultInvocation)
52+
{
53+
sb.AppendLine(" handler.SetVoidResult();");
54+
}
55+
else
56+
{
57+
sb.AppendLine(FormattableString.Invariant($" handler.SetResult({GetExampleResult(invocation.ResultType)});"));
58+
}
59+
60+
sb.AppendLine(" handler.SetCanceled();");
61+
sb.AppendLine(" handler.SetException(new Exception(\"error message\"));");
62+
return sb.ToString();
63+
}
64+
65+
private static string GetArguments(JSRuntimeInvocation invocation)
66+
{
67+
if (!invocation.Arguments.Any())
68+
return $"\"{invocation.Identifier}\"";
69+
70+
var argStrings = invocation.Arguments.Select(FormatArgument).Prepend($"\"{invocation.Identifier}\"");
71+
return string.Join(", ", argStrings);
72+
}
73+
74+
private static string GetGenericInvocationArguments(JSRuntimeInvocation invocation)
75+
{
76+
return GetReturnTypeName(invocation.ResultType);
77+
}
78+
79+
private static string FormatArgument(object? arg)
80+
{
81+
return arg switch
82+
{
83+
null => "null",
84+
string str => $"\"{str}\"",
85+
char c => $"'{c}'",
86+
bool b => b.ToString().ToUpperInvariant(),
87+
_ => arg.ToString() ?? "null"
88+
};
89+
}
90+
91+
private static string GetReturnTypeName(Type resultType)
92+
=> resultType switch
93+
{
94+
Type { FullName: "System.Boolean" } => "bool",
95+
Type { FullName: "System.Byte" } => "byte",
96+
Type { FullName: "System.Char" } => "char",
97+
Type { FullName: "System.Double" } => "double",
98+
Type { FullName: "System.Int16" } => "short",
99+
Type { FullName: "System.Int32" } => "int",
100+
Type { FullName: "System.Int64" } => "long",
101+
Type { FullName: "System.Single" } => "float",
102+
Type { FullName: "System.String" } => "string",
103+
Type { FullName: "System.Decimal" } => "decimal",
104+
Type { FullName: "System.Guid" } => "Guid",
105+
Type { FullName: "System.DateTime" } => "DateTime",
106+
Type { FullName: "System.DateTimeOffset" } => "DateTimeOffset",
107+
Type { FullName: "System.TimeSpan" } => "TimeSpan",
108+
Type { FullName: "System.Object" } => "object",
109+
_ => resultType.Name
110+
};
111+
112+
private static string GetExampleResult(Type resultType)
113+
=> resultType switch
114+
{
115+
Type { FullName: "System.Boolean" } => "true",
116+
Type { FullName: "System.Byte" } => "1",
117+
Type { FullName: "System.Char" } => "'a'",
118+
Type { FullName: "System.Double" } => "1.0",
119+
Type { FullName: "System.Int16" } => "1",
120+
Type { FullName: "System.Int32" } => "1",
121+
Type { FullName: "System.Int64" } => "1L",
122+
Type { FullName: "System.Single" } => "1.0f",
123+
Type { FullName: "System.String" } => "\"result\"",
124+
Type { FullName: "System.Decimal" } => "1.0m",
125+
Type { FullName: "System.Guid" } => "Guid.NewGuid()",
126+
Type { FullName: "System.DateTime" } => "DateTime.Now",
127+
Type { FullName: "System.DateTimeOffset" } => "DateTimeOffset.Now",
128+
Type { FullName: "System.TimeSpan" } => "TimeSpan.FromSeconds(1)",
129+
Type { FullName: "System.Object" } => "new object()",
130+
_ => $"new {resultType.Name}()"
131+
};
132+
}

tests/bunit.tests/JSInterop/BunitJSInteropTest.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,4 +686,17 @@ public void Test308(string identifier, string arg0, string arg1, string arg2)
686686
invocationMethodName: "InvokeUnmarshalled"));
687687
}
688688
#endif
689+
690+
[Fact(DisplayName = "JSRuntime invocation throws exception immediately when handler is not configured with result")]
691+
public void Test309()
692+
{
693+
var identifier = "testFunction";
694+
var sut = CreateSut(JSRuntimeMode.Strict);
695+
sut.Setup<int>(identifier);
696+
697+
var exception = Should.Throw<JSRuntimeInvocationNotSetException>(
698+
() => sut.JSRuntime.InvokeAsync<int>(identifier));
699+
700+
exception.Invocation.Identifier.ShouldBe(identifier);
701+
}
689702
}

0 commit comments

Comments
 (0)