Skip to content

Commit 31cb91d

Browse files
committed
RenderTree and JSInterop added to Fixture/SnapshotTest, WebTestRenderer with .NET WebElementReferenceContext
1 parent d4ede22 commit 31cb91d

15 files changed

Lines changed: 266 additions & 59 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.AspNetCore.Components;
3+
using Microsoft.AspNetCore.Components.Rendering;
4+
5+
namespace Bunit.RazorTesting
6+
{
7+
/// <summary>
8+
/// Creates an instance of the <see cref="FragmentContainer"/>, which is used
9+
/// when a fragment is rendered inside a test contexts render tree.
10+
/// It is primarily used to be able to find the starting point to return.
11+
/// </summary>
12+
internal sealed class FragmentContainer : ComponentBase
13+
{
14+
/// <summary>
15+
/// The content to wrap.
16+
/// </summary>
17+
[Parameter] public RenderFragment? ChildContent { get; set; }
18+
19+
/// <inheritdoc/>
20+
protected override void BuildRenderTree(RenderTreeBuilder builder)
21+
{
22+
builder.AddContent(0, ChildContent);
23+
}
24+
25+
/// <summary>
26+
/// Wraps the <paramref name="wrappingTarget"/> in a <see cref="FragmentContainer"/>.
27+
/// </summary>
28+
internal static RenderFragment Wrap(RenderFragment wrappingTarget)
29+
{
30+
return builder =>
31+
{
32+
builder.OpenComponent<FragmentContainer>(0);
33+
builder.AddAttribute(1, nameof(ChildContent), wrappingTarget);
34+
builder.CloseComponent();
35+
};
36+
}
37+
}
38+
}

src/bunit.web/Rendering/RootRenderTree.cs renamed to src/bunit.core/Rendering/RootRenderTree.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public bool TryAdd<TComponent>(Action<ComponentParameterCollectionBuilder<TCompo
7676
/// </summary>
7777
/// <param name="target"><see cref="RenderFragment"/> to render inside the render tree.</param>
7878
/// <returns>A <see cref="RenderFragment"/> that renders the <paramref name="target"/> inside this <see cref="RootRenderTree"/> render tree.</returns>
79-
internal RenderFragment Wrap(RenderFragment target)
79+
public RenderFragment Wrap(RenderFragment target)
8080
{
8181
// Wrap from the last added to the first added, as we start with the
8282
// target and goes from inside to out.
@@ -94,7 +94,7 @@ internal RenderFragment Wrap(RenderFragment target)
9494
/// </summary>
9595
/// <typeparam name="TComponent">Component type to count.</typeparam>
9696
/// <returns>Number of components of type <typeparamref name="TComponent"/> in render tree.</returns>
97-
internal int GetCountOf<TComponent>() where TComponent : IComponent
97+
public int GetCountOf<TComponent>() where TComponent : IComponent
9898
{
9999
var result = 0;
100100
var countType = typeof(TComponent);

src/bunit.web/Rendering/RootRenderTreeRegistration.cs renamed to src/bunit.core/Rendering/RootRenderTreeRegistration.cs

File renamed without changes.

src/bunit.core/TestContextBase.cs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
2+
using Bunit.RazorTesting;
23
using Bunit.Rendering;
4+
using Microsoft.AspNetCore.Components;
35
using Microsoft.Extensions.DependencyInjection;
46

57
namespace Bunit
@@ -33,13 +35,62 @@ public ITestRenderer Renderer
3335
/// </summary>
3436
public TestServiceProvider Services { get; }
3537

38+
/// <summary>
39+
/// Gets the <see cref="RootRenderTree"/> that all components rendered with the
40+
/// <c>RenderComponent&lt;TComponent&gt;()</c> methods, are rendered inside.
41+
/// </summary>
42+
/// <remarks>
43+
/// Use this to add default layout- or root-components which a component under test
44+
/// should be rendered under.
45+
/// </remarks>
46+
public RootRenderTree RenderTree { get; } = new RootRenderTree();
47+
3648
/// <summary>
3749
/// Creates a new instance of the <see cref="TestContextBase"/> class.
3850
/// </summary>
3951
protected TestContextBase()
4052
{
41-
Services = new TestServiceProvider();
42-
Services.AddSingleton<ITestRenderer, TestRenderer>();
53+
Services = new TestServiceProvider();
54+
}
55+
56+
/// <summary>
57+
/// Renders a component, declared in the <paramref name="renderFragment"/>, inside the <see cref="RenderTree"/>.
58+
/// </summary>
59+
/// <typeparam name="TComponent">The type of component to render.</typeparam>
60+
/// <param name="renderFragment">The <see cref="RenderFragment"/> that contains a declaration of the component.</param>
61+
/// <returns>A <see cref="IRenderedComponentBase{TComponent}"/>.</returns>
62+
protected IRenderedComponentBase<TComponent> RenderComponent<TComponent>(RenderFragment renderFragment) where TComponent : IComponent
63+
{
64+
// Wrap TComponent in any layout components added to the test context.
65+
// If one of the layout components is the same type as TComponent,
66+
// make sure to return the rendered component, not the layout component.
67+
var resultBase = Renderer.RenderFragment(RenderTree.Wrap(renderFragment));
68+
69+
// This ensures that the correct component is returned, in case an added layout component
70+
// is of type TComponent.
71+
var renderTreeTComponentCount = RenderTree.GetCountOf<TComponent>();
72+
var result = renderTreeTComponentCount > 0
73+
? Renderer.FindComponents<TComponent>(resultBase)[renderTreeTComponentCount]
74+
: Renderer.FindComponent<TComponent>(resultBase);
75+
76+
return result;
77+
}
78+
79+
/// <summary>
80+
/// Renders a fragment, declared in the <paramref name="renderFragment"/>, inside the <see cref="RenderTree"/>.
81+
/// </summary>
82+
/// <param name="renderFragment">The <see cref="RenderFragment"/> to render.</param>
83+
/// <returns>A <see cref="IRenderedFragmentBase"/>.</returns>
84+
protected IRenderedFragmentBase RenderFragment(RenderFragment renderFragment)
85+
{
86+
// Wrap fragment in a FragmentContainer so the start of the test supplied
87+
// razor fragment can be found after, and then wrap in any layout components
88+
// added to the test context.
89+
var wrappedInFragmentContainer = FragmentContainer.Wrap(renderFragment);
90+
var wrappedInRenderTree = RenderTree.Wrap(wrappedInFragmentContainer);
91+
var resultBase = Renderer.RenderFragment(wrappedInRenderTree);
92+
93+
return Renderer.FindComponent<FragmentContainer>(resultBase);
4394
}
4495

4596
/// <inheritdoc/>

src/bunit.web/Extensions/TestServiceProviderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public static IServiceCollection AddDefaultTestContextServices(this IServiceColl
3232
services.AddSingleton<IStringLocalizer, PlaceholderStringLocalization>();
3333

3434
// bUnit specific services
35+
services.AddSingleton<ITestRenderer, WebTestRenderer>();
3536
services.AddSingleton<HtmlComparer>();
3637
services.AddSingleton<BunitHtmlParser>();
3738
services.AddSingleton<IRenderedComponentActivator, RenderedComponentActivator>();

src/bunit.web/RazorTesting/Fixture.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using Bunit.Extensions;
66
using Bunit.RazorTesting;
77
using Microsoft.AspNetCore.Components;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.JSInterop;
810

911
namespace Bunit
1012
{
@@ -28,6 +30,21 @@ private IReadOnlyList<FragmentBase> TestData
2830
}
2931
}
3032

33+
/// <summary>
34+
/// Gets bUnits JSInterop, that allows setting up handlers for <see cref="IJSRuntime.InvokeAsync{TValue}(string, object[])"/> invocations
35+
/// that components under tests will issue during testing. It also makes it possible to verify that the invocations has happened as expected.
36+
/// </summary>
37+
public BunitJSInterop JSInterop { get; } = new BunitJSInterop();
38+
39+
/// <summary>
40+
/// Creates an instance of the <see cref="Fixture"/> type.
41+
/// </summary>
42+
public Fixture()
43+
{
44+
Services.AddSingleton<IJSRuntime>(JSInterop.JSRuntime);
45+
Services.AddDefaultTestContextServices();
46+
}
47+
3148
/// <summary>
3249
/// Gets (and renders) the markup/component defined in the &lt;Fixture&gt;&lt;ComponentUnderTest&gt;...&lt;ComponentUnderTest/&gt;&lt;Fixture/&gt; element.
3350
///
@@ -123,13 +140,12 @@ private ComponentUnderTest SelectComponentUnderTest(string _)
123140

124141
private IRenderedComponent<TComponent> Factory<TComponent>(RenderFragment fragment) where TComponent : IComponent
125142
{
126-
var renderedFragment = Renderer.RenderFragment(fragment);
127-
return (IRenderedComponent<TComponent>)Renderer.FindComponent<TComponent>(renderedFragment);
143+
return (IRenderedComponent<TComponent>)RenderComponent<TComponent>(fragment);
128144
}
129145

130146
private IRenderedFragment Factory(RenderFragment fragment)
131147
{
132-
return (IRenderedFragment)Renderer.RenderFragment(fragment);
148+
return (IRenderedFragment)RenderFragment(fragment);
133149
}
134150

135151
private static IRenderedComponent<TComponent> TryCastTo<TComponent>(IRenderedFragment target, [System.Runtime.CompilerServices.CallerMemberName] string sourceMethod = "") where TComponent : IComponent

src/bunit.web/RazorTesting/SnapshotTest.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using Bunit.RazorTesting;
55
using Bunit.Rendering;
66
using Microsoft.AspNetCore.Components;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.JSInterop;
79

810
namespace Bunit
911
{
@@ -14,6 +16,12 @@ namespace Bunit
1416
/// </summary>
1517
public class SnapshotTest : RazorTestBase
1618
{
19+
/// <summary>
20+
/// Gets bUnits JSInterop, that allows setting up handlers for <see cref="IJSRuntime.InvokeAsync{TValue}(string, object[])"/> invocations
21+
/// that components under tests will issue during testing. It also makes it possible to verify that the invocations has happened as expected.
22+
/// </summary>
23+
public BunitJSInterop JSInterop { get; } = new BunitJSInterop();
24+
1725
/// <inheritdoc/>
1826
public override string? DisplayName => Description;
1927

@@ -39,22 +47,29 @@ public class SnapshotTest : RazorTestBase
3947
/// </summary>
4048
[Parameter] public RenderFragment? ExpectedOutput { get; set; }
4149

50+
/// <summary>
51+
/// Creates an instance of the <see cref="SnapshotTest"/> type.
52+
/// </summary>
53+
public SnapshotTest()
54+
{
55+
Services.AddSingleton<IJSRuntime>(JSInterop.JSRuntime);
56+
Services.AddDefaultTestContextServices();
57+
}
58+
4259
/// <inheritdoc/>
4360
protected override async Task Run()
4461
{
4562
Validate();
4663

47-
Services.AddDefaultTestContextServices();
48-
4964
if (Setup is not null)
5065
TryRun(Setup, this);
5166
if (SetupAsync is not null)
5267
await TryRunAsync(SetupAsync, this).ConfigureAwait(false);
5368

54-
var renderedTestInput = (IRenderedFragment)Renderer.RenderFragment(TestInput!);
69+
var renderedTestInput = (IRenderedFragment)RenderFragment(TestInput!);
5570
var inputHtml = renderedTestInput.Markup;
5671

57-
var renderedExpectedRender = (IRenderedFragment)Renderer.RenderFragment(ExpectedOutput!);
72+
var renderedExpectedRender = (IRenderedFragment)RenderFragment(ExpectedOutput!);
5873
var expectedHtml = renderedExpectedRender.Markup;
5974

6075
VerifySnapshot(inputHtml, expectedHtml);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using Microsoft.AspNetCore.Components;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Logging;
5+
using Microsoft.JSInterop;
6+
7+
namespace Bunit.Rendering
8+
{
9+
/// <summary>
10+
/// Represents a <see cref="ITestRenderer"/> that is used when rendering
11+
/// Blazor components for the web.
12+
/// </summary>
13+
public class WebTestRenderer : TestRenderer
14+
{
15+
/// <summary>
16+
/// Creates an instance of the <see cref="WebTestRenderer"/>.
17+
/// </summary>
18+
public WebTestRenderer(IRenderedComponentActivator activator, IServiceProvider services, ILoggerFactory loggerFactory) : base(activator, services, loggerFactory)
19+
{
20+
#if NET5_0
21+
ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService<IJSRuntime>());
22+
#endif
23+
}
24+
}
25+
}

src/bunit.web/TestContext.cs

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System;
22
using Bunit.Extensions;
3-
using Bunit.Rendering;
4-
using Bunit.TestDoubles;
53
using Microsoft.AspNetCore.Components;
64
using Microsoft.Extensions.DependencyInjection;
75
using Microsoft.JSInterop;
@@ -13,16 +11,6 @@ namespace Bunit
1311
/// </summary>
1412
public class TestContext : TestContextBase
1513
{
16-
/// <summary>
17-
/// Gets the <see cref="RootRenderTree"/> that all components rendered with the
18-
/// <c>RenderComponent&lt;TComponent&gt;()</c> methods, are rendered inside.
19-
/// </summary>
20-
/// <remarks>
21-
/// Use this to add default layout- or root-components which a component under test
22-
/// should be rendered under.
23-
/// </remarks>
24-
public RootRenderTree RenderTree { get; } = new RootRenderTree();
25-
2614
/// <summary>
2715
/// Gets bUnits JSInterop, that allows setting up handlers for <see cref="IJSRuntime.InvokeAsync{TValue}(string, object[])"/> invocations
2816
/// that components under tests will issue during testing. It also makes it possible to verify that the invocations has happened as expected.
@@ -34,8 +22,8 @@ public class TestContext : TestContextBase
3422
/// </summary>
3523
public TestContext()
3624
{
37-
Services.AddDefaultTestContextServices();
3825
Services.AddSingleton<IJSRuntime>(JSInterop.JSRuntime);
26+
Services.AddDefaultTestContextServices();
3927
}
4028

4129
/// <summary>
@@ -46,7 +34,8 @@ public TestContext()
4634
/// <returns>The rendered <typeparamref name="TComponent"/></returns>
4735
public virtual IRenderedComponent<TComponent> RenderComponent<TComponent>(params ComponentParameter[] parameters) where TComponent : IComponent
4836
{
49-
return RenderComponent<TComponent>(new ComponentParameterCollection { parameters }.ToRenderFragment<TComponent>());
37+
var renderFragment = new ComponentParameterCollection { parameters }.ToRenderFragment<TComponent>();
38+
return (IRenderedComponent<TComponent>)RenderComponent<TComponent>(renderFragment);
5039
}
5140

5241
/// <summary>
@@ -57,28 +46,8 @@ public virtual IRenderedComponent<TComponent> RenderComponent<TComponent>(params
5746
/// <returns>The rendered <typeparamref name="TComponent"/></returns>
5847
public virtual IRenderedComponent<TComponent> RenderComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>> parameterBuilder) where TComponent : IComponent
5948
{
60-
return RenderComponent<TComponent>(
61-
new ComponentParameterCollectionBuilder<TComponent>(parameterBuilder)
62-
.Build()
63-
.ToRenderFragment<TComponent>()
64-
);
65-
}
66-
67-
private IRenderedComponent<TComponent> RenderComponent<TComponent>(RenderFragment renderFragment) where TComponent : IComponent
68-
{
69-
// Wrap TComponent in any layout components added to the test context.
70-
// If one of the layout components is the same type as TComponent,
71-
// make sure to return the rendered component, not the layout component.
72-
var resultBase = Renderer.RenderFragment(RenderTree.Wrap(renderFragment));
73-
74-
// This ensures that the correct component is returned, in case an added layout component
75-
// is of type TComponent.
76-
var renderTreeTComponentCount = RenderTree.GetCountOf<TComponent>();
77-
var result = renderTreeTComponentCount > 0
78-
? Renderer.FindComponents<TComponent>(resultBase)[renderTreeTComponentCount]
79-
: Renderer.FindComponent<TComponent>(resultBase);
80-
81-
return (IRenderedComponent<TComponent>)result;
49+
var renderFragment = new ComponentParameterCollectionBuilder<TComponent>(parameterBuilder).Build().ToRenderFragment<TComponent>();
50+
return (IRenderedComponent<TComponent>)RenderComponent<TComponent>(renderFragment);
8251
}
8352
}
8453
}

src/bunit.web/TestDoubles/Authorization/FakeAuthorizationExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public static class FakeAuthorizationExtensions
1111
/// <summary>
1212
/// Adds the appropriate Blazor authentication and authorization services to the <see cref="TestServiceProvider"/> to enable
1313
/// an authenticated user, as well as adding the <see cref="CascadingAuthenticationState"/> component to the
14-
/// <see cref="TestContext.RenderTree"/>.
14+
/// test contexts render tree.
1515
/// </summary>
1616
public static TestAuthorizationContext AddTestAuthorization(this TestContext context)
1717
{

0 commit comments

Comments
 (0)