Skip to content

Commit

Permalink
Add IJSObjectReference support / JS Module support (#288)
Browse files Browse the repository at this point in the history
* Code cleanup

* First draft of JS Interop module support

* Added additional tests, implemented #258

* updated changelog

* Added support for casting BunitJSObjectReference to IJSInProcessObjectReference, IJSUnmarshalledObjectReference
  • Loading branch information
egil authored Dec 19, 2020
1 parent 5c668cb commit 0b7692c
Show file tree
Hide file tree
Showing 58 changed files with 1,529 additions and 587 deletions.
17 changes: 16 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,19 @@ dotnet_diagnostic.BL0002.severity = none
dotnet_diagnostic.BL0003.severity = none
dotnet_diagnostic.BL0004.severity = none
dotnet_diagnostic.BL0005.severity = none
dotnet_diagnostic.BL0006.severity = none
dotnet_diagnostic.BL0006.severity = none

## Code analysis configuration

[*.cs]

dotnet_diagnostic.S125.severity = suggestion # S125: Sections of code should not be commented out
dotnet_diagnostic.S927.severity = suggestion # S927: Parameter names should match base declaration and other partial definitions
dotnet_diagnostic.S1075.severity = suggestion # S1075: URIs should not be hardcoded
dotnet_diagnostic.S1186.severity = suggestion # S1186: Methods should not be empty
dotnet_diagnostic.S1199.severity = suggestion # S1199: Nested code blocks should not be used
dotnet_diagnostic.S3925.severity = suggestion # S3925: "ISerializable" should be implemented correctly

[tests/**.cs]

dotnet_diagnostic.S3459.severity = suggestion # S3459: Unassigned members should be removed
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ List of new features.

By [@egil](https://github.com/egil) in [#262](https://github.com/egil/bUnit/pull/262).
- Added support for `IJSRuntime.InvokeAsync<IJSObjectReference>(...)` calls from components. There is now a new setup helper methods for configuring how invocations towards JS modules should be handled. This is done with the various `SetupModule` methods available on the `BunitJSInterop` type available through the `TestContext.JSInterop` property. For example, to set up a module for handling calls to `foo.js`, do the following:

```c#
using var ctx = new TestContext();
var moduleJsInterop = ctx.JSInterop.SetupModule("foo.js");
```

The returned `moduleJsInterop` is a `BunitJSInterop` type, which means all the normal `Setup<TResult>` and `SetupVoid` methods can be used to configure it to handle calls to the module from a component. For example, to configure a handler for a call to `hello` in the `foo.js` module, do the following:

```c#
moduleJsInterop.SetupVoid("hello");
```

By [@egil](https://github.com/egil) in [#288](https://github.com/egil/bUnit/pull/288).
### Changed
List of changes in existing functionality.

Expand Down
6 changes: 6 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sdk": {
"rollForward": "latestMajor",
"allowPrerelease": false
}
}
2 changes: 1 addition & 1 deletion src/bunit.core/ComponentParameterCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ void AddCascadingValue(RenderTreeBuilder builder)

builder.OpenComponent(0, cv.Type);

if (cv.Parameter.Name is string)
if (cv.Parameter.Name is not null)
builder.AddAttribute(1, nameof(CascadingValue<object>.Name), cv.Parameter.Name);

builder.AddAttribute(2, nameof(CascadingValue<object>.Value), cv.Parameter.Value);
Expand Down
2 changes: 0 additions & 2 deletions src/bunit.core/RazorTesting/FixtureBase.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

Expand Down
1 change: 0 additions & 1 deletion src/bunit.core/RazorTesting/FragmentContainer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

Expand Down
4 changes: 4 additions & 0 deletions src/bunit.template/bunit.template.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@
<None Remove="template\obj\**" />
</ItemGroup>

<ItemGroup>
<None Include="..\..\.editorconfig" Link=".editorconfig" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#if NET5_0
using Bunit.JSInterop.InvocationHandlers;
using Bunit.JSInterop.InvocationHandlers.Implementation;
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Collections.Generic;
using AngleSharp.Dom;
using Bunit.Asserting;
using Bunit.JSInterop;
using Bunit.JSInterop.InvocationHandlers;
using Microsoft.AspNetCore.Components;

namespace Bunit
Expand All @@ -11,21 +13,44 @@ namespace Bunit
/// </summary>
public static class JSRuntimeAssertExtensions
{
/// <summary>
/// Verifies that the <paramref name="identifier"/> was never invoked on the <paramref name="jsInterop"/>.
/// </summary>
/// <param name="jsInterop">The bUnit JSInterop to verify against.</param>
/// <param name="identifier">Identifier of invocation that should not have happened.</param>
/// <param name="userMessage">A custom user message to display if the assertion fails.</param>
public static void VerifyNotInvoke(this BunitJSInterop jsInterop, string identifier, string? userMessage = null)
=> VerifyNotInvoke(jsInterop?.Invocations ?? throw new ArgumentNullException(nameof(jsInterop)), identifier, userMessage);

/// <summary>
/// Verifies that the <paramref name="identifier"/> was never invoked on the <paramref name="handler"/>.
/// </summary>
/// <param name="handler">Handler to verify against.</param>
/// <param name="identifier">Identifier of invocation that should not have happened.</param>
/// <param name="userMessage">A custom user message to display if the assertion fails.</param>
public static void VerifyNotInvoke(this BunitJSInterop handler, string identifier, string? userMessage = null)
{
if (handler is null)
throw new ArgumentNullException(nameof(handler));
if (handler.Invocations.TryGetValue(identifier, out var invocations) && invocations.Count > 0)
{
throw new JSInvokeCountExpectedException(identifier, 0, invocations.Count, nameof(VerifyNotInvoke), userMessage);
}
}
public static void VerifyNotInvoke<TResult>(this JSRuntimeInvocationHandlerBase<TResult> handler, string identifier, string? userMessage = null)
=> VerifyNotInvoke(handler?.Invocations ?? throw new ArgumentNullException(nameof(handler)), identifier, userMessage);

/// <summary>
/// Verifies that the <paramref name="identifier"/> has been invoked one time.
/// </summary>
/// <param name="jsInterop">The bUnit JSInterop to verify against.</param>
/// <param name="identifier">Identifier of invocation that should have been invoked.</param>
/// <param name="userMessage">A custom user message to display if the assertion fails.</param>
/// <returns>The <see cref="JSRuntimeInvocation"/>.</returns>
public static JSRuntimeInvocation VerifyInvoke(this BunitJSInterop jsInterop, string identifier, string? userMessage = null)
=> jsInterop.VerifyInvoke(identifier, 1, userMessage)[0];

/// <summary>
/// Verifies that the <paramref name="identifier"/> has been invoked <paramref name="calledTimes"/> times.
/// </summary>
/// <param name="jsInterop">The bUnit JSInterop to verify against.</param>
/// <param name="identifier">Identifier of invocation that should have been invoked.</param>
/// <param name="calledTimes">The number of times the invocation is expected to have been called.</param>
/// <param name="userMessage">A custom user message to display if the assertion fails.</param>
/// <returns>The <see cref="JSRuntimeInvocation"/>.</returns>
public static IReadOnlyList<JSRuntimeInvocation> VerifyInvoke(this BunitJSInterop jsInterop, string identifier, int calledTimes, string? userMessage = null)
=> VerifyInvoke(jsInterop?.Invocations ?? throw new ArgumentNullException(nameof(jsInterop)), identifier, calledTimes, userMessage);

/// <summary>
/// Verifies that the <paramref name="identifier"/> has been invoked one time.
Expand All @@ -34,7 +59,7 @@ public static void VerifyNotInvoke(this BunitJSInterop handler, string identifie
/// <param name="identifier">Identifier of invocation that should have been invoked.</param>
/// <param name="userMessage">A custom user message to display if the assertion fails.</param>
/// <returns>The <see cref="JSRuntimeInvocation"/>.</returns>
public static JSRuntimeInvocation VerifyInvoke(this BunitJSInterop handler, string identifier, string? userMessage = null)
public static JSRuntimeInvocation VerifyInvoke<TResult>(this JSRuntimeInvocationHandlerBase<TResult> handler, string identifier, string? userMessage = null)
=> handler.VerifyInvoke(identifier, 1, userMessage)[0];

/// <summary>
Expand All @@ -45,26 +70,8 @@ public static JSRuntimeInvocation VerifyInvoke(this BunitJSInterop handler, stri
/// <param name="calledTimes">The number of times the invocation is expected to have been called.</param>
/// <param name="userMessage">A custom user message to display if the assertion fails.</param>
/// <returns>The <see cref="JSRuntimeInvocation"/>.</returns>
public static IReadOnlyList<JSRuntimeInvocation> VerifyInvoke(this BunitJSInterop handler, string identifier, int calledTimes, string? userMessage = null)
{
if (handler is null)
throw new ArgumentNullException(nameof(handler));

if (calledTimes < 1)
throw new ArgumentException($"Use {nameof(VerifyNotInvoke)} to verify an identifier has not been invoked.", nameof(calledTimes));

if (!handler.Invocations.TryGetValue(identifier, out var invocations))
{
throw new JSInvokeCountExpectedException(identifier, calledTimes, 0, nameof(VerifyInvoke), userMessage);
}

if (invocations.Count != calledTimes)
{
throw new JSInvokeCountExpectedException(identifier, calledTimes, invocations.Count, nameof(VerifyInvoke), userMessage);
}

return invocations;
}
public static IReadOnlyList<JSRuntimeInvocation> VerifyInvoke<TResult>(this JSRuntimeInvocationHandlerBase<TResult> handler, string identifier, int calledTimes, string? userMessage = null)
=> VerifyInvoke(handler?.Invocations ?? throw new ArgumentNullException(nameof(handler)), identifier, calledTimes, userMessage);

/// <summary>
/// Verifies that an argument <paramref name="actualArgument"/>
Expand Down Expand Up @@ -93,5 +100,41 @@ public static void ShouldBeElementReferenceTo(this object? actualArgument, IElem
"Element does not have a the expected element reference.");
}
}

private static IReadOnlyList<JSRuntimeInvocation> VerifyInvoke(JSRuntimeInvocationDictionary allInvocations, string identifier, int calledTimes, string? userMessage = null)
{
if (string.IsNullOrWhiteSpace(identifier))
throw new ArgumentException($"'{nameof(identifier)}' cannot be null or whitespace.", nameof(identifier));

if (calledTimes < 1)
throw new ArgumentException($"Use {nameof(VerifyNotInvoke)} to verify an identifier has not been invoked.", nameof(calledTimes));

var invocations = allInvocations[identifier];

if (invocations.Count == 0)
{
throw new JSInvokeCountExpectedException(identifier, calledTimes, 0, nameof(VerifyInvoke), userMessage);
}

if (invocations.Count != calledTimes)
{
throw new JSInvokeCountExpectedException(identifier, calledTimes, allInvocations.Count, nameof(VerifyInvoke), userMessage);
}

return invocations;
}

private static void VerifyNotInvoke(JSRuntimeInvocationDictionary allInvocations, string identifier, string? userMessage = null)
{
if (string.IsNullOrWhiteSpace(identifier))
throw new ArgumentException($"'{nameof(identifier)}' cannot be null or whitespace.", nameof(identifier));

var invocationCount = allInvocations[identifier].Count;

if (invocationCount > 0)
{
throw new JSInvokeCountExpectedException(identifier, 0, invocationCount, nameof(VerifyNotInvoke), userMessage);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,28 +56,28 @@ public static void Input(this IElement element, ChangeEventArgs eventArgs)
=> _ = InputAsync(element, eventArgs);

/// <summary>
/// Raises the <c>@oninput</c> event on <paramref name="element"/>, passing the provided <paramref name="eventArgs"/>
/// Raises the <c>@oninput</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
/// to the event handler.
/// </summary>
/// <param name="element">The element to raise the event on.</param>
/// <param name="eventArgs">The event arguments to pass to the event handler.</param>
/// <returns>A task that completes when the event handler is done.</returns>
private static Task InputAsync(this IElement element, ChangeEventArgs eventArgs) => element.TriggerEventAsync("oninput", eventArgs);
public static void Input(this IElement element) => _ = InputAsync(element);

/// <summary>
/// Raises the <c>@oninput</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
/// to the event handler.
/// </summary>
/// <param name="element">The element to raise the event on.</param>
public static void Input(this IElement element) => _ = InputAsync(element);
/// <returns>A task that completes when the event handler is done.</returns>
private static Task InputAsync(this IElement element) => element.TriggerEventAsync("oninput", EventArgs.Empty);

/// <summary>
/// Raises the <c>@oninput</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
/// Raises the <c>@oninput</c> event on <paramref name="element"/>, passing the provided <paramref name="eventArgs"/>
/// to the event handler.
/// </summary>
/// <param name="element">The element to raise the event on.</param>
/// <param name="eventArgs">The event arguments to pass to the event handler.</param>
/// <returns>A task that completes when the event handler is done.</returns>
private static Task InputAsync(this IElement element) => element.TriggerEventAsync("oninput", EventArgs.Empty);
private static Task InputAsync(this IElement element, ChangeEventArgs eventArgs) => element.TriggerEventAsync("oninput", eventArgs);

/// <summary>
/// Raises the <c>@oninvalid</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,9 @@ private static Task TriggerBubblingEventAsync(ITestRenderer renderer, IElement e
if (eventIds.Count == 0)
throw new MissingEventHandlerException(element, eventName);

return Task.WhenAll(eventIds.Select(TriggerEvent).ToArray());
var triggerTasks = eventIds.Select(id => renderer.DispatchEventAsync(id, new EventFieldInfo() { FieldValue = eventName }, eventArgs));

Task TriggerEvent(ulong id)
=> renderer.DispatchEventAsync(id, new EventFieldInfo() { FieldValue = eventName }, eventArgs);
return Task.WhenAll(triggerTasks.ToArray());
}

private static Task TriggerNonBubblingEventAsync(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
Expand Down
2 changes: 0 additions & 2 deletions src/bunit.web/Extensions/TestServiceProviderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Net.Http;
using Bunit.Diffing;
using Bunit.JSInterop;
using Bunit.Rendering;
using Bunit.TestDoubles;
using Microsoft.AspNetCore.Authorization;
Expand Down Expand Up @@ -33,7 +32,6 @@ public static IServiceCollection AddDefaultTestContextServices(this IServiceColl
services.AddSingleton<IStringLocalizer, PlaceholderStringLocalization>();

// bUnits fake JSInterop
jsInterop.AddBuiltInJSRuntimeInvocationHandlers();
services.AddSingleton<IJSRuntime>(jsInterop.JSRuntime);

// bUnit specific services
Expand Down
2 changes: 1 addition & 1 deletion src/bunit.web/IRenderedComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Bunit
{
/// <inheritdoc/>
public interface IRenderedComponent<TComponent> : IRenderedComponentBase<TComponent>, IRenderedFragment
public interface IRenderedComponent<out TComponent> : IRenderedComponentBase<TComponent>, IRenderedFragment
where TComponent : IComponent
{
}
Expand Down
Loading

0 comments on commit 0b7692c

Please sign in to comment.