diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f5bc6e27..f9aa4b0a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to **bUnit** will be documented in this file. The project ad ### Changed - Change all bUnit services registration from singleton to scoped. By [@egil](https://github.com/egil). Closes https://github.com/bUnit-dev/bUnit/issues/1138. +- `MarkupMatches` is using own detached renderer. By [@linkdotnet](https://github.com/linkdotnet). Closes https://github.com/bUnit-dev/bUnit/issues/1143. ## [1.21.9] - 2023-07-02 diff --git a/src/bunit.web/Asserting/MarkupMatchesAssertExtensions.cs b/src/bunit.web/Asserting/MarkupMatchesAssertExtensions.cs index 4d7c2fe5e..993fde627 100644 --- a/src/bunit.web/Asserting/MarkupMatchesAssertExtensions.cs +++ b/src/bunit.web/Asserting/MarkupMatchesAssertExtensions.cs @@ -3,7 +3,6 @@ using Bunit.Diffing; using Bunit.Extensions; using Bunit.Rendering; - namespace Bunit; /// @@ -299,8 +298,8 @@ public static void MarkupMatches(this IRenderedFragment actual, RenderFragment e if (expected is null) throw new ArgumentNullException(nameof(expected)); - var testContext = actual.Services.GetRequiredService(); - var renderedFragment = (IRenderedFragment)testContext.RenderInsideRenderTree(expected); + var detachedRenderer = actual.Services.GetRequiredService(); + var renderedFragment = (IRenderedFragment)detachedRenderer.RenderFragment(expected); MarkupMatches(actual, renderedFragment, userMessage); } diff --git a/src/bunit.web/Extensions/TestServiceProviderExtensions.cs b/src/bunit.web/Extensions/TestServiceProviderExtensions.cs index be293ac3b..4771d0f07 100644 --- a/src/bunit.web/Extensions/TestServiceProviderExtensions.cs +++ b/src/bunit.web/Extensions/TestServiceProviderExtensions.cs @@ -48,6 +48,7 @@ public static IServiceCollection AddDefaultTestContextServices(this IServiceColl // bUnit specific services services.AddScoped(_ => testContext); services.AddScoped(); + services.AddTransient(); services.AddScoped(s => s.GetRequiredService()); services.AddScoped(s => s.GetRequiredService()); services.AddScoped(s => s.GetRequiredService()); diff --git a/src/bunit.web/Rendering/Internal/NullDispatcher.cs b/src/bunit.web/Rendering/Internal/NullDispatcher.cs new file mode 100644 index 000000000..e9bd6c45f --- /dev/null +++ b/src/bunit.web/Rendering/Internal/NullDispatcher.cs @@ -0,0 +1,50 @@ +namespace Bunit.Rendering; + +/// +/// A dispatcher that directly invokes the work item on the current thread. +/// +internal sealed class NullDispatcher : Dispatcher +{ + public override bool CheckAccess() => true; + + public override Task InvokeAsync(Action workItem) + { + if (workItem is null) + { + throw new ArgumentNullException(nameof(workItem)); + } + + workItem(); + return Task.CompletedTask; + } + + public override Task InvokeAsync(Func workItem) + { + if (workItem is null) + { + throw new ArgumentNullException(nameof(workItem)); + } + + return workItem(); + } + + public override Task InvokeAsync(Func workItem) + { + if (workItem is null) + { + throw new ArgumentNullException(nameof(workItem)); + } + + return Task.FromResult(workItem()); + } + + public override Task InvokeAsync(Func> workItem) + { + if (workItem is null) + { + throw new ArgumentNullException(nameof(workItem)); + } + + return workItem(); + } +} diff --git a/src/bunit.web/Rendering/Internal/NullDispatcherTestRenderer.cs b/src/bunit.web/Rendering/Internal/NullDispatcherTestRenderer.cs new file mode 100644 index 000000000..056fcdab5 --- /dev/null +++ b/src/bunit.web/Rendering/Internal/NullDispatcherTestRenderer.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging; + +namespace Bunit.Rendering; + +/// +/// A renderer that uses a to invoke work items. +/// +internal sealed class NullDispatcherTestRenderer : TestRenderer +{ + public override Dispatcher Dispatcher { get; } = new NullDispatcher(); + + public NullDispatcherTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory) + : base(renderedComponentActivator, services, loggerFactory) + { + } + +#if NET5_0_OR_GREATER + public NullDispatcherTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory, IComponentActivator componentActivator) + : base(renderedComponentActivator, services, loggerFactory, componentActivator) + { + } +#endif +} \ No newline at end of file diff --git a/src/bunit.web/Rendering/WebTestRenderer.cs b/src/bunit.web/Rendering/WebTestRenderer.cs index 2249abf8e..0e710b50a 100644 --- a/src/bunit.web/Rendering/WebTestRenderer.cs +++ b/src/bunit.web/Rendering/WebTestRenderer.cs @@ -7,34 +7,33 @@ using Microsoft.JSInterop; #endif -namespace Bunit.Rendering +namespace Bunit.Rendering; + +/// +/// Represents a that is used when rendering +/// Blazor components for the web. +/// +public class WebTestRenderer : TestRenderer { /// - /// Represents a that is used when rendering - /// Blazor components for the web. + /// Initializes a new instance of the class. /// - public class WebTestRenderer : TestRenderer + public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory) + : base(renderedComponentActivator, services, loggerFactory) { - /// - /// Initializes a new instance of the class. - /// - public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory) - : base(renderedComponentActivator, services, loggerFactory) - { #if NET5_0_OR_GREATER - ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService()); + ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService()); #endif - } + } #if NET5_0_OR_GREATER - /// - /// Initializes a new instance of the class. - /// - public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory, IComponentActivator componentActivator) - : base(renderedComponentActivator, services, loggerFactory, componentActivator) - { - ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService()); - } -#endif + /// + /// Initializes a new instance of the class. + /// + public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory, IComponentActivator componentActivator) + : base(renderedComponentActivator, services, loggerFactory, componentActivator) + { + ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService()); } -} +#endif +} \ No newline at end of file diff --git a/tests/bunit.testassets/SampleComponents/InvokeAsyncInsideContinueWith.razor b/tests/bunit.testassets/SampleComponents/InvokeAsyncInsideContinueWith.razor new file mode 100644 index 000000000..f8544b7ac --- /dev/null +++ b/tests/bunit.testassets/SampleComponents/InvokeAsyncInsideContinueWith.razor @@ -0,0 +1,35 @@ +@if (Task != null) +{ + @if (Task.IsCompleted) + { + done + } + else + { + waiting + } +} +@code { + [Parameter] public Task? Task { get; set; } + + private Task? registeredTask; + + protected override void OnParametersSet() + { + var task = Task; + if (task != registeredTask) + { + registeredTask = task; + + _ = task?.ContinueWith((t, o) => + { + if (t == Task) + { + _ = InvokeAsync(StateHasChanged); + } + }, null); + } + + base.OnParametersSet(); + } +} diff --git a/tests/bunit.web.tests/Asserting/MarkupMatchesTests.razor b/tests/bunit.web.tests/Asserting/MarkupMatchesTests.razor new file mode 100644 index 000000000..d8785d125 --- /dev/null +++ b/tests/bunit.web.tests/Asserting/MarkupMatchesTests.razor @@ -0,0 +1,20 @@ +@using Bunit.TestAssets.SampleComponents +@inherits TestContext + +@code { +#if NET5_0_OR_GREATER + [Fact] + public void MarkupMatchesShouldNotBeBlockedByRenderer() + { + var tcs = new TaskCompletionSource(); + + var cut = Render(@); + + cut.MarkupMatches(@waiting); + + tcs.SetCanceled(); + + cut.WaitForAssertion(() => cut.MarkupMatches(@done)); + } +#endif +}