From 1c67ad40a5572eb686c6da116a88aaaa4b0ce351 Mon Sep 17 00:00:00 2001
From: EduardF1 <50618110+EduardF1@users.noreply.github.com>
Date: Thu, 30 Apr 2026 00:12:58 +0200
Subject: [PATCH 01/14] feat(blazor): add CssClass parameter to
ValidationSummary component
Adds a CssClass parameter to ValidationSummary so users can supply additional
CSS classes that get appended to the default validation-errors class.
Fixes #43860
---
src/Components/Web/src/Forms/ValidationSummary.cs | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/Components/Web/src/Forms/ValidationSummary.cs b/src/Components/Web/src/Forms/ValidationSummary.cs
index e2580fd0cf97..04b080657413 100644
--- a/src/Components/Web/src/Forms/ValidationSummary.cs
+++ b/src/Components/Web/src/Forms/ValidationSummary.cs
@@ -23,6 +23,12 @@ public class ValidationSummary : ComponentBase, IDisposable
///
[Parameter] public object? Model { get; set; }
+ ///
+ /// Gets or sets an optional CSS class string appended to the validation-errors
+ /// CSS class on the rendered ul element.
+ ///
+ [Parameter] public string? CssClass { get; set; }
+
///
/// Gets or sets a collection of additional attributes that will be applied to the created ul element.
///
@@ -72,9 +78,12 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
{
first = false;
+ var combinedClass = AttributeUtilities.CombineClassNames(AdditionalAttributes,
+ string.IsNullOrEmpty(CssClass) ? "validation-errors" : $"validation-errors {CssClass}");
+
builder.OpenElement(0, "ul");
- builder.AddAttribute(1, "class", "validation-errors");
- builder.AddMultipleAttributes(2, AdditionalAttributes);
+ builder.AddMultipleAttributes(1, AdditionalAttributes);
+ builder.AddAttribute(2, "class", combinedClass);
}
builder.OpenElement(3, "li");
From b142ffff9deec1181a916beaaea1aef5e14fb312 Mon Sep 17 00:00:00 2001
From: EduardF1 <50618110+EduardF1@users.noreply.github.com>
Date: Thu, 30 Apr 2026 00:13:36 +0200
Subject: [PATCH 02/14] feat(blazor): add CssClass to ValidationSummary public
API
---
src/Components/Web/src/PublicAPI.Unshipped.txt | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt
index 1f1a71e0b1da..5d0d6ba2e9a2 100644
--- a/src/Components/Web/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Web/src/PublicAPI.Unshipped.txt
@@ -106,3 +106,5 @@ Microsoft.AspNetCore.Components.Forms.Label.ChildContent.set -> void
Microsoft.AspNetCore.Components.Forms.Label.For.get -> System.Linq.Expressions.Expression!>?
Microsoft.AspNetCore.Components.Forms.Label.For.set -> void
Microsoft.AspNetCore.Components.Forms.Label.Label() -> void
+Microsoft.AspNetCore.Components.Forms.ValidationSummary.CssClass.get -> string?
+Microsoft.AspNetCore.Components.Forms.ValidationSummary.CssClass.set -> void
From 9f2c80f41fae071b68dffca3b1596eb72511f72f Mon Sep 17 00:00:00 2001
From: EduardF1 <50618110+EduardF1@users.noreply.github.com>
Date: Thu, 30 Apr 2026 00:13:37 +0200
Subject: [PATCH 03/14] test(blazor): add CssClass ValidationSummary to E2E
test component
---
.../BasicTestApp/FormsTest/TypicalValidationComponent.razor | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
index 27e09dd7846f..0e2f335d0f32 100644
--- a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
@@ -144,6 +144,9 @@
+
+
+
@foreach (var entry in submissionLog) {
@entry
}
From 88f0525b6a08de72edd4983a20f9816de326188b Mon Sep 17 00:00:00 2001
From: EduardF1 <50618110+EduardF1@users.noreply.github.com>
Date: Thu, 30 Apr 2026 00:13:39 +0200
Subject: [PATCH 04/14] test(blazor): add E2E test for ValidationSummary
CssClass parameter
---
.../test/E2ETest/Tests/FormsTest.cs | 20 +++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs
index 304ea1721abc..f99411ecb78d 100644
--- a/src/Components/test/E2ETest/Tests/FormsTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsTest.cs
@@ -1096,4 +1096,24 @@ private bool ElementHasAttribute(IWebElement webElement, string attribute)
var jsExecutor = (IJavaScriptExecutor)Browser;
return (bool)jsExecutor.ExecuteScript($"return arguments[0].attributes['{attribute}'] !== undefined;", webElement);
}
+ [Fact]
+ public void ValidationSummaryAppliesCssClass()
+ {
+ var appElement = MountTypicalValidationComponent();
+ var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
+ var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
+
+ // Make the form invalid to trigger rendering of the ValidationSummary elements
+ nameInput.SendKeys("01234567890123456789\t");
+ submitButton.Click();
+
+ // Default ValidationSummary renders with only "validation-errors" class
+ var defaultSummaryElement = appElement.FindElement(By.ClassName("all-errors")).FindElement(By.TagName("ul"));
+ Browser.Equal("validation-errors", () => defaultSummaryElement.GetDomAttribute("class"));
+
+ // ValidationSummary with CssClass appends the custom class to "validation-errors"
+ var customSummaryElement = appElement.FindElement(By.ClassName("all-errors-custom-class")).FindElement(By.TagName("ul"));
+ Browser.Equal("validation-errors custom-summary-css-class", () => customSummaryElement.GetDomAttribute("class"));
+ }
+
}
From 0e5404bbc2f6a9feaf6e3f57d6d45d35ae45c068 Mon Sep 17 00:00:00 2001
From: Eduard Fischer-Szava
Date: Sun, 3 May 2026 18:16:04 +0200
Subject: [PATCH 05/14] fix(test): move FindElement inside Browser.Equal lambda
to avoid race condition
The ul element is only rendered after Blazor processes validation on form
submit. Calling FindElement synchronously after submitButton.Click() could
throw NoSuchElementException before the re-render completes.
Moving FindElement inside the Browser.Equal lambda ensures the element
lookup is retried until the ValidationSummary ul is present in the DOM.
---
src/Components/test/E2ETest/Tests/FormsTest.cs | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs
index f99411ecb78d..6a68436cd52c 100644
--- a/src/Components/test/E2ETest/Tests/FormsTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsTest.cs
@@ -1096,6 +1096,7 @@ private bool ElementHasAttribute(IWebElement webElement, string attribute)
var jsExecutor = (IJavaScriptExecutor)Browser;
return (bool)jsExecutor.ExecuteScript($"return arguments[0].attributes['{attribute}'] !== undefined;", webElement);
}
+
[Fact]
public void ValidationSummaryAppliesCssClass()
{
@@ -1107,13 +1108,14 @@ public void ValidationSummaryAppliesCssClass()
nameInput.SendKeys("01234567890123456789\t");
submitButton.Click();
- // Default ValidationSummary renders with only "validation-errors" class
- var defaultSummaryElement = appElement.FindElement(By.ClassName("all-errors")).FindElement(By.TagName("ul"));
- Browser.Equal("validation-errors", () => defaultSummaryElement.GetDomAttribute("class"));
+ // Default ValidationSummary renders with only "validation-errors" class.
+ // FindElement is inside the lambda so Browser.Equal retries until the ul is rendered.
+ Browser.Equal("validation-errors",
+ () => appElement.FindElement(By.ClassName("all-errors")).FindElement(By.TagName("ul")).GetDomAttribute("class"));
// ValidationSummary with CssClass appends the custom class to "validation-errors"
- var customSummaryElement = appElement.FindElement(By.ClassName("all-errors-custom-class")).FindElement(By.TagName("ul"));
- Browser.Equal("validation-errors custom-summary-css-class", () => customSummaryElement.GetDomAttribute("class"));
+ Browser.Equal("validation-errors custom-summary-css-class",
+ () => appElement.FindElement(By.ClassName("all-errors-custom-class")).FindElement(By.TagName("ul")).GetDomAttribute("class"));
}
}
From 32f2917e73ac4c26d7d5ae61e3e1ecca8f49caa8 Mon Sep 17 00:00:00 2001
From: EduardF1 <50618110+EduardF1@users.noreply.github.com>
Date: Mon, 4 May 2026 23:47:57 +0200
Subject: [PATCH 06/14] test(blazor): add isolated component for
ValidationSummary CssClass E2E test
---
...lidationSummaryWithCssClassComponent.razor | 40 +++++++++++++++++++
1 file changed, 40 insertions(+)
create mode 100644 src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
new file mode 100644
index 000000000000..e908a7d317fa
--- /dev/null
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+@using System.ComponentModel.DataAnnotations
+@using Microsoft.AspNetCore.Components.Forms
+
+
+
+
+
From 20300698071bb88f1d916357445259c3cabee7d5 Mon Sep 17 00:00:00 2001
From: EduardF1 <50618110+EduardF1@users.noreply.github.com>
Date: Mon, 4 May 2026 23:48:22 +0200
Subject: [PATCH 08/14] test(blazor): use isolated component in
ValidationSummaryAppliesCssClass E2E test
---
.../test/E2ETest/Tests/FormsTest.cs | 38 +++++++++----------
1 file changed, 19 insertions(+), 19 deletions(-)
diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs
index 6a68436cd52c..a08a9d898dd0 100644
--- a/src/Components/test/E2ETest/Tests/FormsTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsTest.cs
@@ -97,7 +97,7 @@ public void EditFormWorksWithDataAnnotationsValidatorAndDI()
[Fact]
public void InputTextInteractsWithEditContext()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
var summaryMessagesAccessor = CreateValidationMessagesAccessor(
@@ -132,7 +132,7 @@ public void InputTextInteractsWithEditContext()
[Fact]
public void InputNumberInteractsWithEditContext_NonNullableInt()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var ageInput = appElement.FindElement(By.ClassName("age")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -165,7 +165,7 @@ public void InputNumberInteractsWithEditContext_NonNullableInt()
[Fact]
public void InputNumberInteractsWithEditContext_NullableFloat()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var heightInput = appElement.FindElement(By.ClassName("height")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -189,7 +189,7 @@ public void InputNumberInteractsWithEditContext_NullableFloat()
[Fact]
public void InputTextAreaInteractsWithEditContext()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var descriptionInput = appElement.FindElement(By.ClassName("description")).FindElement(By.TagName("textarea"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -216,7 +216,7 @@ public void InputTextAreaInteractsWithEditContext()
[Fact]
public void InputSelectInteractsWithEditContext()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select")));
var select = ticketClassInput.WrappedElement;
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -238,7 +238,7 @@ public void InputSelectInteractsWithEditContext()
[Fact]
public void InputSelectInteractsWithEditContext_BoolValues()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("select-bool-values")).FindElement(By.TagName("select")));
var select = ticketClassInput.WrappedElement;
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -266,7 +266,7 @@ public void InputSelectInteractsWithEditContext_BoolValues()
[Fact]
public void InputSelectInteractsWithEditContext_MultipleAttribute()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var citiesInput = new SelectElement(appElement.FindElement(By.ClassName("cities")).FindElement(By.TagName("select")));
var select = citiesInput.WrappedElement;
var messagesAccesor = CreateValidationMessagesAccessor(appElement);
@@ -289,7 +289,7 @@ public void InputSelectInteractsWithEditContext_MultipleAttribute()
[Fact]
public void InputSelectIgnoresMultipleAttribute()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select")));
var select = ticketClassInput.WrappedElement;
@@ -308,7 +308,7 @@ public void InputSelectIgnoresMultipleAttribute()
[Fact]
public void InputSelectHandlesHostileStringValues()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var selectParagraph = appElement.FindElement(By.ClassName("select-multiple-hostile"));
var hostileSelectInput = new SelectElement(selectParagraph.FindElement(By.TagName("select")));
var select = hostileSelectInput.WrappedElement;
@@ -327,7 +327,7 @@ public void InputSelectHandlesHostileStringValues()
[Fact]
public void InputCheckboxInteractsWithEditContext()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input"));
var isEvilInput = appElement.FindElement(By.ClassName("is-evil")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -358,7 +358,7 @@ public void InputCheckboxInteractsWithEditContext()
[Fact]
public void InputRadioGroupWithoutNameInteractsWithEditContext()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// By capturing the inputradio elements just once up front, we're implicitly showing
@@ -396,7 +396,7 @@ IReadOnlyCollection FindAirlineInputs()
[Fact]
public void InputRadioGroupsWithNamesNestedInteractWithEditContext()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
var group = appElement.FindElement(By.ClassName("nested-radio-group"));
@@ -431,7 +431,7 @@ public void InputRadioGroupsWithNamesNestedInteractWithEditContext()
[Fact]
public void InputRadioGroupWithBoolValuesInteractsWithEditContext()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Validate selected inputs
@@ -504,7 +504,7 @@ public void CanWireUpINotifyPropertyChangedToEditContext()
[Fact]
public void ValidationMessageDisplaysMessagesForField()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var emailContainer = appElement.FindElement(By.ClassName("email"));
var emailInput = emailContainer.FindElement(By.TagName("input"));
var emailMessagesAccessor = CreateValidationMessagesAccessor(emailContainer, ".special-email-css-class-override");
@@ -531,7 +531,7 @@ public void ValidationMessageDisplaysMessagesForField()
[Fact]
public void ErrorsFromCompareAttribute()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var emailContainer = appElement.FindElement(By.ClassName("email"));
var emailInput = emailContainer.FindElement(By.TagName("input"));
var confirmEmailContainer = appElement.FindElement(By.ClassName("confirm-email"));
@@ -584,7 +584,7 @@ public void DisplayNameReadsAttributesCorrectly()
[Fact]
public void InputComponentsCauseContainerToRerenderOnChange()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select")));
var selectedTicketClassDisplay = appElement.FindElement(By.Id("selected-ticket-class"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -693,7 +693,7 @@ public void SelectWithMultipleAttributeCanUseOnChangedCallback()
[Fact]
public void RespectsCustomFieldCssClassProvider()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var socksInput = appElement.FindElement(By.ClassName("socks")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -721,7 +721,7 @@ public void NavigateOnSubmitWorks()
[Fact]
public void CanRemoveAndReAddDataAnnotationsSupport()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
Func lastLogEntryAccessor = () => appElement.FindElement(By.CssSelector(".submission-log-entry:last-of-type")).Text;
@@ -1100,7 +1100,7 @@ private bool ElementHasAttribute(IWebElement webElement, string attribute)
[Fact]
public void ValidationSummaryAppliesCssClass()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
From d700736764742392bf57bebcc775dedc42883429 Mon Sep 17 00:00:00 2001
From: EduardF1 <50618110+EduardF1@users.noreply.github.com>
Date: Tue, 5 May 2026 01:30:27 +0200
Subject: [PATCH 09/14] test(blazor): register
ValidationSummaryWithCssClassComponent in test app index
---
src/Components/test/testassets/BasicTestApp/Index.razor | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor
index 94d08007f215..2c6351e91a2d 100644
--- a/src/Components/test/testassets/BasicTestApp/Index.razor
+++ b/src/Components/test/testassets/BasicTestApp/Index.razor
@@ -50,6 +50,7 @@
+
From 9f64eb9196f663ffe2b4bd958cbec31bb4a5f28d Mon Sep 17 00:00:00 2001
From: Eduard Fischer-Szava
Date: Tue, 5 May 2026 23:47:52 +0200
Subject: [PATCH 10/14] fix(blazor): scope CssClass test changes to
TypicalValidationComponent
The previous commits replaced MountTypicalValidationComponent() with
Browser.MountTestComponent() in
~19 unrelated form tests. The new minimal component only renders a
Name field, so every test that looks up `age`, `height`, `description`,
`ticket-class`, `cities`, etc. lost its element under test and CI went
red on Ubuntu/Helix.
This restores the original test fixture for all unrelated tests, drops
the redundant ValidationSummaryWithCssClassComponent.razor (and its
Index.razor entry), and adds the CssClass coverage directly to
TypicalValidationComponent.razor where the dedicated
ValidationSummaryAppliesCssClass test already lives.
---
.../test/E2ETest/Tests/FormsTest.cs | 38 +++++++++---------
.../TypicalValidationComponent.razor | 3 ++
...lidationSummaryWithCssClassComponent.razor | 40 -------------------
.../test/testassets/BasicTestApp/Index.razor | 1 -
4 files changed, 22 insertions(+), 60 deletions(-)
delete mode 100644 src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs
index a08a9d898dd0..6a68436cd52c 100644
--- a/src/Components/test/E2ETest/Tests/FormsTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsTest.cs
@@ -97,7 +97,7 @@ public void EditFormWorksWithDataAnnotationsValidatorAndDI()
[Fact]
public void InputTextInteractsWithEditContext()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
var summaryMessagesAccessor = CreateValidationMessagesAccessor(
@@ -132,7 +132,7 @@ public void InputTextInteractsWithEditContext()
[Fact]
public void InputNumberInteractsWithEditContext_NonNullableInt()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var ageInput = appElement.FindElement(By.ClassName("age")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -165,7 +165,7 @@ public void InputNumberInteractsWithEditContext_NonNullableInt()
[Fact]
public void InputNumberInteractsWithEditContext_NullableFloat()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var heightInput = appElement.FindElement(By.ClassName("height")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -189,7 +189,7 @@ public void InputNumberInteractsWithEditContext_NullableFloat()
[Fact]
public void InputTextAreaInteractsWithEditContext()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var descriptionInput = appElement.FindElement(By.ClassName("description")).FindElement(By.TagName("textarea"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -216,7 +216,7 @@ public void InputTextAreaInteractsWithEditContext()
[Fact]
public void InputSelectInteractsWithEditContext()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select")));
var select = ticketClassInput.WrappedElement;
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -238,7 +238,7 @@ public void InputSelectInteractsWithEditContext()
[Fact]
public void InputSelectInteractsWithEditContext_BoolValues()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("select-bool-values")).FindElement(By.TagName("select")));
var select = ticketClassInput.WrappedElement;
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -266,7 +266,7 @@ public void InputSelectInteractsWithEditContext_BoolValues()
[Fact]
public void InputSelectInteractsWithEditContext_MultipleAttribute()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var citiesInput = new SelectElement(appElement.FindElement(By.ClassName("cities")).FindElement(By.TagName("select")));
var select = citiesInput.WrappedElement;
var messagesAccesor = CreateValidationMessagesAccessor(appElement);
@@ -289,7 +289,7 @@ public void InputSelectInteractsWithEditContext_MultipleAttribute()
[Fact]
public void InputSelectIgnoresMultipleAttribute()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select")));
var select = ticketClassInput.WrappedElement;
@@ -308,7 +308,7 @@ public void InputSelectIgnoresMultipleAttribute()
[Fact]
public void InputSelectHandlesHostileStringValues()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var selectParagraph = appElement.FindElement(By.ClassName("select-multiple-hostile"));
var hostileSelectInput = new SelectElement(selectParagraph.FindElement(By.TagName("select")));
var select = hostileSelectInput.WrappedElement;
@@ -327,7 +327,7 @@ public void InputSelectHandlesHostileStringValues()
[Fact]
public void InputCheckboxInteractsWithEditContext()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input"));
var isEvilInput = appElement.FindElement(By.ClassName("is-evil")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -358,7 +358,7 @@ public void InputCheckboxInteractsWithEditContext()
[Fact]
public void InputRadioGroupWithoutNameInteractsWithEditContext()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// By capturing the inputradio elements just once up front, we're implicitly showing
@@ -396,7 +396,7 @@ IReadOnlyCollection FindAirlineInputs()
[Fact]
public void InputRadioGroupsWithNamesNestedInteractWithEditContext()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
var group = appElement.FindElement(By.ClassName("nested-radio-group"));
@@ -431,7 +431,7 @@ public void InputRadioGroupsWithNamesNestedInteractWithEditContext()
[Fact]
public void InputRadioGroupWithBoolValuesInteractsWithEditContext()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// Validate selected inputs
@@ -504,7 +504,7 @@ public void CanWireUpINotifyPropertyChangedToEditContext()
[Fact]
public void ValidationMessageDisplaysMessagesForField()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var emailContainer = appElement.FindElement(By.ClassName("email"));
var emailInput = emailContainer.FindElement(By.TagName("input"));
var emailMessagesAccessor = CreateValidationMessagesAccessor(emailContainer, ".special-email-css-class-override");
@@ -531,7 +531,7 @@ public void ValidationMessageDisplaysMessagesForField()
[Fact]
public void ErrorsFromCompareAttribute()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var emailContainer = appElement.FindElement(By.ClassName("email"));
var emailInput = emailContainer.FindElement(By.TagName("input"));
var confirmEmailContainer = appElement.FindElement(By.ClassName("confirm-email"));
@@ -584,7 +584,7 @@ public void DisplayNameReadsAttributesCorrectly()
[Fact]
public void InputComponentsCauseContainerToRerenderOnChange()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var ticketClassInput = new SelectElement(appElement.FindElement(By.ClassName("ticket-class")).FindElement(By.TagName("select")));
var selectedTicketClassDisplay = appElement.FindElement(By.Id("selected-ticket-class"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -693,7 +693,7 @@ public void SelectWithMultipleAttributeCanUseOnChangedCallback()
[Fact]
public void RespectsCustomFieldCssClassProvider()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var socksInput = appElement.FindElement(By.ClassName("socks")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
@@ -721,7 +721,7 @@ public void NavigateOnSubmitWorks()
[Fact]
public void CanRemoveAndReAddDataAnnotationsSupport()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
Func lastLogEntryAccessor = () => appElement.FindElement(By.CssSelector(".submission-log-entry:last-of-type")).Text;
@@ -1100,7 +1100,7 @@ private bool ElementHasAttribute(IWebElement webElement, string attribute)
[Fact]
public void ValidationSummaryAppliesCssClass()
{
- var appElement = Browser.MountTestComponent();
+ var appElement = MountTypicalValidationComponent();
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
index 27e09dd7846f..0e2f335d0f32 100644
--- a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
@@ -144,6 +144,9 @@
+
+
+
@foreach (var entry in submissionLog) {
@entry
}
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
deleted file mode 100644
index e908a7d317fa..000000000000
--- a/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
+++ /dev/null
@@ -1,40 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-@using System.ComponentModel.DataAnnotations
-@using Microsoft.AspNetCore.Components.Forms
-
-
-
-
-
- Name:
-
-
-
-
-
-
-
-
-
-
-
-
-@code {
- private EditContext editContext = default!;
- private Person person = new();
-
- protected override void OnInitialized()
- {
- editContext = new EditContext(person);
- }
-
- private void HandleValidSubmit() { }
-
- private class Person
- {
- [Required, MaxLength(10)]
- public string Name { get; set; } = string.Empty;
- }
-}
diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor
index 2c6351e91a2d..94d08007f215 100644
--- a/src/Components/test/testassets/BasicTestApp/Index.razor
+++ b/src/Components/test/testassets/BasicTestApp/Index.razor
@@ -50,7 +50,6 @@
-
From 72cbda607cfa43a964016866d648eaad45e93c3e Mon Sep 17 00:00:00 2001
From: Eduard Fischer-Szava
Date: Wed, 6 May 2026 21:23:13 +0200
Subject: [PATCH 11/14] fix(blazor): isolate CssClass test fixture to avoid
duplicate ValidationSummary
The previous attempt added a second alongside
the existing one inside TypicalValidationComponent.razor. Every form test that
counts .validation-message elements globally (FindElements(By.ClassName(...))
in CreateValidationMessagesAccessor) then saw each error twice, which is what
turned the Helix Subset 1 + components-e2e queues red after build 1409664
(InputDateInteractsWithEditContext_*, InputSelectInteractsWithEditContext,
InputTextInteractsWithEditContext, etc., all asserting Expected: ["..."]
vs Actual: ["...", "..."]).
Restores ValidationSummaryWithCssClassComponent.razor as a focused fixture
(name input + both summaries), registers it in Index.razor, removes the
extra from TypicalValidationComponent.razor,
and points only ValidationSummaryAppliesCssClass at the new component.
Net: 19+ unrelated form tests stop seeing duplicates; the dedicated CssClass
test still asserts both 'validation-errors' and 'validation-errors
custom-summary-css-class'.
---
.../test/E2ETest/Tests/FormsTest.cs | 2 +-
.../TypicalValidationComponent.razor | 3 --
...lidationSummaryWithCssClassComponent.razor | 40 +++++++++++++++++++
.../test/testassets/BasicTestApp/Index.razor | 1 +
4 files changed, 42 insertions(+), 4 deletions(-)
create mode 100644 src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs
index 6a68436cd52c..c374ada916b1 100644
--- a/src/Components/test/E2ETest/Tests/FormsTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsTest.cs
@@ -1100,7 +1100,7 @@ private bool ElementHasAttribute(IWebElement webElement, string attribute)
[Fact]
public void ValidationSummaryAppliesCssClass()
{
- var appElement = MountTypicalValidationComponent();
+ var appElement = Browser.MountTestComponent();
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
var submitButton = appElement.FindElement(By.CssSelector("button[type=submit]"));
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
index 0e2f335d0f32..27e09dd7846f 100644
--- a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor
@@ -144,9 +144,6 @@
-
-
-
@foreach (var entry in submissionLog) {
@entry
}
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
new file mode 100644
index 000000000000..e908a7d317fa
--- /dev/null
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+@using System.ComponentModel.DataAnnotations
+@using Microsoft.AspNetCore.Components.Forms
+
+
+
+
+
+ Name:
+
+
+
+
+
+
+
+
+
+
+
+
+@code {
+ private EditContext editContext = default!;
+ private Person person = new();
+
+ protected override void OnInitialized()
+ {
+ editContext = new EditContext(person);
+ }
+
+ private void HandleValidSubmit() { }
+
+ private class Person
+ {
+ [Required, MaxLength(10)]
+ public string Name { get; set; } = string.Empty;
+ }
+}
diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor
index 94d08007f215..d8e2311dd9f2 100644
--- a/src/Components/test/testassets/BasicTestApp/Index.razor
+++ b/src/Components/test/testassets/BasicTestApp/Index.razor
@@ -50,6 +50,7 @@
+
From e940078878fb72a8204bb5aa6456c4421ad07b18 Mon Sep 17 00:00:00 2001
From: EduardF1 <50618110+EduardF1@users.noreply.github.com>
Date: Wed, 13 May 2026 13:42:59 +0200
Subject: [PATCH 12/14] fix(test): remove razor license header, add
CssClass+class interaction fixture
---
.../ValidationSummaryWithCssClassComponent.razor | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
index e908a7d317fa..5834995bc445 100644
--- a/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
@@ -1,6 +1,3 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms
@@ -19,6 +16,9 @@
+
+
+
@code {
@@ -37,4 +37,4 @@
[Required, MaxLength(10)]
public string Name { get; set; } = string.Empty;
}
-}
+}
\ No newline at end of file
From 57896371d7451aeee1bd0112697d69311c1ea770 Mon Sep 17 00:00:00 2001
From: EduardF1 <50618110+EduardF1@users.noreply.github.com>
Date: Wed, 13 May 2026 13:43:36 +0200
Subject: [PATCH 13/14] fix(test): add CssClass + AdditionalAttributes class
interaction assertion
---
src/Components/test/E2ETest/Tests/FormsTest.cs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs
index c374ada916b1..11083a7f0da0 100644
--- a/src/Components/test/E2ETest/Tests/FormsTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsTest.cs
@@ -1116,6 +1116,10 @@ public void ValidationSummaryAppliesCssClass()
// ValidationSummary with CssClass appends the custom class to "validation-errors"
Browser.Equal("validation-errors custom-summary-css-class",
() => appElement.FindElement(By.ClassName("all-errors-custom-class")).FindElement(By.TagName("ul")).GetDomAttribute("class"));
+
+ // Both AdditionalAttributes["class"] and CssClass are combined: splatted class + validation-errors + CssClass
+ Browser.Equal("extra validation-errors custom",
+ () => appElement.FindElement(By.ClassName("all-errors-combined")).FindElement(By.TagName("ul")).GetDomAttribute("class"));
}
}
From 8d295ecf420b9df5df0b79714493b5cbdbb1920e Mon Sep 17 00:00:00 2001
From: Eduard Fischer-Szava
Date: Mon, 25 May 2026 18:18:43 +0200
Subject: [PATCH 14/14] refactor: remove public CssClass parameter, use
internal-only fix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Address @ilonatommy's review feedback: instead of adding a new public
CssClass parameter (which would set a precedent — no other component
exposes this publicly), use the same internal pattern as InputRadio:
builder.AddMultipleAttributes(AdditionalAttributes);
builder.AddAttributeIfNotNullOrEmpty("class",
AttributeUtilities.CombineClassNames(AdditionalAttributes, "validation-errors"));
This ensures:
- The fixed 'validation-errors' class is always present
- Users can add extra classes via AdditionalAttributes (class="pt-2")
- Both are combined correctly ("pt-2 validation-errors")
- No new public API surface
E2E test updated to verify:
1. Default renders with only 'validation-errors'
2. class="pt-2" via splatting combines to 'pt-2 validation-errors'
3. Other splatted attributes (data-testid) pass through correctly
---
.../Web/src/Forms/ValidationSummary.cs | 12 ++----------
src/Components/Web/src/PublicAPI.Unshipped.txt | 2 --
src/Components/test/E2ETest/Tests/FormsTest.cs | 18 ++++++++++--------
...alidationSummaryWithCssClassComponent.razor | 8 ++++----
4 files changed, 16 insertions(+), 24 deletions(-)
diff --git a/src/Components/Web/src/Forms/ValidationSummary.cs b/src/Components/Web/src/Forms/ValidationSummary.cs
index 04b080657413..565252ca85fc 100644
--- a/src/Components/Web/src/Forms/ValidationSummary.cs
+++ b/src/Components/Web/src/Forms/ValidationSummary.cs
@@ -23,12 +23,6 @@ public class ValidationSummary : ComponentBase, IDisposable
///
[Parameter] public object? Model { get; set; }
- ///
- /// Gets or sets an optional CSS class string appended to the validation-errors
- /// CSS class on the rendered ul element.
- ///
- [Parameter] public string? CssClass { get; set; }
-
///
/// Gets or sets a collection of additional attributes that will be applied to the created ul element.
///
@@ -78,12 +72,10 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
{
first = false;
- var combinedClass = AttributeUtilities.CombineClassNames(AdditionalAttributes,
- string.IsNullOrEmpty(CssClass) ? "validation-errors" : $"validation-errors {CssClass}");
-
builder.OpenElement(0, "ul");
builder.AddMultipleAttributes(1, AdditionalAttributes);
- builder.AddAttribute(2, "class", combinedClass);
+ builder.AddAttributeIfNotNullOrEmpty(2, "class",
+ AttributeUtilities.CombineClassNames(AdditionalAttributes, "validation-errors"));
}
builder.OpenElement(3, "li");
diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt
index 5d0d6ba2e9a2..1f1a71e0b1da 100644
--- a/src/Components/Web/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Web/src/PublicAPI.Unshipped.txt
@@ -106,5 +106,3 @@ Microsoft.AspNetCore.Components.Forms.Label.ChildContent.set -> void
Microsoft.AspNetCore.Components.Forms.Label.For.get -> System.Linq.Expressions.Expression!>?
Microsoft.AspNetCore.Components.Forms.Label.For.set -> void
Microsoft.AspNetCore.Components.Forms.Label.Label() -> void
-Microsoft.AspNetCore.Components.Forms.ValidationSummary.CssClass.get -> string?
-Microsoft.AspNetCore.Components.Forms.ValidationSummary.CssClass.set -> void
diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs
index 11083a7f0da0..6b4f0be88b97 100644
--- a/src/Components/test/E2ETest/Tests/FormsTest.cs
+++ b/src/Components/test/E2ETest/Tests/FormsTest.cs
@@ -1098,7 +1098,7 @@ private bool ElementHasAttribute(IWebElement webElement, string attribute)
}
[Fact]
- public void ValidationSummaryAppliesCssClass()
+ public void ValidationSummaryCombinesAdditionalAttributesClassWithDefault()
{
var appElement = Browser.MountTestComponent();
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
@@ -1109,17 +1109,19 @@ public void ValidationSummaryAppliesCssClass()
submitButton.Click();
// Default ValidationSummary renders with only "validation-errors" class.
- // FindElement is inside the lambda so Browser.Equal retries until the ul is rendered.
Browser.Equal("validation-errors",
() => appElement.FindElement(By.ClassName("all-errors")).FindElement(By.TagName("ul")).GetDomAttribute("class"));
- // ValidationSummary with CssClass appends the custom class to "validation-errors"
- Browser.Equal("validation-errors custom-summary-css-class",
- () => appElement.FindElement(By.ClassName("all-errors-custom-class")).FindElement(By.TagName("ul")).GetDomAttribute("class"));
+ // ValidationSummary with class="pt-2" via AdditionalAttributes combines both:
+ // splatted class + fixed "validation-errors"
+ Browser.Equal("pt-2 validation-errors",
+ () => appElement.FindElement(By.ClassName("all-errors-with-class")).FindElement(By.TagName("ul")).GetDomAttribute("class"));
- // Both AdditionalAttributes["class"] and CssClass are combined: splatted class + validation-errors + CssClass
- Browser.Equal("extra validation-errors custom",
- () => appElement.FindElement(By.ClassName("all-errors-combined")).FindElement(By.TagName("ul")).GetDomAttribute("class"));
+ // Additional non-class attributes (e.g. data-testid) are also splatted
+ Browser.Equal("validation-errors",
+ () => appElement.FindElement(By.ClassName("all-errors-with-attr")).FindElement(By.TagName("ul")).GetDomAttribute("class"));
+ Browser.Equal("my-summary",
+ () => appElement.FindElement(By.ClassName("all-errors-with-attr")).FindElement(By.TagName("ul")).GetDomAttribute("data-testid"));
}
}
diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
index 5834995bc445..c668f4e6f61a 100644
--- a/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/FormsTest/ValidationSummaryWithCssClassComponent.razor
@@ -13,11 +13,11 @@