aspnetcore/test/Microsoft.AspNetCore.Blazor.../CascadingParameterTest.cs

391 lines
18 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using System;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Test
{
public class CascadingParameterTest
{
[Fact]
public void PassesCascadingParametersToNestedComponents()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", "Hello");
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", "Goodbye");
childBuilder.CloseComponent();
}));
builder.CloseComponent();
});
// Act/Assert
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var batch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(batch, out var nestedComponentId);
var nestedComponentDiff = batch.DiffsByComponentId[nestedComponentId].Single();
// The nested component was rendered with the correct parameters
Assert.Collection(nestedComponentDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"CascadingParameter=Hello; RegularParameter=Goodbye");
});
Assert.Equal(1, nestedComponent.NumRenders);
}
[Fact]
public void RetainsCascadingParametersWhenUpdatingDirectParameters()
{
// Arrange
var renderer = new TestRenderer();
var regularParameterValue = "Initial value";
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", "Hello");
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", regularParameterValue);
childBuilder.CloseComponent();
}));
builder.CloseComponent();
});
// Act 1: Render in initial state
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
// Capture the nested component so we can verify the update later
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
Assert.Equal(1, nestedComponent.NumRenders);
// Act 2: Render again with updated regular parameter
regularParameterValue = "Changed value";
component.TriggerRender();
// Assert
Assert.Equal(2, renderer.Batches.Count);
var secondBatch = renderer.Batches[1];
var nestedComponentDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
// The nested component was rendered with the correct parameters
Assert.Collection(nestedComponentDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex); // This is the only change
AssertFrame.Text(secondBatch.ReferenceFrames[0], "CascadingParameter=Hello; RegularParameter=Changed value");
});
Assert.Equal(2, nestedComponent.NumRenders);
}
[Fact]
public void NotifiesDescendantsOfUpdatedCascadingParameterValuesAndPreservesDirectParameters()
{
// Arrange
var providedValue = "Initial value";
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", providedValue);
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", "Goodbye");
childBuilder.CloseComponent();
}));
builder.CloseComponent();
});
// Act 1: Initial render; capture nested component ID
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
Assert.Equal(1, nestedComponent.NumRenders);
// Act 2: Re-render CascadingValue with new value
providedValue = "Updated value";
component.TriggerRender();
// Assert: We re-rendered CascadingParameterConsumerComponent
Assert.Equal(2, renderer.Batches.Count);
var secondBatch = renderer.Batches[1];
var nestedComponentDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
// The nested component was rendered with the correct parameters
Assert.Collection(nestedComponentDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
Assert.Equal(0, edit.ReferenceFrameIndex); // This is the only change
AssertFrame.Text(secondBatch.ReferenceFrames[0], "CascadingParameter=Updated value; RegularParameter=Goodbye");
});
Assert.Equal(2, nestedComponent.NumRenders);
}
[Fact]
public void DoesNotNotifyDescendantsIfCascadingParameterValuesAreImmutableAndUnchanged()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", "Unchanging value");
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", "Goodbye");
childBuilder.CloseComponent();
}));
builder.CloseComponent();
});
// Act 1: Initial render
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out _);
Assert.Equal(3, firstBatch.DiffsByComponentId.Count); // Root + CascadingValue + nested
Assert.Equal(1, nestedComponent.NumRenders);
// Act/Assert: Re-render the CascadingValue; observe nested component wasn't re-rendered
component.TriggerRender();
// Assert: We did not re-render CascadingParameterConsumerComponent
Assert.Equal(2, renderer.Batches.Count);
var secondBatch = renderer.Batches[1];
Assert.Equal(2, secondBatch.DiffsByComponentId.Count); // Root + CascadingValue, but not nested one
Assert.Equal(1, nestedComponent.NumRenders);
}
[Fact]
public void StopsNotifyingDescendantsIfTheyAreRemoved()
{
// Arrange
var providedValue = "Initial value";
var displayNestedComponent = true;
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", providedValue);
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
if (displayNestedComponent)
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", "Goodbye");
childBuilder.CloseComponent();
}
}));
builder.CloseComponent();
});
// Act 1: Initial render; capture nested component ID
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
Assert.Equal(1, nestedComponent.NumSetParametersCalls);
Assert.Equal(1, nestedComponent.NumRenders);
// Act/Assert 2: Re-render the CascadingValue; observe nested component wasn't re-rendered
providedValue = "Updated value";
displayNestedComponent = false; // Remove the nested componet
component.TriggerRender();
// Assert: We did not render the nested component now it's been removed
Assert.Equal(2, renderer.Batches.Count);
var secondBatch = renderer.Batches[1];
Assert.Equal(1, nestedComponent.NumRenders);
Assert.Equal(2, secondBatch.DiffsByComponentId.Count); // Root + CascadingValue, but not nested one
// We *did* send updated params during the first render where it was removed,
// because the params are sent before the disposal logic runs. We could avoid
// this by moving the notifications into the OnAfterRender phase, but then we'd
// often render descendants twice (once because they are descendants and some
// direct parameter might have changed, then once because a cascading parameter
// changed). We can't have it both ways, so optimize for the case when the
// nested component *hasn't* just been removed.
Assert.Equal(2, nestedComponent.NumSetParametersCalls);
// Act 3: However, after disposal, the subscription is removed, so we won't send
// updated params on subsequent CascadingValue renders.
providedValue = "Updated value 2";
component.TriggerRender();
Assert.Equal(2, nestedComponent.NumSetParametersCalls);
}
[Fact]
public void DoesNotNotifyDescendantsOfUpdatedCascadingParameterValuesWhenFixed()
{
// Arrange
var providedValue = "Initial value";
var shouldIncludeChild = true;
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<string>>(0);
builder.AddAttribute(1, "Value", providedValue);
builder.AddAttribute(2, "Fixed", true);
builder.AddAttribute(3, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
if (shouldIncludeChild)
{
childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
childBuilder.AddAttribute(1, "RegularParameter", "Goodbye");
childBuilder.CloseComponent();
}
}));
builder.CloseComponent();
});
// Act 1: Initial render; capture nested component ID
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();
var firstBatch = renderer.Batches.Single();
var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
Assert.Equal(1, nestedComponent.NumRenders);
// Assert: Initial value is supplied to descendant
var nestedComponentDiff = firstBatch.DiffsByComponentId[nestedComponentId].Single();
Assert.Collection(nestedComponentDiff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
firstBatch.ReferenceFrames[edit.ReferenceFrameIndex],
"CascadingParameter=Initial value; RegularParameter=Goodbye");
});
// Act 2: Re-render CascadingValue with new value
providedValue = "Updated value";
component.TriggerRender();
// Assert: We did not re-render the descendant
Assert.Equal(2, renderer.Batches.Count);
var secondBatch = renderer.Batches[1];
Assert.Equal(2, secondBatch.DiffsByComponentId.Count); // Root + CascadingValue, but not nested one
Assert.Equal(1, nestedComponent.NumSetParametersCalls);
Assert.Equal(1, nestedComponent.NumRenders);
// Act 3: Dispose
shouldIncludeChild = false;
component.TriggerRender();
// Assert: Absence of an exception here implies we didn't cause a problem by
// trying to remove a non-existent subscription
}
[Fact]
public void CascadingValueThrowsIfFixedFlagChangesToTrue()
{
// Arrange
var renderer = new TestRenderer();
var isFixed = false;
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<object>>(0);
builder.AddAttribute(1, "Fixed", isFixed);
builder.AddAttribute(2, "Value", new object());
builder.CloseComponent();
});
renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act/Assert
isFixed = true;
var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
Assert.Equal("The value of Fixed cannot be changed dynamically.", ex.Message);
}
[Fact]
public void CascadingValueThrowsIfFixedFlagChangesToFalse()
{
// Arrange
var renderer = new TestRenderer();
var isFixed = true;
var component = new TestComponent(builder =>
{
builder.OpenComponent<CascadingValue<object>>(0);
if (isFixed) // Showing also that "unset" is treated as "false"
{
builder.AddAttribute(1, "Fixed", true);
}
builder.AddAttribute(2, "Value", new object());
builder.CloseComponent();
});
renderer.AssignRootComponentId(component);
component.TriggerRender();
// Act/Assert
isFixed = false;
var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
Assert.Equal("The value of Fixed cannot be changed dynamically.", ex.Message);
}
private static T FindComponent<T>(CapturedBatch batch, out int componentId)
{
var componentFrame = batch.ReferenceFrames.Single(
frame => frame.FrameType == RenderTreeFrameType.Component
&& frame.Component is T);
componentId = componentFrame.ComponentId;
return (T)componentFrame.Component;
}
class TestComponent : AutoRenderComponent
{
private readonly RenderFragment _renderFragment;
public TestComponent(RenderFragment renderFragment)
{
_renderFragment = renderFragment;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
=> _renderFragment(builder);
}
class CascadingParameterConsumerComponent<T> : AutoRenderComponent
{
public int NumSetParametersCalls { get; private set; }
public int NumRenders { get; private set; }
[CascadingParameter] T CascadingParameter { get; set; }
[Parameter] string RegularParameter { get; set; }
public override void SetParameters(ParameterCollection parameters)
{
NumSetParametersCalls++;
base.SetParameters(parameters);
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
NumRenders++;
builder.AddContent(0, $"CascadingParameter={CascadingParameter}; RegularParameter={RegularParameter}");
}
}
}
}