Skip to content

Commit b7ec6c5

Browse files
committed
Added AddLayoutComponent to TestContext
1 parent dcf45f5 commit b7ec6c5

File tree

3 files changed

+207
-15
lines changed

3 files changed

+207
-15
lines changed

CHANGELOG.md

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,47 @@ The following section list all changes in beta-11.
1212
List of new features.
1313

1414
- Two new overloads to the `RenderFragment()` and `ChildContent()` component parameter factory methods have been added that takes a `RenderFragment` as input. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203).
15+
1516
- Added a `ComponentParameterCollection` type. The `ComponentParameterCollection` is a collection of component parameters, that knows how to turn those components parameters into a `RenderFragment`, which will render a component and pass any parameters inside the collection to that component. That logic was spread out over multiple places in bUnit, and is now owned by the `ComponentParameterCollection` type. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203).
17+
1618
- Added additional placeholder services for `NavigationManager`, `HttpClient`, and `IStringLocalizer`, to make it easier for users to figure out why a test is failing due to missing service registration before rendering a component. By [@joro550](https://github.com/joro550) in [#223](https://github.com/egil/bUnit/pull/223).
19+
1720
- Added `Key` class that represents a keyboard key and helps to avoid constructing `KeyboardEventArgs` object manually. The key can be passed to `KeyPress`, `KeyDown`, or `KeyUp` helper methods to raise keyboard events. The `Key` class provides static special keys or can be obtained from character or string. Keys can be combined with key modifiers: `Key.Enter + Key.Alt`.
1821

19-
For example, this makes it easier to trigger keyboard events on an element:
22+
For example, this makes it easier to trigger keyboard events on an element:
23+
24+
```csharp
25+
var cut = ctx.RenderComponent<ComponentWithKeyboardEvents>();
26+
var element = cut.Find("input");
27+
28+
element.KeyDown(Key.Enter + Key.Control); // Triggers onkeydown event with Ctrl + Enter
29+
element.KeyUp(Key.Control + Key.Shift + 'B'); // Triggers onkeyup event with Ctrl + Shift + B
30+
element.KeyPress('1'); // Triggers onkeypress event with key 1
31+
element.KeyDown(Key.Alt + "<"); // Triggers onkeydown event with Alt + <
32+
```
2033

21-
```csharp
22-
var cut = ctx.RenderComponent<ComponentWithKeyboardEvents>();
23-
var element = cut.Find("input");
34+
By [@duracellko](https://github.com/duracellko) in [#101](https://github.com/egil/bUnit/issues/101).
2435

25-
element.KeyDown(Key.Enter + Key.Control); // Triggers onkeydown event with Ctrl + Enter
26-
element.KeyUp(Key.Control + Key.Shift + 'B'); // Triggers onkeyup event with Ctrl + Shift + B
27-
element.KeyPress('1'); // Triggers onkeypress event with key 1
28-
element.KeyDown(Key.Alt + "<"); // Triggers onkeydown event with Alt + <
29-
```
30-
By [@duracellko](https://github.com/duracellko) in [#101](https://github.com/egil/bUnit/issues/101).
36+
- Added support for registering/adding "layout" components to a test context, which components should be rendered inside. This allows you to simplify the "arrange" step of a test when a component under test requires a certain render tree as its parent, e.g. a cascading value.
37+
38+
For example, to pass a cascading string value `foo` to all components rendered with the test context, do the following:
39+
40+
```csharp
41+
ctx.AddLayoutComponent<CascadingValue<string>>(parameters => parameters.Add(p => p.Value, "foo"));
42+
var cut = ctx.RenderComponent<ComponentReceivingFoo>();
43+
```
44+
45+
By [@duracellko](https://github.com/duracellko) in [#101](https://github.com/egil/bUnit/issues/101).
3146

3247
### Changed
3348
List of changes in existing functionality.
3449

3550
- The `ComponentParameterBuilder` has been renamed to `ComponentParameterCollectionBuilder`, since it now builds the `ComponentParameterCollection` type, introduced in this release of bUnit. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203).
51+
3652
- `ComponentParameterCollectionBuilder` now allows adding cascading values that is not directly used by the component type it targets. This makes it possible to add cascading values to children of the target component. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203).
53+
3754
- The `Add(object)` has been replaced by `AddCascadingValue(object)` in `ComponentParameterCollectionBuilder`, to make it more clear that an unnamed cascading value is being passed to the target component or one of its child components. It is also possible to pass unnamed cascading values using the `Add(parameterSelector, value)` method, which now correctly detect if the selected cascading value parameter is named or unnamed. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203).
55+
3856
- It is now possible to call the `Add()`, `AddChildContent()` methods on `ComponentParameterCollectionBuilder`, and the factory methods `RenderFragment()`, `ChildContent()`, and `Template()`, _**multiple times**_ for the same parameter, if it is of type `RenderFragment` or `RenderFragment<TValue>`. Doing so previously would either result in an exception or just the last passed `RenderFragment` to be used. Now all the provided `RenderFragment` or `RenderFragment<TValue>` will be combined at runtime into a single `RenderFragment` or `RenderFragment<TValue>`.
3957

4058
For example, this makes it easier to pass e.g. both a markup string and a component to a `ChildContent` parameter:
@@ -50,7 +68,9 @@ List of changes in existing functionality.
5068
);
5169
```
5270
By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203).
71+
5372
- All test doubles are now in the same namespace, `Bunit.TestDoubles`. So all import statements for `Bunit.TestDoubles.JSInterop` and `Bunit.TestDoubles.Authorization` must be changed to `Bunit.TestDoubles`. By [@egil](https://github.com/egil) in [#223](https://github.com/egil/bUnit/pull/223).
73+
5474
- Marked MarkupMatches methods as assertion methods to stop SonarSource analyzers complaining about missing assertions in tests. By [@egil](https://github.com/egil) in [#229](https://github.com/egil/bUnit/pull/229).
5575

5676
### Deprecated

src/bunit.web/TestContext.cs

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using Bunit.Extensions;
34
using Microsoft.AspNetCore.Components;
45

@@ -9,12 +10,37 @@ namespace Bunit
910
/// </summary>
1011
public class TestContext : TestContextBase, ITestContext
1112
{
13+
private readonly List<(Type LayoutType, RenderFragment<RenderFragment> LayoutFragment)> _layouts = new();
14+
1215
/// <summary>
1316
/// Creates a new instance of the <see cref="TestContext"/> class.
1417
/// </summary>
15-
public TestContext()
18+
public TestContext() => Services.AddDefaultTestContextServices();
19+
20+
/// <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.
26+
/// </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
1630
{
17-
Services.AddDefaultTestContextServices();
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));
1844
}
1945

2046
/// <summary>
@@ -23,8 +49,10 @@ public TestContext()
2349
/// <typeparam name="TComponent">Type of the component to render</typeparam>
2450
/// <param name="parameters">Parameters to pass to the component when it is rendered</param>
2551
/// <returns>The rendered <typeparamref name="TComponent"/></returns>
26-
public IRenderedComponent<TComponent> RenderComponent<TComponent>(params ComponentParameter[] parameters) where TComponent : IComponent
27-
=> TestRendererExtensions.RenderComponent<TComponent>(Renderer, parameters);
52+
public virtual IRenderedComponent<TComponent> RenderComponent<TComponent>(params ComponentParameter[] parameters) where TComponent : IComponent
53+
{
54+
return RenderComponent<TComponent>(new ComponentParameterCollection { parameters }.ToRenderFragment<TComponent>());
55+
}
2856

2957
/// <summary>
3058
/// Instantiates and performs a first render of a component of type <typeparamref name="TComponent"/>.
@@ -33,6 +61,40 @@ public IRenderedComponent<TComponent> RenderComponent<TComponent>(params Compone
3361
/// <param name="parameterBuilder">The ComponentParameterBuilder action to add type safe parameters to pass to the component when it is rendered</param>
3462
/// <returns>The rendered <typeparamref name="TComponent"/></returns>
3563
public virtual IRenderedComponent<TComponent> RenderComponent<TComponent>(Action<ComponentParameterCollectionBuilder<TComponent>> parameterBuilder) where TComponent : IComponent
36-
=> TestRendererExtensions.RenderComponent<TComponent>(Renderer, parameterBuilder);
64+
{
65+
return RenderComponent<TComponent>(
66+
new ComponentParameterCollectionBuilder<TComponent>(parameterBuilder)
67+
.Build()
68+
.ToRenderFragment<TComponent>()
69+
);
70+
}
71+
72+
private IRenderedComponent<TComponent> RenderComponent<TComponent>(RenderFragment renderFragment) where TComponent : IComponent
73+
{
74+
// Wrap TComponent in any layout components added to the test context.
75+
// 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);
90+
91+
// This ensures that the correct component is returned, in case an added layout component
92+
// is of type TComponent.
93+
var result = rcInLayoutCount > 0
94+
? Renderer.FindComponents<TComponent>(resultBase)[rcInLayoutCount]
95+
: Renderer.FindComponent<TComponent>(resultBase);
96+
97+
return (IRenderedComponent<TComponent>)result;
98+
}
3799
}
38100
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using System;
2+
using Bunit.TestAssets.SampleComponents;
3+
using Microsoft.AspNetCore.Components;
4+
using Microsoft.AspNetCore.Components.Rendering;
5+
using Shouldly;
6+
using Xunit;
7+
8+
namespace Bunit
9+
{
10+
public class TestContextAddLayoutComponentTest : TestContext
11+
{
12+
[Fact(DisplayName = "AddLayoutComponent<T> throws when T doesn't have a ChildContent or Body parameter")]
13+
public void Test100()
14+
{
15+
Should.Throw<ArgumentException>(() =>
16+
{
17+
AddLayoutComponent<Simple1>();
18+
RenderComponent<InnerComponent>();
19+
});
20+
}
21+
22+
[Fact(DisplayName = "AddLayoutComponent<T> adds T as a layout component which CUT is rendered as child of")]
23+
public void Test110()
24+
{
25+
AddLayoutComponent<LayoutComponent>();
26+
27+
var cut = RenderComponent<InnerComponent>();
28+
29+
cut.Markup.ShouldBe($"<div>LAYOUT VALUE</div>");
30+
}
31+
32+
[Fact(DisplayName = "AddLayoutComponent<T> allows passing parameters to layout components")]
33+
public void Test111()
34+
{
35+
AddLayoutComponent<LayoutComponent>(parameters => parameters.Add(p => p.Value, "ANOTHER VALUE"));
36+
37+
var cut = RenderComponent<InnerComponent>();
38+
39+
cut.Markup.ShouldBe($"<div>ANOTHER VALUE</div>");
40+
}
41+
42+
[Fact(DisplayName = "AddLayoutComponent<T> can be called multiple times")]
43+
public void Test112()
44+
{
45+
AddLayoutComponent<CascadingValue<string>>(parameters => parameters.Add(p => p.Value, "VALUE"));
46+
AddLayoutComponent<CascadingValue<int>>(parameters => parameters.Add(p => p.Value, 42));
47+
48+
var cut = RenderComponent<MultipleParametersInnerComponent>();
49+
50+
cut.Markup.ShouldBe($"<div>VALUE42</div>");
51+
}
52+
53+
[Fact(DisplayName = "RenderComponent<T> finds correct component when T is also added via AddLayoutComponent<T>")]
54+
public void Test113()
55+
{
56+
AddLayoutComponent<CascadingValue<string>>(parameters => parameters.Add(p => p.Value, "VALUE"));
57+
AddLayoutComponent<MultipleParametersInnerComponent>();
58+
AddLayoutComponent<CascadingValue<int>>(parameters => parameters.Add(p => p.Value, 42));
59+
AddLayoutComponent<MultipleParametersInnerComponent>();
60+
61+
var cut = RenderComponent<MultipleParametersInnerComponent>();
62+
63+
cut.Markup.ShouldBe($"<div>VALUE42</div>");
64+
}
65+
66+
private class LayoutComponent : LayoutComponentBase
67+
{
68+
[Parameter] public string Value { get; set; } = "LAYOUT VALUE";
69+
[Parameter] public string? Name { get; set; }
70+
71+
protected override void BuildRenderTree(RenderTreeBuilder builder)
72+
{
73+
builder.OpenComponent<CascadingValue<string>>(0);
74+
builder.AddAttribute(1, "Value", Value);
75+
if (Name is not null)
76+
builder.AddAttribute(2, "Name", Name);
77+
builder.AddAttribute(3, "ChildContent", Body);
78+
builder.CloseComponent();
79+
}
80+
}
81+
82+
private class InnerComponent : ComponentBase
83+
{
84+
[CascadingParameter] public string? LayoutValue { get; set; }
85+
86+
protected override void BuildRenderTree(RenderTreeBuilder builder)
87+
{
88+
builder.OpenElement(0, "div");
89+
builder.AddContent(1, LayoutValue);
90+
builder.CloseElement();
91+
}
92+
}
93+
94+
private class MultipleParametersInnerComponent : ComponentBase
95+
{
96+
[CascadingParameter] public string StringValue { get; set; } = string.Empty;
97+
[CascadingParameter] public int IntValue { get; set; }
98+
[Parameter] public RenderFragment? ChildContent { get; set; }
99+
100+
protected override void BuildRenderTree(RenderTreeBuilder builder)
101+
{
102+
builder.OpenElement(0, "div");
103+
builder.AddContent(1, StringValue);
104+
builder.AddContent(2, IntValue);
105+
builder.AddContent(3, ChildContent);
106+
builder.CloseElement();
107+
}
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)