929 lines
37 KiB
C#
929 lines
37 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 System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Microsoft.AspNetCore.Blazor.Components;
|
|
using Microsoft.AspNetCore.Blazor.Rendering;
|
|
using Microsoft.AspNetCore.Blazor.RenderTree;
|
|
using Microsoft.AspNetCore.Blazor.Test.Shared;
|
|
using Xunit;
|
|
|
|
namespace Microsoft.AspNetCore.Blazor.Test
|
|
{
|
|
public partial class RendererTest
|
|
{
|
|
[Fact]
|
|
public void CanRenderTopLevelComponents()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
var component = new TestComponent(builder =>
|
|
{
|
|
builder.OpenElement(0, "my element");
|
|
builder.AddContent(1, "some text");
|
|
builder.CloseElement();
|
|
});
|
|
|
|
// Act
|
|
var componentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
|
|
// Assert
|
|
var batch = renderer.Batches.Single();
|
|
var diff = batch.DiffsByComponentId[componentId].Single();
|
|
Assert.Collection(diff.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
|
Assert.Equal(0, edit.ReferenceFrameIndex);
|
|
});
|
|
AssertFrame.Element(batch.ReferenceFrames[0], "my element", 2);
|
|
AssertFrame.Text(batch.ReferenceFrames[1], "some text");
|
|
}
|
|
|
|
[Fact]
|
|
public void CanRenderNestedComponents()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
var component = new TestComponent(builder =>
|
|
{
|
|
builder.AddContent(0, "Hello");
|
|
builder.OpenComponent<MessageComponent>(1);
|
|
builder.AddAttribute(2, nameof(MessageComponent.Message), "Nested component output");
|
|
builder.CloseComponent();
|
|
});
|
|
|
|
// Act/Assert
|
|
var componentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
var batch = renderer.Batches.Single();
|
|
var componentFrame = batch.ReferenceFrames
|
|
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
|
|
var nestedComponentId = componentFrame.ComponentId;
|
|
var nestedComponentDiff = batch.DiffsByComponentId[nestedComponentId].Single();
|
|
|
|
// We rendered both components
|
|
Assert.Equal(2, batch.DiffsByComponentId.Count);
|
|
|
|
// The nested component exists
|
|
Assert.IsType<MessageComponent>(componentFrame.Component);
|
|
|
|
// The nested component was rendered as part of the batch
|
|
Assert.Collection(nestedComponentDiff.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
|
AssertFrame.Text(
|
|
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
|
"Nested component output");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CanReRenderTopLevelComponents()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
var component = new MessageComponent { Message = "Initial message" };
|
|
var componentId = renderer.AssignComponentId(component);
|
|
|
|
// Act/Assert: first render
|
|
component.TriggerRender();
|
|
var batch = renderer.Batches.Single();
|
|
var firstDiff = batch.DiffsByComponentId[componentId].Single();
|
|
Assert.Collection(firstDiff.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
|
Assert.Equal(0, edit.ReferenceFrameIndex);
|
|
AssertFrame.Text(batch.ReferenceFrames[0], "Initial message");
|
|
});
|
|
|
|
// Act/Assert: second render
|
|
component.Message = "Modified message";
|
|
component.TriggerRender();
|
|
var secondBatch = renderer.Batches.Skip(1).Single();
|
|
var secondDiff = secondBatch.DiffsByComponentId[componentId].Single();
|
|
Assert.Collection(secondDiff.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
|
|
Assert.Equal(0, edit.ReferenceFrameIndex);
|
|
AssertFrame.Text(secondBatch.ReferenceFrames[0], "Modified message");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CanReRenderNestedComponents()
|
|
{
|
|
// Arrange: parent component already rendered
|
|
var renderer = new TestRenderer();
|
|
var parentComponent = new TestComponent(builder =>
|
|
{
|
|
builder.OpenComponent<MessageComponent>(0);
|
|
builder.CloseComponent();
|
|
});
|
|
var parentComponentId = renderer.AssignComponentId(parentComponent);
|
|
parentComponent.TriggerRender();
|
|
var nestedComponentFrame = renderer.Batches.Single()
|
|
.ReferenceFrames
|
|
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
|
|
var nestedComponent = (MessageComponent)nestedComponentFrame.Component;
|
|
var nestedComponentId = nestedComponentFrame.ComponentId;
|
|
|
|
// Assert: inital render
|
|
nestedComponent.Message = "Render 1";
|
|
nestedComponent.TriggerRender();
|
|
var batch = renderer.Batches[1];
|
|
var firstDiff = batch.DiffsByComponentId[nestedComponentId].Single();
|
|
Assert.Collection(firstDiff.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
|
|
Assert.Equal(0, edit.ReferenceFrameIndex);
|
|
AssertFrame.Text(batch.ReferenceFrames[0], "Render 1");
|
|
});
|
|
|
|
// Act/Assert: re-render
|
|
nestedComponent.Message = "Render 2";
|
|
nestedComponent.TriggerRender();
|
|
var secondBatch = renderer.Batches[2];
|
|
var secondDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
|
|
Assert.Collection(secondDiff.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
|
|
Assert.Equal(0, edit.ReferenceFrameIndex);
|
|
AssertFrame.Text(secondBatch.ReferenceFrames[0], "Render 2");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CanDispatchEventsToTopLevelComponents()
|
|
{
|
|
// Arrange: Render a component with an event handler
|
|
var renderer = new TestRenderer();
|
|
UIEventArgs receivedArgs = null;
|
|
|
|
var component = new EventComponent
|
|
{
|
|
Handler = args => { receivedArgs = args; }
|
|
};
|
|
var componentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
|
|
var eventHandlerId = renderer.Batches.Single()
|
|
.ReferenceFrames
|
|
.First(frame => frame.AttributeValue != null)
|
|
.AttributeEventHandlerId;
|
|
|
|
// Assert: Event not yet fired
|
|
Assert.Null(receivedArgs);
|
|
|
|
// Act/Assert: Event can be fired
|
|
var eventArgs = new UIEventArgs();
|
|
renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
|
|
Assert.Same(eventArgs, receivedArgs);
|
|
}
|
|
|
|
[Fact]
|
|
public void CanDispatchEventsToNestedComponents()
|
|
{
|
|
UIEventArgs receivedArgs = null;
|
|
|
|
// Arrange: Render parent component
|
|
var renderer = new TestRenderer();
|
|
var parentComponent = new TestComponent(builder =>
|
|
{
|
|
builder.OpenComponent<EventComponent>(0);
|
|
builder.CloseComponent();
|
|
});
|
|
var parentComponentId = renderer.AssignComponentId(parentComponent);
|
|
parentComponent.TriggerRender();
|
|
|
|
// Arrange: Render nested component
|
|
var nestedComponentFrame = renderer.Batches.Single()
|
|
.ReferenceFrames
|
|
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
|
|
var nestedComponent = (EventComponent)nestedComponentFrame.Component;
|
|
nestedComponent.Handler = args => { receivedArgs = args; };
|
|
var nestedComponentId = nestedComponentFrame.ComponentId;
|
|
nestedComponent.TriggerRender();
|
|
|
|
// Find nested component's event handler ID
|
|
var eventHandlerId = renderer.Batches[1]
|
|
.ReferenceFrames
|
|
.First(frame => frame.AttributeValue != null)
|
|
.AttributeEventHandlerId;
|
|
|
|
// Assert: Event not yet fired
|
|
Assert.Null(receivedArgs);
|
|
|
|
// Act/Assert: Event can be fired
|
|
var eventArgs = new UIEventArgs();
|
|
renderer.DispatchEvent(nestedComponentId, eventHandlerId, eventArgs);
|
|
Assert.Same(eventArgs, receivedArgs);
|
|
}
|
|
|
|
[Fact]
|
|
public void ThrowsIfComponentDoesNotHandleEvents()
|
|
{
|
|
// Arrange: Render a component with an event handler
|
|
var renderer = new TestRenderer();
|
|
UIEventHandler handler = args => throw new NotImplementedException();
|
|
var component = new TestComponent(builder =>
|
|
{
|
|
builder.OpenElement(0, "mybutton");
|
|
builder.AddAttribute(1, "my click event", handler);
|
|
builder.CloseElement();
|
|
});
|
|
|
|
var componentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
|
|
var eventHandlerId = renderer.Batches.Single()
|
|
.ReferenceFrames
|
|
.First(frame => frame.AttributeValue != null)
|
|
.AttributeEventHandlerId;
|
|
var eventArgs = new UIEventArgs();
|
|
|
|
// Act/Assert
|
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
|
{
|
|
renderer.DispatchEvent(componentId, eventHandlerId, eventArgs);
|
|
});
|
|
Assert.Equal($"The component of type {typeof(TestComponent).FullName} cannot receive " +
|
|
$"events because it does not implement {typeof(IHandleEvent).FullName}.", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void CannotDispatchEventsToUnknownComponents()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
|
|
// Act/Assert
|
|
Assert.Throws<ArgumentException>(() =>
|
|
{
|
|
renderer.DispatchEvent(123, 0, new UIEventArgs());
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void ComponentsCanBeAssociatedWithMultipleRenderers()
|
|
{
|
|
// Arrange
|
|
var renderer1 = new TestRenderer();
|
|
var renderer2 = new TestRenderer();
|
|
var component = new MultiRendererComponent();
|
|
var renderer1ComponentId = renderer1.AssignComponentId(component);
|
|
renderer2.AssignComponentId(new TestComponent(null)); // Just so they don't get the same IDs
|
|
var renderer2ComponentId = renderer2.AssignComponentId(component);
|
|
|
|
// Act/Assert
|
|
component.TriggerRender();
|
|
var renderer1Batch = renderer1.Batches.Single();
|
|
var renderer1Diff = renderer1Batch.DiffsByComponentId[renderer1ComponentId].Single();
|
|
Assert.Collection(renderer1Diff.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
|
AssertFrame.Text(renderer1Batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
|
$"Hello from {nameof(MultiRendererComponent)}", 0);
|
|
});
|
|
|
|
var renderer2Batch = renderer2.Batches.Single();
|
|
var renderer2Diff = renderer2Batch.DiffsByComponentId[renderer2ComponentId].Single();
|
|
Assert.Collection(renderer2Diff.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
|
AssertFrame.Text(renderer2Batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
|
$"Hello from {nameof(MultiRendererComponent)}", 0);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void PreservesChildComponentInstancesWithNoAttributes()
|
|
{
|
|
// Arrange: First render, capturing child component instance
|
|
var renderer = new TestRenderer();
|
|
var message = "Hello";
|
|
var component = new TestComponent(builder =>
|
|
{
|
|
builder.AddContent(0, message);
|
|
builder.OpenComponent<MessageComponent>(1);
|
|
builder.CloseComponent();
|
|
});
|
|
|
|
var rootComponentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
|
|
var nestedComponentFrame = renderer.Batches.Single()
|
|
.ReferenceFrames
|
|
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
|
|
var nestedComponentInstance = (MessageComponent)nestedComponentFrame.Component;
|
|
|
|
// Act: Second render
|
|
message = "Modified message";
|
|
component.TriggerRender();
|
|
|
|
// Assert
|
|
var batch = renderer.Batches[1];
|
|
var diff = batch.DiffsByComponentId[rootComponentId].Single();
|
|
Assert.Collection(diff.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
|
|
Assert.Equal(0, edit.ReferenceFrameIndex);
|
|
});
|
|
AssertFrame.Text(batch.ReferenceFrames[0], "Modified message");
|
|
Assert.Empty(batch.DiffsByComponentId[nestedComponentFrame.ComponentId].Single().Edits);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdatesPropertiesOnRetainedChildComponentInstances()
|
|
{
|
|
// Arrange: First render, capturing child component instance
|
|
var renderer = new TestRenderer();
|
|
var objectThatWillNotChange = new object();
|
|
var firstRender = true;
|
|
var component = new TestComponent(builder =>
|
|
{
|
|
builder.OpenComponent<FakeComponent>(1);
|
|
builder.AddAttribute(2, nameof(FakeComponent.IntProperty), firstRender ? 123 : 256);
|
|
builder.AddAttribute(3, nameof(FakeComponent.ObjectProperty), objectThatWillNotChange);
|
|
builder.AddAttribute(4, nameof(FakeComponent.StringProperty), firstRender ? "String that will change" : "String that did change");
|
|
builder.CloseComponent();
|
|
});
|
|
|
|
var rootComponentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
|
|
var originalComponentFrame = renderer.Batches.Single()
|
|
.ReferenceFrames
|
|
.Single(frame => frame.FrameType == RenderTreeFrameType.Component);
|
|
var childComponentInstance = (FakeComponent)originalComponentFrame.Component;
|
|
|
|
// Assert 1: properties were assigned
|
|
Assert.Equal(123, childComponentInstance.IntProperty);
|
|
Assert.Equal("String that will change", childComponentInstance.StringProperty);
|
|
Assert.Same(objectThatWillNotChange, childComponentInstance.ObjectProperty);
|
|
|
|
// Act: Second render
|
|
firstRender = false;
|
|
component.TriggerRender();
|
|
|
|
// Assert
|
|
Assert.Equal(256, childComponentInstance.IntProperty);
|
|
Assert.Equal("String that did change", childComponentInstance.StringProperty);
|
|
Assert.Same(objectThatWillNotChange, childComponentInstance.ObjectProperty);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReRendersChildComponentsWhenPropertiesChange()
|
|
{
|
|
// Arrange: First render
|
|
var renderer = new TestRenderer();
|
|
var firstRender = true;
|
|
var component = new TestComponent(builder =>
|
|
{
|
|
builder.OpenComponent<MessageComponent>(1);
|
|
builder.AddAttribute(2, nameof(MessageComponent.Message), firstRender ? "first" : "second");
|
|
builder.CloseComponent();
|
|
});
|
|
|
|
var rootComponentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
|
|
var childComponentId = renderer.Batches.Single()
|
|
.ReferenceFrames
|
|
.Single(frame => frame.FrameType == RenderTreeFrameType.Component)
|
|
.ComponentId;
|
|
|
|
// Act: Second render
|
|
firstRender = false;
|
|
component.TriggerRender();
|
|
var diff = renderer.Batches[1].DiffsByComponentId[childComponentId].Single();
|
|
|
|
// Assert
|
|
Assert.Collection(diff.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
|
|
Assert.Equal(0, edit.ReferenceFrameIndex);
|
|
});
|
|
AssertFrame.Text(renderer.Batches[1].ReferenceFrames[0], "second");
|
|
}
|
|
|
|
[Fact]
|
|
public void RenderBatchIncludesListOfDisposedComponents()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
var firstRender = true;
|
|
var component = new TestComponent(builder =>
|
|
{
|
|
if (firstRender)
|
|
{
|
|
// Nested descendants
|
|
builder.OpenComponent<ConditionalParentComponent<FakeComponent>>(100);
|
|
builder.AddAttribute(101, nameof(ConditionalParentComponent<FakeComponent>.IncludeChild), true);
|
|
builder.CloseComponent();
|
|
}
|
|
builder.OpenComponent<FakeComponent>(200);
|
|
builder.CloseComponent();
|
|
});
|
|
|
|
var rootComponentId = renderer.AssignComponentId(component);
|
|
|
|
// Act/Assert 1: First render, capturing child component IDs
|
|
component.TriggerRender();
|
|
var batch = renderer.Batches.Single();
|
|
var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single();
|
|
var childComponentIds = rootComponentDiff
|
|
.Edits
|
|
.Select(edit => batch.ReferenceFrames[edit.ReferenceFrameIndex])
|
|
.Where(frame => frame.FrameType == RenderTreeFrameType.Component)
|
|
.Select(frame => frame.ComponentId)
|
|
.ToList();
|
|
Assert.Equal(new[] { 1, 2 }, childComponentIds);
|
|
|
|
// Act: Second render
|
|
firstRender = false;
|
|
component.TriggerRender();
|
|
|
|
// Assert: Applicable children are included in disposal list
|
|
Assert.Equal(new[] { 1, 3 }, renderer.Batches[1].DisposedComponentIDs);
|
|
}
|
|
|
|
[Fact]
|
|
public void DisposesEventHandlersWhenAttributeValueChanged()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
var eventCount = 0;
|
|
UIEventHandler origEventHandler = args => { eventCount++; };
|
|
var component = new EventComponent { Handler = origEventHandler };
|
|
var componentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
var origEventHandlerId = renderer.Batches.Single()
|
|
.ReferenceFrames
|
|
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
|
|
.Single(f => f.AttributeEventHandlerId != 0)
|
|
.AttributeEventHandlerId;
|
|
|
|
// Act/Assert 1: Event handler fires when we trigger it
|
|
Assert.Equal(0, eventCount);
|
|
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
|
|
Assert.Equal(1, eventCount);
|
|
|
|
// Now change the attribute value
|
|
var newEventCount = 0;
|
|
component.Handler = args => { newEventCount++; };
|
|
component.TriggerRender();
|
|
|
|
// Act/Assert 2: Can no longer fire the original event, but can fire the new event
|
|
Assert.Throws<ArgumentException>(() =>
|
|
{
|
|
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
|
|
});
|
|
Assert.Equal(1, eventCount);
|
|
Assert.Equal(0, newEventCount);
|
|
renderer.DispatchEvent(componentId, origEventHandlerId + 1, args: null);
|
|
Assert.Equal(1, newEventCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DisposesEventHandlersWhenAttributeRemoved()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
var eventCount = 0;
|
|
UIEventHandler origEventHandler = args => { eventCount++; };
|
|
var component = new EventComponent { Handler = origEventHandler };
|
|
var componentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
var origEventHandlerId = renderer.Batches.Single()
|
|
.ReferenceFrames
|
|
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
|
|
.Single(f => f.AttributeEventHandlerId != 0)
|
|
.AttributeEventHandlerId;
|
|
|
|
// Act/Assert 1: Event handler fires when we trigger it
|
|
Assert.Equal(0, eventCount);
|
|
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
|
|
Assert.Equal(1, eventCount);
|
|
|
|
// Now remove the event attribute
|
|
component.Handler = null;
|
|
component.TriggerRender();
|
|
|
|
// Act/Assert 2: Can no longer fire the original event
|
|
Assert.Throws<ArgumentException>(() =>
|
|
{
|
|
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
|
|
});
|
|
Assert.Equal(1, eventCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DisposesEventHandlersWhenOwnerComponentRemoved()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
var eventCount = 0;
|
|
UIEventHandler origEventHandler = args => { eventCount++; };
|
|
var component = new ConditionalParentComponent<EventComponent>
|
|
{
|
|
IncludeChild = true,
|
|
ChildParameters = new Dictionary<string, object>
|
|
{
|
|
{ nameof(EventComponent.Handler), origEventHandler }
|
|
}
|
|
};
|
|
var rootComponentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
var batch = renderer.Batches.Single();
|
|
var rootComponentDiff = batch.DiffsByComponentId[rootComponentId].Single();
|
|
var rootComponentFrame = batch.ReferenceFrames[0];
|
|
var childComponentFrame = rootComponentDiff.Edits
|
|
.Select(e => batch.ReferenceFrames[e.ReferenceFrameIndex])
|
|
.Where(f => f.FrameType == RenderTreeFrameType.Component)
|
|
.Single();
|
|
var childComponentId = childComponentFrame.ComponentId;
|
|
var childComponentDiff = batch.DiffsByComponentId[childComponentFrame.ComponentId].Single();
|
|
var eventHandlerId = batch.ReferenceFrames
|
|
.Skip(childComponentDiff.Edits[0].ReferenceFrameIndex) // Search from where the child component frames start
|
|
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
|
|
.Single(f => f.AttributeEventHandlerId != 0)
|
|
.AttributeEventHandlerId;
|
|
|
|
// Act/Assert 1: Event handler fires when we trigger it
|
|
Assert.Equal(0, eventCount);
|
|
renderer.DispatchEvent(childComponentId, eventHandlerId, args: null);
|
|
Assert.Equal(1, eventCount);
|
|
|
|
// Now remove the EventComponent
|
|
component.IncludeChild = false;
|
|
component.TriggerRender();
|
|
|
|
// Act/Assert 2: Can no longer fire the original event
|
|
Assert.Throws<ArgumentException>(() =>
|
|
{
|
|
renderer.DispatchEvent(eventHandlerId, eventHandlerId, args: null);
|
|
});
|
|
Assert.Equal(1, eventCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DisposesEventHandlersWhenAncestorElementRemoved()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
var eventCount = 0;
|
|
UIEventHandler origEventHandler = args => { eventCount++; };
|
|
var component = new EventComponent { Handler = origEventHandler };
|
|
var componentId = renderer.AssignComponentId(component);
|
|
component.TriggerRender();
|
|
var origEventHandlerId = renderer.Batches.Single()
|
|
.ReferenceFrames
|
|
.Where(f => f.FrameType == RenderTreeFrameType.Attribute)
|
|
.Single(f => f.AttributeEventHandlerId != 0)
|
|
.AttributeEventHandlerId;
|
|
|
|
// Act/Assert 1: Event handler fires when we trigger it
|
|
Assert.Equal(0, eventCount);
|
|
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
|
|
Assert.Equal(1, eventCount);
|
|
|
|
// Now remove the ancestor element
|
|
component.SkipElement = true;
|
|
component.TriggerRender();
|
|
|
|
// Act/Assert 2: Can no longer fire the original event
|
|
Assert.Throws<ArgumentException>(() =>
|
|
{
|
|
renderer.DispatchEvent(componentId, origEventHandlerId, args: null);
|
|
});
|
|
Assert.Equal(1, eventCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ComponentCannotTriggerRenderBeforeRenderHandleAssigned()
|
|
{
|
|
// Arrange
|
|
var component = new TestComponent(builder => { });
|
|
|
|
// Act/Assert
|
|
var ex = Assert.Throws<InvalidOperationException>(() =>
|
|
{
|
|
component.TriggerRender();
|
|
});
|
|
Assert.Equal("The render handle is not yet assigned.", ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void ComponentCanTriggerRenderWhenNoBatchIsInProgress()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
var renderCount = 0;
|
|
var component = new TestComponent(builder =>
|
|
{
|
|
builder.AddContent(0, $"Render count: {++renderCount}");
|
|
});
|
|
var componentId = renderer.AssignComponentId(component);
|
|
|
|
// Act/Assert: Can trigger initial render
|
|
Assert.Equal(0, renderCount);
|
|
component.TriggerRender();
|
|
Assert.Equal(1, renderCount);
|
|
var batch1 = renderer.Batches.Single();
|
|
var edit1 = batch1.DiffsByComponentId[componentId].Single().Edits.Single();
|
|
Assert.Equal(RenderTreeEditType.PrependFrame, edit1.Type);
|
|
AssertFrame.Text(batch1.ReferenceFrames[edit1.ReferenceFrameIndex],
|
|
"Render count: 1", 0);
|
|
|
|
// Act/Assert: Can trigger subsequent render
|
|
component.TriggerRender();
|
|
Assert.Equal(2, renderCount);
|
|
var batch2 = renderer.Batches.Skip(1).Single();
|
|
var edit2 = batch2.DiffsByComponentId[componentId].Single().Edits.Single();
|
|
Assert.Equal(RenderTreeEditType.UpdateText, edit2.Type);
|
|
AssertFrame.Text(batch2.ReferenceFrames[edit2.ReferenceFrameIndex],
|
|
"Render count: 2", 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void ComponentCanTriggerRenderWhenExistingBatchIsInProgress()
|
|
{
|
|
// Arrange
|
|
var renderer = new TestRenderer();
|
|
TestComponent parent = null;
|
|
var parentRenderCount = 0;
|
|
parent = new TestComponent(builder =>
|
|
{
|
|
builder.OpenComponent<ReRendersParentComponent>(0);
|
|
builder.AddAttribute(1, nameof(ReRendersParentComponent.Parent), parent);
|
|
builder.CloseComponent();
|
|
builder.AddContent(2, $"Parent render count: {++parentRenderCount}");
|
|
});
|
|
var parentComponentId = renderer.AssignComponentId(parent);
|
|
|
|
// Act
|
|
parent.TriggerRender();
|
|
|
|
// Assert
|
|
var batch = renderer.Batches.Single();
|
|
Assert.Equal(4, batch.DiffsInOrder.Count);
|
|
|
|
// First is the parent component's initial render
|
|
var diff1 = batch.DiffsInOrder[0];
|
|
Assert.Equal(parentComponentId, diff1.ComponentId);
|
|
Assert.Collection(diff1.Edits,
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
|
AssertFrame.Component<ReRendersParentComponent>(
|
|
batch.ReferenceFrames[edit.ReferenceFrameIndex]);
|
|
},
|
|
edit =>
|
|
{
|
|
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
|
|
AssertFrame.Text(
|
|
batch.ReferenceFrames[edit.ReferenceFrameIndex],
|
|
"Parent render count: 1");
|
|
});
|
|
|
|
// Second is the child component's single render
|
|
var diff2 = batch.DiffsInOrder[1];
|
|
Assert.NotEqual(parentComponentId, diff2.ComponentId);
|
|
var diff2edit = diff2.Edits.Single();
|
|
Assert.Equal(RenderTreeEditType.PrependFrame, diff2edit.Type);
|
|
AssertFrame.Text(batch.ReferenceFrames[diff2edit.ReferenceFrameIndex],
|
|
"Child is here");
|
|
|
|
// Third is the parent's triggered render
|
|
var diff3 = batch.DiffsInOrder[2];
|
|
Assert.Equal(parentComponentId, diff3.ComponentId);
|
|
var diff3edit = diff3.Edits.Single();
|
|
Assert.Equal(RenderTreeEditType.UpdateText, diff3edit.Type);
|
|
AssertFrame.Text(batch.ReferenceFrames[diff3edit.ReferenceFrameIndex],
|
|
"Parent render count: 2");
|
|
|
|
// Fourth is child's rerender due to parent rendering
|
|
var diff4 = batch.DiffsInOrder[3];
|
|
Assert.NotEqual(parentComponentId, diff4.ComponentId);
|
|
Assert.Empty(diff4.Edits);
|
|
}
|
|
|
|
private class NoOpRenderer : Renderer
|
|
{
|
|
public new int AssignComponentId(IComponent component)
|
|
=> base.AssignComponentId(component);
|
|
|
|
protected override void UpdateDisplay(RenderBatch renderBatch)
|
|
{
|
|
}
|
|
}
|
|
|
|
private class TestRenderer : Renderer
|
|
{
|
|
public List<CapturedBatch> Batches { get; }
|
|
= new List<CapturedBatch>();
|
|
|
|
public new int AssignComponentId(IComponent component)
|
|
=> base.AssignComponentId(component);
|
|
|
|
public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args)
|
|
=> base.DispatchEvent(componentId, eventHandlerId, args);
|
|
|
|
protected override void UpdateDisplay(RenderBatch renderBatch)
|
|
{
|
|
var capturedBatch = new CapturedBatch();
|
|
Batches.Add(capturedBatch);
|
|
|
|
for (var i = 0; i < renderBatch.UpdatedComponents.Count; i++)
|
|
{
|
|
ref var renderTreeDiff = ref renderBatch.UpdatedComponents.Array[i];
|
|
capturedBatch.AddDiff(renderTreeDiff);
|
|
}
|
|
|
|
// Clone other data, as underlying storage will get reused by later batches
|
|
capturedBatch.ReferenceFrames = renderBatch.ReferenceFrames.ToArray();
|
|
capturedBatch.DisposedComponentIDs = renderBatch.DisposedComponentIDs.ToList();
|
|
}
|
|
}
|
|
|
|
private class CapturedBatch
|
|
{
|
|
public IDictionary<int, List<RenderTreeDiff>> DiffsByComponentId { get; }
|
|
= new Dictionary<int, List<RenderTreeDiff>>();
|
|
|
|
public IList<RenderTreeDiff> DiffsInOrder { get; }
|
|
= new List<RenderTreeDiff>();
|
|
|
|
public IList<int> DisposedComponentIDs { get; set; }
|
|
public RenderTreeFrame[] ReferenceFrames { get; set; }
|
|
|
|
internal void AddDiff(RenderTreeDiff diff)
|
|
{
|
|
var componentId = diff.ComponentId;
|
|
if (!DiffsByComponentId.ContainsKey(componentId))
|
|
{
|
|
DiffsByComponentId.Add(componentId, new List<RenderTreeDiff>());
|
|
}
|
|
|
|
// Clone the diff, because its underlying storage will get reused in subsequent batches
|
|
var diffClone = new RenderTreeDiff(
|
|
diff.ComponentId,
|
|
new ArraySegment<RenderTreeEdit>(diff.Edits.ToArray()));
|
|
DiffsByComponentId[componentId].Add(diffClone);
|
|
DiffsInOrder.Add(diffClone);
|
|
}
|
|
}
|
|
|
|
private class TestComponent : IComponent
|
|
{
|
|
private RenderHandle _renderHandle;
|
|
private RenderFragment _renderFragment;
|
|
|
|
public TestComponent(RenderFragment renderFragment)
|
|
{
|
|
_renderFragment = renderFragment;
|
|
}
|
|
|
|
public void Init(RenderHandle renderHandle)
|
|
{
|
|
_renderHandle = renderHandle;
|
|
}
|
|
|
|
public void SetParameters(ParameterCollection parameters)
|
|
=> TriggerRender();
|
|
|
|
public void TriggerRender()
|
|
=> _renderHandle.Render(_renderFragment);
|
|
}
|
|
|
|
private class MessageComponent : AutoRenderComponent
|
|
{
|
|
public string Message { get; set; }
|
|
|
|
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
|
{
|
|
builder.AddContent(0, Message);
|
|
}
|
|
}
|
|
|
|
private class FakeComponent : IComponent
|
|
{
|
|
public int IntProperty { get; set; }
|
|
public string StringProperty { get; set; }
|
|
public object ObjectProperty { get; set; }
|
|
|
|
public void Init(RenderHandle renderHandle)
|
|
{
|
|
}
|
|
|
|
public void SetParameters(ParameterCollection parameters)
|
|
=> parameters.AssignToProperties(this);
|
|
}
|
|
|
|
private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent
|
|
{
|
|
public UIEventHandler Handler { get; set; }
|
|
public bool SkipElement { get; set; }
|
|
|
|
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
|
{
|
|
builder.OpenElement(0, "grandparent");
|
|
if (!SkipElement)
|
|
{
|
|
builder.OpenElement(1, "parent");
|
|
builder.OpenElement(2, "some element");
|
|
if (Handler != null)
|
|
{
|
|
builder.AddAttribute(3, "some event", Handler);
|
|
}
|
|
builder.CloseElement();
|
|
builder.CloseElement();
|
|
}
|
|
builder.CloseElement();
|
|
}
|
|
|
|
public void HandleEvent(UIEventHandler handler, UIEventArgs args)
|
|
=> handler(args);
|
|
}
|
|
|
|
private class ConditionalParentComponent<T> : AutoRenderComponent where T : IComponent
|
|
{
|
|
public bool IncludeChild { get; set; }
|
|
public IDictionary<string, object> ChildParameters { get; set; }
|
|
|
|
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
|
{
|
|
builder.AddContent(0, "Parent here");
|
|
|
|
if (IncludeChild)
|
|
{
|
|
builder.OpenComponent<T>(1);
|
|
if (ChildParameters != null)
|
|
{
|
|
var sequence = 2;
|
|
foreach (var kvp in ChildParameters)
|
|
{
|
|
builder.AddAttribute(sequence++, kvp.Key, kvp.Value);
|
|
}
|
|
}
|
|
builder.CloseComponent();
|
|
}
|
|
}
|
|
}
|
|
|
|
private class ReRendersParentComponent : AutoRenderComponent
|
|
{
|
|
public TestComponent Parent { get; set; }
|
|
private bool _isFirstTime = true;
|
|
|
|
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
|
{
|
|
if (_isFirstTime) // Don't want an infinite loop
|
|
{
|
|
_isFirstTime = false;
|
|
Parent.TriggerRender();
|
|
}
|
|
|
|
builder.AddContent(0, "Child is here");
|
|
}
|
|
}
|
|
|
|
private class MultiRendererComponent : IComponent
|
|
{
|
|
private readonly List<RenderHandle> _renderHandles
|
|
= new List<RenderHandle>();
|
|
|
|
public void Init(RenderHandle renderHandle)
|
|
=> _renderHandles.Add(renderHandle);
|
|
|
|
public void SetParameters(ParameterCollection parameters)
|
|
{
|
|
}
|
|
|
|
public void TriggerRender()
|
|
{
|
|
foreach (var renderHandle in _renderHandles)
|
|
{
|
|
renderHandle.Render(builder =>
|
|
{
|
|
builder.AddContent(0, $"Hello from {nameof(MultiRendererComponent)}");
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|