Skip to content

Commit 2144361

Browse files
committed
fix: Add impl for JS in net10 (fixes #1818)
1 parent 0782331 commit 2144361

File tree

5 files changed

+133
-0
lines changed

5 files changed

+133
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ All notable changes to **bUnit** will be documented in this file. The project ad
66

77
## [Unreleased]
88

9+
### Fixed
10+
11+
- Implemented `InvokeConstructorAsync` on `BunitJSRuntime` and `BunitJSObjectReference` for .NET 10+, which previously threw `NotImplementedException`. Reported by [@Floopy-Doo](https://github.com/Floopy-Doo) in #1818. Fixed by [@linkdotnet](https://github.com/linkdotnet).
12+
913
## [2.6.2] - 2026-02-27
1014

1115
### Added

src/bunit/JSInterop/Implementation/BunitJSObjectReference.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
2525

2626
#if NET10_0_OR_GREATER
2727
/// <inheritdoc/>
28+
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, object?[]? args)
29+
=> JSInterop.HandleInvokeConstructorAsync(identifier, args);
30+
31+
/// <inheritdoc/>
32+
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
33+
=> JSInterop.HandleInvokeConstructorAsync(identifier, cancellationToken, args);
2834

2935
/// <inheritdoc/>
3036
public ValueTask<TValue> GetValueAsync<TValue>(string identifier) => throw new NotImplementedException();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#if NET10_0_OR_GREATER
2+
using Bunit.JSInterop.Implementation;
3+
4+
namespace Bunit.JSInterop;
5+
6+
/// <summary>
7+
/// bUnit's implementation of the <c>InvokeConstructorAsync</c> methods on <see cref="IJSRuntime"/>.
8+
/// </summary>
9+
internal sealed partial class BunitJSRuntime
10+
{
11+
/// <inheritdoc/>
12+
ValueTask<IJSObjectReference> IJSRuntime.InvokeConstructorAsync(string identifier, object?[]? args)
13+
=> JSInterop.HandleInvokeConstructorAsync(identifier, args);
14+
15+
/// <inheritdoc/>
16+
ValueTask<IJSObjectReference> IJSRuntime.InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
17+
=> JSInterop.HandleInvokeConstructorAsync(identifier, cancellationToken, args);
18+
}
19+
#endif

src/bunit/JSInterop/Implementation/JSRuntimeExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,21 @@ internal static TResult HandleInvokeUnmarshalled<T0, T1, T2, TResult>(this Bunit
8585
.GetResult();
8686
}
8787

88+
#if NET10_0_OR_GREATER
89+
internal static ValueTask<IJSObjectReference> HandleInvokeConstructorAsync(this BunitJSInterop jSInterop, string identifier, object?[]? args)
90+
{
91+
var invocation = new JSRuntimeInvocation(identifier, null, args, typeof(IJSObjectReference), "InvokeConstructorAsync");
92+
return jSInterop.HandleInvocation<IJSObjectReference>(invocation);
93+
}
94+
95+
[SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification = "Matching Blazor's JSRuntime design.")]
96+
internal static ValueTask<IJSObjectReference> HandleInvokeConstructorAsync(this BunitJSInterop jSInterop, string identifier, CancellationToken cancellationToken, object?[]? args)
97+
{
98+
var invocation = new JSRuntimeInvocation(identifier, cancellationToken, args, typeof(IJSObjectReference), "InvokeConstructorAsync");
99+
return jSInterop.HandleInvocation<IJSObjectReference>(invocation);
100+
}
101+
#endif
102+
88103
private static string GetInvokeAsyncMethodName<TValue>()
89104
=> typeof(TValue) == typeof(Microsoft.JSInterop.Infrastructure.IJSVoidResult)
90105
? "InvokeVoidAsync"

tests/bunit.tests/JSInterop/BunitJSInteropTest.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,4 +702,93 @@ public async Task Test309()
702702
var exception = await Should.ThrowAsync<JSRuntimeInvocationNotSetException>(invocationTask.AsTask());
703703
exception.Invocation.Identifier.ShouldBe(identifier);
704704
}
705+
706+
#if NET10_0_OR_GREATER
707+
[Fact(DisplayName = "InvokeConstructorAsync returns IJSObjectReference in loose mode without setup")]
708+
public async Task Test400()
709+
{
710+
var sut = CreateSut(JSRuntimeMode.Loose);
711+
712+
var result = await sut.JSRuntime.InvokeConstructorAsync("SomeClass");
713+
714+
result.ShouldNotBeNull();
715+
result.ShouldBeAssignableTo<IJSObjectReference>();
716+
}
717+
718+
[Fact(DisplayName = "InvokeConstructorAsync throws in strict mode when no handler is set up")]
719+
public void Test401()
720+
{
721+
var sut = CreateSut(JSRuntimeMode.Strict);
722+
723+
Should.Throw<JSRuntimeUnhandledInvocationException>(
724+
async () => await sut.JSRuntime.InvokeConstructorAsync("SomeClass"));
725+
}
726+
727+
[Theory(DisplayName = "InvokeConstructorAsync records invocation with correct method name and arguments"), AutoData]
728+
public void Test402(string identifier)
729+
{
730+
var args = new object[] { "arg1", 42 };
731+
var sut = CreateSut(JSRuntimeMode.Loose);
732+
733+
sut.JSRuntime.InvokeConstructorAsync(identifier, args);
734+
735+
var invocation = sut.Invocations[identifier].ShouldHaveSingleItem();
736+
invocation.Identifier.ShouldBe(identifier);
737+
invocation.Arguments.ShouldBe(args);
738+
invocation.InvocationMethodName.ShouldBe("InvokeConstructorAsync");
739+
invocation.ResultType.ShouldBe(typeof(IJSObjectReference));
740+
}
741+
742+
[Theory(DisplayName = "InvokeConstructorAsync with CancellationToken records invocation correctly"), AutoData]
743+
public void Test403(string identifier)
744+
{
745+
var args = new object[] { "arg1" };
746+
using var cts = new CancellationTokenSource();
747+
var sut = CreateSut(JSRuntimeMode.Loose);
748+
749+
sut.JSRuntime.InvokeConstructorAsync(identifier, cts.Token, args);
750+
751+
var invocation = sut.Invocations[identifier].ShouldHaveSingleItem();
752+
invocation.Identifier.ShouldBe(identifier);
753+
invocation.Arguments.ShouldBe(args);
754+
invocation.CancellationToken.ShouldBe(cts.Token);
755+
invocation.InvocationMethodName.ShouldBe("InvokeConstructorAsync");
756+
}
757+
758+
[Fact(DisplayName = "InvokeConstructorAsync with SetupModule handler returns configured object reference")]
759+
public async Task Test404()
760+
{
761+
var sut = CreateSut(JSRuntimeMode.Strict);
762+
sut.SetupModule(inv => inv.Identifier == "SomeClass" && inv.InvocationMethodName == "InvokeConstructorAsync");
763+
764+
var result = await sut.JSRuntime.InvokeConstructorAsync("SomeClass");
765+
766+
result.ShouldNotBeNull();
767+
result.ShouldBeAssignableTo<IJSObjectReference>();
768+
}
769+
770+
[Fact(DisplayName = "InvokeConstructorAsync on IJSObjectReference from module import works in loose mode")]
771+
public async Task Test405()
772+
{
773+
var sut = CreateSut(JSRuntimeMode.Loose);
774+
775+
var module = await sut.JSRuntime.InvokeAsync<IJSObjectReference>("import", "./myModule.js");
776+
var result = await module.InvokeConstructorAsync("JsClass", "arg1", "arg2");
777+
778+
result.ShouldNotBeNull();
779+
result.ShouldBeAssignableTo<IJSObjectReference>();
780+
}
781+
782+
[Fact(DisplayName = "InvokeConstructorAsync on IJSObjectReference records invocation")]
783+
public async Task Test406()
784+
{
785+
var sut = CreateSut(JSRuntimeMode.Loose);
786+
787+
var module = await sut.JSRuntime.InvokeAsync<IJSObjectReference>("import", "./myModule.js");
788+
await module.InvokeConstructorAsync("JsClass", "arg1");
789+
790+
sut.Invocations["JsClass"].ShouldHaveSingleItem()
791+
.InvocationMethodName.ShouldBe("InvokeConstructorAsync");
792+
}
793+
#endif
705794
}

0 commit comments

Comments
 (0)