diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dddd023ad..ed97f5e09 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -78,7 +78,7 @@ jobs:
dotnet pack src/bunit.generators/ -c release --output ${{ env.NUGET_DIRECTORY }} -p:ContinuousIntegrationBuild=true -p:publicrelease=true
# Publish the NuGet package as an artifact, so they can be used in the following jobs
- - uses: actions/upload-artifact@v6
+ - uses: actions/upload-artifact@v7
with:
name: ${{ env.NUGET_PACKAGES_ARTIFACT }}
if-no-files-found: error
@@ -93,7 +93,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v5
- - uses: actions/download-artifact@v7
+ - uses: actions/download-artifact@v8
with:
name: ${{ env.NUGET_PACKAGES_ARTIFACT }}
path: ${{ env.NUGET_DIRECTORY }}
@@ -141,7 +141,7 @@ jobs:
- name: 📛 Upload hang- and crash-dumps on test failure
if: success() || failure()
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
with:
if-no-files-found: ignore
name: test-dumps
@@ -163,7 +163,7 @@ jobs:
dotnet-version: |
10.0.x
- - uses: actions/download-artifact@v7
+ - uses: actions/download-artifact@v8
with:
name: ${{ env.NUGET_PACKAGES_ARTIFACT }}
path: ${{ env.NUGET_DIRECTORY }}
@@ -278,7 +278,7 @@ jobs:
permissions:
id-token: write
steps:
- - uses: actions/download-artifact@v7
+ - uses: actions/download-artifact@v8
with:
name: ${{ env.NUGET_PACKAGES_ARTIFACT }}
path: ${{ env.NUGET_DIRECTORY }}
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
index b12424a89..75a18d275 100644
--- a/.github/workflows/docs-deploy.yml
+++ b/.github/workflows/docs-deploy.yml
@@ -37,7 +37,7 @@ jobs:
- name: ⚙️ Import GPG key
id: import_gpg
- uses: crazy-max/ghaction-import-gpg@v6
+ uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.BUNIT_BOT_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.BUNIT_BOT_GPG_KEY_PASSPHRASE }}
@@ -98,7 +98,7 @@ jobs:
- name: 🛠️ Deploy to GitHub Pages
if: success()
- uses: crazy-max/ghaction-github-pages@v4
+ uses: crazy-max/ghaction-github-pages@v5
with:
build_dir: docs/site/_site
fqdn: bunit.dev
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index 9f22471b8..ed15be861 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -47,7 +47,7 @@ jobs:
- name: ⚙️ Import GPG key
id: import_gpg
- uses: crazy-max/ghaction-import-gpg@v6
+ uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.BUNIT_BOT_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.BUNIT_BOT_GPG_KEY_PASSPHRASE }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a51e8bffa..4dd478931 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -41,7 +41,7 @@ jobs:
- name: ⚙️ Import GPG key
id: import_gpg
- uses: crazy-max/ghaction-import-gpg@v6
+ uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.BUNIT_BOT_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.BUNIT_BOT_GPG_KEY_PASSPHRASE }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6aad902f8..4ae0c4144 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ All notable changes to **bUnit** will be documented in this file. The project ad
## [Unreleased]
+### Fixed
+
+- 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).
+
## [2.6.2] - 2026-02-27
### Added
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 720f03d11..9a478743b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -72,18 +72,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/bunit/JSInterop/Implementation/BunitJSObjectReference.cs b/src/bunit/JSInterop/Implementation/BunitJSObjectReference.cs
index bf6c434a2..fdaaac958 100644
--- a/src/bunit/JSInterop/Implementation/BunitJSObjectReference.cs
+++ b/src/bunit/JSInterop/Implementation/BunitJSObjectReference.cs
@@ -25,6 +25,12 @@ public ValueTask InvokeAsync(string identifier, CancellationToke
#if NET10_0_OR_GREATER
///
+ public ValueTask InvokeConstructorAsync(string identifier, object?[]? args)
+ => JSInterop.HandleInvokeConstructorAsync(identifier, args);
+
+ ///
+ public ValueTask InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
+ => JSInterop.HandleInvokeConstructorAsync(identifier, cancellationToken, args);
///
public ValueTask GetValueAsync(string identifier) => throw new NotImplementedException();
diff --git a/src/bunit/JSInterop/Implementation/BunitJSRuntime.net10.cs b/src/bunit/JSInterop/Implementation/BunitJSRuntime.net10.cs
new file mode 100644
index 000000000..5fd991935
--- /dev/null
+++ b/src/bunit/JSInterop/Implementation/BunitJSRuntime.net10.cs
@@ -0,0 +1,19 @@
+#if NET10_0_OR_GREATER
+using Bunit.JSInterop.Implementation;
+
+namespace Bunit.JSInterop;
+
+///
+/// bUnit's implementation of the InvokeConstructorAsync methods on .
+///
+internal sealed partial class BunitJSRuntime
+{
+ ///
+ ValueTask IJSRuntime.InvokeConstructorAsync(string identifier, object?[]? args)
+ => JSInterop.HandleInvokeConstructorAsync(identifier, args);
+
+ ///
+ ValueTask IJSRuntime.InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
+ => JSInterop.HandleInvokeConstructorAsync(identifier, cancellationToken, args);
+}
+#endif
diff --git a/src/bunit/JSInterop/Implementation/JSRuntimeExtensions.cs b/src/bunit/JSInterop/Implementation/JSRuntimeExtensions.cs
index 2490e4bc6..e5d9f5159 100644
--- a/src/bunit/JSInterop/Implementation/JSRuntimeExtensions.cs
+++ b/src/bunit/JSInterop/Implementation/JSRuntimeExtensions.cs
@@ -85,6 +85,21 @@ internal static TResult HandleInvokeUnmarshalled(this Bunit
.GetResult();
}
+#if NET10_0_OR_GREATER
+ internal static ValueTask HandleInvokeConstructorAsync(this BunitJSInterop jSInterop, string identifier, object?[]? args)
+ {
+ var invocation = new JSRuntimeInvocation(identifier, null, args, typeof(IJSObjectReference), "InvokeConstructorAsync");
+ return jSInterop.HandleInvocation(invocation);
+ }
+
+ [SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification = "Matching Blazor's JSRuntime design.")]
+ internal static ValueTask HandleInvokeConstructorAsync(this BunitJSInterop jSInterop, string identifier, CancellationToken cancellationToken, object?[]? args)
+ {
+ var invocation = new JSRuntimeInvocation(identifier, cancellationToken, args, typeof(IJSObjectReference), "InvokeConstructorAsync");
+ return jSInterop.HandleInvocation(invocation);
+ }
+#endif
+
private static string GetInvokeAsyncMethodName()
=> typeof(TValue) == typeof(Microsoft.JSInterop.Infrastructure.IJSVoidResult)
? "InvokeVoidAsync"
diff --git a/tests/bunit.tests/JSInterop/BunitJSInteropTest.cs b/tests/bunit.tests/JSInterop/BunitJSInteropTest.cs
index f5140f855..2591a9031 100644
--- a/tests/bunit.tests/JSInterop/BunitJSInteropTest.cs
+++ b/tests/bunit.tests/JSInterop/BunitJSInteropTest.cs
@@ -702,4 +702,93 @@ public async Task Test309()
var exception = await Should.ThrowAsync(invocationTask.AsTask());
exception.Invocation.Identifier.ShouldBe(identifier);
}
+
+#if NET10_0_OR_GREATER
+ [Fact(DisplayName = "InvokeConstructorAsync returns IJSObjectReference in loose mode without setup")]
+ public async Task Test400()
+ {
+ var sut = CreateSut(JSRuntimeMode.Loose);
+
+ var result = await sut.JSRuntime.InvokeConstructorAsync("SomeClass");
+
+ result.ShouldNotBeNull();
+ result.ShouldBeAssignableTo();
+ }
+
+ [Fact(DisplayName = "InvokeConstructorAsync throws in strict mode when no handler is set up")]
+ public void Test401()
+ {
+ var sut = CreateSut(JSRuntimeMode.Strict);
+
+ Should.Throw(
+ async () => await sut.JSRuntime.InvokeConstructorAsync("SomeClass"));
+ }
+
+ [Theory(DisplayName = "InvokeConstructorAsync records invocation with correct method name and arguments"), AutoData]
+ public void Test402(string identifier)
+ {
+ var args = new object[] { "arg1", 42 };
+ var sut = CreateSut(JSRuntimeMode.Loose);
+
+ sut.JSRuntime.InvokeConstructorAsync(identifier, args);
+
+ var invocation = sut.Invocations[identifier].ShouldHaveSingleItem();
+ invocation.Identifier.ShouldBe(identifier);
+ invocation.Arguments.ShouldBe(args);
+ invocation.InvocationMethodName.ShouldBe("InvokeConstructorAsync");
+ invocation.ResultType.ShouldBe(typeof(IJSObjectReference));
+ }
+
+ [Theory(DisplayName = "InvokeConstructorAsync with CancellationToken records invocation correctly"), AutoData]
+ public void Test403(string identifier)
+ {
+ var args = new object[] { "arg1" };
+ using var cts = new CancellationTokenSource();
+ var sut = CreateSut(JSRuntimeMode.Loose);
+
+ sut.JSRuntime.InvokeConstructorAsync(identifier, cts.Token, args);
+
+ var invocation = sut.Invocations[identifier].ShouldHaveSingleItem();
+ invocation.Identifier.ShouldBe(identifier);
+ invocation.Arguments.ShouldBe(args);
+ invocation.CancellationToken.ShouldBe(cts.Token);
+ invocation.InvocationMethodName.ShouldBe("InvokeConstructorAsync");
+ }
+
+ [Fact(DisplayName = "InvokeConstructorAsync with SetupModule handler returns configured object reference")]
+ public async Task Test404()
+ {
+ var sut = CreateSut(JSRuntimeMode.Strict);
+ sut.SetupModule(inv => inv.Identifier == "SomeClass" && inv.InvocationMethodName == "InvokeConstructorAsync");
+
+ var result = await sut.JSRuntime.InvokeConstructorAsync("SomeClass");
+
+ result.ShouldNotBeNull();
+ result.ShouldBeAssignableTo();
+ }
+
+ [Fact(DisplayName = "InvokeConstructorAsync on IJSObjectReference from module import works in loose mode")]
+ public async Task Test405()
+ {
+ var sut = CreateSut(JSRuntimeMode.Loose);
+
+ var module = await sut.JSRuntime.InvokeAsync("import", "./myModule.js");
+ var result = await module.InvokeConstructorAsync("JsClass", "arg1", "arg2");
+
+ result.ShouldNotBeNull();
+ result.ShouldBeAssignableTo();
+ }
+
+ [Fact(DisplayName = "InvokeConstructorAsync on IJSObjectReference records invocation")]
+ public async Task Test406()
+ {
+ var sut = CreateSut(JSRuntimeMode.Loose);
+
+ var module = await sut.JSRuntime.InvokeAsync("import", "./myModule.js");
+ await module.InvokeConstructorAsync("JsClass", "arg1");
+
+ sut.Invocations["JsClass"].ShouldHaveSingleItem()
+ .InvocationMethodName.ShouldBe("InvokeConstructorAsync");
+ }
+#endif
}
diff --git a/version.json b/version.json
index 6dcb543f4..d486ef3e8 100644
--- a/version.json
+++ b/version.json
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
- "version": "2.6",
+ "version": "2.7",
"assemblyVersion": {
"precision": "revision"
},