Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions src/Components/Endpoints/test/EndpointHtmlRendererTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,133 @@ public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode
Assert.Null(epilogueMarker.Type);
}

[Fact]
public async Task CanRender_ClosedGenericComponent()
{
// Arrange
var httpContext = GetHttpContext();
var writer = new StringWriter();

// Act
var parameters = ParameterView.FromDictionary(new Dictionary<string, object> { { "Value", 42 } });
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GenericComponent<int>), null, parameters);
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
var content = writer.ToString();

// Assert
Assert.Equal("<p>Generic value: 42</p>", content);
}

[Fact]
public async Task CanRender_ClosedGenericComponent_ServerMode()
{
// Arrange
var httpContext = GetHttpContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();

// Act
var parameters = ParameterView.FromDictionary(new Dictionary<string, object> { { "Value", "TestString" } });
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GenericComponent<string>), new InteractiveServerRenderMode(false), parameters);
var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result));
var match = Regex.Match(content, ComponentPattern);

// Assert
Assert.True(match.Success);
var marker = JsonSerializer.Deserialize<ComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, marker.Sequence);
Assert.Null(marker.PrerenderId);
Assert.NotNull(marker.Descriptor);
Assert.Equal("server", marker.Type);

var unprotectedServerComponent = protector.Unprotect(marker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(GenericComponent<string>).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(GenericComponent<string>).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);

var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
Assert.Equal("Value", parameterDefinition.Name);
Assert.Equal("System.String", parameterDefinition.TypeName);
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);

var value = Assert.Single(serverComponent.ParameterValues);
var rawValue = Assert.IsType<JsonElement>(value);
Assert.Equal("TestString", rawValue.GetString());
}

[Fact]
public async Task CanPrerender_ClosedGenericComponent_ServerMode()
{
// Arrange
var httpContext = GetHttpContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();

// Act
var parameters = ParameterView.FromDictionary(new Dictionary<string, object> { { "Value", 123 } });
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GenericComponent<int>), RenderMode.InteractiveServer, parameters);
var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result));
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);

// Assert
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<ComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, preambleMarker.Sequence);
Assert.NotNull(preambleMarker.PrerenderId);
Assert.NotNull(preambleMarker.Descriptor);
Assert.Equal("server", preambleMarker.Type);

var unprotectedServerComponent = protector.Unprotect(preambleMarker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.NotEqual(default, serverComponent);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(GenericComponent<int>).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(GenericComponent<int>).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);

var prerenderedContent = match.Groups["content"].Value;
Assert.Equal("<p>Generic value: 123</p>", prerenderedContent);

var epilogue = match.Groups["epilogue"].Value;
var epilogueMarker = JsonSerializer.Deserialize<ComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
}

[Fact]
public async Task CanPrerender_ClosedGenericComponent_ClientMode()
{
// Arrange
var httpContext = GetHttpContext();
var writer = new StringWriter();

// Act
var parameters = ParameterView.FromDictionary(new Dictionary<string, object> { { "Value", 456 } });
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GenericComponent<int>), RenderMode.InteractiveWebAssembly, parameters);
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
var content = writer.ToString();
content = AssertAndStripWebAssemblyOptions(content);
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);

// Assert
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<ComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.NotNull(preambleMarker.PrerenderId);
Assert.Equal("webassembly", preambleMarker.Type);
Assert.Equal(typeof(GenericComponent<string>).Assembly.GetName().Name, preambleMarker.Assembly);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: Type mismatch: assertions use GenericComponent but component is

In CanPrerender_ClosedGenericComponent_ClientMode, the component is rendered as GenericComponent<int> with an integer value 456, but the assertions on lines 837-838 check against typeof(GenericComponent<string>) for both Assembly and TypeName. This means the test is asserting the wrong type metadata:

  • Line 825: typeof(GenericComponent<int>) is used to render
  • Line 837: typeof(GenericComponent<string>).Assembly.GetName().Name is used to assert
  • Line 838: typeof(GenericComponent<string>).FullName is used to assert

Since both GenericComponent<int> and GenericComponent<string> share the same assembly, the assembly assertion will pass coincidentally. However, the FullName assertion will either fail (if the serialized marker correctly records GenericComponent<int>) or, if it passes, it means the test is validating the wrong expected value. Either way, these should use typeof(GenericComponent<int>) to match the rendered component type.

Was this helpful? React with 👍 / 👎

Suggested change
Assert.Equal(typeof(GenericComponent<string>).Assembly.GetName().Name, preambleMarker.Assembly);
Assert.Equal(typeof(GenericComponent<int>).Assembly.GetName().Name, preambleMarker.Assembly);
Assert.Equal(typeof(GenericComponent<int>).FullName, preambleMarker.TypeName);
  • Apply suggested fix

Assert.Equal(typeof(GenericComponent<string>).FullName, preambleMarker.TypeName);

var prerenderedContent = match.Groups["content"].Value;
Assert.Equal("<p>Generic value: 456</p>", prerenderedContent);

var epilogue = match.Groups["epilogue"].Value;
var epilogueMarker = JsonSerializer.Deserialize<ComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
}

[Fact]
public async Task ComponentWithInvalidRenderMode_Throws()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@typeparam TValue

<p>Generic value: @(Value?.ToString() ?? "(null)")</p>
@code {
[Parameter] public TValue Value { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging.Abstractions;

namespace Microsoft.AspNetCore.Components.Server.Circuits;

public class ServerComponentDeserializerTest
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
public class ServerComponentDeserializerTest
{
private readonly IDataProtectionProvider _ephemeralDataProtectionProvider;
private readonly ITimeLimitedDataProtector _protector;
private ServerComponentInvocationSequence _invocationSequence = new();
Expand Down Expand Up @@ -75,6 +76,74 @@ public void CanParseSingleMarkerWithNullParameters()
Assert.Null(parameters["Parameter"]);
}

[Fact]
public void CanParseSingleMarkerForClosedGenericComponent()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(typeof(GenericTestComponent<int>)));
var serverComponentDeserializer = CreateServerComponentDeserializer();

// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
var deserializedDescriptor = Assert.Single(descriptors);
Assert.Equal(typeof(GenericTestComponent<int>).FullName, deserializedDescriptor.ComponentType.FullName);
Assert.Equal(0, deserializedDescriptor.Sequence);
}

[Fact]
public void CanParseSingleMarkerForClosedGenericComponentWithStringTypeParameter()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(typeof(GenericTestComponent<string>)));
var serverComponentDeserializer = CreateServerComponentDeserializer();

// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
var deserializedDescriptor = Assert.Single(descriptors);
Assert.Equal(typeof(GenericTestComponent<string>).FullName, deserializedDescriptor.ComponentType.FullName);
Assert.Equal(0, deserializedDescriptor.Sequence);
}

[Fact]
public void CanParseSingleMarkerForClosedGenericComponentWithParameters()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(
(typeof(GenericTestComponent<int>), new Dictionary<string, object> { ["Value"] = 42 })));
var serverComponentDeserializer = CreateServerComponentDeserializer();

// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
var deserializedDescriptor = Assert.Single(descriptors);
Assert.Equal(typeof(GenericTestComponent<int>).FullName, deserializedDescriptor.ComponentType.FullName);
Assert.Equal(0, deserializedDescriptor.Sequence);

var parameters = deserializedDescriptor.Parameters.ToDictionary();
Assert.Single(parameters);
Assert.Contains("Value", parameters.Keys);
Assert.Equal(42, Convert.ToInt64(parameters["Value"]!, CultureInfo.InvariantCulture));
}

[Fact]
public void CanParseMultipleMarkersForClosedGenericComponents()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(typeof(GenericTestComponent<int>), typeof(GenericTestComponent<string>)));
var serverComponentDeserializer = CreateServerComponentDeserializer();

// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Equal(2, descriptors.Count);

var firstDescriptor = descriptors[0];
Assert.Equal(typeof(GenericTestComponent<int>).FullName, firstDescriptor.ComponentType.FullName);
Assert.Equal(0, firstDescriptor.Sequence);

var secondDescriptor = descriptors[1];
Assert.Equal(typeof(GenericTestComponent<string>).FullName, secondDescriptor.ComponentType.FullName);
Assert.Equal(0, secondDescriptor.Sequence);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: Second descriptor sequence should be 1, not 0

In CanParseMultipleMarkersForClosedGenericComponents, the second descriptor's sequence number is asserted as 0, but it should be 1. The existing CanParseMultipleMarkers test (line 164) correctly asserts Assert.Equal(1, secondDescriptor.Sequence) for the second component. When multiple markers are serialized via CreateMarkers, each gets an incrementing sequence number starting from 0. The second component should have sequence 1.

This assertion either:

  1. Will fail at runtime (if the framework correctly assigns sequence 1), making this a broken test.
  2. Will pass incorrectly if something else is wrong, masking a real bug.

Based on the existing test pattern, this should be Assert.Equal(1, secondDescriptor.Sequence).

Was this helpful? React with 👍 / 👎

Suggested change
Assert.Equal(0, secondDescriptor.Sequence);
Assert.Equal(1, secondDescriptor.Sequence);
  • Apply suggested fix

}

[Fact]
public void CanParseMultipleMarkers()
{
Expand Down Expand Up @@ -517,4 +586,13 @@ private class DynamicallyAddedComponent : IComponent
public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
}

private class GenericTestComponent<T> : IComponent
{
[Parameter] public T Value { get; set; }

public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
}
}
}