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" },