diff --git a/.editorconfig b/.editorconfig index 5c753d118..c554ed090 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 \ No newline at end of file +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 diff --git a/CHANGELOG.md b/CHANGELOG.md index a473f3b3a..e74c53fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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(...)` 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` 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. diff --git a/global.json b/global.json new file mode 100644 index 000000000..2a75c791d --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "rollForward": "latestMajor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/src/bunit.core/ComponentParameterCollection.cs b/src/bunit.core/ComponentParameterCollection.cs index 377db60d7..2cd277482 100644 --- a/src/bunit.core/ComponentParameterCollection.cs +++ b/src/bunit.core/ComponentParameterCollection.cs @@ -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.Name), cv.Parameter.Name); builder.AddAttribute(2, nameof(CascadingValue.Value), cv.Parameter.Value); diff --git a/src/bunit.core/RazorTesting/FixtureBase.cs b/src/bunit.core/RazorTesting/FixtureBase.cs index 08b756cd4..cbb9b351a 100644 --- a/src/bunit.core/RazorTesting/FixtureBase.cs +++ b/src/bunit.core/RazorTesting/FixtureBase.cs @@ -1,6 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; diff --git a/src/bunit.core/RazorTesting/FragmentContainer.cs b/src/bunit.core/RazorTesting/FragmentContainer.cs index 59d7585f7..ca386224c 100644 --- a/src/bunit.core/RazorTesting/FragmentContainer.cs +++ b/src/bunit.core/RazorTesting/FragmentContainer.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; diff --git a/src/bunit.template/bunit.template.csproj b/src/bunit.template/bunit.template.csproj index 2bd30f1a3..308f15b13 100644 --- a/src/bunit.template/bunit.template.csproj +++ b/src/bunit.template/bunit.template.csproj @@ -35,4 +35,8 @@ + + + + diff --git a/src/bunit.web/JSInterop/InvocationHandlers/FocusAsyncAssertJSInteropExtensions.cs b/src/bunit.web/Asserting/FocusAsyncAssertJSInteropExtensions.cs similarity index 96% rename from src/bunit.web/JSInterop/InvocationHandlers/FocusAsyncAssertJSInteropExtensions.cs rename to src/bunit.web/Asserting/FocusAsyncAssertJSInteropExtensions.cs index 84fdfc27d..787094856 100644 --- a/src/bunit.web/JSInterop/InvocationHandlers/FocusAsyncAssertJSInteropExtensions.cs +++ b/src/bunit.web/Asserting/FocusAsyncAssertJSInteropExtensions.cs @@ -1,5 +1,5 @@ #if NET5_0 -using Bunit.JSInterop.InvocationHandlers; +using Bunit.JSInterop.InvocationHandlers.Implementation; using Microsoft.AspNetCore.Components; using System.Collections.Generic; diff --git a/src/bunit.web/JSInterop/JSInvokeCountExpectedException.cs b/src/bunit.web/Asserting/JSInvokeCountExpectedException.cs similarity index 100% rename from src/bunit.web/JSInterop/JSInvokeCountExpectedException.cs rename to src/bunit.web/Asserting/JSInvokeCountExpectedException.cs diff --git a/src/bunit.web/JSInterop/JSRuntimeAssertExtensions.cs b/src/bunit.web/Asserting/JSRuntimeAssertExtensions.cs similarity index 50% rename from src/bunit.web/JSInterop/JSRuntimeAssertExtensions.cs rename to src/bunit.web/Asserting/JSRuntimeAssertExtensions.cs index 067e0807c..88da5341d 100644 --- a/src/bunit.web/JSInterop/JSRuntimeAssertExtensions.cs +++ b/src/bunit.web/Asserting/JSRuntimeAssertExtensions.cs @@ -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 @@ -11,21 +13,44 @@ namespace Bunit /// public static class JSRuntimeAssertExtensions { + /// + /// Verifies that the was never invoked on the . + /// + /// The bUnit JSInterop to verify against. + /// Identifier of invocation that should not have happened. + /// A custom user message to display if the assertion fails. + public static void VerifyNotInvoke(this BunitJSInterop jsInterop, string identifier, string? userMessage = null) + => VerifyNotInvoke(jsInterop?.Invocations ?? throw new ArgumentNullException(nameof(jsInterop)), identifier, userMessage); + /// /// Verifies that the was never invoked on the . /// /// Handler to verify against. /// Identifier of invocation that should not have happened. /// A custom user message to display if the assertion fails. - 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(this JSRuntimeInvocationHandlerBase handler, string identifier, string? userMessage = null) + => VerifyNotInvoke(handler?.Invocations ?? throw new ArgumentNullException(nameof(handler)), identifier, userMessage); + + /// + /// Verifies that the has been invoked one time. + /// + /// The bUnit JSInterop to verify against. + /// Identifier of invocation that should have been invoked. + /// A custom user message to display if the assertion fails. + /// The . + public static JSRuntimeInvocation VerifyInvoke(this BunitJSInterop jsInterop, string identifier, string? userMessage = null) + => jsInterop.VerifyInvoke(identifier, 1, userMessage)[0]; + + /// + /// Verifies that the has been invoked times. + /// + /// The bUnit JSInterop to verify against. + /// Identifier of invocation that should have been invoked. + /// The number of times the invocation is expected to have been called. + /// A custom user message to display if the assertion fails. + /// The . + public static IReadOnlyList VerifyInvoke(this BunitJSInterop jsInterop, string identifier, int calledTimes, string? userMessage = null) + => VerifyInvoke(jsInterop?.Invocations ?? throw new ArgumentNullException(nameof(jsInterop)), identifier, calledTimes, userMessage); /// /// Verifies that the has been invoked one time. @@ -34,7 +59,7 @@ public static void VerifyNotInvoke(this BunitJSInterop handler, string identifie /// Identifier of invocation that should have been invoked. /// A custom user message to display if the assertion fails. /// The . - public static JSRuntimeInvocation VerifyInvoke(this BunitJSInterop handler, string identifier, string? userMessage = null) + public static JSRuntimeInvocation VerifyInvoke(this JSRuntimeInvocationHandlerBase handler, string identifier, string? userMessage = null) => handler.VerifyInvoke(identifier, 1, userMessage)[0]; /// @@ -45,26 +70,8 @@ public static JSRuntimeInvocation VerifyInvoke(this BunitJSInterop handler, stri /// The number of times the invocation is expected to have been called. /// A custom user message to display if the assertion fails. /// The . - public static IReadOnlyList 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 VerifyInvoke(this JSRuntimeInvocationHandlerBase handler, string identifier, int calledTimes, string? userMessage = null) + => VerifyInvoke(handler?.Invocations ?? throw new ArgumentNullException(nameof(handler)), identifier, calledTimes, userMessage); /// /// Verifies that an argument @@ -93,5 +100,41 @@ public static void ShouldBeElementReferenceTo(this object? actualArgument, IElem "Element does not have a the expected element reference."); } } + + private static IReadOnlyList 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); + } + } } } diff --git a/src/bunit.web/EventDispatchExtensions/InputEventDispatchExtensions.cs b/src/bunit.web/EventDispatchExtensions/InputEventDispatchExtensions.cs index 235514c37..718ec58c4 100644 --- a/src/bunit.web/EventDispatchExtensions/InputEventDispatchExtensions.cs +++ b/src/bunit.web/EventDispatchExtensions/InputEventDispatchExtensions.cs @@ -56,28 +56,28 @@ public static void Input(this IElement element, ChangeEventArgs eventArgs) => _ = InputAsync(element, eventArgs); /// - /// Raises the @oninput event on , passing the provided + /// Raises the @oninput event on , passing an empty () /// to the event handler. /// /// The element to raise the event on. - /// The event arguments to pass to the event handler. - /// A task that completes when the event handler is done. - private static Task InputAsync(this IElement element, ChangeEventArgs eventArgs) => element.TriggerEventAsync("oninput", eventArgs); + public static void Input(this IElement element) => _ = InputAsync(element); /// /// Raises the @oninput event on , passing an empty () /// to the event handler. /// /// The element to raise the event on. - public static void Input(this IElement element) => _ = InputAsync(element); + /// A task that completes when the event handler is done. + private static Task InputAsync(this IElement element) => element.TriggerEventAsync("oninput", EventArgs.Empty); /// - /// Raises the @oninput event on , passing an empty () + /// Raises the @oninput event on , passing the provided /// to the event handler. /// /// The element to raise the event on. + /// The event arguments to pass to the event handler. /// A task that completes when the event handler is done. - 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); /// /// Raises the @oninvalid event on , passing an empty () diff --git a/src/bunit.web/EventDispatchExtensions/TriggerEventDispatchExtensions.cs b/src/bunit.web/EventDispatchExtensions/TriggerEventDispatchExtensions.cs index 37997f9a2..605627b45 100644 --- a/src/bunit.web/EventDispatchExtensions/TriggerEventDispatchExtensions.cs +++ b/src/bunit.web/EventDispatchExtensions/TriggerEventDispatchExtensions.cs @@ -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) diff --git a/src/bunit.web/Extensions/TestServiceProviderExtensions.cs b/src/bunit.web/Extensions/TestServiceProviderExtensions.cs index a57483092..6c25a1e19 100644 --- a/src/bunit.web/Extensions/TestServiceProviderExtensions.cs +++ b/src/bunit.web/Extensions/TestServiceProviderExtensions.cs @@ -1,6 +1,5 @@ using System.Net.Http; using Bunit.Diffing; -using Bunit.JSInterop; using Bunit.Rendering; using Bunit.TestDoubles; using Microsoft.AspNetCore.Authorization; @@ -33,7 +32,6 @@ public static IServiceCollection AddDefaultTestContextServices(this IServiceColl services.AddSingleton(); // bUnits fake JSInterop - jsInterop.AddBuiltInJSRuntimeInvocationHandlers(); services.AddSingleton(jsInterop.JSRuntime); // bUnit specific services diff --git a/src/bunit.web/IRenderedComponent.cs b/src/bunit.web/IRenderedComponent.cs index dd12da98c..83cfe12ac 100644 --- a/src/bunit.web/IRenderedComponent.cs +++ b/src/bunit.web/IRenderedComponent.cs @@ -3,7 +3,7 @@ namespace Bunit { /// - public interface IRenderedComponent : IRenderedComponentBase, IRenderedFragment + public interface IRenderedComponent : IRenderedComponentBase, IRenderedFragment where TComponent : IComponent { } diff --git a/src/bunit.web/JSInterop/BunitJSInterop.cs b/src/bunit.web/JSInterop/BunitJSInterop.cs index 8bc4e13de..ae3146559 100644 --- a/src/bunit.web/JSInterop/BunitJSInterop.cs +++ b/src/bunit.web/JSInterop/BunitJSInterop.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using Bunit.JSInterop; +using Bunit.JSInterop.InvocationHandlers; +#if NET5_0 +using Bunit.JSInterop.InvocationHandlers.Implementation; +#endif using Microsoft.JSInterop; namespace Bunit @@ -14,19 +15,18 @@ namespace Bunit /// public class BunitJSInterop { - private readonly Dictionary> _invocations = new(); private readonly Dictionary> _handlers = new(); /// /// Gets a dictionary of all this mock has observed. /// - public IReadOnlyDictionary> Invocations => _invocations; + public JSRuntimeInvocationDictionary Invocations { get; } = new(); /// /// Gets or sets whether the mock is running in or /// . /// - public JSRuntimeMode Mode { get; set; } + public virtual JSRuntimeMode Mode { get; set; } /// /// Gets the mocked instance. @@ -40,104 +40,10 @@ public class BunitJSInterop public BunitJSInterop() { Mode = JSRuntimeMode.Strict; - JSRuntime = new BUnitJSRuntime(this); + JSRuntime = new BunitJSRuntime(this); + AddCustomHandlers(); } - /// - /// Configure a catch all JSInterop invocation handler for a specific return type. - /// This will match only on the , and any arguments passed to - /// . - /// - /// The result type of the invocation. - /// A . - public JSRuntimeInvocationHandler Setup() - { - var result = new JSRuntimeInvocationHandler(JSRuntimeInvocationHandler.CatchAllIdentifier, _ => true); - AddInvocationHandler(result); - return result; - } - - /// - /// Configure a JSInterop invocation handler with the and arguments - /// passing the test. - /// - /// The result type of the invocation. - /// The identifier to setup a response for. - /// A matcher that is passed an associated with the. If it returns true the invocation is matched. - /// A . - public JSRuntimeInvocationHandler Setup(string identifier, InvocationMatcher invocationMatcher) - { - var result = new JSRuntimeInvocationHandler(identifier, invocationMatcher); - AddInvocationHandler(result); - return result; - } - - /// - /// Configure a JSInterop invocation handler with the and . - /// - /// - /// The identifier to setup a response for. - /// The arguments that an invocation to should match. - /// A . - public JSRuntimeInvocationHandler Setup(string identifier, params object[] arguments) - { - return Setup(identifier, invocation => invocation.Arguments.SequenceEqual(arguments)); - } - - /// - /// Configure a JSInterop invocation handler with the and arguments - /// passing the test, that should not receive any result. - /// - /// The identifier to setup a response for. - /// A matcher that is passed an associated with the. If it returns true the invocation is matched. - /// A . - public JSRuntimeInvocationHandler SetupVoid(string identifier, InvocationMatcher invocationMatcher) - { - var result = new JSRuntimeInvocationHandler(identifier, invocationMatcher); - AddInvocationHandler(result); - return result; - } - - /// - /// Configure a JSInterop invocation handler with the - /// and , that should not receive any result. - /// - /// The identifier to setup a response for. - /// The arguments that an invocation to should match. - /// A . - public JSRuntimeInvocationHandler SetupVoid(string identifier, params object[] arguments) - { - return SetupVoid(identifier, invocation => invocation.Arguments.SequenceEqual(arguments)); - } - - /// - /// Configure a catch all JSInterop invocation handler, that should not receive any result. - /// - /// A . - public JSRuntimeInvocationHandler SetupVoid() - { - var result = new JSRuntimeInvocationHandler(JSRuntimeInvocationHandler.CatchAllIdentifier, _ => true); - AddInvocationHandler(result); - return result; - } - - /// - /// Looks through the registered handlers and returns the latest registered that can handle - /// the provided and , and that - /// will return . - /// - /// Returns the or null if no one is found. - public JSRuntimeInvocationHandler? TryGetInvokeHandler(string identifier, object?[]? args = null) - => TryGetHandlerFor(new JSRuntimeInvocation(identifier, default, args)) as JSRuntimeInvocationHandler; - - /// - /// Looks through the registered handlers and returns the latest registered that can handle - /// the provided and , and that returns a "void" result. - /// - /// Returns the or null if no one is found. - public JSRuntimeInvocationHandler? TryGetInvokeVoidHandler(string identifier, object?[]? args = null) - => TryGetHandlerFor(new JSRuntimeInvocation(identifier, default, args), x => x.IsVoidResultHandler) as JSRuntimeInvocationHandler; - /// /// Adds an invocation handler to bUnit's JSInterop. Can be used to register /// custom invocation handlers. @@ -151,16 +57,13 @@ public void AddInvocationHandler(JSRuntimeInvocationHandlerBase()); - } - _invocations[invocation.Identifier].Add(invocation); + Invocations.RegisterInvocation(invocation); } - private JSRuntimeInvocationHandlerBase? TryGetHandlerFor(JSRuntimeInvocation invocation, Predicate>? handlerPredicate = null) + internal JSRuntimeInvocationHandlerBase? TryGetHandlerFor(JSRuntimeInvocation invocation, Predicate>? handlerPredicate = null) { handlerPredicate ??= _ => true; JSRuntimeInvocationHandlerBase? result = default; @@ -168,75 +71,25 @@ private void RegisterInvocation(JSRuntimeInvocation invocation) if (_handlers.TryGetValue(invocation.Identifier, out var plannedInvocations)) { result = plannedInvocations.OfType>() - .LastOrDefault(x => handlerPredicate(x) && x.Matches(invocation)); + .LastOrDefault(x => handlerPredicate(x) && x.CanHandle(invocation)); } if (result is null && _handlers.TryGetValue(JSRuntimeInvocationHandler.CatchAllIdentifier, out var catchAllHandlers)) { result = catchAllHandlers.OfType>() - .LastOrDefault(x => handlerPredicate(x) && x.Matches(invocation)); + .LastOrDefault(x => handlerPredicate(x) && x.CanHandle(invocation)); } return result; } - - [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 void AddCustomHandlers() { - private readonly BunitJSInterop _jsInterop; - - public BUnitJSRuntime(BunitJSInterop bunitJsInterop) - { - _jsInterop = bunitJsInterop; - } - - public TResult Invoke(string identifier, params object?[]? args) => - InvokeAsync(identifier, args).GetAwaiter().GetResult(); - - public ValueTask InvokeAsync(string identifier, object?[]? args) - => InvokeAsync(identifier, default, args); - - public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args) - { - var invocation = new JSRuntimeInvocation(identifier, cancellationToken, args); - _jsInterop.RegisterInvocation(invocation); - - return TryHandlePlannedInvocation(invocation) ?? new ValueTask(default(TValue)!); - } - - public TResult InvokeUnmarshalled(string identifier) => - InvokeAsync(identifier, Array.Empty()).GetAwaiter().GetResult(); - - public TResult InvokeUnmarshalled(string identifier, T0 arg0) => - InvokeAsync(identifier, new object?[] {arg0}).GetAwaiter().GetResult(); - - public TResult InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1) => - InvokeAsync(identifier, new object?[] { arg0, arg1 }).GetAwaiter().GetResult(); - - public TResult InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1, T2 arg2) => - InvokeAsync(identifier, new object?[] { arg0, arg1, arg2 }).GetAwaiter().GetResult(); - - private ValueTask? TryHandlePlannedInvocation(JSRuntimeInvocation invocation) - { - ValueTask? result = default; - - if (_jsInterop.TryGetHandlerFor(invocation) is JSRuntimeInvocationHandlerBase handler) - { - var task = handler.RegisterInvocation(invocation); - result = new ValueTask(task); - } - else if (_jsInterop.Mode == JSRuntimeMode.Strict) - { - throw new JSRuntimeUnhandledInvocationException(invocation); - } - - return result; - } +#if NET5_0 + AddInvocationHandler(new FocusAsyncInvocationHandler()); + AddInvocationHandler(new VirtualizeJSRuntimeInvocationHandler()); + AddInvocationHandler(new LooseModeJSObjectReferenceInvocationHandler(this)); +#endif } } } diff --git a/src/bunit.web/JSInterop/BunitJSInteropExtensions.cs b/src/bunit.web/JSInterop/BunitJSInteropExtensions.cs deleted file mode 100644 index 347ec8e2d..000000000 --- a/src/bunit.web/JSInterop/BunitJSInteropExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -#if NET5_0 -using Bunit.JSInterop.InvocationHandlers; -#endif - -namespace Bunit.JSInterop -{ - /// - /// Helper methods for registering handlers on the . - /// - public static class BunitJSInteropExtensions - { - /// - /// Adds the built-in JSRuntime invocation handlers to the . - /// - public static BunitJSInterop AddBuiltInJSRuntimeInvocationHandlers(this BunitJSInterop jsInterop) - { -#if NET5_0 - jsInterop.AddInvocationHandler(new FocusAsyncInvocationHandler()); - jsInterop.AddInvocationHandler(new VirtualizeJSRuntimeInvocationHandler()); -#endif - return jsInterop; - } - } -} diff --git a/src/bunit.web/JSInterop/BunitJSInteropSetupExtensions.cs b/src/bunit.web/JSInterop/BunitJSInteropSetupExtensions.cs new file mode 100644 index 000000000..6375062ff --- /dev/null +++ b/src/bunit.web/JSInterop/BunitJSInteropSetupExtensions.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using Bunit.JSInterop.InvocationHandlers; +using Microsoft.JSInterop; + +namespace Bunit.JSInterop +{ + /// + /// Helper methods for creating invocation handlers and adding the to a . + /// + public static partial class BunitJSInteropSetupExtensions + { + /// + /// Configure a JSInterop invocation handler with the and arguments + /// passing the test. + /// + /// The result type of the invocation. + /// The bUnit JSInterop to setup the invocation handling with. + /// The identifier to setup a response for. + /// A matcher that is passed an associated with the. If it returns true the invocation is matched. + /// A . + public static JSRuntimeInvocationHandler Setup(this BunitJSInterop jsInterop, string identifier, InvocationMatcher invocationMatcher) + { +#if NET5_0 + EnsureResultNotIJSObjectReference(); +#endif + + var result = new JSRuntimeInvocationHandler(identifier, invocationMatcher); + jsInterop.AddInvocationHandler(result); + return result; + } + + /// + /// Configure a JSInterop invocation handler with the and . + /// + /// + /// The bUnit JSInterop to setup the invocation handling with. + /// The identifier to setup a response for. + /// The arguments that an invocation to should match. + /// A . + public static JSRuntimeInvocationHandler Setup(this BunitJSInterop jsInterop, string identifier, params object?[]? arguments) + => Setup(jsInterop, identifier, invocation => invocation.Arguments.SequenceEqual(arguments ?? Array.Empty())); + + /// + /// Configure a catch all JSInterop invocation handler for a specific return type. + /// This will match only on the , and any arguments passed to + /// . + /// + /// The result type of the invocation. + /// The bUnit JSInterop to setup the invocation handling with. + /// A . + public static JSRuntimeInvocationHandler Setup(this BunitJSInterop jsInterop) + => Setup(jsInterop, JSRuntimeInvocationHandler.CatchAllIdentifier, _ => true); + + /// + /// Configure a JSInterop invocation handler with the and arguments + /// passing the test, that should not receive any result. + /// + /// The bUnit JSInterop to setup the invocation handling with. + /// The identifier to setup a response for. + /// A matcher that is passed an associated with the. If it returns true the invocation is matched. + /// A . + public static JSRuntimeInvocationHandler SetupVoid(this BunitJSInterop jsInterop, string identifier, InvocationMatcher invocationMatcher) + { + var result = new JSRuntimeInvocationHandler(identifier, invocationMatcher); + jsInterop.AddInvocationHandler(result); + return result; + } + + /// + /// Configure a JSInterop invocation handler with the + /// and , that should not receive any result. + /// + /// The bUnit JSInterop to setup the invocation handling with. + /// The identifier to setup a response for. + /// The arguments that an invocation to should match. + /// A . + public static JSRuntimeInvocationHandler SetupVoid(this BunitJSInterop jsInterop, string identifier, params object?[]? arguments) + => SetupVoid(jsInterop, identifier, invocation => invocation.Arguments.SequenceEqual(arguments ?? Array.Empty())); + + /// + /// Configure a catch all JSInterop invocation handler, that should not receive any result. + /// + /// The bUnit JSInterop to setup the invocation handling with. + /// A . + public static JSRuntimeInvocationHandler SetupVoid(this BunitJSInterop jsInterop) + => SetupVoid(jsInterop, JSRuntimeInvocationHandler.CatchAllIdentifier, _ => true); + + /// + /// Looks through the registered handlers and returns the latest registered that can handle + /// the provided and , and that + /// will return . + /// + /// The bUnit JSInterop to setup the invocation handling with. + /// The identifier the handler should match with. + /// The arguments that an invocation to should match. + /// Returns the or null if no one is found. + public static JSRuntimeInvocationHandler? TryGetInvokeHandler(this BunitJSInterop jsInterop, string identifier, params object?[]? arguments) + => jsInterop.TryGetHandlerFor(new JSRuntimeInvocation(identifier, default, arguments)) as JSRuntimeInvocationHandler; + + /// + /// Looks through the registered handlers and returns the latest registered that can handle + /// the provided and , and that returns a "void" result. + /// + /// The bUnit JSInterop to setup the invocation handling with. + /// The identifier the handler should match with. + /// The arguments that an invocation to should match. + /// Returns the or null if no one is found. + public static JSRuntimeInvocationHandler? TryGetInvokeVoidHandler(this BunitJSInterop jsInterop, string identifier, params object?[]? arguments) + => jsInterop.TryGetHandlerFor(new JSRuntimeInvocation(identifier, default, arguments), x => x.IsVoidResultHandler) as JSRuntimeInvocationHandler; + +#if NET5_0 + private static void EnsureResultNotIJSObjectReference() + { + const string UseSetupModuleErrorMessage = "Use one of the SetupModule() methods instead to set up an invocation handler that returns an IJSObjectReference."; + var resultType = typeof(TResult); + if (resultType == typeof(IJSObjectReference)) + throw new ArgumentException(UseSetupModuleErrorMessage); + if (resultType == typeof(Microsoft.JSInterop.Implementation.JSObjectReference)) + throw new ArgumentException(UseSetupModuleErrorMessage); + } +#endif + } +} diff --git a/src/bunit.web/JSInterop/BunitJSInteropSetupExtensions.net5.cs b/src/bunit.web/JSInterop/BunitJSInteropSetupExtensions.net5.cs new file mode 100644 index 000000000..1e751cdc4 --- /dev/null +++ b/src/bunit.web/JSInterop/BunitJSInteropSetupExtensions.net5.cs @@ -0,0 +1,141 @@ +#if NET5_0 +using System; +using System.Linq; +using Bunit.JSInterop; +using Bunit.JSInterop.InvocationHandlers; +using Bunit.JSInterop.InvocationHandlers.Implementation; +using Microsoft.JSInterop; + +namespace Bunit +{ + /// + /// Helper methods for creating invocation handlers and adding the to a . + /// + public static partial class BunitJSInteropSetupExtensions + { + private const string DEFAULT_IMPORT_IDENTIFER = "import"; + + /// + /// Setup a handler for a IJSRuntime.InvokeAsync<IJSObjectReference>("import", ) + /// call. + /// + /// + /// The returned can be used to setup handlers for + /// InvokeAsync<TValue>(string, object?[]?)" calls to the module, using either + /// or Setup calls. + /// + /// The JSInterop to setup the handler for. + /// The name of the JavaScript module to handle invocations for. + /// Thrown when is null. + /// Thrown when is null or whitespace. + /// A . + public static BunitJSModuleInterop SetupModule(this BunitJSInterop jsInterop, string moduleName) + { + if (string.IsNullOrWhiteSpace(moduleName)) + throw new ArgumentException($"'{nameof(moduleName)}' cannot be null or whitespace.", nameof(moduleName)); + + return SetupModule(jsInterop, + DEFAULT_IMPORT_IDENTIFER, + invocation => invocation.Arguments?[0] is string requestedModuleName + && requestedModuleName.Equals(moduleName, StringComparison.Ordinal)); + } + + /// + /// Setup a handler for a IJSRuntime.InvokeAsync<IJSObjectReference>(, ) + /// call. + /// + /// + /// The returned can be used to setup handlers for + /// InvokeAsync<TValue>(string, object?[]?)" calls to the module, using either + /// or Setup calls. + /// + /// The JSInterop to setup the handler for. + /// The identifier to setup a response for. + /// The arguments that an invocation to should match. Use Array.Empty<object?>() for none. + /// Thrown when is null. + /// Thrown when is null or whitespace. + /// A . + public static BunitJSModuleInterop SetupModule(this BunitJSInterop jsInterop, string identifier, object?[] arguments) + => SetupModule(jsInterop, identifier, invocation => invocation.Arguments.SequenceEqual(arguments ?? Array.Empty())); + + /// + /// Setup a handler for a IJSRuntime.InvokeAsync<IJSObjectReference>() call whose input parameters is matched by the provided + /// . + /// + /// + /// The returned can be used to setup handlers for + /// InvokeAsync<TValue>(string, object?[]?)" calls to the module, using either + /// or Setup calls. + /// + /// The JSInterop to setup the handler for. + /// The matcher to use to match 's with. + /// Thrown when is null. + /// Thrown when is null. + /// A . + public static BunitJSModuleInterop SetupModule(this BunitJSInterop jsInterop, InvocationMatcher invocationMatcher) + => SetupModule(jsInterop, DEFAULT_IMPORT_IDENTIFER, invocationMatcher); + + /// + /// Setup a handler for a IJSRuntime.InvokeAsync<IJSObjectReference>() call whose input parameters is matched by the provided + /// and the . + /// + /// + /// The returned can be used to setup handlers for + /// InvokeAsync<TValue>(string, object?[]?)" calls to the module, using either + /// or Setup calls. + /// + /// The JSInterop to setup the handler for. + /// The identifier to setup a response for. + /// The matcher to use to match 's with. + /// Thrown when is null. + /// Thrown when is null. + /// A . + public static BunitJSModuleInterop SetupModule(this BunitJSInterop jsInterop, string identifier, InvocationMatcher invocationMatcher) + { + if (jsInterop is null) + throw new ArgumentNullException(nameof(jsInterop)); + if (string.IsNullOrEmpty(identifier)) + throw new ArgumentException($"'{nameof(identifier)}' cannot be null or empty.", nameof(identifier)); + if (invocationMatcher is null) + throw new ArgumentNullException(nameof(invocationMatcher)); + + var result = CreateJSObjectReferenceInvocationHandler(jsInterop, identifier, invocationMatcher); + jsInterop.AddInvocationHandler(result); + return result.JSInterop; + } + + /// + /// Configure a catch all JSObjectReferenceInvocationHandler invocation handler for any module load and invocations + /// on those modules. + /// + /// + /// The returned can be used to setup handlers for + /// InvokeAsync<TValue>(string, object?[]?)" calls to the module, using either + /// or Setup calls. + /// + /// The JSInterop to setup the handler for. + /// Thrown when is null. + /// A . + public static BunitJSModuleInterop SetupModule(this BunitJSInterop jsInterop) + => SetupModule(jsInterop, JSRuntimeInvocationHandlerBase.CatchAllIdentifier, _ => true); + + /// + /// Looks through the registered handlers and returns the latest registered that can handle + /// the provided and , and that + /// will return . + /// + /// The JSInterop to setup the handler for. + /// The identifier the handler should match with. + /// The arguments that an invocation to should match. + /// A or null if no one is found. + public static BunitJSModuleInterop? TryGetModuleJSInterop(this BunitJSInterop jsInterop, string identifier, params object?[]? arguments) + { + var handler = jsInterop.TryGetHandlerFor(new JSRuntimeInvocation(identifier, default, arguments)) as JSObjectReferenceInvocationHandler; + return handler?.JSInterop; + } + + private static JSObjectReferenceInvocationHandler CreateJSObjectReferenceInvocationHandler(BunitJSInterop parent, string identifier, InvocationMatcher invocationMatcher) + => new JSObjectReferenceInvocationHandler(parent, identifier, invocationMatcher); + } +} +#endif diff --git a/src/bunit.web/JSInterop/BunitJSModuleInterop.cs b/src/bunit.web/JSInterop/BunitJSModuleInterop.cs new file mode 100644 index 000000000..2a33a8c59 --- /dev/null +++ b/src/bunit.web/JSInterop/BunitJSModuleInterop.cs @@ -0,0 +1,45 @@ +#if NET5_0 +namespace Bunit.JSInterop +{ + /// + /// Represents a bUnit JSInterop module. + /// + public sealed class BunitJSModuleInterop : BunitJSInterop + { + private readonly BunitJSInterop _parent; + private JSRuntimeMode? _handlerMode; + + /// + /// Gets or sets whether this + /// is running in or . + /// + /// + /// When this is not set explicitly, the mode from is used. + /// As soon as this is set, the mode will no longer be changed when the + /// changes. + /// + public override JSRuntimeMode Mode + { + get => _handlerMode ?? _parent.Mode; + set => _handlerMode = value; + } + + /// + /// Creates an instance of the . + /// + /// The parent . + public BunitJSModuleInterop(BunitJSInterop parent) + { + _parent = parent; + _handlerMode = null; + } + + /// + internal override void RegisterInvocation(JSRuntimeInvocation invocation) + { + Invocations.RegisterInvocation(invocation); + _parent.RegisterInvocation(invocation); + } + } +} +#endif diff --git a/src/bunit.web/JSInterop/Implementation/BunitJSObjectReference.cs b/src/bunit.web/JSInterop/Implementation/BunitJSObjectReference.cs new file mode 100644 index 000000000..586f74d41 --- /dev/null +++ b/src/bunit.web/JSInterop/Implementation/BunitJSObjectReference.cs @@ -0,0 +1,56 @@ +#if NET5_0 +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.JSInterop; + +namespace Bunit +{ + [SuppressMessage("Minor Code Smell", "S1939:Inheritance list should not be redundant", Justification = "By design. To make it obvious that both is implemented.")] + [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.")] + internal sealed class BunitJSObjectReference : IJSObjectReference, IJSInProcessObjectReference, IJSUnmarshalledObjectReference + { + private readonly IJSRuntime _jsRuntime; + + public BunitJSObjectReference(IJSRuntime jsRuntime) + { + _jsRuntime = jsRuntime; + } + + /// + public ValueTask InvokeAsync(string identifier, object?[]? args) + => _jsRuntime.InvokeAsync(identifier, CancellationToken.None, args); + + /// + public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args) + => _jsRuntime.InvokeAsync(identifier, cancellationToken, args); + + /// + public TValue Invoke(string identifier, params object?[]? args) + => InvokeAsync(identifier, args).GetAwaiter().GetResult(); + + /// + public TResult InvokeUnmarshalled(string identifier) => + InvokeAsync(identifier, Array.Empty()).GetAwaiter().GetResult(); + + /// + public TResult InvokeUnmarshalled(string identifier, T0 arg0) => + InvokeAsync(identifier, new object?[] { arg0 }).GetAwaiter().GetResult(); + + /// + public TResult InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1) => + InvokeAsync(identifier, new object?[] { arg0, arg1 }).GetAwaiter().GetResult(); + + /// + public TResult InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1, T2 arg2) => + InvokeAsync(identifier, new object?[] { arg0, arg1, arg2 }).GetAwaiter().GetResult(); + + /// + public void Dispose() { } + + /// + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } +} +#endif diff --git a/src/bunit.web/JSInterop/Implementation/BunitJSRuntime.cs b/src/bunit.web/JSInterop/Implementation/BunitJSRuntime.cs new file mode 100644 index 000000000..89185353e --- /dev/null +++ b/src/bunit.web/JSInterop/Implementation/BunitJSRuntime.cs @@ -0,0 +1,56 @@ +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Bunit.JSInterop.InvocationHandlers; +using Microsoft.JSInterop; + +namespace Bunit.JSInterop +{ + [SuppressMessage("Minor Code Smell", "S1939:Inheritance list should not be redundant", Justification = "By design. To make it obvious that both is implemented.")] + [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.")] + internal sealed partial class BunitJSRuntime : IJSRuntime, IJSInProcessRuntime + { + private BunitJSInterop _jsInterop { get; } + + public BunitJSRuntime(BunitJSInterop jsInterop) + { + _jsInterop = jsInterop; + } + + /// + public ValueTask InvokeAsync(string identifier, object?[]? args) + => InvokeAsync(identifier, default, args); + + /// + public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object?[]? args) + { + var invocation = new JSRuntimeInvocation(identifier, cancellationToken, args); + + _jsInterop.RegisterInvocation(invocation); + + return TryHandlePlannedInvocation(invocation) + ?? new ValueTask(default(TValue)!); + } + + /// + public TResult Invoke(string identifier, params object?[]? args) + => InvokeAsync(identifier, args).GetAwaiter().GetResult(); + + private ValueTask? TryHandlePlannedInvocation(JSRuntimeInvocation invocation) + { + ValueTask? result = default; + + if (_jsInterop.TryGetHandlerFor(invocation) is JSRuntimeInvocationHandlerBase handler) + { + var task = handler.Handle(invocation); + result = new ValueTask(task); + } + else if (_jsInterop.Mode == JSRuntimeMode.Strict) + { + throw new JSRuntimeUnhandledInvocationException(invocation); + } + + return result; + } + } +} diff --git a/src/bunit.web/JSInterop/Implementation/BunitJSRuntime.net5.cs b/src/bunit.web/JSInterop/Implementation/BunitJSRuntime.net5.cs new file mode 100644 index 000000000..991536955 --- /dev/null +++ b/src/bunit.web/JSInterop/Implementation/BunitJSRuntime.net5.cs @@ -0,0 +1,26 @@ +#if NET5_0 +using System; +using Microsoft.JSInterop; + +namespace Bunit.JSInterop +{ + internal sealed partial class BunitJSRuntime : IJSUnmarshalledRuntime + { + /// + public TResult InvokeUnmarshalled(string identifier) => + InvokeAsync(identifier, Array.Empty()).GetAwaiter().GetResult(); + + /// + public TResult InvokeUnmarshalled(string identifier, T0 arg0) => + InvokeAsync(identifier, new object?[] { arg0 }).GetAwaiter().GetResult(); + + /// + public TResult InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1) => + InvokeAsync(identifier, new object?[] { arg0, arg1 }).GetAwaiter().GetResult(); + + /// + public TResult InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1, T2 arg2) => + InvokeAsync(identifier, new object?[] { arg0, arg1, arg2 }).GetAwaiter().GetResult(); + } +} +#endif diff --git a/src/bunit.web/JSInterop/InvocationHandlers/FocusAsyncInvocationHandler.cs b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/FocusAsyncInvocationHandler.cs similarity index 64% rename from src/bunit.web/JSInterop/InvocationHandlers/FocusAsyncInvocationHandler.cs rename to src/bunit.web/JSInterop/InvocationHandlers/Implementation/FocusAsyncInvocationHandler.cs index 534f49d76..df4ddd5e9 100644 --- a/src/bunit.web/JSInterop/InvocationHandlers/FocusAsyncInvocationHandler.cs +++ b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/FocusAsyncInvocationHandler.cs @@ -1,18 +1,13 @@ #if NET5_0 -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Components; -namespace Bunit.JSInterop.InvocationHandlers +namespace Bunit.JSInterop.InvocationHandlers.Implementation { /// /// Represents a handler for Blazor's /// feature. /// - public class FocusAsyncInvocationHandler : JSRuntimeInvocationHandler + internal sealed class FocusAsyncInvocationHandler : JSRuntimeInvocationHandler { /// /// The internal identifier used by @@ -23,7 +18,7 @@ public class FocusAsyncInvocationHandler : JSRuntimeInvocationHandler /// /// Creates an instance of the . /// - protected internal FocusAsyncInvocationHandler() : base(FocusIdentifier, _ => true) + internal FocusAsyncInvocationHandler() : base(FocusIdentifier, _ => true) { } } diff --git a/src/bunit.web/JSInterop/InvocationHandlers/Implementation/JSObjectReferenceInvocationHandler.cs b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/JSObjectReferenceInvocationHandler.cs new file mode 100644 index 000000000..b37d91840 --- /dev/null +++ b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/JSObjectReferenceInvocationHandler.cs @@ -0,0 +1,29 @@ +#if NET5_0 +using Microsoft.JSInterop; + +namespace Bunit.JSInterop.InvocationHandlers.Implementation +{ + /// + /// Represents a JavaScript module handler for requests for from bUnit's + /// . This handler allows the user to setup invocation handlers + /// for the modules it is configured to handle. + /// + internal sealed class JSObjectReferenceInvocationHandler : JSRuntimeInvocationHandler + { + /// + /// Gets the for modules matching with this invocation handler. + /// + public BunitJSModuleInterop JSInterop { get; } + + /// + /// Creates an instance of the . + /// + public JSObjectReferenceInvocationHandler(BunitJSInterop parent, string identifier, InvocationMatcher invocationMatcher) + : base(identifier, invocationMatcher) + { + JSInterop = new BunitJSModuleInterop(parent); + SetResult(new BunitJSObjectReference(JSInterop.JSRuntime)); + } + } +} +#endif diff --git a/src/bunit.web/JSInterop/InvocationHandlers/Implementation/LooseModeJSObjectReferenceInvocationHandler.cs b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/LooseModeJSObjectReferenceInvocationHandler.cs new file mode 100644 index 000000000..b642218be --- /dev/null +++ b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/LooseModeJSObjectReferenceInvocationHandler.cs @@ -0,0 +1,19 @@ +#if NET5_0 +using Microsoft.JSInterop; + +namespace Bunit.JSInterop.InvocationHandlers.Implementation +{ + /// + /// Special mode invocation handler for . + /// Will match all loose mode calls of the parent . + /// + internal sealed class LooseModeJSObjectReferenceInvocationHandler : JSRuntimeInvocationHandler + { + internal LooseModeJSObjectReferenceInvocationHandler(BunitJSInterop parent) + : base(CatchAllIdentifier, _ => parent.Mode == JSRuntimeMode.Loose) + { + SetResult(new BunitJSObjectReference(parent.JSRuntime)); + } + } +} +#endif diff --git a/src/bunit.web/JSInterop/InvocationHandlers/VirtualizeJSRuntimeInvocationHandler.cs b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs similarity index 68% rename from src/bunit.web/JSInterop/InvocationHandlers/VirtualizeJSRuntimeInvocationHandler.cs rename to src/bunit.web/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs index 1821fa20e..f8451db31 100644 --- a/src/bunit.web/JSInterop/InvocationHandlers/VirtualizeJSRuntimeInvocationHandler.cs +++ b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/VirtualizeJSRuntimeInvocationHandler.cs @@ -2,15 +2,16 @@ using System; using System.Diagnostics; using System.Reflection; +using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Web.Virtualization; using Microsoft.JSInterop; -namespace Bunit.JSInterop.InvocationHandlers +namespace Bunit.JSInterop.InvocationHandlers.Implementation { /// /// Represents an JSInterop handler for the component. /// - public class VirtualizeJSRuntimeInvocationHandler : JSRuntimeInvocationHandler + internal sealed class VirtualizeJSRuntimeInvocationHandler : JSRuntimeInvocationHandler { private const string JsFunctionsPrefix = "Blazor._internal.Virtualize."; private static readonly Lazy<(PropertyInfo, MethodInfo)> VirtualizeReflection = new Lazy<(PropertyInfo, MethodInfo)>(() => @@ -36,20 +37,22 @@ internal VirtualizeJSRuntimeInvocationHandler() : base(CatchAllIdentifier, i => i.Identifier.StartsWith(JsFunctionsPrefix, StringComparison.Ordinal)) { } - /// - protected override void OnInvocation(JSRuntimeInvocation invocation) + /// + protected internal override Task Handle(JSRuntimeInvocation invocation) { - if (invocation.Identifier.Equals(JsFunctionsPrefix + "dispose", StringComparison.Ordinal)) - return; + if (!invocation.Identifier.Equals(JsFunctionsPrefix + "dispose", StringComparison.Ordinal)) + { + // Assert expectations about the internals of the component + Debug.Assert(invocation.Identifier.Equals(JsFunctionsPrefix + "init", StringComparison.Ordinal), "Received an unexpected invocation identifier from the component."); + Debug.Assert(invocation.Arguments.Count == 3, "Received an unexpected amount of arguments from the component."); + Debug.Assert(invocation.Arguments[0] is not null, "Received an unexpected null argument, expected an DotNetObjectReference instance."); - // Assert expectations about the internals of the component - Debug.Assert(invocation.Identifier.Equals(JsFunctionsPrefix + "init", StringComparison.Ordinal), "Received an unexpected invocation identifier from the component."); - Debug.Assert(invocation.Arguments.Count == 3, "Received an unexpected amount of arguments from the component."); - Debug.Assert(invocation.Arguments[0] is not null, "Received an unexpected null argument, expected an DotNetObjectReference instance."); + InvokeOnSpacerBeforeVisible(invocation.Arguments[0]!); - InvokeOnSpacerBeforeVisible(invocation.Arguments[0]!); + SetVoidResult(); + } - SetVoidResult(); + return base.Handle(invocation); } private static void InvokeOnSpacerBeforeVisible(object dotNetObjectReference) diff --git a/src/bunit.web/JSInterop/InvocationHandlers/JSRuntimeInvocationHandler.cs b/src/bunit.web/JSInterop/InvocationHandlers/JSRuntimeInvocationHandler.cs new file mode 100644 index 000000000..1f1ab9ea2 --- /dev/null +++ b/src/bunit.web/JSInterop/InvocationHandlers/JSRuntimeInvocationHandler.cs @@ -0,0 +1,97 @@ +using System; +using System.Threading.Tasks; + +namespace Bunit.JSInterop.InvocationHandlers +{ + /// + /// Represents a handler for an invocation of a JavaScript function with specific arguments + /// and returns . + /// + /// The expect result type. + public class JSRuntimeInvocationHandler : JSRuntimeInvocationHandlerBase + { + /// + /// Creates an instance of a type. + /// + protected internal JSRuntimeInvocationHandler(string identifier, InvocationMatcher matcher) : base(identifier, matcher) { } + + /// + /// Sets the result that invocations will receive. + /// + /// + /// This handler to allow calls to be chained. + public JSRuntimeInvocationHandler SetResult(TResult result) + { + SetResultBase(result); + return this; + } + + /// + /// Marks the that invocations will receive as canceled. + /// + /// This handler to allow calls to be chained. + public JSRuntimeInvocationHandler SetCanceled() + { + SetCanceledBase(); + return this; + } + + /// + /// Sets the exception that invocations will receive. + /// + /// The exception to set. + /// This handler to allow calls to be chained. + public JSRuntimeInvocationHandler SetException(TException exception) + where TException : Exception + { + SetExceptionBase(exception); + return this; + } + } + + /// + /// Represents a handler for an invocation of a JavaScript function which returns nothing, with specific arguments. + /// + public class JSRuntimeInvocationHandler : JSRuntimeInvocationHandlerBase + { + /// + public override sealed bool IsVoidResultHandler { get; } = true; + + /// + /// Creates an instance of a type. + /// + protected internal JSRuntimeInvocationHandler(string identifier, InvocationMatcher matcher) : base(identifier, matcher) { } + + /// + /// Completes the current awaiting void invocation requests. + /// + /// This handler to allow calls to be chained. + public JSRuntimeInvocationHandler SetVoidResult() + { + SetResultBase(default!); + return this; + } + + /// + /// Marks the that invocations will receive as canceled. + /// + /// This handler to allow calls to be chained. + public JSRuntimeInvocationHandler SetCanceled() + { + SetCanceledBase(); + return this; + } + + /// + /// Sets the exception that invocations will receive. + /// + /// The exception to set. + /// This handler to allow calls to be chained. + public JSRuntimeInvocationHandler SetException(TException exception) + where TException : Exception + { + SetExceptionBase(exception); + return this; + } + } +} diff --git a/src/bunit.web/JSInterop/JSRuntimeInvocationHandlerBase.cs b/src/bunit.web/JSInterop/InvocationHandlers/JSRuntimeInvocationHandlerBase.cs similarity index 69% rename from src/bunit.web/JSInterop/JSRuntimeInvocationHandlerBase.cs rename to src/bunit.web/JSInterop/InvocationHandlers/JSRuntimeInvocationHandlerBase.cs index 627e22980..7c39eb61f 100644 --- a/src/bunit.web/JSInterop/JSRuntimeInvocationHandlerBase.cs +++ b/src/bunit.web/JSInterop/InvocationHandlers/JSRuntimeInvocationHandlerBase.cs @@ -1,8 +1,7 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; -namespace Bunit +namespace Bunit.JSInterop.InvocationHandlers { /// /// Represents an invocation handler for instances. @@ -15,7 +14,6 @@ public abstract class JSRuntimeInvocationHandlerBase protected internal const string CatchAllIdentifier = "*"; private readonly InvocationMatcher _invocationMatcher; - private readonly List _invocations; private TaskCompletionSource _completionSource; /// @@ -36,7 +34,7 @@ public abstract class JSRuntimeInvocationHandlerBase /// /// Gets the invocations that this has matched with. /// - public IReadOnlyList Invocations => _invocations.AsReadOnly(); + public JSRuntimeInvocationDictionary Invocations { get; } = new(); /// /// Creates an instance of the . @@ -48,14 +46,13 @@ protected JSRuntimeInvocationHandlerBase(string identifier, InvocationMatcher ma Identifier = identifier; IsCatchAllHandler = identifier == CatchAllIdentifier; _invocationMatcher = matcher; - _invocations = new List(); _completionSource = new TaskCompletionSource(); } /// /// Marks the that invocations will receive as canceled. /// - public void SetCanceled() + protected void SetCanceledBase() { if (_completionSource.Task.IsCompleted) _completionSource = new TaskCompletionSource(); @@ -67,7 +64,7 @@ public void SetCanceled() /// Sets the exception that invocations will receive. /// /// - public void SetException(TException exception) + protected void SetExceptionBase(TException exception) where TException : Exception { if (_completionSource.Task.IsCompleted) @@ -80,7 +77,7 @@ public void SetException(TException exception) /// Sets the result that invocations will receive. /// /// - public void SetResultBase(TResult result) + protected void SetResultBase(TResult result) { if (_completionSource.Task.IsCompleted) _completionSource = new TaskCompletionSource(); @@ -89,21 +86,26 @@ public void SetResultBase(TResult result) } /// - /// This method is called when a new invocation is registered with the handler, - /// but before the invocation receives the result task from the handler. + /// Call this to have the this handler handle the . /// - /// The received invocation. - protected virtual void OnInvocation(JSRuntimeInvocation invocation) { } - - internal bool Matches(JSRuntimeInvocation invocation) => (IsCatchAllHandler || MatchesIdentifier(invocation)) && _invocationMatcher(invocation); - - private bool MatchesIdentifier(JSRuntimeInvocation invocation) => Identifier.Equals(invocation.Identifier, StringComparison.Ordinal); - - internal Task RegisterInvocation(JSRuntimeInvocation invocation) + /// + /// Note to implementors: Always call the + /// method when overriding it in a sub class. It will make sure the invocation is correctly registered in the dictionary. + /// + /// Invocation to handle. + protected internal virtual Task Handle(JSRuntimeInvocation invocation) { - _invocations.Add(invocation); - OnInvocation(invocation); + Invocations.RegisterInvocation(invocation); return _completionSource.Task; } + + /// + /// Checks whether this invocation handler can handle the . + /// + /// Invocation to check. + /// True if the handler can handle the invocation, false otherwise. + internal bool CanHandle(JSRuntimeInvocation invocation) => (IsCatchAllHandler || MatchesIdentifier(invocation)) && _invocationMatcher(invocation); + + private bool MatchesIdentifier(JSRuntimeInvocation invocation) => Identifier.Equals(invocation.Identifier, StringComparison.Ordinal); } } diff --git a/src/bunit.web/JSInterop/InvocationMatcher.cs b/src/bunit.web/JSInterop/InvocationMatcher.cs index 38b2c2206..52dc81b9f 100644 --- a/src/bunit.web/JSInterop/InvocationMatcher.cs +++ b/src/bunit.web/JSInterop/InvocationMatcher.cs @@ -1,11 +1,13 @@ +using Bunit.JSInterop.InvocationHandlers; + namespace Bunit { /// /// Represents a invocation matcher / predicate, that is used to determine - /// if a matches a specific + /// if a matches a specific /// . /// /// The invocation to match against. - /// True if the can handle the invocation, false otherwise. + /// True if the can handle the invocation, false otherwise. public delegate bool InvocationMatcher(JSRuntimeInvocation invocation); } diff --git a/src/bunit.web/JSInterop/JSRuntimeInvocationDictionary.cs b/src/bunit.web/JSInterop/JSRuntimeInvocationDictionary.cs new file mode 100644 index 000000000..5f5e99d4f --- /dev/null +++ b/src/bunit.web/JSInterop/JSRuntimeInvocationDictionary.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Bunit.JSInterop +{ + /// + /// Represents a dictionary of , keyed by their identifier. + /// + public sealed class JSRuntimeInvocationDictionary : IReadOnlyCollection + { + private readonly Dictionary> _invocations = new(); + private int _count; + + /// + /// Gets all invocations for a specific . + /// + /// The identifier to get invocations for. + /// An . + public IReadOnlyList this[string identifier] + { + get => _invocations.ContainsKey(identifier) + ? _invocations[identifier] + : Array.Empty(); + } + + /// + /// Gets a read only collection of all the identifiers used in invocations in this dictionary. + /// + public IReadOnlyCollection Identifiers => _invocations.Keys; + + /// + /// Gets the total number of invocations registered in the dictionary. + /// + public int Count => _count; + + /// + /// Gets an that will + /// iterate over all invocations in the dictionary. + /// + /// + public IEnumerator GetEnumerator() + { + foreach (var kvp in _invocations) + { + foreach (var item in kvp.Value) + { + yield return item; + } + } + } + + /// + /// Register a new invocation with this dictionary. + /// + internal void RegisterInvocation(JSRuntimeInvocation invocation) + { + _count++; + if (!_invocations.ContainsKey(invocation.Identifier)) + { + _invocations.Add(invocation.Identifier, new List()); + } + _invocations[invocation.Identifier].Add(invocation); + } + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/src/bunit.web/JSInterop/JSRuntimeInvocationHandler.cs b/src/bunit.web/JSInterop/JSRuntimeInvocationHandler.cs deleted file mode 100644 index dd4570255..000000000 --- a/src/bunit.web/JSInterop/JSRuntimeInvocationHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Bunit -{ - /// - /// Represents a handler for an invocation of a JavaScript function with specific arguments - /// and returns . - /// - /// The expect result type. - public class JSRuntimeInvocationHandler : JSRuntimeInvocationHandlerBase - { - /// - /// Creates an instance of a type. - /// - protected internal JSRuntimeInvocationHandler(string identifier, InvocationMatcher matcher) : base(identifier, matcher) { } - - /// - /// Sets the result that invocations will receive. - /// - /// - public void SetResult(TResult result) => SetResultBase(result); - } - - /// - /// Represents a handler for an invocation of a JavaScript function which returns nothing, with specific arguments. - /// - public class JSRuntimeInvocationHandler : JSRuntimeInvocationHandlerBase - { - /// - public override sealed bool IsVoidResultHandler { get; } = true; - - /// - /// Creates an instance of a type. - /// - protected internal JSRuntimeInvocationHandler(string identifier, InvocationMatcher matcher) : base(identifier, matcher) { } - - /// - /// Completes the current awaiting void invocation requests. - /// - public void SetVoidResult() => SetResultBase(default!); - } -} diff --git a/src/bunit.web/JSInterop/JSRuntimeUnhandledInvocationException.cs b/src/bunit.web/JSInterop/JSRuntimeUnhandledInvocationException.cs index f285fd90c..7eda234ab 100644 --- a/src/bunit.web/JSInterop/JSRuntimeUnhandledInvocationException.cs +++ b/src/bunit.web/JSInterop/JSRuntimeUnhandledInvocationException.cs @@ -9,7 +9,7 @@ namespace Bunit /// received by the running in mode, /// which didn't contain a matching invocation handler. /// - public class JSRuntimeUnhandledInvocationException : Exception + public sealed class JSRuntimeUnhandledInvocationException : Exception { /// /// Gets the unplanned invocation. diff --git a/src/bunit.web/RazorTesting/Fixture.cs b/src/bunit.web/RazorTesting/Fixture.cs index 77ebe1a08..e4018c402 100644 --- a/src/bunit.web/RazorTesting/Fixture.cs +++ b/src/bunit.web/RazorTesting/Fixture.cs @@ -3,10 +3,8 @@ using System.Linq; using System.Threading.Tasks; using Bunit.Extensions; -using Bunit.JSInterop; using Bunit.RazorTesting; using Microsoft.AspNetCore.Components; -using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; namespace Bunit diff --git a/src/bunit.web/RazorTesting/SnapshotTest.cs b/src/bunit.web/RazorTesting/SnapshotTest.cs index db9b44bcd..852f6cb0c 100644 --- a/src/bunit.web/RazorTesting/SnapshotTest.cs +++ b/src/bunit.web/RazorTesting/SnapshotTest.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using Bunit.Extensions; -using Bunit.JSInterop; using Bunit.RazorTesting; using Bunit.Rendering; using Microsoft.AspNetCore.Components; diff --git a/src/bunit.web/Rendering/BunitHtmlParser.cs b/src/bunit.web/Rendering/BunitHtmlParser.cs index 4ff23f5f5..1bade2944 100644 --- a/src/bunit.web/Rendering/BunitHtmlParser.cs +++ b/src/bunit.web/Rendering/BunitHtmlParser.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading.Tasks; using AngleSharp; @@ -117,7 +118,10 @@ private static (IElement? ctx, string? matchedElement) GetParseContextFromTag(st context = CreateTable().AppendElement(document.CreateElement("colgroup")); matchedElement = COLGROUP_SUB_ELEMENT; } - else if (markup.StartsWithElements(SPECIAL_HTML_ELEMENTS, startIndex, out matchedElement)) { } + else if (markup.StartsWithElements(SPECIAL_HTML_ELEMENTS, startIndex, out matchedElement)) + { + // default case, nothing to do. + } else { context = document.Body; @@ -148,6 +152,9 @@ public void Dispose() private class SingleNodeNodeList : INodeList { private readonly INode node; + + [SuppressMessage("Major Code Smell", "S112:General exceptions should never be thrown", + Justification = "This is an indexer, thus it makes sense in to throw IndexOutOfRangeException here")] public INode this[int index] { get diff --git a/src/bunit.web/Rendering/Internal/Htmlizer.cs b/src/bunit.web/Rendering/Internal/Htmlizer.cs index 2f218d5c7..00adb9e23 100644 --- a/src/bunit.web/Rendering/Internal/Htmlizer.cs +++ b/src/bunit.web/Rendering/Internal/Htmlizer.cs @@ -216,9 +216,7 @@ private static int RenderAttributes( if (frame.AttributeEventHandlerId > 0) { - // NOTE: this was changed from - // result.Add($" {frame.AttributeName}=\"{frame.AttributeEventHandlerId}\""); - // to the following to make it more obvious + // NOTE: this was changed to make it more obvious // that this is a generated/special blazor attribute // used for tracking event handler id's result.Add(" "); diff --git a/src/bunit.web/Rendering/RenderedFragment.cs b/src/bunit.web/Rendering/RenderedFragment.cs index 447574245..6e7da448e 100644 --- a/src/bunit.web/Rendering/RenderedFragment.cs +++ b/src/bunit.web/Rendering/RenderedFragment.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Threading; using AngleSharp.Diffing.Core; using AngleSharp.Dom; diff --git a/src/bunit.web/Rendering/WebTestRenderer.cs b/src/bunit.web/Rendering/WebTestRenderer.cs index 7da5bb5a5..476142723 100644 --- a/src/bunit.web/Rendering/WebTestRenderer.cs +++ b/src/bunit.web/Rendering/WebTestRenderer.cs @@ -1,8 +1,12 @@ using System; +#if NET5_0 using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; +# endif using Microsoft.Extensions.Logging; +#if NET5_0 using Microsoft.JSInterop; +#endif namespace Bunit.Rendering { diff --git a/src/bunit.web/TestContext.cs b/src/bunit.web/TestContext.cs index d0a7c3bf4..8f6edd78f 100644 --- a/src/bunit.web/TestContext.cs +++ b/src/bunit.web/TestContext.cs @@ -1,8 +1,6 @@ using System; using Bunit.Extensions; -using Bunit.JSInterop; using Microsoft.AspNetCore.Components; -using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; namespace Bunit diff --git a/src/bunit.xunit/Xunit.Sdk/RazorTest.cs b/src/bunit.xunit/Xunit.Sdk/RazorTest.cs index ba1fe5af4..3411beb3b 100644 --- a/src/bunit.xunit/Xunit.Sdk/RazorTest.cs +++ b/src/bunit.xunit/Xunit.Sdk/RazorTest.cs @@ -1,5 +1,3 @@ -using Xunit.Abstractions; - namespace Xunit.Sdk { internal class RazorTest : XunitTest diff --git a/src/bunit.xunit/Xunit.Sdk/RazorTestDiscoverer.cs b/src/bunit.xunit/Xunit.Sdk/RazorTestDiscoverer.cs index 90f999724..36ee8e17e 100644 --- a/src/bunit.xunit/Xunit.Sdk/RazorTestDiscoverer.cs +++ b/src/bunit.xunit/Xunit.Sdk/RazorTestDiscoverer.cs @@ -19,7 +19,7 @@ public RazorTestDiscoverer(IMessageSink diagnosticMessageSink) } /// - public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) + public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) { try { diff --git a/src/bunit.xunit/Xunit.Sdk/RazorTestInvoker.cs b/src/bunit.xunit/Xunit.Sdk/RazorTestInvoker.cs index 4d2e1395f..ed9cb9cf8 100644 --- a/src/bunit.xunit/Xunit.Sdk/RazorTestInvoker.cs +++ b/src/bunit.xunit/Xunit.Sdk/RazorTestInvoker.cs @@ -32,8 +32,6 @@ protected override object CreateTestClass() var test = tests[razorTest.TestNumber - 1]; - // TODO: ensure that test has not been replaced somehow. - return test; } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 8e924783a..51dc6420a 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,10 +1,9 @@ - + netcoreapp3.1;net5.0 false 9.0 enable - CS8600;CS8602;CS8603;CS8625 true latest true @@ -29,9 +28,9 @@ - - - + + + diff --git a/tests/bunit.testassets/SampleComponents/WrapperDiv.cs b/tests/bunit.testassets/SampleComponents/WrapperDiv.cs index ba72a3e59..5f88ed192 100644 --- a/tests/bunit.testassets/SampleComponents/WrapperDiv.cs +++ b/tests/bunit.testassets/SampleComponents/WrapperDiv.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; diff --git a/tests/bunit.web.tests/Asserting/JSRuntimeAssertExtensionsTest.cs b/tests/bunit.web.tests/Asserting/JSRuntimeAssertExtensionsTest.cs new file mode 100644 index 000000000..d7ef8237b --- /dev/null +++ b/tests/bunit.web.tests/Asserting/JSRuntimeAssertExtensionsTest.cs @@ -0,0 +1,226 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using AngleSharp.Dom; +using Bunit.JSInterop; +using Bunit.JSInterop.InvocationHandlers; +using Bunit.Rendering; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using Moq; +using Shouldly; +using Xunit; + +namespace Bunit.Asserting +{ + public class JSRuntimeAssertExtensionsTest + { + private static BunitJSInterop CreateSut(JSRuntimeMode mode = JSRuntimeMode.Loose) => new BunitJSInterop { Mode = mode }; + + [Fact(DisplayName = "BunitJSInterop.VerifyNotInvoke throws if handler is null")] + public void Test001() + { + Should.Throw(() => default(BunitJSInterop)!.VerifyNotInvoke("")); + } + + [Fact(DisplayName = "BunitJSInterop.VerifyNotInvoke throws JSInvokeCountExpectedException if identifier " + + "has been invoked one or more times")] + public async Task Test002() + { + var identifier = "test"; + var sut = CreateSut(); + + await sut.JSRuntime.InvokeVoidAsync(identifier); + + Should.Throw(() => sut.VerifyNotInvoke(identifier)); + } + + [Fact(DisplayName = "BunitJSInterop.VerifyNotInvoke throws JSInvokeCountExpectedException if identifier " + + "has been invoked one or more times, with custom error message")] + public async Task Test003() + { + var identifier = "test"; + var errMsg = "HELLO WORLD"; + var sut = CreateSut(); + + await sut.JSRuntime.InvokeVoidAsync(identifier); + + Should.Throw(() => sut.VerifyNotInvoke(identifier, errMsg)) + .Message.ShouldContain(errMsg); + } + + [Fact(DisplayName = "BunitJSInterop.VerifyNotInvoke does not throw if identifier has not been invoked")] + public void Test004() + { + var sut = CreateSut(); + sut.VerifyNotInvoke("FOOBAR"); + } + + [Fact(DisplayName = "BunitJSInterop.VerifyInvoke throws if handler is null")] + public void Test100() + { + BunitJSInterop? sut = null; + Should.Throw(() => (sut!).VerifyInvoke("")); + Should.Throw(() => (sut!).VerifyInvoke("", 42)); + } + + [Fact(DisplayName = "BunitJSInterop.VerifyInvoke throws invokeCount is less than 1")] + public void Test101() + { + var sut = CreateSut(); + + Should.Throw(() => sut.VerifyInvoke("", 0)); + } + + [Fact(DisplayName = "BunitJSInterop.VerifyInvoke throws JSInvokeCountExpectedException when " + + "invocation count doesn't match the expected")] + public async Task Test103() + { + var sut = CreateSut(); + var identifier = "test"; + await sut.JSRuntime.InvokeVoidAsync(identifier); + + var actual = Should.Throw(() => sut.VerifyInvoke(identifier, 2)); + actual.ExpectedInvocationCount.ShouldBe(2); + actual.ActualInvocationCount.ShouldBe(1); + actual.Identifier.ShouldBe(identifier); + } + + [Fact(DisplayName = "BunitJSInterop.VerifyInvoke returns the invocation(s) if the expected count matched")] + public async Task Test104() + { + var sut = CreateSut(); + var identifier = "test"; + await sut.JSRuntime.InvokeVoidAsync(identifier); + + var invocations = sut.VerifyInvoke(identifier, 1); + invocations.ShouldBeSameAs(sut.Invocations[identifier]); + + var invocation = sut.VerifyInvoke(identifier); + invocation.ShouldBe(sut.Invocations[identifier][0]); + } + + [Fact(DisplayName = "ShouldBeElementReferenceTo throws if actualArgument or targeted element is null")] + public void Test200() + { + Should.Throw(() => JSRuntimeAssertExtensions.ShouldBeElementReferenceTo(null!, null!)) + .ParamName.ShouldBe("actualArgument"); + Should.Throw(() => string.Empty.ShouldBeElementReferenceTo(null!)) + .ParamName.ShouldBe("expectedTargetElement"); + } + + [Fact(DisplayName = "ShouldBeElementReferenceTo throws if actualArgument is not a ElementReference")] + public void Test201() + { + var obj = new object(); + Should.Throw(() => obj.ShouldBeElementReferenceTo(Mock.Of())); + } + + [Fact(DisplayName = "ShouldBeElementReferenceTo throws if element reference does not point to the provided element")] + public void Test202() + { + using var htmlParser = new BunitHtmlParser(); + var elmRef = new ElementReference(Guid.NewGuid().ToString()); + var elm = (IElement)htmlParser.Parse($"

").First(); + + Should.Throw(() => elmRef.ShouldBeElementReferenceTo(elm)); + + var elmWithoutRefAttr = (IElement)htmlParser.Parse($"

").First(); + + Should.Throw(() => elmRef.ShouldBeElementReferenceTo(elmWithoutRefAttr)); + } + + [Fact(DisplayName = "JSRuntimeInvocationHandler.VerifyNotInvoke throws if handler is null")] + public void Test301() + { + Should.Throw(() => default(JSRuntimeInvocationHandler)!.VerifyNotInvoke("")); + } + + [Fact(DisplayName = "JSRuntimeInvocationHandler.VerifyNotInvoke throws JSInvokeCountExpectedException if identifier " + + "has been invoked one or more times")] + public async Task Test302() + { + var identifier = "test"; + var sut = CreateSut(); + var handler = sut.SetupVoid().SetVoidResult(); + + await sut.JSRuntime.InvokeVoidAsync(identifier); + + Should.Throw(() => handler.VerifyNotInvoke(identifier)); + } + + [Fact(DisplayName = "JSRuntimeInvocationHandler.VerifyNotInvoke throws JSInvokeCountExpectedException if identifier " + + "has been invoked one or more times, with custom error message")] + public async Task Test303() + { + var identifier = "test"; + var errMsg = "HELLO WORLD"; + var sut = CreateSut(); + var handler = sut.SetupVoid().SetVoidResult(); + + await sut.JSRuntime.InvokeVoidAsync(identifier); + + Should.Throw(() => handler.VerifyNotInvoke(identifier, errMsg)) + .Message.ShouldContain(errMsg); + } + + + [Fact(DisplayName = "JSRuntimeInvocationHandler.VerifyNotInvoke does not throw if identifier has not been invoked")] + public void Test304() + { + var sut = CreateSut(); + var handler = sut.SetupVoid().SetVoidResult(); + + handler.VerifyNotInvoke("FOOBAR"); + } + + [Fact(DisplayName = "JSRuntimeInvocationHandler.VerifyInvoke throws if handler is null")] + public void Test305() + { + Should.Throw(() => default(JSRuntimeInvocationHandler)!.VerifyInvoke("")); + Should.Throw(() => default(JSRuntimeInvocationHandler)!.VerifyInvoke("", 42)); + } + + [Fact(DisplayName = "JSRuntimeInvocationHandler.VerifyInvoke throws invokeCount is less than 1")] + public void Test306() + { + var sut = CreateSut(); + var handler = sut.SetupVoid().SetVoidResult(); + + Should.Throw(() => handler.VerifyInvoke("", 0)); + } + + [Fact(DisplayName = "JSRuntimeInvocationHandler.VerifyInvoke throws JSInvokeCountExpectedException when " + + "invocation count doesn't match the expected")] + public async Task Test307() + { + var sut = CreateSut(); + var identifier = "test"; + var handler = sut.SetupVoid().SetVoidResult(); + + await sut.JSRuntime.InvokeVoidAsync(identifier); + + var actual = Should.Throw(() => handler.VerifyInvoke(identifier, 2)); + actual.ExpectedInvocationCount.ShouldBe(2); + actual.ActualInvocationCount.ShouldBe(1); + actual.Identifier.ShouldBe(identifier); + } + + [Fact(DisplayName = "JSRuntimeInvocationHandler.VerifyInvoke returns the invocation(s) if the expected count matched")] + public async Task Test308() + { + var sut = CreateSut(); + var identifier = "test"; + var handler = sut.SetupVoid().SetVoidResult(); + + await sut.JSRuntime.InvokeVoidAsync(identifier); + + var invocations = handler.VerifyInvoke(identifier, 1); + invocations.ShouldBeSameAs(handler.Invocations[identifier]); + + var invocation = handler.VerifyInvoke(identifier); + invocation.ShouldBe(handler.Invocations[identifier][0]); + } + + } +} diff --git a/tests/bunit.web.tests/Asserting/MarkupMatchesAssertExtensionsTest.cs b/tests/bunit.web.tests/Asserting/MarkupMatchesAssertExtensionsTest.cs index fa7e20b27..c1493c4ef 100644 --- a/tests/bunit.web.tests/Asserting/MarkupMatchesAssertExtensionsTest.cs +++ b/tests/bunit.web.tests/Asserting/MarkupMatchesAssertExtensionsTest.cs @@ -6,7 +6,7 @@ namespace Bunit.Asserting { - public class MarkupMatchesAssertExtensionsTest : TestContext + public partial class MarkupMatchesAssertExtensionsTest : TestContext { private const string ActualMarkup = "

FOO

"; private const string ExpectedMarkup = "
BAR
"; diff --git a/tests/bunit.web.tests/Asserting/MarkupMatchesAssertExtensionsTest.net5.cs b/tests/bunit.web.tests/Asserting/MarkupMatchesAssertExtensionsTest.net5.cs index 8c857b94e..416b1e3d1 100644 --- a/tests/bunit.web.tests/Asserting/MarkupMatchesAssertExtensionsTest.net5.cs +++ b/tests/bunit.web.tests/Asserting/MarkupMatchesAssertExtensionsTest.net5.cs @@ -1,14 +1,9 @@ #if NET5_0 -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Bunit.TestAssets.SampleComponents; using Xunit; -namespace Bunit +namespace Bunit.Asserting { public partial class MarkupMatchesAssertExtensionsTest : TestContext { diff --git a/tests/bunit.web.tests/BlazorE2E/ComponentRenderingTest.cs b/tests/bunit.web.tests/BlazorE2E/ComponentRenderingTest.cs index ca0f9d061..5e6f21b6c 100644 --- a/tests/bunit.web.tests/BlazorE2E/ComponentRenderingTest.cs +++ b/tests/bunit.web.tests/BlazorE2E/ComponentRenderingTest.cs @@ -4,7 +4,6 @@ using System.Numerics; using Bunit.TestAssets.BlazorE2E; using Bunit.TestAssets.BlazorE2E.HierarchicalImportsTest.Subdir; -using Bunit.TestDoubles; using Microsoft.AspNetCore.Components; using Shouldly; using Xunit; @@ -21,7 +20,7 @@ public class ComponentRenderingTest : TestContext { public ComponentRenderingTest() { - JSInterop.Mode = JSRuntimeMode.Loose; + JSInterop.Mode = JSRuntimeMode.Loose; } [Fact] @@ -372,7 +371,7 @@ public void CanUseJSInteropToReferenceElements() // Assert.Equal("Clicks: 1", inputElement.GetAttribute("value")); // buttonElement.Click(); // Assert.Equal("Clicks: 2", inputElement.GetAttribute("value")); - + var cut = RenderComponent(); var inputElement = cut.Find("#capturedElement"); var refId = inputElement.GetAttribute(Htmlizer.ELEMENT_REFERENCE_ATTR_NAME); diff --git a/tests/bunit.web.tests/EventDispatchExtensions/DetailsElementEventDispatcherExtensionsTest.cs b/tests/bunit.web.tests/EventDispatchExtensions/DetailsElementEventDispatcherExtensionsTest.cs index 01f26ecd4..eef3e4c66 100644 --- a/tests/bunit.web.tests/EventDispatchExtensions/DetailsElementEventDispatcherExtensionsTest.cs +++ b/tests/bunit.web.tests/EventDispatchExtensions/DetailsElementEventDispatcherExtensionsTest.cs @@ -1,10 +1,6 @@ #if NET5_0 -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; -using Bunit; using Bunit.TestAssets.SampleComponents; using Shouldly; using Xunit; diff --git a/tests/bunit.web.tests/JSInterop/BunitJSInteropTest.cs b/tests/bunit.web.tests/JSInterop/BunitJSInteropTest.cs index 1864678fb..1169bb17e 100644 --- a/tests/bunit.web.tests/JSInterop/BunitJSInteropTest.cs +++ b/tests/bunit.web.tests/JSInterop/BunitJSInteropTest.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Bunit; using Microsoft.JSInterop; using Shouldly; using Xunit; @@ -32,7 +31,7 @@ public void Test002() using var cts = new CancellationTokenSource(); var sut = CreateSut(JSRuntimeMode.Loose); - var _ = sut.JSRuntime.InvokeAsync(identifier, cts.Token, args); + sut.JSRuntime.InvokeAsync(identifier, cts.Token, args); var invocation = sut.Invocations[identifier].Single(); invocation.Identifier.ShouldBe(identifier); @@ -41,19 +40,23 @@ public void Test002() } [Fact(DisplayName = "Mock throws exception when in strict mode and invocation has not been setup")] - public async Task Test003() + public void Test003() { var sut = CreateSut(JSRuntimeMode.Strict); var identifier = "func"; var args = new[] { "bar", "baz" }; - var exception = await Should.ThrowAsync(sut.JSRuntime.InvokeVoidAsync(identifier, args).AsTask()); - exception.Invocation.Identifier.ShouldBe(identifier); - exception.Invocation.Arguments.ShouldBe(args); - - exception = Should.Throw(() => { var _ = sut.JSRuntime.InvokeAsync(identifier, args); }); - exception.Invocation.Identifier.ShouldBe(identifier); - exception.Invocation.Arguments.ShouldBe(args); + Should.Throw(async () => await sut.JSRuntime.InvokeVoidAsync(identifier, args)) + .Invocation.ShouldSatisfyAllConditions( + x => x.Identifier.ShouldBe(identifier), + x => x.Arguments.ShouldBe(args) + ); + + Should.Throw(async () => await sut.JSRuntime.InvokeAsync(identifier, args)) + .Invocation.ShouldSatisfyAllConditions( + x => x.Identifier.ShouldBe(identifier), + x => x.Arguments.ShouldBe(args) + ); } [Fact(DisplayName = "All invocations received AFTER a invocation handler " + @@ -127,6 +130,22 @@ public void Test007() invocation.IsCanceled.ShouldBeTrue(); } + [Fact(DisplayName = "A invocation handler can be canceled after it has been set to a different result")] + public void Test107() + { + var identifier = "func"; + var sut = CreateSut(JSRuntimeMode.Strict); + var handler = sut.Setup(identifier); + + var invocation1 = sut.JSRuntime.InvokeAsync(identifier); + handler.SetResult(Guid.NewGuid()); + invocation1.IsCompletedSuccessfully.ShouldBeTrue(); + + handler.SetCanceled(); + var invocation2 = sut.JSRuntime.InvokeAsync(identifier); + invocation2.IsCanceled.ShouldBeTrue(); + } + [Fact(DisplayName = "A invocation handler can throw an exception for any waiting received invocations")] public async Task Test008() { @@ -143,20 +162,37 @@ public async Task Test008() invocation.IsFaulted.ShouldBeTrue(); } + [Fact(DisplayName = "A invocation handler can throw an exception after it has been set to a different result")] + public void Test108() + { + var identifier = "func"; + var sut = CreateSut(JSRuntimeMode.Strict); + var handler = sut.Setup(identifier); + var expectedException = new InvalidOperationException("TADA"); + + var invocation1 = sut.JSRuntime.InvokeAsync(identifier); + handler.SetResult(Guid.NewGuid()); + invocation1.IsCompletedSuccessfully.ShouldBeTrue(); + + handler.SetException(expectedException); + var invocation2 = sut.JSRuntime.InvokeAsync(identifier); + invocation2.IsFaulted.ShouldBeTrue(); + } + [Fact(DisplayName = "Invocations returns all from a invocation handler")] public void Test009() { var identifier = "func"; var sut = new BunitJSInterop(); var handler = sut.Setup(identifier, x => true); - var i1 = sut.JSRuntime.InvokeAsync(identifier, "first"); - var i2 = sut.JSRuntime.InvokeAsync(identifier, "second"); + sut.JSRuntime.InvokeAsync(identifier, "first"); + sut.JSRuntime.InvokeAsync(identifier, "second"); var invocations = handler.Invocations; - invocations.Count.ShouldBe(2); - invocations[0].Arguments[0].ShouldBe("first"); - invocations[1].Arguments[0].ShouldBe("second"); + invocations[identifier].Count.ShouldBe(2); + invocations[identifier][0].Arguments[0].ShouldBe("first"); + invocations[identifier][1].Arguments[0].ShouldBe("second"); } [Fact(DisplayName = "Arguments used in Setup are matched with invocations")] @@ -165,12 +201,12 @@ public void Test010() var sut = CreateSut(JSRuntimeMode.Strict); var planned = sut.Setup("foo", "bar", 42); - var _ = sut.JSRuntime.InvokeAsync("foo", "bar", 42); + sut.JSRuntime.InvokeAsync("foo", "bar", 42); - Should.Throw(() => { var _ = sut.JSRuntime.InvokeAsync("foo", "bar", 41); }); + Should.Throw(() => sut.JSRuntime.InvokeAsync("foo", "bar", 41)); planned.Invocations.Count.ShouldBe(1); - var invocation = planned.Invocations[0]; + var invocation = planned.Invocations["foo"][0]; invocation.Identifier.ShouldBe("foo"); invocation.Arguments[0].ShouldBe("bar"); invocation.Arguments[1].ShouldBe(42); @@ -182,12 +218,12 @@ public void Test011() var sut = CreateSut(JSRuntimeMode.Strict); var planned = sut.Setup("foo", x => x.Arguments.Count == 1); - var _ = sut.JSRuntime.InvokeAsync("foo", 42); + sut.JSRuntime.InvokeAsync("foo", 42); - Should.Throw(() => { var _ = sut.JSRuntime.InvokeAsync("foo", "bar", 42); }); + Should.Throw(() => sut.JSRuntime.InvokeAsync("foo", "bar", 42)); planned.Invocations.Count.ShouldBe(1); - var invocation = planned.Invocations[0]; + var invocation = planned.Invocations["foo"][0]; invocation.Identifier.ShouldBe("foo"); invocation.Arguments.Count.ShouldBe(1); invocation.Arguments[0].ShouldBe(42); @@ -209,42 +245,34 @@ public async Task Test012() } [Fact(DisplayName = "Arguments used in SetupVoid are matched with invocations")] - public async Task Test013() + public void Test013() { var sut = CreateSut(JSRuntimeMode.Strict); var planned = sut.SetupVoid("foo", "bar", 42); - var _ = sut.JSRuntime.InvokeVoidAsync("foo", "bar", 42); + sut.JSRuntime.InvokeVoidAsync("foo", "bar", 42); - await Should.ThrowAsync( - sut.JSRuntime.InvokeVoidAsync("foo", "bar", 41).AsTask() - ); + Should.Throw(async () => await sut.JSRuntime.InvokeVoidAsync("foo", "bar", 41)); planned.Invocations.Count.ShouldBe(1); - var invocation = planned.Invocations[0]; + var invocation = planned.Invocations["foo"][0]; invocation.Identifier.ShouldBe("foo"); invocation.Arguments[0].ShouldBe("bar"); invocation.Arguments[1].ShouldBe(42); } [Fact(DisplayName = "Argument matcher used in SetupVoid are matched with invocations")] - public async Task Test014() + public void Test014() { var sut = CreateSut(JSRuntimeMode.Strict); var planned = sut.SetupVoid("foo", x => x.Arguments.Count == 2); + sut.JSRuntime.InvokeVoidAsync("foo", "bar", 42); - var i1 = sut.JSRuntime.InvokeVoidAsync("foo", "bar", 42); - - await Should.ThrowAsync( - sut.JSRuntime.InvokeVoidAsync("foo", 42).AsTask() - ); - - await Should.ThrowAsync( - sut.JSRuntime.InvokeVoidAsync("foo").AsTask() - ); + Should.Throw(async () => await sut.JSRuntime.InvokeVoidAsync("foo", 42)); + Should.Throw(async () => await sut.JSRuntime.InvokeVoidAsync("foo")); planned.Invocations.Count.ShouldBe(1); - var invocation = planned.Invocations[0]; + var invocation = planned.Invocations["foo"][0]; invocation.Identifier.ShouldBe("foo"); invocation.Arguments.Count.ShouldBe(2); invocation.Arguments[0].ShouldBe("bar"); @@ -274,7 +302,7 @@ public void Test016() var sut = CreateSut(JSRuntimeMode.Strict); var planned = sut.Setup(); - Should.Throw(() => { var _ = sut.JSRuntime.InvokeAsync("foo"); }); + Should.Throw(() => sut.JSRuntime.InvokeAsync("foo")); planned.Invocations.Count.ShouldBe(0); } @@ -347,7 +375,7 @@ public async Task Test020() var sut = CreateSut(JSRuntimeMode.Strict); var handler = sut.SetupVoid(); - Should.Throw(() => { var _ = sut.JSRuntime.InvokeAsync(identifier); }); + Should.Throw(() => sut.JSRuntime.InvokeAsync(identifier)); var invocation = sut.JSRuntime.InvokeVoidAsync(identifier); handler.SetVoidResult(); @@ -359,13 +387,13 @@ public async Task Test020() } [Fact(DisplayName = "Empty Setup is not used for invocation with void return types")] - public async Task Test021() + public void Test021() { var sut = CreateSut(JSRuntimeMode.Strict); sut.Setup(); - await Should.ThrowAsync(sut.JSRuntime.InvokeVoidAsync("someFunc").AsTask()); + Should.Throw(async () => await sut.JSRuntime.InvokeVoidAsync("someFunc")); } [Fact(DisplayName = "SetupVoid is only used when there is no void handler")] @@ -453,7 +481,7 @@ public void Test044() var args = new[] { "bar", "baz" }; var sut = CreateSut(JSRuntimeMode.Loose); - var _ = ((IJSInProcessRuntime)sut.JSRuntime).Invoke(identifier, args); + ((IJSInProcessRuntime)sut.JSRuntime).Invoke(identifier, args); var invocation = sut.Invocations[identifier].Single(); invocation.Identifier.ShouldBe(identifier); diff --git a/tests/bunit.web.tests/JSInterop/BunitJSInteropTest.net5.cs b/tests/bunit.web.tests/JSInterop/BunitJSInteropTest.net5.cs index b456ab867..12bc3efa6 100644 --- a/tests/bunit.web.tests/JSInterop/BunitJSInteropTest.net5.cs +++ b/tests/bunit.web.tests/JSInterop/BunitJSInteropTest.net5.cs @@ -1,10 +1,6 @@ #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; @@ -60,7 +56,7 @@ public void Test050() exception.Invocation.Arguments.ShouldBeEmpty(); } - [Fact(DisplayName = "After IJSUnmarshalledRuntime invocation a invocation should be visible from the Invocations list when parsed one arguments.")] + [Fact(DisplayName = "After IJSUnmarshalledRuntime invocation a invocation should be visible from the Invocations list when passed one arguments.")] public void Test051() { var identifier = "fooFunc"; @@ -70,14 +66,14 @@ public void Test051() var planned = sut.Setup("fooFunc", args); planned.SetResult(Guid.NewGuid()); - var _ = ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled(identifier, "bar"); + ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled(identifier, "bar"); var invocation = sut.Invocations[identifier].Single(); invocation.Identifier.ShouldBe(identifier); invocation.Arguments.ShouldBe(args); } - [Fact(DisplayName = "After IJSUnmarshalledRuntime invocation a invocation should be visible from the Invocations list when parsed two arguments.")] + [Fact(DisplayName = "After IJSUnmarshalledRuntime invocation a invocation should be visible from the Invocations list when passed two arguments.")] public void Test052() { var identifier = "fooFunc"; @@ -87,14 +83,14 @@ public void Test052() var planned = sut.Setup("fooFunc", args); planned.SetResult(Guid.NewGuid()); - var _ = ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled(identifier, "bar", "baz"); + ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled(identifier, "bar", "baz"); var invocation = sut.Invocations[identifier].Single(); invocation.Identifier.ShouldBe(identifier); invocation.Arguments.ShouldBe(args); } - [Fact(DisplayName = "After IJSUnmarshalledRuntime invocation a invocation should be visible from the Invocations list when parsed three arguments.")] + [Fact(DisplayName = "After IJSUnmarshalledRuntime invocation a invocation should be visible from the Invocations list when passed three arguments.")] public void Test053() { var identifier = "fooFunc"; @@ -104,31 +100,30 @@ public void Test053() var planned = sut.Setup("fooFunc", args); planned.SetResult(Guid.NewGuid()); - var _ = ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled(identifier, "bar", "baz", "boa"); + ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled(identifier, "bar", "baz", "boa"); var invocation = sut.Invocations[identifier].Single(); invocation.Identifier.ShouldBe(identifier); invocation.Arguments.ShouldBe(args); } - [Fact(DisplayName = "After IJSUnmarshalledRuntime invocation a invocation should be visible from the Invocations list when parsed zero arguments.")] + [Fact(DisplayName = "After IJSUnmarshalledRuntime invocation a invocation should be visible from the Invocations list when passed zero arguments.")] public void Test054() { var identifier = "fooFunc"; - var args = new[] { "bar", "baz", "boa" }; var sut = CreateSut(JSRuntimeMode.Strict); - var planned = sut.Setup("fooFunc", args); + var planned = sut.Setup("fooFunc"); planned.SetResult(Guid.NewGuid()); - var _ = ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled(identifier, "bar", "baz", "boa"); + ((IJSUnmarshalledRuntime)sut.JSRuntime).InvokeUnmarshalled(identifier); var invocation = sut.Invocations[identifier].Single(); invocation.Identifier.ShouldBe(identifier); - invocation.Arguments.ShouldBe(args); + invocation.Arguments.ShouldBeEmpty(); } - [Fact(DisplayName = "An IJSUnmarshalledRuntime invocation should return the correct result when parsed one arguments.")] + [Fact(DisplayName = "An IJSUnmarshalledRuntime invocation should return the correct result when passed one arguments.")] public void Test055() { var identifier = "fooFunc"; @@ -143,7 +138,7 @@ public void Test055() actual.ShouldBe(expectedResult); } - [Fact(DisplayName = "An IJSUnmarshalledRuntime invocation should return the correct result when parsed two arguments.")] + [Fact(DisplayName = "An IJSUnmarshalledRuntime invocation should return the correct result when passed two arguments.")] public void Test056() { var identifier = "fooFunc"; @@ -158,7 +153,7 @@ public void Test056() actual.ShouldBe(expectedResult); } - [Fact(DisplayName = "An IJSUnmarshalledRuntime invocation should return the correct result when parsed three arguments.")] + [Fact(DisplayName = "An IJSUnmarshalledRuntime invocation should return the correct result when passed three arguments.")] public void Test057() { var identifier = "fooFunc"; @@ -173,7 +168,7 @@ public void Test057() actual.ShouldBe(expectedResult); } - [Fact(DisplayName = "An IJSUnmarshalledRuntime invocation should return the correct result when parsed zero arguments.")] + [Fact(DisplayName = "An IJSUnmarshalledRuntime invocation should return the correct result when passed zero arguments.")] public void Test058() { var identifier = "fooFunc"; diff --git a/tests/bunit.web.tests/JSInterop/BunitJSObjectReferenceTest.cs b/tests/bunit.web.tests/JSInterop/BunitJSObjectReferenceTest.cs new file mode 100644 index 000000000..46ec771b3 --- /dev/null +++ b/tests/bunit.web.tests/JSInterop/BunitJSObjectReferenceTest.cs @@ -0,0 +1,335 @@ +#if NET5_0 +using System; +using System.Linq; +using System.Threading.Tasks; +using Bunit.JSInterop.InvocationHandlers.Implementation; +using Microsoft.JSInterop; +using Microsoft.JSInterop.Implementation; +using Shouldly; +using Xunit; + +namespace Bunit.JSInterop +{ + public class BunitJSObjectReferenceTest : TestContext + { + [Theory(DisplayName = "Calling Setup or Setup throws")] + [InlineData("import", null)] + [InlineData("import", "file.js")] + [InlineData("customImport", null)] + [InlineData("customImport", "file.js")] + public void Test001(string identifier, object arg1) + { + Should.Throw(() => JSInterop.Setup(identifier, arg1)); + Should.Throw(() => JSInterop.Setup(identifier, arg1)); + } + + [Theory(DisplayName = "Calling SetupModule(null) or SetupModule(empty string) throws")] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Test002(string url) + => Should.Throw(() => JSInterop.SetupModule(url)); + + [Fact(DisplayName = "Calling SetupModule(jsInterop, identifier, invocationMatcher) with any null values throws")] + public void Test003() + { + Should.Throw(() => default(BunitJSInterop)!.SetupModule("identifier", _ => true)); + Should.Throw(() => JSInterop.SetupModule(string.Empty, _ => true)); + Should.Throw(() => JSInterop.SetupModule("import", default(InvocationMatcher)!)); + } + + [Fact(DisplayName = "Calling SetupModule(uri) registers handler for module JS Interop")] + public void Test010() + { + JSInterop.SetupModule("FOO.js"); + + JSInterop.TryGetInvokeHandler("import", "FOO.js") + .ShouldNotBeNull() + .ShouldSatisfyAllConditions( + x => x.Identifier.ShouldBe("import"), + x => x.ShouldBeOfType() + ); + } + + [Fact(DisplayName = "Calling SetupModule(invocationMatcher) registers handler for module JS Interop")] + public void Test011() + { + JSInterop.SetupModule(invocation => true); + + JSInterop.TryGetInvokeHandler("import", "FOO.js") + .ShouldNotBeNull() + .ShouldSatisfyAllConditions( + x => x.Identifier.ShouldBe("import"), + x => x.ShouldBeOfType() + ); + } + + [Fact(DisplayName = "Calling the catch-all SetupModule() registers handler for module JS Interop")] + public void Test012() + { + JSInterop.SetupModule(); + + JSInterop.TryGetInvokeHandler("foo") + .ShouldNotBeNull() + .ShouldSatisfyAllConditions( + x => x.Identifier.ShouldBe(JSObjectReferenceInvocationHandler.CatchAllIdentifier), + x => x.ShouldBeOfType() + ); + } + + [Fact(DisplayName = "Calling SetupModule(customImport, args) registers handler for module JS Interop")] + public void Test013() + { + JSInterop.SetupModule("foo", Array.Empty()); + + JSInterop.TryGetInvokeHandler("foo") + .ShouldNotBeNull() + .ShouldSatisfyAllConditions( + x => x.Identifier.ShouldBe("foo"), + x => x.ShouldBeOfType() + ); + } + + [Fact(DisplayName = "Handler for specific module name returns IJSObjectReference when receiving matching invocation")] + public async Task Test020() + { + var moduleName = "FOO.js"; + JSInterop.SetupModule(moduleName); + + var module = await JSInterop.JSRuntime.InvokeAsync("import", moduleName); + + module.ShouldNotBeNull(); + } + + [Theory(DisplayName = "Handler for specific module name doesn't match other module names")] + [InlineData(null)] + [InlineData("")] + [InlineData("BAR.js")] + public void Test021(string requestedRoduleName) + { + JSInterop.SetupModule("FOO.js"); + + Should.Throw( + () => JSInterop.JSRuntime.InvokeAsync("import", requestedRoduleName) + ); + } + + [Fact(DisplayName = "Handler for matcher returns IJSObjectReference when receiving matching invocation")] + public async Task Test022() + { + var moduleName = "FOO.js"; + JSInterop.SetupModule(x => x.Arguments?[0]?.ToString() == moduleName); + + var module = await JSInterop.JSRuntime.InvokeAsync("import", moduleName); + + module.ShouldBeAssignableTo(); + } + + [Fact(DisplayName = "Handler for matcher returns IJSObjectReference when receiving matching invocation")] + public async Task Test026() + { + var moduleName = "FOO.js"; + JSInterop.SetupModule("customImport", new[] { moduleName }); + + var module = await JSInterop.JSRuntime.InvokeAsync("customImport", moduleName); + + module.ShouldBeAssignableTo(); + } + + [Theory(DisplayName = "Catch-all handler returns IJSObjectReference for all non-empty module names")] + [InlineData("import", "FOO.js")] + [InlineData("customImport", null)] + [InlineData("customImport", "BAR.js")] + public async Task Test023(string identifier, object? arg1) + { + JSInterop.SetupModule(); + + var module = await JSInterop.JSRuntime.InvokeAsync(identifier, new[] { arg1 }); + + module.ShouldBeAssignableTo(); + } + + [Theory(DisplayName = "JSInterop in loose mode returns IJSObjectReference for all non-empty module names without explicit SetupModule call")] + [InlineData("import", "FOO.js")] + [InlineData("customImport", null)] + [InlineData("customImport", "BAR.js")] + public async Task Test025(string identifier, object? arg1) + { + JSInterop.Mode = JSRuntimeMode.Loose; + + var module = await JSInterop.JSRuntime.InvokeAsync(identifier, new[] { arg1 }); + + module.ShouldBeAssignableTo(); + } + + [Fact(DisplayName = "Module JSInterop inherits the root JSInterop's Mode")] + public void Test030() + { + JSInterop.Mode = JSRuntimeMode.Loose; + JSInterop.SetupModule().Mode.ShouldBe(JSInterop.Mode); + + JSInterop.Mode = JSRuntimeMode.Strict; + JSInterop.SetupModule("foo.js").Mode.ShouldBe(JSInterop.Mode); + } + + [Fact(DisplayName = "Changing mode in root JSInterop changes it in module JSInterop when it's not been set explicitly there")] + public void Test031() + { + var moduleJSInterop = JSInterop.SetupModule(); + + JSInterop.Mode = JSRuntimeMode.Loose; + moduleJSInterop.Mode.ShouldBe(JSInterop.Mode); + + JSInterop.Mode = JSRuntimeMode.Strict; + moduleJSInterop.Mode.ShouldBe(JSInterop.Mode); + } + + [Fact(DisplayName = "Changing mode on module JSInterop breaks inherited mode from root JSInterop")] + public void Test032() + { + var moduleJSInterop = JSInterop.SetupModule(); + moduleJSInterop.Mode = JSRuntimeMode.Strict; + + JSInterop.Mode = JSRuntimeMode.Loose; + + moduleJSInterop.Mode.ShouldBe(JSRuntimeMode.Strict); + } + + [Fact(DisplayName = "InvokeAsync on module in loose mode returns default TValue when no matching module JSInterops are registered")] + public async Task Test040() + { + JSInterop.Mode = JSRuntimeMode.Loose; + var module = await JSInterop.JSRuntime.InvokeAsync("import", "FOO.js"); + + var actual = await module.InvokeAsync("helloWorld"); + + actual.ShouldBe(default(string)); + } + + [Fact(DisplayName = "InvokeAsync calls is registered in the root JSInterop Invocations list only")] + public async Task Test050() + { + var moduleJSInterop = JSInterop.SetupModule("FOO.js"); + + await JSInterop.JSRuntime.InvokeAsync("import", "FOO.js"); + + JSInterop.Invocations + .ShouldHaveSingleItem() + .ShouldSatisfyAllConditions( + x => x.Identifier.ShouldBe("import"), + x => x.Arguments.ShouldHaveSingleItem().ShouldBe("FOO.js") + ); + + moduleJSInterop.Invocations.ShouldBeEmpty(); + } + + [Fact(DisplayName = "Module.Invocation is registered in both module and root JSInterop")] + public async Task Test055() + { + JSInterop.Mode = JSRuntimeMode.Loose; + var moduleJSInterop = JSInterop.SetupModule("FOO.js"); + var module = await JSInterop.JSRuntime.InvokeAsync("import", "FOO.js"); + + await module.InvokeAsync("helloWorld"); + + moduleJSInterop.Invocations + .ShouldHaveSingleItem() + .Identifier.ShouldBe("helloWorld"); + + JSInterop.Invocations + .Last() + .Identifier.ShouldBe("helloWorld"); + } + + [Fact(DisplayName = "TryGetModuleJSInterop returns registered module handler when called with parameters that the handler matches with")] + public void Test060() + { + var expected = JSInterop.SetupModule("FOO.js"); + + var actual = JSInterop.TryGetModuleJSInterop("import", "FOO.js"); + + actual.ShouldBe(expected); + } + + [Fact(DisplayName = "TryGetModuleJSInterop returns null when called with parameters that the handler does not matches with")] + public void Test061() + { + JSInterop.SetupModule("FOO.js"); + + var actual = JSInterop.TryGetModuleJSInterop("import", "BAR.js"); + + actual.ShouldBeNull(); + } + + [Fact(DisplayName = "IJSObjectReference can be cast to IJSInProcessObjectReference")] + public async Task Test070() + { + JSInterop.Mode = JSRuntimeMode.Loose; + + var jsRuntime = await JSInterop.JSRuntime.InvokeAsync("FOO.js"); + + jsRuntime.ShouldBeAssignableTo(); + } + + [Fact(DisplayName = "IJSObjectReference can be cast to IJSUnmarshalledObjectReference")] + public async Task Test071() + { + JSInterop.Mode = JSRuntimeMode.Loose; + + var jsRuntime = await JSInterop.JSRuntime.InvokeAsync("FOO.js"); + + jsRuntime.ShouldBeAssignableTo(); + } + + [Fact(DisplayName = "IJSInProcessObjectReference-invocations is handled by handlers from BunitJSInterop")] + public async Task Test080() + { + JSInterop.Mode = JSRuntimeMode.Loose; + var jsInProcess = (IJSInProcessObjectReference)(await JSInterop.JSRuntime.InvokeAsync("FOO.js")); + + await jsInProcess.InvokeAsync("bar1"); + await jsInProcess.InvokeAsync("bar2", "baz"); + await jsInProcess.InvokeVoidAsync("bar3"); + await jsInProcess.InvokeVoidAsync("bar4", "baz"); + jsInProcess.Invoke("bar5"); + jsInProcess.Invoke("bar6", "baz"); + + JSInterop.VerifyInvoke("bar1"); + JSInterop.VerifyInvoke("bar2").Arguments.ShouldBe(new[] { "baz" }); + JSInterop.VerifyInvoke("bar3"); + JSInterop.VerifyInvoke("bar4").Arguments.ShouldBe(new[] { "baz" }); + JSInterop.VerifyInvoke("bar5"); + JSInterop.VerifyInvoke("bar6").Arguments.ShouldBe(new[] { "baz" }); + } + + [Fact(DisplayName = "IJSUnmarshalledObjectReference-invocations is handled by handlers from BunitJSInterop")] + public async Task Test081() + { + JSInterop.Mode = JSRuntimeMode.Loose; + var jsUnmarshalled = (IJSUnmarshalledObjectReference)(await JSInterop.JSRuntime.InvokeAsync("FOO.js")); + + await jsUnmarshalled.InvokeAsync("bar1"); + await jsUnmarshalled.InvokeAsync("bar2", "baz"); + await jsUnmarshalled.InvokeVoidAsync("bar3"); + await jsUnmarshalled.InvokeVoidAsync("bar4", "baz"); + jsUnmarshalled.Invoke("bar5"); + jsUnmarshalled.Invoke("bar6", "baz"); + jsUnmarshalled.InvokeUnmarshalled("bar7"); + jsUnmarshalled.InvokeUnmarshalled("bar8", "baz"); + jsUnmarshalled.InvokeUnmarshalled("bar9", "baz", "boo"); + jsUnmarshalled.InvokeUnmarshalled("bar10", "baz", "boo", "bah"); + + JSInterop.VerifyInvoke("bar1"); + JSInterop.VerifyInvoke("bar2").Arguments.ShouldBe(new[] { "baz" }); + JSInterop.VerifyInvoke("bar3"); + JSInterop.VerifyInvoke("bar4").Arguments.ShouldBe(new[] { "baz" }); + JSInterop.VerifyInvoke("bar5"); + JSInterop.VerifyInvoke("bar6").Arguments.ShouldBe(new[] { "baz" }); + JSInterop.VerifyInvoke("bar7"); + JSInterop.VerifyInvoke("bar8").Arguments.ShouldBe(new[] { "baz" }); + JSInterop.VerifyInvoke("bar9").Arguments.ShouldBe(new[] { "baz", "boo" }); + JSInterop.VerifyInvoke("bar10").Arguments.ShouldBe(new[] { "baz", "boo", "bah" }); + } + } +} +#endif diff --git a/tests/bunit.web.tests/JSInterop/InvocationHandlers/FocusAsyncInvocationHandlerTest.cs b/tests/bunit.web.tests/JSInterop/InvocationHandlers/FocusAsyncInvocationHandlerTest.cs index 60a29c504..eb82f6931 100644 --- a/tests/bunit.web.tests/JSInterop/InvocationHandlers/FocusAsyncInvocationHandlerTest.cs +++ b/tests/bunit.web.tests/JSInterop/InvocationHandlers/FocusAsyncInvocationHandlerTest.cs @@ -1,5 +1,4 @@ #if NET5_0 -using System; using System.Threading.Tasks; using Bunit.TestAssets.SampleComponents; using Microsoft.AspNetCore.Components; diff --git a/tests/bunit.web.tests/JSInterop/JSRuntimeAssertExtensionsTest.cs b/tests/bunit.web.tests/JSInterop/JSRuntimeAssertExtensionsTest.cs deleted file mode 100644 index 549df17b7..000000000 --- a/tests/bunit.web.tests/JSInterop/JSRuntimeAssertExtensionsTest.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using AngleSharp.Dom; -using Bunit.Asserting; -using Bunit.Rendering; -using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; -using Moq; -using Shouldly; -using Xunit; - -namespace Bunit.JSInterop -{ - public class JSRuntimeAssertExtensionsTest - { - private static BunitJSInterop CreateSut(JSRuntimeMode mode = JSRuntimeMode.Loose) => new BunitJSInterop { Mode = mode }; - - [Fact(DisplayName = "VerifyNotInvoke throws if handler is null")] - public void Test001() - { - BunitJSInterop? sut = null; - Should.Throw(() => (sut!).VerifyNotInvoke("")); - } - - [Fact(DisplayName = "VerifyNotInvoke throws JSInvokeCountExpectedException if identifier " + - "has been invoked one or more times")] - public async Task Test002() - { - var identifier = "test"; - var sut = CreateSut(); - await sut.JSRuntime.InvokeVoidAsync(identifier); - - Should.Throw(() => sut.VerifyNotInvoke(identifier)); - } - - [Fact(DisplayName = "VerifyNotInvoke throws JSInvokeCountExpectedException if identifier " + - "has been invoked one or more times, with custom error message")] - public async Task Test003() - { - var identifier = "test"; - var errMsg = "HELLO WORLD"; - var sut = CreateSut(); - await sut.JSRuntime.InvokeVoidAsync(identifier); - - Should.Throw(() => sut.VerifyNotInvoke(identifier, errMsg)) - .Message.ShouldContain(errMsg); - } - - [Fact(DisplayName = "VerifyNotInvoke does not throw if identifier has not been invoked")] - public void Test004() - { - var sut = CreateSut(); - sut.VerifyNotInvoke("FOOBAR"); - } - - [Fact(DisplayName = "VerifyInvoke throws if handler is null")] - public void Test100() - { - BunitJSInterop? sut = null; - Should.Throw(() => (sut!).VerifyInvoke("")); - Should.Throw(() => (sut!).VerifyInvoke("", 42)); - } - - [Fact(DisplayName = "VerifyInvoke throws invokeCount is less than 1")] - public void Test101() - { - var sut = CreateSut(); - - Should.Throw(() => sut.VerifyInvoke("", 0)); - } - - [Fact(DisplayName = "VerifyInvoke throws JSInvokeCountExpectedException when " + - "invocation count doesn't match the expected")] - public async Task Test103() - { - var sut = CreateSut(); - var identifier = "test"; - await sut.JSRuntime.InvokeVoidAsync(identifier); - - var actual = Should.Throw(() => sut.VerifyInvoke(identifier, 2)); - actual.ExpectedInvocationCount.ShouldBe(2); - actual.ActualInvocationCount.ShouldBe(1); - actual.Identifier.ShouldBe(identifier); - } - - [Fact(DisplayName = "VerifyInvoke returns the invocation(s) if the expected count matched")] - public async Task Test104() - { - var sut = CreateSut(); - var identifier = "test"; - await sut.JSRuntime.InvokeVoidAsync(identifier); - - var invocations = sut.VerifyInvoke(identifier, 1); - invocations.ShouldBeSameAs(sut.Invocations[identifier]); - - var invocation = sut.VerifyInvoke(identifier); - invocation.ShouldBe(sut.Invocations[identifier][0]); - } - - [Fact(DisplayName = "ShouldBeElementReferenceTo throws if actualArgument or targeted element is null")] - public void Test200() - { - Should.Throw(() => JSRuntimeAssertExtensions.ShouldBeElementReferenceTo(null!, null!)) - .ParamName.ShouldBe("actualArgument"); - Should.Throw(() => string.Empty.ShouldBeElementReferenceTo(null!)) - .ParamName.ShouldBe("expectedTargetElement"); - } - - [Fact(DisplayName = "ShouldBeElementReferenceTo throws if actualArgument is not a ElementReference")] - public void Test201() - { - var obj = new object(); - Should.Throw(() => obj.ShouldBeElementReferenceTo(Mock.Of())); - } - - [Fact(DisplayName = "ShouldBeElementReferenceTo throws if element reference does not point to the provided element")] - public void Test202() - { - using var htmlParser = new BunitHtmlParser(); - var elmRef = new ElementReference(Guid.NewGuid().ToString()); - var elm = (IElement)htmlParser.Parse($"

").First(); - - Should.Throw(() => elmRef.ShouldBeElementReferenceTo(elm)); - - var elmWithoutRefAttr = (IElement)htmlParser.Parse($"

").First(); - - Should.Throw(() => elmRef.ShouldBeElementReferenceTo(elmWithoutRefAttr)); - } - } -} diff --git a/tests/bunit.web.tests/Rendering/BunitHtmlParserTest.cs b/tests/bunit.web.tests/Rendering/BunitHtmlParserTest.cs index 854847fcc..508cc50a0 100644 --- a/tests/bunit.web.tests/Rendering/BunitHtmlParserTest.cs +++ b/tests/bunit.web.tests/Rendering/BunitHtmlParserTest.cs @@ -31,6 +31,10 @@ public class BunitHtmlParserTest // "frame","frameset","image","isindex" // not supported }).Select(x => new[] { x }); + public static readonly IEnumerable BODY_HTML_AND_SPEACIAL_ELEMENTS = BODY_HTML_ELEMENTS.Concat( + (new[] { "html", "head", "body", }).Select(x => new[] { x }) + ); + [Fact(DisplayName = "Parse() called with null")] public void ParseCalledWithNull() { @@ -48,7 +52,7 @@ public void ParseWithWhitespaceOnly(string text) } [Theory(DisplayName = "Parse() passed ")] - [MemberData(nameof(BODY_HTML_ELEMENTS))] + [MemberData(nameof(BODY_HTML_AND_SPEACIAL_ELEMENTS))] public void Test001(string elementName) { var actual = Parser.Parse($@"<{elementName} id=""{elementName}"">").ToList(); @@ -108,18 +112,7 @@ public void Test006(string elementName) actual.ShouldHaveSingleItem() .TextContent.ShouldBe(" "); } - - [Theory(DisplayName = "Parse() with special tags")] - [InlineData("html")] - [InlineData("head")] - [InlineData("body")] - public void Test007(string elementName) - { - var actual = Parser.Parse($@"<{elementName} id=""{elementName}"">").ToList(); - - VerifyElementParsedWithId(elementName, actual); - } - + private static void VerifyElementParsedWithId(string expectedElementName, List actual) { var elm = actual.OfType() diff --git a/tests/bunit.web.tests/Rendering/Internal/HtmlizerTests.net5.cs b/tests/bunit.web.tests/Rendering/Internal/HtmlizerTests.net5.cs index e9536e8c9..498b98617 100644 --- a/tests/bunit.web.tests/Rendering/Internal/HtmlizerTests.net5.cs +++ b/tests/bunit.web.tests/Rendering/Internal/HtmlizerTests.net5.cs @@ -1,11 +1,4 @@ #if NET5_0 -using System; -using Bunit.TestAssets.BlazorE2E; -using Bunit.TestAssets.SampleComponents; - -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Rendering; -using Microsoft.AspNetCore.Components.Web; using Shouldly; using Xunit; diff --git a/tests/bunit.web.tests/Rendering/RenderedComponentTest.cs b/tests/bunit.web.tests/Rendering/RenderedComponentTest.cs index 2c2f7b0a4..cb0fedf05 100644 --- a/tests/bunit.web.tests/Rendering/RenderedComponentTest.cs +++ b/tests/bunit.web.tests/Rendering/RenderedComponentTest.cs @@ -1,9 +1,7 @@ using System; using Bunit.Rendering; using Bunit.TestAssets.SampleComponents; -using Bunit.TestDoubles; using Shouldly; - using Xunit; namespace Bunit @@ -37,12 +35,11 @@ public void Test0041() { var cut = RenderComponent(parameters => parameters.AddChildContent("

")); - cut.SetParametersAndRender(parameters => parameters.AddChildContent("

")); + cut.SetParametersAndRender(ComponentParameterFactory.ChildContent("

")); cut.Find("p").ShouldNotBeNull(); } - [Fact(DisplayName = "Trying to set CascadingValue during SetParametersAndRender throws")] public void Test003() { diff --git a/tests/bunit.web.tests/Rendering/RenderedFragmentTest.cs b/tests/bunit.web.tests/Rendering/RenderedFragmentTest.cs index 7489b0b44..5b9f0f487 100644 --- a/tests/bunit.web.tests/Rendering/RenderedFragmentTest.cs +++ b/tests/bunit.web.tests/Rendering/RenderedFragmentTest.cs @@ -1,17 +1,13 @@ using Bunit.Rendering; using Bunit.TestAssets.SampleComponents; - using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; - using Shouldly; - using Xunit; using Xunit.Abstractions; namespace Bunit { - public class RenderedFragmentTest : TestContext { public RenderedFragmentTest(ITestOutputHelper output)