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<int>).Assembly.GetName().Name, preambleMarker.Assembly);
Assert.Equal(typeof(GenericComponent<int>).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,6 +1,7 @@
// 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;
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.ToInt32(parameters["Value"]!, CultureInfo.InvariantCulture));
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The use of Convert.ToInt32 with CultureInfo.InvariantCulture is unnecessary here. The parameter value is already an integer (42), and after deserialization it should be directly castable or assertable. In similar tests in this file (e.g., CanParseSingleMarkerWithParameters at line 56), values are asserted directly without conversion.

Consider simplifying to:

Assert.Equal(42, parameters["Value"]);

If type conversion is genuinely needed due to JSON deserialization, consider using a direct cast instead:

Assert.Equal(42, Assert.IsType<JsonElement>(parameters["Value"]).GetInt32());
Suggested change
Assert.Equal(42, Convert.ToInt32(parameters["Value"]!, CultureInfo.InvariantCulture));
Assert.Equal(42, parameters["Value"]);

Copilot uses AI. Check for mistakes.
}

[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(1, secondDescriptor.Sequence);
}

[Fact]
public void CanParseMultipleMarkers()
{
Expand Down Expand Up @@ -517,4 +586,12 @@ 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();
}
}
Loading