Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
Expand Down Expand Up @@ -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
Expand All @@ -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 }}
Expand Down Expand Up @@ -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 }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/docs-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,18 @@
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net11.0'">
<PackageVersion Include="Microsoft.Extensions.Logging" Version="11.0.0-preview.1.26104.118"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="11.0.0-preview.1.26104.118"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="11.0.0-preview.1.26104.118"/>

<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="11.0.0-preview.1.26104.118"/>
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="11.0.0-preview.1.26104.118"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="11.0.0-preview.1.26104.118"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="11.0.0-preview.1.26104.118"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="11.0.0-preview.1.26104.118"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="11.0.0-preview.1.26104.118"/>

<PackageVersion Include="System.Text.Json" Version="11.0.0-preview.1.26104.118"/>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="11.0.0-preview.2.26159.112"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="11.0.0-preview.2.26159.112"/>
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="11.0.0-preview.2.26159.112"/>

<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="11.0.0-preview.2.26159.112"/>
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="11.0.0-preview.2.26159.112"/>
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="11.0.0-preview.2.26159.112"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="11.0.0-preview.2.26159.112"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="11.0.0-preview.2.26159.112"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="11.0.0-preview.2.26159.112"/>

<PackageVersion Include="System.Text.Json" Version="11.0.0-preview.2.26159.112"/>
</ItemGroup>

<ItemGroup Label="Test Dependencies">
Expand Down
6 changes: 6 additions & 0 deletions src/bunit/JSInterop/Implementation/BunitJSObjectReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke

#if NET10_0_OR_GREATER
/// <inheritdoc/>
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, object?[]? args)
=> JSInterop.HandleInvokeConstructorAsync(identifier, args);

/// <inheritdoc/>
public ValueTask<IJSObjectReference> InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
=> JSInterop.HandleInvokeConstructorAsync(identifier, cancellationToken, args);

/// <inheritdoc/>
public ValueTask<TValue> GetValueAsync<TValue>(string identifier) => throw new NotImplementedException();
Expand Down
19 changes: 19 additions & 0 deletions src/bunit/JSInterop/Implementation/BunitJSRuntime.net10.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#if NET10_0_OR_GREATER
using Bunit.JSInterop.Implementation;

namespace Bunit.JSInterop;

/// <summary>
/// bUnit's implementation of the <c>InvokeConstructorAsync</c> methods on <see cref="IJSRuntime"/>.
/// </summary>
internal sealed partial class BunitJSRuntime
{
/// <inheritdoc/>
ValueTask<IJSObjectReference> IJSRuntime.InvokeConstructorAsync(string identifier, object?[]? args)
=> JSInterop.HandleInvokeConstructorAsync(identifier, args);

/// <inheritdoc/>
ValueTask<IJSObjectReference> IJSRuntime.InvokeConstructorAsync(string identifier, CancellationToken cancellationToken, object?[]? args)
=> JSInterop.HandleInvokeConstructorAsync(identifier, cancellationToken, args);
}
#endif
15 changes: 15 additions & 0 deletions src/bunit/JSInterop/Implementation/JSRuntimeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ internal static TResult HandleInvokeUnmarshalled<T0, T1, T2, TResult>(this Bunit
.GetResult();
}

#if NET10_0_OR_GREATER
internal static ValueTask<IJSObjectReference> HandleInvokeConstructorAsync(this BunitJSInterop jSInterop, string identifier, object?[]? args)
{
var invocation = new JSRuntimeInvocation(identifier, null, args, typeof(IJSObjectReference), "InvokeConstructorAsync");
return jSInterop.HandleInvocation<IJSObjectReference>(invocation);
}

[SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification = "Matching Blazor's JSRuntime design.")]
internal static ValueTask<IJSObjectReference> HandleInvokeConstructorAsync(this BunitJSInterop jSInterop, string identifier, CancellationToken cancellationToken, object?[]? args)
{
var invocation = new JSRuntimeInvocation(identifier, cancellationToken, args, typeof(IJSObjectReference), "InvokeConstructorAsync");
return jSInterop.HandleInvocation<IJSObjectReference>(invocation);
}
#endif

private static string GetInvokeAsyncMethodName<TValue>()
=> typeof(TValue) == typeof(Microsoft.JSInterop.Infrastructure.IJSVoidResult)
? "InvokeVoidAsync"
Expand Down
89 changes: 89 additions & 0 deletions tests/bunit.tests/JSInterop/BunitJSInteropTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -702,4 +702,93 @@ public async Task Test309()
var exception = await Should.ThrowAsync<JSRuntimeInvocationNotSetException>(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<IJSObjectReference>();
}

[Fact(DisplayName = "InvokeConstructorAsync throws in strict mode when no handler is set up")]
public void Test401()
{
var sut = CreateSut(JSRuntimeMode.Strict);

Should.Throw<JSRuntimeUnhandledInvocationException>(
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<IJSObjectReference>();
}

[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<IJSObjectReference>("import", "./myModule.js");
var result = await module.InvokeConstructorAsync("JsClass", "arg1", "arg2");

result.ShouldNotBeNull();
result.ShouldBeAssignableTo<IJSObjectReference>();
}

[Fact(DisplayName = "InvokeConstructorAsync on IJSObjectReference records invocation")]
public async Task Test406()
{
var sut = CreateSut(JSRuntimeMode.Loose);

var module = await sut.JSRuntime.InvokeAsync<IJSObjectReference>("import", "./myModule.js");
await module.InvokeConstructorAsync("JsClass", "arg1");

sut.Invocations["JsClass"].ShouldHaveSingleItem()
.InvocationMethodName.ShouldBe("InvokeConstructorAsync");
}
#endif
}
2 changes: 1 addition & 1 deletion version.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand Down
Loading