From b034fa34029ffae1eb84813426777570e18b6066 Mon Sep 17 00:00:00 2001 From: James Gunn Date: Sun, 1 Feb 2026 13:03:13 +0000 Subject: [PATCH 1/9] Add tag helper model binding integration tests for text input --- .../FormGroupErrorMessageTagHelperBase.cs | 2 +- ...rontend.AspNetCore.IntegrationTests.csproj | 1 + .../TagHelperModelBindingTestsFixture.cs | 23 ++++++ .../TagHelperModelBindingTests/Tests.cs | 76 +++++++++++++++++++ .../TextInput.cshtml | 3 + .../TextInputOverridden.cshtml | 7 ++ .../_ViewImports.cshtml | 3 + .../_ViewStart.cshtml | 3 + 8 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TagHelperModelBindingTestsFixture.cs create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextInput.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextInputOverridden.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/_ViewImports.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/_ViewStart.cshtml diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/FormGroupErrorMessageTagHelperBase.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/FormGroupErrorMessageTagHelperBase.cs index c2d5e8fde..a9f8a5dfe 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/FormGroupErrorMessageTagHelperBase.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/FormGroupErrorMessageTagHelperBase.cs @@ -52,7 +52,7 @@ private protected virtual void SetErrorMessage(TagHelperContent? content, TagHel var formGroupContext3 = context.GetContextItem(); formGroupContext3.SetErrorMessage( - VisuallyHiddenText, + VisuallyHiddenText is not null ? new TemplateString(VisuallyHiddenText) : null, new AttributeCollection(output.Attributes), content?.ToTemplateString(), output.TagName); diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/GovUk.Frontend.AspNetCore.IntegrationTests.csproj b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/GovUk.Frontend.AspNetCore.IntegrationTests.csproj index 1b08e71ef..1b3b91395 100644 --- a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/GovUk.Frontend.AspNetCore.IntegrationTests.csproj +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/GovUk.Frontend.AspNetCore.IntegrationTests.csproj @@ -12,6 +12,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TagHelperModelBindingTestsFixture.cs b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TagHelperModelBindingTestsFixture.cs new file mode 100644 index 000000000..fe63c81ba --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TagHelperModelBindingTestsFixture.cs @@ -0,0 +1,23 @@ +namespace GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests; + +public class TagHelperModelBindingTestsFixture : ServerFixture +{ + protected override void Configure(IApplicationBuilder app) + { + base.Configure(app); + + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } + + protected override void ConfigureServices(IServiceCollection services) + { + services.AddGovUkFrontend(); + + services + .AddMvc() + .AddRazorOptions(options => + { + options.ViewLocationFormats.Add("/TagHelperModelBindingTests/{0}.cshtml"); + }); + } +} diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs new file mode 100644 index 000000000..ebe4065d8 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs @@ -0,0 +1,76 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; + +namespace GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests; + +public class Tests(TagHelperModelBindingTestsFixture fixture) : IClassFixture +{ + [Fact] + public async Task TextInput_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/TextInput"); + + var input = page.Locator("input").First; + Assert.Equal("Text", await input.GetAttributeAsync("name")); + Assert.Equal("Text", await input.GetAttributeAsync("id")); + Assert.Equal("Model value", await input.GetAttributeAsync("value")); + + var label = page.Locator("label").First; + Assert.Equal("Text", await label.GetAttributeAsync("for")); + Assert.Equal("ModelMetadata display name", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("ModelMetadata description", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Model error message", await errorMessage.TextContentAsync()); + } + + [Fact] + public async Task TextInputOverridden_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/TextInputOverridden"); + await page.PauseAsync(); + + var input = page.Locator("input").First; + Assert.Equal("OverriddenName", await input.GetAttributeAsync("name")); + Assert.Equal("OverriddenId", await input.GetAttributeAsync("id")); + Assert.Equal("Overridden value", await input.GetAttributeAsync("value")); + + var label = page.Locator("label").First; + Assert.Equal("OverriddenId", await label.GetAttributeAsync("for")); + Assert.Equal("Overridden label", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("Overridden hint", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); + } +} + +[Route("/[controller]/[action]")] +public class ModelBindingTestsController : Controller +{ + [HttpGet] + public IActionResult TextInput() + { + ModelState.AddModelError(nameof(TextInputTestsModel.Text), "Model error message"); + return View(new TextInputTestsModel { Text = "Model value" }); + } + + [HttpGet] + public IActionResult TextInputOverridden() + { + ModelState.AddModelError(nameof(TextInputTestsModel.Text), "Model error message"); + return View(new TextInputTestsModel { Text = "Model value" }); + } +} + +public record TextInputTestsModel +{ + [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] + public string? Text { get; set; } +} diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextInput.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextInput.cshtml new file mode 100644 index 000000000..ab3f59137 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextInput.cshtml @@ -0,0 +1,3 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.TextInputTestsModel + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextInputOverridden.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextInputOverridden.cshtml new file mode 100644 index 000000000..badff6ae3 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextInputOverridden.cshtml @@ -0,0 +1,7 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.TextInputTestsModel + + + Overridden label + Overridden hint + Overridden error message + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/_ViewImports.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/_ViewImports.cshtml new file mode 100644 index 000000000..8ced5bf7a --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@addTagHelper *, GovUk.Frontend.AspNetCore +@addTagHelper *, GovUk.Frontend.AspNetCore.IntegrationTests +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/_ViewStart.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/_ViewStart.cshtml new file mode 100644 index 000000000..36592afb9 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_GovUkPageTemplate"; +} From ad297d2c6c4e41a5b5eab88975103a6d84e50a79 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:54:15 +0000 Subject: [PATCH 2/9] Add password input tests to TagHelperModelBindingTests (#440) * Initial plan * Add password input tests to TagHelperModelBindingTests Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> * Remove PauseAsync debug statement Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --- .../PasswordInput.cshtml | 3 + .../PasswordInputOverridden.cshtml | 7 ++ .../TagHelperModelBindingTests/Tests.cs | 64 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/PasswordInput.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/PasswordInputOverridden.cshtml diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/PasswordInput.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/PasswordInput.cshtml new file mode 100644 index 000000000..f0551632e --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/PasswordInput.cshtml @@ -0,0 +1,3 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.PasswordInputTestsModel + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/PasswordInputOverridden.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/PasswordInputOverridden.cshtml new file mode 100644 index 000000000..97deb8b7c --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/PasswordInputOverridden.cshtml @@ -0,0 +1,7 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.PasswordInputTestsModel + + + Overridden label + Overridden hint + Overridden error message + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs index ebe4065d8..97042f85b 100644 --- a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs @@ -49,6 +49,50 @@ public async Task TextInputOverridden_RendersCorrectly() var errorMessage = page.Locator(".govuk-error-message").First; Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); } + + [Fact] + public async Task PasswordInput_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/PasswordInput"); + + var input = page.Locator("input[type='password']").First; + Assert.Equal("Password", await input.GetAttributeAsync("name")); + Assert.Equal("Password", await input.GetAttributeAsync("id")); + Assert.Equal("Model value", await input.GetAttributeAsync("value")); + + var label = page.Locator("label").First; + Assert.Equal("Password", await label.GetAttributeAsync("for")); + Assert.Equal("ModelMetadata display name", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("ModelMetadata description", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Model error message", await errorMessage.TextContentAsync()); + } + + [Fact] + public async Task PasswordInputOverridden_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/PasswordInputOverridden"); + + var input = page.Locator("input[type='password']").First; + Assert.Equal("OverriddenName", await input.GetAttributeAsync("name")); + Assert.Equal("OverriddenId", await input.GetAttributeAsync("id")); + Assert.Equal("Overridden value", await input.GetAttributeAsync("value")); + + var label = page.Locator("label").First; + Assert.Equal("OverriddenId", await label.GetAttributeAsync("for")); + Assert.Equal("Overridden label", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("Overridden hint", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); + } } [Route("/[controller]/[action]")] @@ -67,6 +111,20 @@ public IActionResult TextInputOverridden() ModelState.AddModelError(nameof(TextInputTestsModel.Text), "Model error message"); return View(new TextInputTestsModel { Text = "Model value" }); } + + [HttpGet] + public IActionResult PasswordInput() + { + ModelState.AddModelError(nameof(PasswordInputTestsModel.Password), "Model error message"); + return View(new PasswordInputTestsModel { Password = "Model value" }); + } + + [HttpGet] + public IActionResult PasswordInputOverridden() + { + ModelState.AddModelError(nameof(PasswordInputTestsModel.Password), "Model error message"); + return View(new PasswordInputTestsModel { Password = "Model value" }); + } } public record TextInputTestsModel @@ -74,3 +132,9 @@ public record TextInputTestsModel [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] public string? Text { get; set; } } + +public record PasswordInputTestsModel +{ + [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] + public string? Password { get; set; } +} From c8dafbca7faae2dca41522332389109025197d29 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:30:07 +0000 Subject: [PATCH 3/9] Add date input tests to TagHelperModelBindingTests (#441) * Initial plan * Add date input tests to TagHelperModelBindingTests Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> * Add separate test for overriden item names and IDs --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> Co-authored-by: James Gunn --- .../TagHelpers/DateInputContext.cs | 4 +- .../DateInputErrorMessageTagHelper.cs | 2 +- .../DateInput.cshtml | 7 ++ .../DateInputOverridden.cshtml | 9 ++ .../DateInputOverriddenItems.cshtml | 7 ++ .../TagHelperModelBindingTests/Tests.cs | 109 +++++++++++++++++- 6 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInput.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInputOverridden.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInputOverriddenItems.cshtml diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputContext.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputContext.cs index d51548d03..cbdb0abb2 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputContext.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputContext.cs @@ -128,7 +128,7 @@ public override void SetLabel(bool? isPageHeading, AttributeCollection attribute public override void SetHint(AttributeCollection attributes, TemplateString? html, string tagName) { - if (Fieldset is not null) + if (Fieldset is not null && !_fieldsetIsOpen) { throw new InvalidOperationException($"<{tagName}> must be inside <{FieldsetTagName}>."); } @@ -159,7 +159,7 @@ public void SetErrorMessage( TemplateString? html, string tagName) { - if (Fieldset is not null) + if (Fieldset is not null && !_fieldsetIsOpen) { throw new InvalidOperationException($"<{tagName}> must be inside <{FieldsetTagName}>."); } diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputErrorMessageTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputErrorMessageTagHelper.cs index 407598f95..13b17eacc 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputErrorMessageTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/DateInputErrorMessageTagHelper.cs @@ -46,7 +46,7 @@ private protected override void SetErrorMessage(TagHelperContent? content, TagHe dateInputContext.SetErrorMessage( ErrorItems, - VisuallyHiddenText, + VisuallyHiddenText is not null ? new TemplateString(VisuallyHiddenText) : null, attributes, content?.ToTemplateString(), output.TagName); diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInput.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInput.cshtml new file mode 100644 index 000000000..58044507e --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInput.cshtml @@ -0,0 +1,7 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.DateInputTestsModel + + + + + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInputOverridden.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInputOverridden.cshtml new file mode 100644 index 000000000..6542b4946 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInputOverridden.cshtml @@ -0,0 +1,9 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.DateInputTestsModel + + + + Overridden legend + Overridden hint + Overridden error message + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInputOverriddenItems.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInputOverriddenItems.cshtml new file mode 100644 index 000000000..338f2fded --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/DateInputOverriddenItems.cshtml @@ -0,0 +1,7 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.DateInputTestsModel + + + + + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs index 97042f85b..a63a91d7f 100644 --- a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs @@ -32,7 +32,6 @@ public async Task TextInputOverridden_RendersCorrectly() { var page = await fixture.Browser!.NewPageAsync(); await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/TextInputOverridden"); - await page.PauseAsync(); var input = page.Locator("input").First; Assert.Equal("OverriddenName", await input.GetAttributeAsync("name")); @@ -93,6 +92,87 @@ public async Task PasswordInputOverridden_RendersCorrectly() var errorMessage = page.Locator(".govuk-error-message").First; Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); } + + [Fact] + public async Task DateInput_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/DateInput"); + + var dayInput = page.Locator("input[name='Date.Day']").First; + Assert.Equal("Date.Day", await dayInput.GetAttributeAsync("name")); + Assert.Equal("Date.Day", await dayInput.GetAttributeAsync("id")); + Assert.Equal("15", await dayInput.GetAttributeAsync("value")); + + var monthInput = page.Locator("input[name='Date.Month']").First; + Assert.Equal("Date.Month", await monthInput.GetAttributeAsync("name")); + Assert.Equal("Date.Month", await monthInput.GetAttributeAsync("id")); + Assert.Equal("3", await monthInput.GetAttributeAsync("value")); + + var yearInput = page.Locator("input[name='Date.Year']").First; + Assert.Equal("Date.Year", await yearInput.GetAttributeAsync("name")); + Assert.Equal("Date.Year", await yearInput.GetAttributeAsync("id")); + Assert.Equal("2024", await yearInput.GetAttributeAsync("value")); + + var legend = page.Locator(".govuk-fieldset__legend").First; + Assert.Equal("ModelMetadata display name", await legend.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("ModelMetadata description", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Model error message", await errorMessage.TextContentAsync()); + } + + [Fact] + public async Task DateInputOverridden_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/DateInputOverridden"); + + var dayInput = page.Locator("input[name='OverriddenName.Day']").First; + Assert.Equal("OverriddenName.Day", await dayInput.GetAttributeAsync("name")); + Assert.Equal("OverriddenId.Day", await dayInput.GetAttributeAsync("id")); + Assert.Equal("15", await dayInput.GetAttributeAsync("value")); + + var monthInput = page.Locator("input[name='OverriddenName.Month']").First; + Assert.Equal("OverriddenName.Month", await monthInput.GetAttributeAsync("name")); + Assert.Equal("OverriddenId.Month", await monthInput.GetAttributeAsync("id")); + Assert.Equal("3", await monthInput.GetAttributeAsync("value")); + + var yearInput = page.Locator("input[name='OverriddenName.Year']").First; + Assert.Equal("OverriddenName.Year", await yearInput.GetAttributeAsync("name")); + Assert.Equal("OverriddenId.Year", await yearInput.GetAttributeAsync("id")); + Assert.Equal("2024", await yearInput.GetAttributeAsync("value")); + + var legend = page.Locator(".govuk-fieldset__legend").First; + Assert.Equal("Overridden legend", await legend.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("Overridden hint", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); + } + + [Fact] + public async Task DateInputOverriddenItems_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/DateInputOverriddenItems"); + + var dayInput = page.Locator("input[name='OverriddenDayName']").First; + Assert.Equal("OverriddenDayId", await dayInput.GetAttributeAsync("id")); + Assert.Equal("1", await dayInput.GetAttributeAsync("value")); + + var monthInput = page.Locator("input[name='OverriddenMonthName']").First; + Assert.Equal("OverriddenMonthId", await monthInput.GetAttributeAsync("id")); + Assert.Equal("2", await monthInput.GetAttributeAsync("value")); + + var yearInput = page.Locator("input[name='OverriddenYearName']").First; + Assert.Equal("OverriddenYearId", await yearInput.GetAttributeAsync("id")); + Assert.Equal("2020", await yearInput.GetAttributeAsync("value")); + } } [Route("/[controller]/[action]")] @@ -125,6 +205,27 @@ public IActionResult PasswordInputOverridden() ModelState.AddModelError(nameof(PasswordInputTestsModel.Password), "Model error message"); return View(new PasswordInputTestsModel { Password = "Model value" }); } + + [HttpGet] + public IActionResult DateInput() + { + ModelState.AddModelError(nameof(DateInputTestsModel.Date), "Model error message"); + return View(new DateInputTestsModel { Date = new DateOnly(2024, 3, 15) }); + } + + [HttpGet] + public IActionResult DateInputOverridden() + { + ModelState.AddModelError(nameof(DateInputTestsModel.Date), "Model error message"); + return View(new DateInputTestsModel { Date = new DateOnly(2024, 3, 15) }); + } + + [HttpGet] + public IActionResult DateInputOverriddenItems() + { + ModelState.AddModelError(nameof(DateInputTestsModel.Date), "Model error message"); + return View(new DateInputTestsModel { Date = new DateOnly(2024, 3, 15) }); + } } public record TextInputTestsModel @@ -138,3 +239,9 @@ public record PasswordInputTestsModel [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] public string? Password { get; set; } } + +public record DateInputTestsModel +{ + [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] + public DateOnly? Date { get; set; } +} From 2fdc3c6b5d3605983c6740aca4c40cc80496317e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:42:54 +0000 Subject: [PATCH 4/9] Add textarea model binding integration tests (#442) * Initial plan * Add textarea tests to TagHelperModelBindingTests Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --- .../TagHelperModelBindingTests/Tests.cs | 64 +++++++++++++++++++ .../Textarea.cshtml | 3 + .../TextareaOverridden.cshtml | 8 +++ 3 files changed, 75 insertions(+) create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Textarea.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextareaOverridden.cshtml diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs index a63a91d7f..c5b599ec4 100644 --- a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs @@ -173,6 +173,50 @@ public async Task DateInputOverriddenItems_RendersCorrectly() Assert.Equal("OverriddenYearId", await yearInput.GetAttributeAsync("id")); Assert.Equal("2020", await yearInput.GetAttributeAsync("value")); } + + [Fact] + public async Task Textarea_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/Textarea"); + + var textarea = page.Locator("textarea").First; + Assert.Equal("Text", await textarea.GetAttributeAsync("name")); + Assert.Equal("Text", await textarea.GetAttributeAsync("id")); + Assert.Equal("Model value", await textarea.InputValueAsync()); + + var label = page.Locator("label").First; + Assert.Equal("Text", await label.GetAttributeAsync("for")); + Assert.Equal("ModelMetadata display name", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("ModelMetadata description", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Model error message", await errorMessage.TextContentAsync()); + } + + [Fact] + public async Task TextareaOverridden_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/TextareaOverridden"); + + var textarea = page.Locator("textarea").First; + Assert.Equal("OverriddenName", await textarea.GetAttributeAsync("name")); + Assert.Equal("OverriddenId", await textarea.GetAttributeAsync("id")); + Assert.Equal("Overridden value", await textarea.InputValueAsync()); + + var label = page.Locator("label").First; + Assert.Equal("OverriddenId", await label.GetAttributeAsync("for")); + Assert.Equal("Overridden label", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("Overridden hint", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); + } } [Route("/[controller]/[action]")] @@ -226,6 +270,20 @@ public IActionResult DateInputOverriddenItems() ModelState.AddModelError(nameof(DateInputTestsModel.Date), "Model error message"); return View(new DateInputTestsModel { Date = new DateOnly(2024, 3, 15) }); } + + [HttpGet] + public IActionResult Textarea() + { + ModelState.AddModelError(nameof(TextareaTestsModel.Text), "Model error message"); + return View(new TextareaTestsModel { Text = "Model value" }); + } + + [HttpGet] + public IActionResult TextareaOverridden() + { + ModelState.AddModelError(nameof(TextareaTestsModel.Text), "Model error message"); + return View(new TextareaTestsModel { Text = "Model value" }); + } } public record TextInputTestsModel @@ -245,3 +303,9 @@ public record DateInputTestsModel [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] public DateOnly? Date { get; set; } } + +public record TextareaTestsModel +{ + [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] + public string? Text { get; set; } +} diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Textarea.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Textarea.cshtml new file mode 100644 index 000000000..be31000e9 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Textarea.cshtml @@ -0,0 +1,3 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.TextareaTestsModel + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextareaOverridden.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextareaOverridden.cshtml new file mode 100644 index 000000000..b7c3498da --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/TextareaOverridden.cshtml @@ -0,0 +1,8 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.TextareaTestsModel + + + Overridden label + Overridden hint + Overridden error message + Overridden value + From be11f9f557fdec0b4285b1b4e5382c1b6e2541f3 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:52:35 +0000 Subject: [PATCH 5/9] Add character count model binding integration tests (#443) * Initial plan * Add character count model binding integration tests Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --- .../CharacterCount.cshtml | 3 + .../CharacterCountOverridden.cshtml | 8 +++ .../TagHelperModelBindingTests/Tests.cs | 64 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CharacterCount.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CharacterCountOverridden.cshtml diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CharacterCount.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CharacterCount.cshtml new file mode 100644 index 000000000..cf0075f4c --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CharacterCount.cshtml @@ -0,0 +1,3 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.CharacterCountTestsModel + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CharacterCountOverridden.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CharacterCountOverridden.cshtml new file mode 100644 index 000000000..fa06a4cff --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CharacterCountOverridden.cshtml @@ -0,0 +1,8 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.CharacterCountTestsModel + + + Overridden label + Overridden hint + Overridden error message + Overridden value + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs index c5b599ec4..931db491a 100644 --- a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs @@ -217,6 +217,50 @@ public async Task TextareaOverridden_RendersCorrectly() var errorMessage = page.Locator(".govuk-error-message").First; Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); } + + [Fact] + public async Task CharacterCount_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/CharacterCount"); + + var textarea = page.Locator("textarea").First; + Assert.Equal("Text", await textarea.GetAttributeAsync("name")); + Assert.Equal("Text", await textarea.GetAttributeAsync("id")); + Assert.Equal("Model value", await textarea.InputValueAsync()); + + var label = page.Locator("label").First; + Assert.Equal("Text", await label.GetAttributeAsync("for")); + Assert.Equal("ModelMetadata display name", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("ModelMetadata description", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Model error message", await errorMessage.TextContentAsync()); + } + + [Fact] + public async Task CharacterCountOverridden_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/CharacterCountOverridden"); + + var textarea = page.Locator("textarea").First; + Assert.Equal("OverriddenName", await textarea.GetAttributeAsync("name")); + Assert.Equal("OverriddenId", await textarea.GetAttributeAsync("id")); + Assert.Equal("Overridden value", await textarea.InputValueAsync()); + + var label = page.Locator("label").First; + Assert.Equal("OverriddenId", await label.GetAttributeAsync("for")); + Assert.Equal("Overridden label", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("Overridden hint", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); + } } [Route("/[controller]/[action]")] @@ -284,6 +328,20 @@ public IActionResult TextareaOverridden() ModelState.AddModelError(nameof(TextareaTestsModel.Text), "Model error message"); return View(new TextareaTestsModel { Text = "Model value" }); } + + [HttpGet] + public IActionResult CharacterCount() + { + ModelState.AddModelError(nameof(CharacterCountTestsModel.Text), "Model error message"); + return View(new CharacterCountTestsModel { Text = "Model value" }); + } + + [HttpGet] + public IActionResult CharacterCountOverridden() + { + ModelState.AddModelError(nameof(CharacterCountTestsModel.Text), "Model error message"); + return View(new CharacterCountTestsModel { Text = "Model value" }); + } } public record TextInputTestsModel @@ -309,3 +367,9 @@ public record TextareaTestsModel [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] public string? Text { get; set; } } + +public record CharacterCountTestsModel +{ + [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] + public string? Text { get; set; } +} From 923ed6ff39ca1bb4f2f114464e92b0ca24ec4fc7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:01:50 +0000 Subject: [PATCH 6/9] Add file upload model binding integration tests (#444) * Initial plan * Add file upload model binding integration tests Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --- .../FileUpload.cshtml | 3 + .../FileUploadOverridden.cshtml | 7 +++ .../TagHelperModelBindingTests/Tests.cs | 62 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/FileUpload.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/FileUploadOverridden.cshtml diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/FileUpload.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/FileUpload.cshtml new file mode 100644 index 000000000..eb8a59094 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/FileUpload.cshtml @@ -0,0 +1,3 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.FileUploadTestsModel + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/FileUploadOverridden.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/FileUploadOverridden.cshtml new file mode 100644 index 000000000..427fa3c75 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/FileUploadOverridden.cshtml @@ -0,0 +1,7 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.FileUploadTestsModel + + + Overridden label + Overridden hint + Overridden error message + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs index 931db491a..585234372 100644 --- a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs @@ -261,6 +261,48 @@ public async Task CharacterCountOverridden_RendersCorrectly() var errorMessage = page.Locator(".govuk-error-message").First; Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); } + + [Fact] + public async Task FileUpload_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/FileUpload"); + + var input = page.Locator("input[type='file']").First; + Assert.Equal("File", await input.GetAttributeAsync("name")); + Assert.Equal("File", await input.GetAttributeAsync("id")); + + var label = page.Locator("label").First; + Assert.Equal("File", await label.GetAttributeAsync("for")); + Assert.Equal("ModelMetadata display name", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("ModelMetadata description", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Model error message", await errorMessage.TextContentAsync()); + } + + [Fact] + public async Task FileUploadOverridden_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/FileUploadOverridden"); + + var input = page.Locator("input[type='file']").First; + Assert.Equal("OverriddenName", await input.GetAttributeAsync("name")); + Assert.Equal("OverriddenId", await input.GetAttributeAsync("id")); + + var label = page.Locator("label").First; + Assert.Equal("OverriddenId", await label.GetAttributeAsync("for")); + Assert.Equal("Overridden label", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("Overridden hint", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); + } } [Route("/[controller]/[action]")] @@ -342,6 +384,20 @@ public IActionResult CharacterCountOverridden() ModelState.AddModelError(nameof(CharacterCountTestsModel.Text), "Model error message"); return View(new CharacterCountTestsModel { Text = "Model value" }); } + + [HttpGet] + public IActionResult FileUpload() + { + ModelState.AddModelError(nameof(FileUploadTestsModel.File), "Model error message"); + return View(new FileUploadTestsModel()); + } + + [HttpGet] + public IActionResult FileUploadOverridden() + { + ModelState.AddModelError(nameof(FileUploadTestsModel.File), "Model error message"); + return View(new FileUploadTestsModel()); + } } public record TextInputTestsModel @@ -373,3 +429,9 @@ public record CharacterCountTestsModel [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] public string? Text { get; set; } } + +public record FileUploadTestsModel +{ + [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] + public Microsoft.AspNetCore.Http.IFormFile? File { get; set; } +} From 398dbba5bc766fb4f1f1fba94788932bf27f1fc4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:13:27 +0000 Subject: [PATCH 7/9] Add select tag helper model binding integration tests (#445) * Initial plan * Add select model binding integration tests Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --- .../TagHelperModelBindingTests/Select.cshtml | 7 ++ .../SelectOverridden.cshtml | 10 +++ .../TagHelperModelBindingTests/Tests.cs | 64 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Select.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/SelectOverridden.cshtml diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Select.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Select.cshtml new file mode 100644 index 000000000..76755ca32 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Select.cshtml @@ -0,0 +1,7 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.SelectTestsModel + + + Choose an option + Option 1 + Option 2 + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/SelectOverridden.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/SelectOverridden.cshtml new file mode 100644 index 000000000..1b8b7dd3f --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/SelectOverridden.cshtml @@ -0,0 +1,10 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.SelectTestsModel + + + Overridden label + Overridden hint + Overridden error message + Choose an option + Option 1 + Overridden option + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs index 585234372..dfe95e92d 100644 --- a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs @@ -303,6 +303,50 @@ public async Task FileUploadOverridden_RendersCorrectly() var errorMessage = page.Locator(".govuk-error-message").First; Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); } + + [Fact] + public async Task Select_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/Select"); + + var select = page.Locator("select").First; + Assert.Equal("Option", await select.GetAttributeAsync("name")); + Assert.Equal("Option", await select.GetAttributeAsync("id")); + Assert.Equal("option2", await select.InputValueAsync()); + + var label = page.Locator("label").First; + Assert.Equal("Option", await label.GetAttributeAsync("for")); + Assert.Equal("ModelMetadata display name", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("ModelMetadata description", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Model error message", await errorMessage.TextContentAsync()); + } + + [Fact] + public async Task SelectOverridden_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/SelectOverridden"); + + var select = page.Locator("select").First; + Assert.Equal("OverriddenName", await select.GetAttributeAsync("name")); + Assert.Equal("OverriddenId", await select.GetAttributeAsync("id")); + Assert.Equal("overridden-value", await select.InputValueAsync()); + + var label = page.Locator("label").First; + Assert.Equal("OverriddenId", await label.GetAttributeAsync("for")); + Assert.Equal("Overridden label", await label.InnerTextAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("Overridden hint", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); + } } [Route("/[controller]/[action]")] @@ -398,6 +442,20 @@ public IActionResult FileUploadOverridden() ModelState.AddModelError(nameof(FileUploadTestsModel.File), "Model error message"); return View(new FileUploadTestsModel()); } + + [HttpGet] + public IActionResult Select() + { + ModelState.AddModelError(nameof(SelectTestsModel.Option), "Model error message"); + return View(new SelectTestsModel { Option = "option2" }); + } + + [HttpGet] + public IActionResult SelectOverridden() + { + ModelState.AddModelError(nameof(SelectTestsModel.Option), "Model error message"); + return View(new SelectTestsModel { Option = "option2" }); + } } public record TextInputTestsModel @@ -435,3 +493,9 @@ public record FileUploadTestsModel [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] public Microsoft.AspNetCore.Http.IFormFile? File { get; set; } } + +public record SelectTestsModel +{ + [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] + public string? Option { get; set; } +} From e98d07d4452714f280d14c2af744c18a9a59d89f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 09:30:45 +0000 Subject: [PATCH 8/9] Add checkbox model binding integration tests (#446) * Initial plan * Add checkbox model binding integration test Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> * Add test for overridden attributes --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> Co-authored-by: James Gunn --- .../TagHelpers/CheckboxesContext.cs | 2 +- .../CheckboxesErrorMessageTagHelper.cs | 2 +- .../TagHelpers/CheckboxesTagHelper.cs | 4 +- .../TagHelpers/RadiosContext.cs | 2 +- .../Checkboxes.cshtml | 9 ++ .../CheckboxesOverridden.cshtml | 13 +++ .../TagHelperModelBindingTests/Tests.cs | 85 ++++++++++++++++++- 7 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Checkboxes.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CheckboxesOverridden.cshtml diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesContext.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesContext.cs index ef62a6edc..2b9f5f14c 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesContext.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesContext.cs @@ -149,7 +149,7 @@ public override void SetErrorMessage( TemplateString? html, string tagName) { - if (Fieldset is not null) + if (Fieldset is not null && !_fieldsetIsOpen) { throw new InvalidOperationException($"<{tagName}> must be inside <{FieldsetTagName}>."); } diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesErrorMessageTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesErrorMessageTagHelper.cs index fc033290e..a58751ac8 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesErrorMessageTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesErrorMessageTagHelper.cs @@ -33,7 +33,7 @@ private protected override void SetErrorMessage(TagHelperContent? content, TagHe var attributes = new AttributeCollection(output.Attributes); checkboxesContext.SetErrorMessage( - VisuallyHiddenText, + VisuallyHiddenText is not null ? new TemplateString(VisuallyHiddenText) : null, attributes, content?.ToTemplateString(), output.TagName); diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesTagHelper.cs index bb2cc24ee..4b63d392e 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesTagHelper.cs @@ -107,7 +107,9 @@ internal CheckboxesTagHelper(IComponentGenerator componentGenerator, IModelHelpe /// public override void Init(TagHelperContext context) { - context.SetContextItem(new CheckboxesContext(Name, For)); + var checkboxesContext = new CheckboxesContext(Name, For); + context.SetContextItem(checkboxesContext); + context.SetContextItem(checkboxesContext); } /// diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/RadiosContext.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/RadiosContext.cs index 99d6e38db..0d9371b6a 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/RadiosContext.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/RadiosContext.cs @@ -149,7 +149,7 @@ public override void SetErrorMessage( TemplateString? html, string tagName) { - if (Fieldset is not null) + if (Fieldset is not null && !_fieldsetIsOpen) { throw new InvalidOperationException($"<{tagName}> must be inside <{FieldsetTagName}>."); } diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Checkboxes.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Checkboxes.cshtml new file mode 100644 index 000000000..897150530 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Checkboxes.cshtml @@ -0,0 +1,9 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.CheckboxesTestsModel + + + + + Option 1 + Option 2 + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CheckboxesOverridden.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CheckboxesOverridden.cshtml new file mode 100644 index 000000000..5bcd163c1 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/CheckboxesOverridden.cshtml @@ -0,0 +1,13 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.CheckboxesTestsModel + + + + + Overridden legend + + Overridden hint + Overridden error message + Option 1 + Option 2 + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs index dfe95e92d..f6ee238c4 100644 --- a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs @@ -347,6 +347,52 @@ public async Task SelectOverridden_RendersCorrectly() var errorMessage = page.Locator(".govuk-error-message").First; Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); } + + [Fact] + public async Task Checkboxes_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/Checkboxes"); + + var checkbox1 = page.Locator("input[value='option1']").First; + Assert.Equal("Options", await checkbox1.GetAttributeAsync("name")); + Assert.Equal("Options", await checkbox1.GetAttributeAsync("id")); + Assert.False(await checkbox1.IsCheckedAsync()); + + var checkbox2 = page.Locator("input[value='option2']").First; + Assert.Equal("Options", await checkbox2.GetAttributeAsync("name")); + Assert.Equal("Options-2", await checkbox2.GetAttributeAsync("id")); + Assert.True(await checkbox2.IsCheckedAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("ModelMetadata description", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Model error message", await errorMessage.TextContentAsync()); + } + + [Fact] + public async Task CheckboxesOverridden_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/CheckboxesOverridden"); + + var checkbox1 = page.Locator("input[value='option1']").First; + Assert.Equal("OverriddenName", await checkbox1.GetAttributeAsync("name")); + Assert.Equal("OverriddenIdPrefix", await checkbox1.GetAttributeAsync("id")); + Assert.True(await checkbox1.IsCheckedAsync()); + + var checkbox2 = page.Locator("input[value='option2']").First; + Assert.Equal("OverriddenName", await checkbox2.GetAttributeAsync("name")); + Assert.Equal("OverriddenIdPrefix-2", await checkbox2.GetAttributeAsync("id")); + Assert.False(await checkbox2.IsCheckedAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("Overridden hint", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); + } } [Route("/[controller]/[action]")] @@ -355,6 +401,7 @@ public class ModelBindingTestsController : Controller [HttpGet] public IActionResult TextInput() { + ModelState.SetModelValue(nameof(TextInputTestsModel.Text), "Model value", "Model value"); ModelState.AddModelError(nameof(TextInputTestsModel.Text), "Model error message"); return View(new TextInputTestsModel { Text = "Model value" }); } @@ -362,6 +409,7 @@ public IActionResult TextInput() [HttpGet] public IActionResult TextInputOverridden() { + ModelState.SetModelValue(nameof(TextInputTestsModel.Text), "Model value", "Model value"); ModelState.AddModelError(nameof(TextInputTestsModel.Text), "Model error message"); return View(new TextInputTestsModel { Text = "Model value" }); } @@ -369,6 +417,7 @@ public IActionResult TextInputOverridden() [HttpGet] public IActionResult PasswordInput() { + ModelState.SetModelValue(nameof(PasswordInputTestsModel.Password), "Model value", "Model value"); ModelState.AddModelError(nameof(PasswordInputTestsModel.Password), "Model error message"); return View(new PasswordInputTestsModel { Password = "Model value" }); } @@ -376,6 +425,7 @@ public IActionResult PasswordInput() [HttpGet] public IActionResult PasswordInputOverridden() { + ModelState.SetModelValue(nameof(PasswordInputTestsModel.Password), "Model value", "Model value"); ModelState.AddModelError(nameof(PasswordInputTestsModel.Password), "Model error message"); return View(new PasswordInputTestsModel { Password = "Model value" }); } @@ -383,6 +433,7 @@ public IActionResult PasswordInputOverridden() [HttpGet] public IActionResult DateInput() { + ModelState.SetModelValue(nameof(DateInputTestsModel.Date), new DateOnly(2024, 3, 15), "15,3,2024"); ModelState.AddModelError(nameof(DateInputTestsModel.Date), "Model error message"); return View(new DateInputTestsModel { Date = new DateOnly(2024, 3, 15) }); } @@ -390,6 +441,7 @@ public IActionResult DateInput() [HttpGet] public IActionResult DateInputOverridden() { + ModelState.SetModelValue(nameof(DateInputTestsModel.Date), new DateOnly(2024, 3, 15), "15,3,2024"); ModelState.AddModelError(nameof(DateInputTestsModel.Date), "Model error message"); return View(new DateInputTestsModel { Date = new DateOnly(2024, 3, 15) }); } @@ -397,6 +449,7 @@ public IActionResult DateInputOverridden() [HttpGet] public IActionResult DateInputOverriddenItems() { + ModelState.SetModelValue(nameof(DateInputTestsModel.Date), new DateOnly(2024, 3, 15), "15,3,2024"); ModelState.AddModelError(nameof(DateInputTestsModel.Date), "Model error message"); return View(new DateInputTestsModel { Date = new DateOnly(2024, 3, 15) }); } @@ -404,6 +457,7 @@ public IActionResult DateInputOverriddenItems() [HttpGet] public IActionResult Textarea() { + ModelState.SetModelValue(nameof(TextareaTestsModel.Text), "Model value", "Model value"); ModelState.AddModelError(nameof(TextareaTestsModel.Text), "Model error message"); return View(new TextareaTestsModel { Text = "Model value" }); } @@ -411,6 +465,7 @@ public IActionResult Textarea() [HttpGet] public IActionResult TextareaOverridden() { + ModelState.SetModelValue(nameof(TextareaTestsModel.Text), "Model value", "Model value"); ModelState.AddModelError(nameof(TextareaTestsModel.Text), "Model error message"); return View(new TextareaTestsModel { Text = "Model value" }); } @@ -418,6 +473,7 @@ public IActionResult TextareaOverridden() [HttpGet] public IActionResult CharacterCount() { + ModelState.SetModelValue(nameof(CharacterCountTestsModel.Text), "Model value", "Model value"); ModelState.AddModelError(nameof(CharacterCountTestsModel.Text), "Model error message"); return View(new CharacterCountTestsModel { Text = "Model value" }); } @@ -425,6 +481,7 @@ public IActionResult CharacterCount() [HttpGet] public IActionResult CharacterCountOverridden() { + ModelState.SetModelValue(nameof(CharacterCountTestsModel.Text), "Model value", "Model value"); ModelState.AddModelError(nameof(CharacterCountTestsModel.Text), "Model error message"); return View(new CharacterCountTestsModel { Text = "Model value" }); } @@ -432,6 +489,7 @@ public IActionResult CharacterCountOverridden() [HttpGet] public IActionResult FileUpload() { + ModelState.SetModelValue(nameof(FileUploadTestsModel.File), "", ""); ModelState.AddModelError(nameof(FileUploadTestsModel.File), "Model error message"); return View(new FileUploadTestsModel()); } @@ -439,6 +497,7 @@ public IActionResult FileUpload() [HttpGet] public IActionResult FileUploadOverridden() { + ModelState.SetModelValue(nameof(FileUploadTestsModel.File), "", ""); ModelState.AddModelError(nameof(FileUploadTestsModel.File), "Model error message"); return View(new FileUploadTestsModel()); } @@ -446,6 +505,7 @@ public IActionResult FileUploadOverridden() [HttpGet] public IActionResult Select() { + ModelState.SetModelValue(nameof(SelectTestsModel.Option), "option2", "option2"); ModelState.AddModelError(nameof(SelectTestsModel.Option), "Model error message"); return View(new SelectTestsModel { Option = "option2" }); } @@ -453,9 +513,26 @@ public IActionResult Select() [HttpGet] public IActionResult SelectOverridden() { + ModelState.SetModelValue(nameof(SelectTestsModel.Option), "option2", "option2"); ModelState.AddModelError(nameof(SelectTestsModel.Option), "Model error message"); return View(new SelectTestsModel { Option = "option2" }); } + + [HttpGet] + public IActionResult Checkboxes() + { + ModelState.SetModelValue(nameof(CheckboxesTestsModel.Options), new[] { "option2" }, null); + ModelState.AddModelError(nameof(CheckboxesTestsModel.Options), "Model error message"); + return View(new CheckboxesTestsModel { Options = ["option2"] }); + } + + [HttpGet] + public IActionResult CheckboxesOverridden() + { + ModelState.SetModelValue(nameof(CheckboxesTestsModel.Options), new[] { "option2" }, null); + ModelState.AddModelError(nameof(CheckboxesTestsModel.Options), "Model error message"); + return View(new CheckboxesTestsModel { Options = ["option2"] }); + } } public record TextInputTestsModel @@ -491,7 +568,7 @@ public record CharacterCountTestsModel public record FileUploadTestsModel { [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] - public Microsoft.AspNetCore.Http.IFormFile? File { get; set; } + public IFormFile? File { get; set; } } public record SelectTestsModel @@ -499,3 +576,9 @@ public record SelectTestsModel [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] public string? Option { get; set; } } + +public record CheckboxesTestsModel +{ + [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] + public List? Options { get; set; } +} From 2662e43f32912bc90d24df9e2c8b2493cb6ab267 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 09:55:03 +0000 Subject: [PATCH 9/9] Add radios model binding integration tests (#447) * Initial plan * Add radios model binding integration tests Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --- .../TagHelpers/RadiosErrorMessageTagHelper.cs | 2 +- .../TagHelperModelBindingTests/Radios.cshtml | 9 +++ .../RadiosOverridden.cshtml | 13 ++++ .../TagHelperModelBindingTests/Tests.cs | 68 +++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Radios.cshtml create mode 100644 tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/RadiosOverridden.cshtml diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/RadiosErrorMessageTagHelper.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/RadiosErrorMessageTagHelper.cs index db25fbb19..b966a9e28 100644 --- a/src/GovUk.Frontend.AspNetCore/TagHelpers/RadiosErrorMessageTagHelper.cs +++ b/src/GovUk.Frontend.AspNetCore/TagHelpers/RadiosErrorMessageTagHelper.cs @@ -31,7 +31,7 @@ private protected override void SetErrorMessage(TagHelperContent? content, TagHe var attributes = new AttributeCollection(output.Attributes); radiosContext.SetErrorMessage( - VisuallyHiddenText, + VisuallyHiddenText is not null ? new TemplateString(VisuallyHiddenText) : null, attributes, content?.ToTemplateString(), output.TagName); diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Radios.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Radios.cshtml new file mode 100644 index 000000000..40ba8faae --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Radios.cshtml @@ -0,0 +1,9 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.RadiosTestsModel + + + + + Option 1 + Option 2 + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/RadiosOverridden.cshtml b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/RadiosOverridden.cshtml new file mode 100644 index 000000000..5a6a77d99 --- /dev/null +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/RadiosOverridden.cshtml @@ -0,0 +1,13 @@ +@model GovUk.Frontend.AspNetCore.IntegrationTests.TagHelperModelBindingTests.RadiosTestsModel + + + + + Overridden legend + + Overridden hint + Overridden error message + Option 1 + Option 2 + + diff --git a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs index f6ee238c4..0f99b6c0a 100644 --- a/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs +++ b/tests/GovUk.Frontend.AspNetCore.IntegrationTests/TagHelperModelBindingTests/Tests.cs @@ -393,6 +393,52 @@ public async Task CheckboxesOverridden_RendersCorrectly() var errorMessage = page.Locator(".govuk-error-message").First; Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); } + + [Fact] + public async Task Radios_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/Radios"); + + var radio1 = page.Locator("input[value='option1']").First; + Assert.Equal("Option", await radio1.GetAttributeAsync("name")); + Assert.Equal("Option", await radio1.GetAttributeAsync("id")); + Assert.False(await radio1.IsCheckedAsync()); + + var radio2 = page.Locator("input[value='option2']").First; + Assert.Equal("Option", await radio2.GetAttributeAsync("name")); + Assert.Equal("Option-2", await radio2.GetAttributeAsync("id")); + Assert.True(await radio2.IsCheckedAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("ModelMetadata description", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Model error message", await errorMessage.TextContentAsync()); + } + + [Fact] + public async Task RadiosOverridden_RendersCorrectly() + { + var page = await fixture.Browser!.NewPageAsync(); + await page.GotoAsync($"{ServerFixture.BaseUrl}/ModelBindingTests/RadiosOverridden"); + + var radio1 = page.Locator("input[value='option1']").First; + Assert.Equal("OverriddenName", await radio1.GetAttributeAsync("name")); + Assert.Equal("OverriddenIdPrefix", await radio1.GetAttributeAsync("id")); + Assert.True(await radio1.IsCheckedAsync()); + + var radio2 = page.Locator("input[value='option2']").First; + Assert.Equal("OverriddenName", await radio2.GetAttributeAsync("name")); + Assert.Equal("OverriddenIdPrefix-2", await radio2.GetAttributeAsync("id")); + Assert.False(await radio2.IsCheckedAsync()); + + var hint = page.Locator(".govuk-hint").First; + Assert.Equal("Overridden hint", await hint.InnerTextAsync()); + + var errorMessage = page.Locator(".govuk-error-message").First; + Assert.Equal("Error: Overridden error message", await errorMessage.TextContentAsync()); + } } [Route("/[controller]/[action]")] @@ -533,6 +579,22 @@ public IActionResult CheckboxesOverridden() ModelState.AddModelError(nameof(CheckboxesTestsModel.Options), "Model error message"); return View(new CheckboxesTestsModel { Options = ["option2"] }); } + + [HttpGet] + public IActionResult Radios() + { + ModelState.SetModelValue(nameof(RadiosTestsModel.Option), "option2", "option2"); + ModelState.AddModelError(nameof(RadiosTestsModel.Option), "Model error message"); + return View(new RadiosTestsModel { Option = "option2" }); + } + + [HttpGet] + public IActionResult RadiosOverridden() + { + ModelState.SetModelValue(nameof(RadiosTestsModel.Option), "option2", "option2"); + ModelState.AddModelError(nameof(RadiosTestsModel.Option), "Model error message"); + return View(new RadiosTestsModel { Option = "option2" }); + } } public record TextInputTestsModel @@ -582,3 +644,9 @@ public record CheckboxesTestsModel [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] public List? Options { get; set; } } + +public record RadiosTestsModel +{ + [Display(Name = "ModelMetadata display name", Description = "ModelMetadata description")] + public string? Option { get; set; } +}