Skip to content

Commit

Permalink
fix: Use NullDispatchTestRenderer for detached rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
linkdotnet committed Jul 13, 2023
1 parent dced948 commit 411d054
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 2 additions & 3 deletions src/bunit.web/Asserting/MarkupMatchesAssertExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Bunit.Diffing;
using Bunit.Extensions;
using Bunit.Rendering;

namespace Bunit;

/// <summary>
Expand Down Expand Up @@ -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<TestContextBase>();
var renderedFragment = (IRenderedFragment)testContext.RenderInsideRenderTree(expected);
var detachedRenderer = actual.Services.GetRequiredService<NullDispatcherTestRenderer>();
var renderedFragment = (IRenderedFragment)detachedRenderer.RenderFragment(expected);
MarkupMatches(actual, renderedFragment, userMessage);
}

Expand Down
1 change: 1 addition & 0 deletions src/bunit.web/Extensions/TestServiceProviderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static IServiceCollection AddDefaultTestContextServices(this IServiceColl
// bUnit specific services
services.AddScoped<TestContextBase>(_ => testContext);
services.AddScoped<WebTestRenderer>();
services.AddTransient<NullDispatcherTestRenderer>();
services.AddScoped<TestRenderer>(s => s.GetRequiredService<WebTestRenderer>());
services.AddScoped<Renderer>(s => s.GetRequiredService<WebTestRenderer>());
services.AddScoped<ITestRenderer>(s => s.GetRequiredService<WebTestRenderer>());
Expand Down
50 changes: 50 additions & 0 deletions src/bunit.web/Rendering/Internal/NullDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace Bunit.Rendering;

/// <summary>
/// A dispatcher that directly invokes the work item on the current thread.
/// </summary>
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<Task> workItem)
{
if (workItem is null)
{
throw new ArgumentNullException(nameof(workItem));
}

return workItem();
}

public override Task<TResult> InvokeAsync<TResult>(Func<TResult> workItem)
{
if (workItem is null)
{
throw new ArgumentNullException(nameof(workItem));
}

return Task.FromResult(workItem());
}

public override Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> workItem)
{
if (workItem is null)
{
throw new ArgumentNullException(nameof(workItem));
}

return workItem();
}
}
23 changes: 23 additions & 0 deletions src/bunit.web/Rendering/Internal/NullDispatcherTestRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Microsoft.Extensions.Logging;

namespace Bunit.Rendering;

/// <summary>
/// A renderer that uses a <see cref="NullDispatcher"/> to invoke work items.
/// </summary>
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
}
43 changes: 21 additions & 22 deletions src/bunit.web/Rendering/WebTestRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,33 @@
using Microsoft.JSInterop;
#endif

namespace Bunit.Rendering
namespace Bunit.Rendering;

/// <summary>
/// Represents a <see cref="ITestRenderer"/> that is used when rendering
/// Blazor components for the web.
/// </summary>
public class WebTestRenderer : TestRenderer
{
/// <summary>
/// Represents a <see cref="ITestRenderer"/> that is used when rendering
/// Blazor components for the web.
/// Initializes a new instance of the <see cref="WebTestRenderer"/> class.
/// </summary>
public class WebTestRenderer : TestRenderer
public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory)
: base(renderedComponentActivator, services, loggerFactory)
{
/// <summary>
/// Initializes a new instance of the <see cref="WebTestRenderer"/> class.
/// </summary>
public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory)
: base(renderedComponentActivator, services, loggerFactory)
{
#if NET5_0_OR_GREATER
ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService<IJSRuntime>());
ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService<IJSRuntime>());
#endif
}
}

#if NET5_0_OR_GREATER
/// <summary>
/// Initializes a new instance of the <see cref="WebTestRenderer"/> class.
/// </summary>
public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory, IComponentActivator componentActivator)
: base(renderedComponentActivator, services, loggerFactory, componentActivator)
{
ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService<IJSRuntime>());
}
#endif
/// <summary>
/// Initializes a new instance of the <see cref="WebTestRenderer"/> class.
/// </summary>
public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory, IComponentActivator componentActivator)
: base(renderedComponentActivator, services, loggerFactory, componentActivator)
{
ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService<IJSRuntime>());
}
}
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@if (Task != null)
{
@if (Task.IsCompleted)
{
<span>done</span>
}
else
{
<span>waiting</span>
}
}
@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();
}
}
20 changes: 20 additions & 0 deletions tests/bunit.web.tests/Asserting/MarkupMatchesTests.razor
Original file line number Diff line number Diff line change
@@ -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(@<InvokeAsyncInsideContinueWith Task="@tcs.Task"/>);

cut.MarkupMatches(@<span>waiting</span>);

tcs.SetCanceled();

cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
}
#endif
}

0 comments on commit 411d054

Please sign in to comment.