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
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