Skip to content
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The following section list all changes in 1.0.0 preview 01.
### Added
List of new features.

- Added support for casting `BUnitJSRuntime` to `IJSInProcessRuntime` and `IJSUnmarshalledRuntime`. By [@KristofferStrube](https://github.com/KristofferStrube) in [#279](https://github.com/egil/bUnit/pull/279)

- Added support for triggering `@ontoggle` event handlers through a dedicated `Toggle()` method. By [@egil](https://github.com/egil) in [#256](https://github.com/egil/bUnit/pull/256).

- Added out of the box support for `<Virtualize>` component. When a `<Virtualize>` component is used in a component under test, it's JavaScript interop-calls are faked by bUnits JSInterop, and it should result in all items being rendered immediately. By [@egil](https://github.com/egil) in [#240](https://github.com/egil/bUnit/issues/240).
Expand Down
24 changes: 23 additions & 1 deletion src/bunit.web/JSInterop/BunitJSInterop.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -179,7 +180,13 @@ private void RegisterInvocation(JSRuntimeInvocation invocation)
return result;
}

private class BUnitJSRuntime : IJSRuntime

[SuppressMessage("Design", "CA2012:ValueTask instances should not have their result directly accessed unless the instance has already completed.", Justification = "The ValueTask always wraps a Task object.")]
#if NET5_0
private class BUnitJSRuntime : IJSRuntime, IJSInProcessRuntime, IJSUnmarshalledRuntime
#else
private class BUnitJSRuntime : IJSRuntime, IJSInProcessRuntime
#endif
{
private readonly BunitJSInterop _jsInterop;

Expand All @@ -188,6 +195,9 @@ public BUnitJSRuntime(BunitJSInterop bunitJsInterop)
_jsInterop = bunitJsInterop;
}

public TResult Invoke<TResult>(string identifier, params object?[]? args) =>
InvokeAsync<TResult>(identifier, args).GetAwaiter().GetResult();

public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object?[]? args)
=> InvokeAsync<TValue>(identifier, default, args);

Expand All @@ -199,6 +209,18 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
return TryHandlePlannedInvocation<TValue>(invocation) ?? new ValueTask<TValue>(default(TValue)!);
}

public TResult InvokeUnmarshalled<TResult>(string identifier) =>
InvokeAsync<TResult>(identifier, Array.Empty<object?>()).GetAwaiter().GetResult();

public TResult InvokeUnmarshalled<T0, TResult>(string identifier, T0 arg0) =>
InvokeAsync<TResult>(identifier, new object?[] {arg0}).GetAwaiter().GetResult();

public TResult InvokeUnmarshalled<T0, T1, TResult>(string identifier, T0 arg0, T1 arg1) =>
InvokeAsync<TResult>(identifier, new object?[] { arg0, arg1 }).GetAwaiter().GetResult();

public TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2) =>
InvokeAsync<TResult>(identifier, new object?[] { arg0, arg1, arg2 }).GetAwaiter().GetResult();

private ValueTask<TValue>? TryHandlePlannedInvocation<TValue>(JSRuntimeInvocation invocation)
{
ValueTask<TValue>? result = default;
Expand Down
55 changes: 54 additions & 1 deletion tests/bunit.web.tests/JSInterop/BunitJSInteropTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Bunit.JSInterop
{
public class BunitJSInteropTest
public partial class BunitJSInteropTest
{
private static BunitJSInterop CreateSut(JSRuntimeMode mode) => new BunitJSInterop { Mode = mode };

Expand Down Expand Up @@ -435,5 +435,58 @@ public void Test042()

actual.ShouldBe(expected);
}

[Fact(DisplayName = "Mock returns default value from IJSInProcessRuntime's invoke method in loose mode without invocation setup")]
public void Test043()
{
var sut = CreateSut(JSRuntimeMode.Loose);

var result = ((IJSInProcessRuntime)sut.JSRuntime).Invoke<object>("ident", Array.Empty<object>());

result.ShouldBe(default);
}

[Fact(DisplayName = "After IJSInProcessRuntime invocation a invocation should be visible from the Invocations list")]
public void Test044()
{
var identifier = "fooFunc";
var args = new[] { "bar", "baz" };
using var cts = new CancellationTokenSource();
var sut = CreateSut(JSRuntimeMode.Loose);

var _ = ((IJSInProcessRuntime)sut.JSRuntime).Invoke<object>(identifier, args);

var invocation = sut.Invocations[identifier].Single();
invocation.Identifier.ShouldBe(identifier);
invocation.Arguments.ShouldBe(args);
}

[Fact(DisplayName = "IJSInProcessRuntime invocations receive the result set in a planned invocation")]
public void Test045()
{
var identifier = "func";
var args = new[] { "bar", "baz" };
var sut = CreateSut(JSRuntimeMode.Strict);

var expectedResult = Guid.NewGuid();
var planned = sut.Setup<Guid>(identifier, args);
planned.SetResult(expectedResult);

var i = ((IJSInProcessRuntime)sut.JSRuntime).Invoke<Guid>(identifier, args);

i.ShouldBe(expectedResult);
}

[Fact(DisplayName = "Mock throws exception when in strict mode and IJSInProcessRuntime invocation has not been setup")]
public void Test046()
{
var sut = CreateSut(JSRuntimeMode.Strict);
var identifier = "func";
var args = new[] { "bar", "baz" };

var exception = Should.Throw<JSRuntimeUnhandledInvocationException>(() => { var _ = ((IJSInProcessRuntime)sut.JSRuntime).Invoke<object>(identifier, args); });
exception.Invocation.Identifier.ShouldBe(identifier);
exception.Invocation.Arguments.ShouldBe(args);
}
}
}
98 changes: 98 additions & 0 deletions tests/bunit.web.tests/JSInterop/BunitJSInteropTest.net5.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#if NET5_0
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Bunit;
using Microsoft.JSInterop;
using Shouldly;
using Xunit;

namespace Bunit.JSInterop
{
public partial class BunitJSInteropTest
{
[Fact(DisplayName = "Mock throws exception when in strict mode and IJSUnmarshalledRuntime invocation has not been setup with one argument")]
public void Test047()
{
var sut = CreateSut(JSRuntimeMode.Strict);
var identifier = "func";
var args = new[] { "bar" };

var exception = Should.Throw<JSRuntimeUnhandledInvocationException>(() => { var _ = ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled<string, object>(identifier, "bar"); });
exception.Invocation.Identifier.ShouldBe(identifier);
exception.Invocation.Arguments.ShouldBe(args);
}

[Fact(DisplayName = "Mock throws exception when in strict mode and IJSUnmarshalledRuntime invocation has not been setup with two arguments")]
public void Test048()
{
var sut = CreateSut(JSRuntimeMode.Strict);
var identifier = "func";
var args = new[] { "bar", "baz" };

var exception = Should.Throw<JSRuntimeUnhandledInvocationException>(() => { var _ = ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled<string, string, object>(identifier, "bar", "baz"); });
exception.Invocation.Identifier.ShouldBe(identifier);
exception.Invocation.Arguments.ShouldBe(args);
}

[Fact(DisplayName = "Mock throws exception when in strict mode and IJSUnmarshalledRuntime invocation has not been setup with three arguments")]
public void Test049()
{
var sut = CreateSut(JSRuntimeMode.Strict);
var identifier = "func";
var args = new[] { "bar", "baz", "bau" };

var exception = Should.Throw<JSRuntimeUnhandledInvocationException>(() => { var _ = ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled<string, string, string, object>(identifier, "bar", "baz", "bau"); });
exception.Invocation.Identifier.ShouldBe(identifier);
exception.Invocation.Arguments.ShouldBe(args);
}

[Fact(DisplayName = "Mock throws exception when in strict mode and IJSUnmarshalledRuntime invocation has not been setup with zero arguments")]
public void Test050()
{
var sut = CreateSut(JSRuntimeMode.Strict);
var identifier = "func";

var exception = Should.Throw<JSRuntimeUnhandledInvocationException>(() => { var _ = ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled<object>(identifier); });
exception.Invocation.Identifier.ShouldBe(identifier);
exception.Invocation.Arguments.ShouldBeEmpty();
}

[Fact(DisplayName = "After IJSUnmarshalledRuntime invocation a invocation should be visible from the Invocations list.")]
public void Test051()
{
var identifier = "fooFunc";
var args = new[] { "bar", "baz" };
using var cts = new CancellationTokenSource();
var sut = CreateSut(JSRuntimeMode.Strict);

var planned = sut.Setup<Guid>("fooFunc", args);
planned.SetResult(Guid.NewGuid());

var _ = ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled<string, string, Guid>(identifier, "bar", "baz");

var invocation = sut.Invocations[identifier].Single();
invocation.Identifier.ShouldBe(identifier);
invocation.Arguments.ShouldBe(args);
}

[Fact(DisplayName = "An IJSUnmarshalledRuntime invocation should return the correct result.")]
public void Test052()
{
var identifier = "fooFunc";
var args = new[] { "bar", "baz" };
using var cts = new CancellationTokenSource();
var sut = CreateSut(JSRuntimeMode.Strict);

var expectedResult = Guid.NewGuid();
var planned = sut.Setup<Guid>("fooFunc", args);
planned.SetResult(expectedResult);

var actual = ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled<string, string, Guid>(identifier, "bar", "baz");
actual.ShouldBe(expectedResult);
}
}
}
#endif