Skip to content

Commit

Permalink
fix: support finding render modes specified via @rendermode directive
Browse files Browse the repository at this point in the history
  • Loading branch information
egil committed Nov 15, 2024
1 parent fcd47d2 commit f44ac45
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 83 deletions.
22 changes: 22 additions & 0 deletions src/bunit.core/Rendering/RenderModeMisMatchException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#if NET9_0_OR_GREATER
namespace Bunit.Rendering;

/// <summary>
/// Represents an exception that is thrown when a component under test has mismatching render modes assigned between parent and child components.
/// </summary>
public sealed class RenderModeMisMatchException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="MissingRendererInfoException"/> class.
/// </summary>
public RenderModeMisMatchException()
: base("""
A component under test has mismatching render modes assigned between parent and child components.
Ensure that the render mode of the parent component matches the render mode of the child component.
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.
""")
{
HelpLink = "https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-9.0#render-mode-propagation";
}
}
#endif
55 changes: 46 additions & 9 deletions src/bunit.core/Rendering/TestRenderer.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Microsoft.Extensions.Logging;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using Microsoft.Extensions.Logging;

namespace Bunit.Rendering;

Expand Down Expand Up @@ -247,18 +247,55 @@ protected override IComponent ResolveComponentForRenderMode(Type componentType,
{
ArgumentNullException.ThrowIfNull(component);

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

if (renderModeAttribute is not null)
IComponentRenderMode? GetAndValidateRenderMode(IComponent component, IComponentRenderMode? childRenderMode)
{
return renderModeAttribute.Mode;
var componentState = GetComponentState(component);
var renderMode = GetRenderModeForComponent(componentState);

if (childRenderMode is not null && renderMode is not null && childRenderMode != renderMode)
{
throw new RenderModeMisMatchException();
}

return componentState.ParentComponentState is null
? renderMode ?? childRenderMode
: GetAndValidateRenderMode(componentState.ParentComponentState.Component, renderMode ?? childRenderMode);
}

var parentComponentState = GetComponentState(component).ParentComponentState;
return parentComponentState is not null
? GetComponentRenderMode(parentComponentState.Component)
: null;
IComponentRenderMode? GetRenderModeForComponent(ComponentState componentState)
{
var renderModeAttribute = componentState.Component.GetType().GetCustomAttribute<RenderModeAttribute>();
if (renderModeAttribute is { Mode: not null })
{
return renderModeAttribute.Mode;
}

if (componentState.ParentComponentState is not null)
{
var parentFrames = GetCurrentRenderTreeFrames(componentState.ParentComponentState.ComponentId);
var foundComponentStart = false;
for (var i = 0; i < parentFrames.Count; i++)
{
ref var frame = ref parentFrames.Array[i];

if (frame.FrameType is RenderTreeFrameType.Component)
{
foundComponentStart = frame.ComponentId == componentState.ComponentId;
}
else if (foundComponentStart && frame.FrameType is RenderTreeFrameType.ComponentRenderMode)
{
return frame.ComponentRenderMode;
}
}
}

return null;
}
}
#endif

Expand Down
73 changes: 0 additions & 73 deletions tests/bunit.core.tests/Rendering/RenderModeTests.cs

This file was deleted.

142 changes: 142 additions & 0 deletions tests/bunit.core.tests/Rendering/RenderModeTests.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
@using Bunit.TestAssets.RenderModes;
@inherits TestContext
@code {
#if NET9_0_OR_GREATER
[Fact(DisplayName = "TestRenderer provides RendererInfo")]
public void Test001()
{
Renderer.SetRendererInfo(new RendererInfo("Server", true));
var cut = RenderComponent<RendererInfoComponent>();

cut.MarkupMatches(
@<text>
<p>Is interactive: True</p>
<p>Rendermode: Server</p>
</text>);
}

[Fact(DisplayName = "Renderer throws exception if RendererInfo is not specified")]
public void Test002()
{
Action act = () => RenderComponent<RendererInfoComponent>();

act.ShouldThrow<MissingRendererInfoException>();
}

[Fact(DisplayName = "Renderer should set the RenderModeAttribute on the component")]
public void Test003()
{
var cut = RenderComponent<ComponentWithServerRenderMode>();

cut.MarkupMatches(@<div>Assigned render mode: InteractiveServerRenderMode</div>);
}

[Fact(DisplayName = "The AssignedRenderMode is based on the RenderModeAttribute in the component hierarchy where parent component has no RenderMode")]
public void Test004()
{
var cut = Render(
@<ComponentWithoutRenderMode>
<ComponentWithWebAssemblyRenderMode />
</ComponentWithoutRenderMode>);

cut.MarkupMatches(
@<text>
<div>Parent assigned render mode: </div>
<div>Assigned render mode: InteractiveWebAssemblyRenderMode</div>
</text>);
}

[Fact(DisplayName = "Parent and child render mode is specified")]
public void Test005()
{
var cut = Render(
@<ComponentWithServerRenderMode>
<ComponentWithServerRenderMode />
</ComponentWithServerRenderMode>);

cut.MarkupMatches(
@<text>
<div>Parent assigned render mode: InteractiveServerRenderMode</div>
<div>Assigned render mode: InteractiveServerRenderMode</div>
</text>);
}

[Fact(DisplayName = "Parent and child render mode is not specified")]
public void Test006()
{
var cut = Render(
@<ComponentWithoutRenderMode>
<ComponentWithoutRenderMode />
</ComponentWithoutRenderMode>);

cut.MarkupMatches(
@<text>
<div>Parent assigned render mode: </div>
<div>Assigned render mode: </div>
</text>);
}

[Fact(DisplayName = "Rendermode specified on child")]
public void Test007()
{
var cut = Render(
@<ComponentWithChildContent>
<ComponentThatPrintsAssignedRenderMode @rendermode="RenderMode.InteractiveServer" />
</ComponentWithChildContent>);

cut.MarkupMatches(@<p>Assigned Render Mode: InteractiveServerRenderMode</p>);
}

[Fact(DisplayName = "Assigned Render Mode is inherited all the way down the component hierarchy")]
public void Test008()
{
var cut = Render(
@<ComponentWithChildContent @rendermode="RenderMode.InteractiveServer">
<ComponentWithChildContent>
<ComponentThatPrintsAssignedRenderMode />
</ComponentWithChildContent>
</ComponentWithChildContent>);

cut.MarkupMatches(@<p>Assigned Render Mode: InteractiveServerRenderMode</p>);
}

[Fact(DisplayName = "Having a component with section outlet and RenderMode is specifying for child component")]
public void Test009()
{
// See: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/sections?view=aspnetcore-8.0#section-interaction-with-other-blazor-features
var cut = Render(@<SectionOutletComponent />);

cut.MarkupMatches(@<p>Assigned Render Mode: InteractiveWebAssemblyRenderMode</p>);
}

[Fact(DisplayName = "Assigned Render Mode on siblings")]
public void Test010()
{
var cut = Render(
@<ComponentWithChildContent>
<ComponentThatPrintsAssignedRenderMode @rendermode="RenderMode.InteractiveServer"/>
<ComponentThatPrintsAssignedRenderMode @rendermode="RenderMode.InteractiveWebAssembly"/>
</ComponentWithChildContent>);

cut.MarkupMatches(
@<text>
<p>Assigned Render Mode: InteractiveServerRenderMode</p>
<p>Assigned Render Mode: InteractiveWebAssemblyRenderMode</p>
</text>);
}


[Fact(DisplayName = "Different assigned RenderMode between child and parent throws")]
public void Test020()
{
var act = () => Render(
@<ComponentWithChildContent @rendermode="RenderMode.InteractiveServer">
<ComponentWithChildContent @rendermode="RenderMode.InteractiveWebAssembly">
<ComponentThatPrintsAssignedRenderMode />
</ComponentWithChildContent>
</ComponentWithChildContent>);

act.ShouldThrow<RenderModeMisMatchException>(); // todo: figure out good exception to use
}
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@{
#if NET9_0_OR_GREATER
}

<p>Assigned Render Mode: @AssignedRenderMode?.GetType().Name</p>

@{
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@ChildContent
@code {

[Parameter] public RenderFragment ChildContent { get; set; } = default!;

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@

@{
#endif
}
}
12 changes: 12 additions & 0 deletions tests/bunit.testassets/RenderModes/SectionOutletComponent.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@{
#if NET9_0_OR_GREATER
}

<Microsoft.AspNetCore.Components.Sections.SectionOutlet SectionId="1" @rendermode="RenderMode.InteractiveWebAssembly" />
<Microsoft.AspNetCore.Components.Sections.SectionContent SectionId="1">
<ComponentThatPrintsAssignedRenderMode/>
</Microsoft.AspNetCore.Components.Sections.SectionContent>

@{
#endif
}

0 comments on commit f44ac45

Please sign in to comment.