diff --git a/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesContext.cs b/src/GovUk.Frontend.AspNetCore/TagHelpers/CheckboxesContext.cs index ef62a6ed..2b9f5f14 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 fc033290..a58751ac 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 bb2cc24e..4b63d392 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 99d6e38d..0d9371b6 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 00000000..89715053 --- /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 00000000..5bcd163c --- /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 dfe95e92..f6ee238c 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; } +}