Skip to content

Commit 424c1ce

Browse files
committed
Moved to property based exposure of test contexts render tree for better consistancy with Services and JSInterop property
1 parent b7ec6c5 commit 424c1ce

File tree

8 files changed

+387
-182
lines changed

8 files changed

+387
-182
lines changed

src/bunit.core/ITestContext.cs

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/bunit.core/RazorTesting/RazorTestBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace Bunit.RazorTesting
77
/// <summary>
88
/// Represents a component used to define tests in Razor files.
99
/// </summary>
10-
public abstract class RazorTestBase : TestContextBase, ITestContext, IComponent
10+
public abstract class RazorTestBase : TestContextBase, IComponent
1111
{
1212
/// <summary>
1313
/// Gets the name of the test, which is displayed by the test runner/explorer.

src/bunit.core/TestContextBase.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ namespace Bunit
77
/// <summary>
88
/// A test context is a factory that makes it possible to create components under tests.
99
/// </summary>
10-
public class TestContextBase : ITestContext, IDisposable
10+
public abstract class TestContextBase : IDisposable
1111
{
1212
private ITestRenderer? _testRenderer;
1313

14-
/// <inheritdoc/>
14+
/// <summary>
15+
/// Gets the renderer used by the test context.
16+
/// </summary>
1517
public ITestRenderer Renderer
1618
{
1719
get
@@ -24,13 +26,16 @@ public ITestRenderer Renderer
2426
}
2527
}
2628

27-
/// <inheritdoc/>
29+
/// <summary>
30+
/// Gets the service collection and service provider that is used when a
31+
/// component is rendered by the test context.
32+
/// </summary>
2833
public TestServiceProvider Services { get; }
2934

3035
/// <summary>
31-
/// Creates a new instance of the <see cref="ITestContext"/> class.
36+
/// Creates a new instance of the <see cref="TestContextBase"/> class.
3237
/// </summary>
33-
public TestContextBase()
38+
protected TestContextBase()
3439
{
3540
Services = new TestServiceProvider();
3641
Services.AddSingleton<ITestRenderer, TestRenderer>();
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using Microsoft.AspNetCore.Components;
6+
7+
namespace Bunit.Rendering
8+
{
9+
/// <summary>
10+
/// Represents a root render tree, wherein components under tests will be rendered.
11+
/// Components added to the render tree must have either a <c>ChildContent</c> or
12+
/// <c>Body</c> parameter.
13+
/// </summary>
14+
public sealed class RootRenderTree : IReadOnlyCollection<RootRenderTreeRegistration>
15+
{
16+
private readonly List<RootRenderTreeRegistration> _registrations = new();
17+
18+
/// <summary>
19+
/// Adds a component to the render tree. This method can
20+
/// be called multiple times, with each invocation adding a component
21+
/// to the render tree. The <typeparamref name="TComponent"/> must have a <c>ChildContent</c>
22+
/// or <c>Body</c> parameter.
23+
/// </summary>
24+
/// <typeparam name="TComponent">The type of the component to add to the render tree.</typeparam>
25+
/// <param name="parameterBuilder">An optional parameter builder, used to pass parameters to <typeparamref name="TComponent"/>.</param>
26+
public void Add<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>? parameterBuilder = null) where TComponent : IComponent
27+
{
28+
var registration = new RootRenderTreeRegistration(
29+
typeof(TComponent),
30+
CreateRenderFragmentBuilder<TComponent>(parameterBuilder)
31+
);
32+
_registrations.Add(registration);
33+
}
34+
35+
/// <summary>
36+
/// Try to add a component to the render tree if it has not already been added. This method can
37+
/// be called multiple times, with each invocation adding a component
38+
/// to the render tree. The <typeparamref name="TComponent"/> must have a <c>ChildContent</c>
39+
/// or <c>Body</c> parameter.
40+
/// </summary>
41+
/// <remarks>
42+
/// This method will only add the component to the render tree if it has not already been added.
43+
/// Use <see cref="Add{TComponent}(Action{ComponentParameterCollectionBuilder{TComponent}}?)"/> to
44+
/// add the same component multiple times.
45+
/// </remarks>
46+
/// <typeparam name="TComponent">The type of the component to add to the render tree.</typeparam>
47+
/// <param name="parameterBuilder">An optional parameter builder, used to pass parameters to <typeparamref name="TComponent"/>.</param>
48+
/// <returns>True if component was added, false if it was previously added and not added again.</returns>
49+
public bool TryAdd<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>? parameterBuilder = null) where TComponent : IComponent
50+
{
51+
var componentType = typeof(TComponent);
52+
if (_registrations.Any(x => x.ComponentType == componentType))
53+
return false;
54+
55+
var registration = new RootRenderTreeRegistration(
56+
componentType,
57+
CreateRenderFragmentBuilder<TComponent>(parameterBuilder)
58+
);
59+
_registrations.Add(registration);
60+
61+
return true;
62+
}
63+
64+
/// <inheritdoc/>
65+
public int Count => _registrations.Count;
66+
67+
/// <inheritdoc/>
68+
public IEnumerator<RootRenderTreeRegistration> GetEnumerator() => _registrations.GetEnumerator();
69+
70+
/// <inheritdoc/>
71+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
72+
73+
/// <summary>
74+
/// Creates a new <see cref="RenderFragment"/> that wraps <paramref name="target"/>
75+
/// inside the components registered in this <see cref="RootRenderTree"/>.
76+
/// </summary>
77+
/// <param name="target"><see cref="RenderFragment"/> to render inside the render tree.</param>
78+
/// <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)
80+
{
81+
// Wrap from the last added to the first added, as we start with the
82+
// target and goes from inside to out.
83+
var result = target;
84+
for (int i = _registrations.Count - 1; i >= 0; i--)
85+
{
86+
result = _registrations[i].RenderFragmentBuilder(result);
87+
}
88+
return result;
89+
}
90+
91+
/// <summary>
92+
/// Gets the number of registered components of type <typeparamref name="TComponent"/>
93+
/// in the render tree.
94+
/// </summary>
95+
/// <typeparam name="TComponent">Component type to count.</typeparam>
96+
/// <returns>Number of components of type <typeparamref name="TComponent"/> in render tree.</returns>
97+
internal int GetCountOf<TComponent>() where TComponent : IComponent
98+
{
99+
var result = 0;
100+
var countType = typeof(TComponent);
101+
102+
for (int i = 0; i < _registrations.Count; i++)
103+
{
104+
if (countType == _registrations[i].ComponentType)
105+
result++;
106+
}
107+
108+
return result;
109+
}
110+
111+
private static RenderFragment<RenderFragment> CreateRenderFragmentBuilder<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>? parameterBuilder) where TComponent : IComponent
112+
{
113+
return rc =>
114+
{
115+
var builder = new ComponentParameterCollectionBuilder<TComponent>();
116+
parameterBuilder?.Invoke(builder);
117+
118+
var added = builder.TryAdd("ChildContent", rc) || builder.TryAdd("Body", rc);
119+
if (!added)
120+
throw new ArgumentException($"The {typeof(TComponent)} does not have a ChildContent or Body parameter. Only components with one of these parameters can be added to the root render tree.");
121+
122+
return builder.Build().ToRenderFragment<TComponent>();
123+
};
124+
}
125+
}
126+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using Microsoft.AspNetCore.Components;
3+
4+
namespace Bunit.Rendering
5+
{
6+
/// <summary>
7+
/// Represents an added component with parameters in an <see cref="RootRenderTree"/>.
8+
/// </summary>
9+
public sealed class RootRenderTreeRegistration
10+
{
11+
/// <summary>
12+
/// Gets the type of component registered.
13+
/// </summary>
14+
public Type ComponentType { get; }
15+
16+
/// <summary>
17+
/// Gets the render fragment builder that renders the component of type <see cref="ComponentType"/>
18+
/// with the specified parameters and the provided <see cref="RenderFragment"/> passed to its
19+
/// ChildContent or Body parameter.
20+
/// </summary>
21+
public RenderFragment<RenderFragment> RenderFragmentBuilder { get; }
22+
23+
/// <summary>
24+
/// Creates an instance of the <see cref="RootRenderTreeRegistration"/> type.
25+
/// </summary>
26+
internal RootRenderTreeRegistration(Type componentType, RenderFragment<RenderFragment> renderFragmentBuilder)
27+
{
28+
ComponentType = componentType;
29+
RenderFragmentBuilder = renderFragmentBuilder;
30+
}
31+
}
32+
}

src/bunit.web/TestContext.cs

Lines changed: 16 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,30 @@
11
using System;
22
using System.Collections.Generic;
33
using Bunit.Extensions;
4+
using Bunit.Rendering;
45
using Microsoft.AspNetCore.Components;
56

67
namespace Bunit
78
{
89
/// <summary>
910
/// A test context is a factory that makes it possible to create components under tests.
1011
/// </summary>
11-
public class TestContext : TestContextBase, ITestContext
12+
public class TestContext : TestContextBase
1213
{
13-
private readonly List<(Type LayoutType, RenderFragment<RenderFragment> LayoutFragment)> _layouts = new();
14-
1514
/// <summary>
16-
/// Creates a new instance of the <see cref="TestContext"/> class.
15+
/// Gets the <see cref="RootRenderTree"/> that all components rendered with the
16+
/// <c>RenderComponent&lt;TComponent&gt;()</c> methods, are rendered inside.
1717
/// </summary>
18-
public TestContext() => Services.AddDefaultTestContextServices();
18+
/// <remarks>
19+
/// Use this to add default layout- or root-components which a component under test
20+
/// should be rendered under.
21+
/// </remarks>
22+
public RootRenderTree RenderTree { get; } = new RootRenderTree();
1923

2024
/// <summary>
21-
/// Adds a "layout" component to the test context, which components rendered using one of the
22-
/// <c>RenderComponent&lt;TComponent&gt;()</c>> methods will be rendered inside. This method can
23-
/// be called multiple times, with each invocation adding a <typeparamref name="TComponent"/>
24-
/// to the "layout" render tree. The <typeparamref name="TComponent"/> must have a <c>ChildContent</c>
25-
/// or <c>Body</c> parameter.
25+
/// Creates a new instance of the <see cref="TestContext"/> class.
2626
/// </summary>
27-
/// <typeparam name="TComponent">The type of the component to use as a layout component.</typeparam>
28-
/// <param name="parameterBuilder">An optional parameter builder, used to pass parameters to <typeparamref name="TComponent"/>.</param>
29-
public virtual void AddLayoutComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>>? parameterBuilder = null) where TComponent : IComponent
30-
{
31-
RenderFragment<RenderFragment> layoutFragment = rc =>
32-
{
33-
var builder = new ComponentParameterCollectionBuilder<TComponent>();
34-
parameterBuilder?.Invoke(builder);
35-
36-
var added = builder.TryAdd("ChildContent", rc) || builder.TryAdd("Body", rc);
37-
if (!added)
38-
throw new ArgumentException($"The {typeof(TComponent)} does not have a ChildContent or Body parameter. Only components with one of these parameters can be used as layout components.");
39-
40-
return builder.Build().ToRenderFragment<TComponent>();
41-
};
42-
43-
_layouts.Add((typeof(TComponent), layoutFragment));
44-
}
27+
public TestContext() => Services.AddDefaultTestContextServices();
4528

4629
/// <summary>
4730
/// Instantiates and performs a first render of a component of type <typeparamref name="TComponent"/>.
@@ -73,25 +56,14 @@ private IRenderedComponent<TComponent> RenderComponent<TComponent>(RenderFragmen
7356
{
7457
// Wrap TComponent in any layout components added to the test context.
7558
// If one of the layout components is the same type as TComponent,
76-
// make sure to return the rendered component, not the layout compnent.
77-
var rcType = typeof(TComponent);
78-
var rcInLayoutCount = 0;
79-
var wrappedRenderFragment = renderFragment;
80-
81-
for (int i = 0; i < _layouts.Count; i++)
82-
{
83-
if (rcType == _layouts[i].LayoutType)
84-
rcInLayoutCount++;
85-
86-
wrappedRenderFragment = _layouts[i].LayoutFragment(wrappedRenderFragment);
87-
}
88-
89-
var resultBase = Renderer.RenderFragment(wrappedRenderFragment);
59+
// make sure to return the rendered component, not the layout component.
60+
var resultBase = Renderer.RenderFragment(RenderTree.Wrap(renderFragment));
9061

9162
// This ensures that the correct component is returned, in case an added layout component
9263
// is of type TComponent.
93-
var result = rcInLayoutCount > 0
94-
? Renderer.FindComponents<TComponent>(resultBase)[rcInLayoutCount]
64+
var renderTreeTComponentCount = RenderTree.GetCountOf<TComponent>();
65+
var result = renderTreeTComponentCount > 0
66+
? Renderer.FindComponents<TComponent>(resultBase)[renderTreeTComponentCount]
9567
: Renderer.FindComponent<TComponent>(resultBase);
9668

9769
return (IRenderedComponent<TComponent>)result;

0 commit comments

Comments
 (0)