Skip to content

Commit 244680c

Browse files
committed
fix: support finding render modes specified via @rendermode directive
1 parent b5bcbd2 commit 244680c

File tree

8 files changed

+238
-83
lines changed

8 files changed

+238
-83
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#if NET9_0_OR_GREATER
2+
namespace Bunit.Rendering;
3+
4+
/// <summary>
5+
/// Represents an exception that is thrown when a component under test has mismatching render modes assigned between parent and child components.
6+
/// </summary>
7+
public sealed class RenderModeMisMatchException : Exception
8+
{
9+
/// <summary>
10+
/// Initializes a new instance of the <see cref="MissingRendererInfoException"/> class.
11+
/// </summary>
12+
public RenderModeMisMatchException()
13+
: base("""
14+
A component under test has mismatching render modes assigned between parent and child components.
15+
Ensure that the render mode of the parent component matches the render mode of the child component.
16+
Learn more about render modes at https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-9.0#render-mode-propagation.
17+
""")
18+
{
19+
HelpLink = "https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-9.0#render-mode-propagation";
20+
}
21+
}
22+
#endif

src/bunit.core/Rendering/TestRenderer.cs

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
using Microsoft.Extensions.Logging;
21
using System.Reflection;
32
using System.Runtime.CompilerServices;
43
using System.Runtime.ExceptionServices;
4+
using Microsoft.Extensions.Logging;
55

66
namespace Bunit.Rendering;
77

@@ -247,18 +247,55 @@ protected override IComponent ResolveComponentForRenderMode(Type componentType,
247247
{
248248
ArgumentNullException.ThrowIfNull(component);
249249

250-
var renderModeAttribute = component.GetType()
251-
.GetCustomAttribute<RenderModeAttribute>();
250+
// Search from the current component all the way up the render tree.
251+
// All components must have the same render mode specified (or none at all).
252+
// Return the render mode that is found after checking the full tree.
253+
return GetAndValidateRenderMode(component, childRenderMode: null);
252254

253-
if (renderModeAttribute is not null)
255+
IComponentRenderMode? GetAndValidateRenderMode(IComponent component, IComponentRenderMode? childRenderMode)
254256
{
255-
return renderModeAttribute.Mode;
257+
var componentState = GetComponentState(component);
258+
var renderMode = GetRenderModeForComponent(componentState);
259+
260+
if (childRenderMode is not null && renderMode is not null && childRenderMode != renderMode)
261+
{
262+
throw new RenderModeMisMatchException();
263+
}
264+
265+
return componentState.ParentComponentState is null
266+
? renderMode ?? childRenderMode
267+
: GetAndValidateRenderMode(componentState.ParentComponentState.Component, renderMode ?? childRenderMode);
256268
}
257269

258-
var parentComponentState = GetComponentState(component).ParentComponentState;
259-
return parentComponentState is not null
260-
? GetComponentRenderMode(parentComponentState.Component)
261-
: null;
270+
IComponentRenderMode? GetRenderModeForComponent(ComponentState componentState)
271+
{
272+
var renderModeAttribute = componentState.Component.GetType().GetCustomAttribute<RenderModeAttribute>();
273+
if (renderModeAttribute is { Mode: not null })
274+
{
275+
return renderModeAttribute.Mode;
276+
}
277+
278+
if (componentState.ParentComponentState is not null)
279+
{
280+
var parentFrames = GetCurrentRenderTreeFrames(componentState.ParentComponentState.ComponentId);
281+
var foundComponentStart = false;
282+
for (var i = 0; i < parentFrames.Count; i++)
283+
{
284+
ref var frame = ref parentFrames.Array[i];
285+
286+
if (frame.FrameType is RenderTreeFrameType.Component)
287+
{
288+
foundComponentStart = frame.ComponentId == componentState.ComponentId;
289+
}
290+
else if (foundComponentStart && frame.FrameType is RenderTreeFrameType.ComponentRenderMode)
291+
{
292+
return frame.ComponentRenderMode;
293+
}
294+
}
295+
}
296+
297+
return null;
298+
}
262299
}
263300
#endif
264301

tests/bunit.core.tests/Rendering/RenderModeTests.cs

Lines changed: 0 additions & 73 deletions
This file was deleted.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
@using Bunit.TestAssets.RenderModes;
2+
@inherits TestContext
3+
@code {
4+
#if NET9_0_OR_GREATER
5+
[Fact(DisplayName = "TestRenderer provides RendererInfo")]
6+
public void Test001()
7+
{
8+
Renderer.SetRendererInfo(new RendererInfo("Server", true));
9+
var cut = RenderComponent<RendererInfoComponent>();
10+
11+
cut.MarkupMatches(
12+
@<text>
13+
<p>Is interactive: True</p>
14+
<p>Rendermode: Server</p>
15+
</text>);
16+
}
17+
18+
[Fact(DisplayName = "Renderer throws exception if RendererInfo is not specified")]
19+
public void Test002()
20+
{
21+
Action act = () => RenderComponent<RendererInfoComponent>();
22+
23+
act.ShouldThrow<MissingRendererInfoException>();
24+
}
25+
26+
[Fact(DisplayName = "Renderer should set the RenderModeAttribute on the component")]
27+
public void Test003()
28+
{
29+
var cut = RenderComponent<ComponentWithServerRenderMode>();
30+
31+
cut.MarkupMatches(@<div>Assigned render mode: InteractiveServerRenderMode</div>);
32+
}
33+
34+
[Fact(DisplayName = "The AssignedRenderMode is based on the RenderModeAttribute in the component hierarchy where parent component has no RenderMode")]
35+
public void Test004()
36+
{
37+
var cut = Render(
38+
@<ComponentWithoutRenderMode>
39+
<ComponentWithWebAssemblyRenderMode />
40+
</ComponentWithoutRenderMode>);
41+
42+
cut.MarkupMatches(
43+
@<text>
44+
<div>Parent assigned render mode: </div>
45+
<div>Assigned render mode: InteractiveWebAssemblyRenderMode</div>
46+
</text>);
47+
}
48+
49+
[Fact(DisplayName = "Parent and child render mode is specified")]
50+
public void Test005()
51+
{
52+
var cut = Render(
53+
@<ComponentWithServerRenderMode>
54+
<ComponentWithServerRenderMode />
55+
</ComponentWithServerRenderMode>);
56+
57+
cut.MarkupMatches(
58+
@<text>
59+
<div>Parent assigned render mode: InteractiveServerRenderMode</div>
60+
<div>Assigned render mode: InteractiveServerRenderMode</div>
61+
</text>);
62+
}
63+
64+
[Fact(DisplayName = "Parent and child render mode is not specified")]
65+
public void Test006()
66+
{
67+
var cut = Render(
68+
@<ComponentWithoutRenderMode>
69+
<ComponentWithoutRenderMode />
70+
</ComponentWithoutRenderMode>);
71+
72+
cut.MarkupMatches(
73+
@<text>
74+
<div>Parent assigned render mode: </div>
75+
<div>Assigned render mode: </div>
76+
</text>);
77+
}
78+
79+
[Fact(DisplayName = "Rendermode specified on child")]
80+
public void Test007()
81+
{
82+
var cut = Render(
83+
@<ComponentWithChildContent>
84+
<ComponentThatPrintsAssignedRenderMode @rendermode="RenderMode.InteractiveServer" />
85+
</ComponentWithChildContent>);
86+
87+
cut.MarkupMatches(@<p>Assigned Render Mode: InteractiveServerRenderMode</p>);
88+
}
89+
90+
[Fact(DisplayName = "Assigned Render Mode is inherited all the way down the component hierarchy")]
91+
public void Test008()
92+
{
93+
var cut = Render(
94+
@<ComponentWithChildContent @rendermode="RenderMode.InteractiveServer">
95+
<ComponentWithChildContent>
96+
<ComponentThatPrintsAssignedRenderMode />
97+
</ComponentWithChildContent>
98+
</ComponentWithChildContent>);
99+
100+
cut.MarkupMatches(@<p>Assigned Render Mode: InteractiveServerRenderMode</p>);
101+
}
102+
103+
[Fact(DisplayName = "Having a component with section outlet and RenderMode is specifying for child component")]
104+
public void Test009()
105+
{
106+
// See: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/sections?view=aspnetcore-8.0#section-interaction-with-other-blazor-features
107+
var cut = Render(@<SectionOutletComponent />);
108+
109+
cut.MarkupMatches(@<p>Assigned Render Mode: InteractiveWebAssemblyRenderMode</p>);
110+
}
111+
112+
[Fact(DisplayName = "Assigned Render Mode on siblings")]
113+
public void Test010()
114+
{
115+
var cut = Render(
116+
@<ComponentWithChildContent>
117+
<ComponentThatPrintsAssignedRenderMode @rendermode="RenderMode.InteractiveServer"/>
118+
<ComponentThatPrintsAssignedRenderMode @rendermode="RenderMode.InteractiveWebAssembly"/>
119+
</ComponentWithChildContent>);
120+
121+
cut.MarkupMatches(
122+
@<text>
123+
<p>Assigned Render Mode: InteractiveServerRenderMode</p>
124+
<p>Assigned Render Mode: InteractiveWebAssemblyRenderMode</p>
125+
</text>);
126+
}
127+
128+
129+
[Fact(DisplayName = "Different assigned RenderMode between child and parent throws")]
130+
public void Test020()
131+
{
132+
var act = () => Render(
133+
@<ComponentWithChildContent @rendermode="RenderMode.InteractiveServer">
134+
<ComponentWithChildContent @rendermode="RenderMode.InteractiveWebAssembly">
135+
<ComponentThatPrintsAssignedRenderMode />
136+
</ComponentWithChildContent>
137+
</ComponentWithChildContent>);
138+
139+
act.ShouldThrow<RenderModeMisMatchException>(); // todo: figure out good exception to use
140+
}
141+
#endif
142+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@{
2+
#if NET9_0_OR_GREATER
3+
}
4+
5+
<p>Assigned Render Mode: @AssignedRenderMode?.GetType().Name</p>
6+
7+
@{
8+
#endif
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@ChildContent
2+
@code {
3+
4+
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
5+
6+
}

tests/bunit.testassets/RenderModes/ComponentWithServerRenderMode.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212

1313
@{
1414
#endif
15-
}
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@{
2+
#if NET9_0_OR_GREATER
3+
}
4+
5+
<Microsoft.AspNetCore.Components.Sections.SectionOutlet SectionId="1" @rendermode="RenderMode.InteractiveWebAssembly" />
6+
<Microsoft.AspNetCore.Components.Sections.SectionContent SectionId="1">
7+
<ComponentThatPrintsAssignedRenderMode/>
8+
</Microsoft.AspNetCore.Components.Sections.SectionContent>
9+
10+
@{
11+
#endif
12+
}

0 commit comments

Comments
 (0)