From 5182c86e70def7b5d451f337c927c9de652a0de9 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Mon, 30 Oct 2023 11:08:37 -0500 Subject: [PATCH 01/42] add label, aria-label, wrapped label --- src/bunit.web.query/LabelQueryExtensions.cs | 63 +++++++++++++++++++ src/bunit.web.query/bunit.web.query.csproj | 4 ++ .../BlazorE2E/LabelQueryComponent.razor | 19 ++++++ .../Labels/LabelQueryExtensions.cs | 61 ++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 src/bunit.web.query/LabelQueryExtensions.cs create mode 100644 tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor create mode 100644 tests/bunit.web.query.tests/Labels/LabelQueryExtensions.cs diff --git a/src/bunit.web.query/LabelQueryExtensions.cs b/src/bunit.web.query/LabelQueryExtensions.cs new file mode 100644 index 000000000..81714c156 --- /dev/null +++ b/src/bunit.web.query/LabelQueryExtensions.cs @@ -0,0 +1,63 @@ +using AngleSharp.Dom; + +namespace Bunit; + +public static class LabelQueryExtensions +{ + public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText) + { + try + { + // TODO: switch to strategy pattern? + var element = FindByLabelTextWithForAttribute(renderedFragment, labelText); + + if (element != null) + return element; + + element = FindByLabelTextWithWrappedElement(renderedFragment, labelText); + if (element != null) + return element; + + element = FindByLabelWithAriaLabelAttribute(renderedFragment, labelText); + + if (element != null) + return element; + } + catch (DomException exception) when (exception.Message == "The string did not match the expected pattern.") + { + throw new ElementNotFoundException(labelText); + } + + throw new ElementNotFoundException(labelText); + + } + + private static IElement? FindByLabelTextWithForAttribute(IRenderedFragment renderedFragment, string labelText) + { + var labels = renderedFragment.FindAll("label"); + var matchingLabel = labels.SingleOrDefault(l => l.TextContent == labelText); + + return matchingLabel == null + ? null + : renderedFragment.Find($"#{matchingLabel.GetAttribute("for")}"); + } + + private static IElement? FindByLabelTextWithWrappedElement(IRenderedFragment renderedFragment, string labelText) + { + var labels = renderedFragment.FindAll("label"); + var matchingLabel = labels.SingleOrDefault(l => l.InnerHtml.StartsWith(labelText)); + + return matchingLabel? + .Children + .SingleOrDefault(n => n.NodeName == "INPUT"); + } + + private static IElement? FindByLabelWithAriaLabelAttribute(IRenderedFragment renderedFragment, string labelText) + { + var results = renderedFragment.FindAll($"[aria-label='{labelText}']"); + + return results.Count == 0 + ? null + : results[0]; + } +} diff --git a/src/bunit.web.query/bunit.web.query.csproj b/src/bunit.web.query/bunit.web.query.csproj index 46f303535..254433b64 100644 --- a/src/bunit.web.query/bunit.web.query.csproj +++ b/src/bunit.web.query/bunit.web.query.csproj @@ -50,4 +50,8 @@ + + + + diff --git a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor new file mode 100644 index 000000000..7f91ce22d --- /dev/null +++ b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + +@code { + +} diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensions.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensions.cs new file mode 100644 index 000000000..a3aa0e74b --- /dev/null +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensions.cs @@ -0,0 +1,61 @@ +using Bunit.TestAssets.BlazorE2E; + +namespace Bunit.Labels; + +public class LabelQueryExtensionsTests : TestContext +{ + [Fact(DisplayName = "Should return back input associated with label when using the for attribute")] + public void Test001() + { + var cut = RenderComponent(); + + var input = cut.FindByLabelText("Label for Input 1"); + + input.ShouldNotBeNull(); + input.Id.ShouldBe("input-with-label"); + } + + [Fact(DisplayName = "Should throw exception when label text does not exist in the DOM")] + public void Test002() + { + var expectedLabelText = Guid.NewGuid().ToString(); + var cut = RenderComponent(); + + Should.Throw(() => cut.FindByLabelText(expectedLabelText)) + .CssSelector.ShouldBe(expectedLabelText); + } + + [Fact(DisplayName = "Should return back input associated with label when label when is wrapped around input")] + public void Test003() + { + var cut = RenderComponent(); + + var input = cut.FindByLabelText("Wrapped Label"); + + input.ShouldNotBeNull(); + input.Id.ShouldBe("wrapped-label"); + } + + [Fact(DisplayName = "Should throw exception when label text exists but is not tied to any input")] + public void Test004() + { + var expectedLabelText = "Label With Missing Input"; + var cut = RenderComponent(); + + Should.Throw(() => cut.FindByLabelText(expectedLabelText)) + .CssSelector.ShouldBe(expectedLabelText); + } + + [Fact(DisplayName = "Should return back input associated with label when label using the aria-label")] + public void Test005() + { + var cut = RenderComponent(); + + var input = cut.FindByLabelText("Aria Label"); + + input.ShouldNotBeNull(); + input.Id.ShouldBe("input-with-aria-label"); + } + + // TODO: get aria-labelledby +} From d456d61f647a05ecf8cf34a668955aef42888535 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 31 Oct 2023 22:11:15 -0500 Subject: [PATCH 02/42] switch to strategy pattern --- src/bunit.web.query/LabelQueryExtensions.cs | 63 ------------------- .../Labels/LabelQueryExtensions.cs | 39 ++++++++++++ .../Strategies/ILabelTextQueryStrategy.cs | 8 +++ .../LabelTextUsingAriaLabelStrategy.cs | 15 +++++ .../LabelTextUsingForAttributeStrategy.cs | 16 +++++ .../LabelTextUsingWrappedElementStrategy.cs | 16 +++++ ...nsions.cs => LabelQueryExtensionsTests.cs} | 4 +- 7 files changed, 97 insertions(+), 64 deletions(-) delete mode 100644 src/bunit.web.query/LabelQueryExtensions.cs create mode 100644 src/bunit.web.query/Labels/LabelQueryExtensions.cs create mode 100644 src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs create mode 100644 src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs create mode 100644 src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs create mode 100644 src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs rename tests/bunit.web.query.tests/Labels/{LabelQueryExtensions.cs => LabelQueryExtensionsTests.cs} (92%) diff --git a/src/bunit.web.query/LabelQueryExtensions.cs b/src/bunit.web.query/LabelQueryExtensions.cs deleted file mode 100644 index 81714c156..000000000 --- a/src/bunit.web.query/LabelQueryExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -using AngleSharp.Dom; - -namespace Bunit; - -public static class LabelQueryExtensions -{ - public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText) - { - try - { - // TODO: switch to strategy pattern? - var element = FindByLabelTextWithForAttribute(renderedFragment, labelText); - - if (element != null) - return element; - - element = FindByLabelTextWithWrappedElement(renderedFragment, labelText); - if (element != null) - return element; - - element = FindByLabelWithAriaLabelAttribute(renderedFragment, labelText); - - if (element != null) - return element; - } - catch (DomException exception) when (exception.Message == "The string did not match the expected pattern.") - { - throw new ElementNotFoundException(labelText); - } - - throw new ElementNotFoundException(labelText); - - } - - private static IElement? FindByLabelTextWithForAttribute(IRenderedFragment renderedFragment, string labelText) - { - var labels = renderedFragment.FindAll("label"); - var matchingLabel = labels.SingleOrDefault(l => l.TextContent == labelText); - - return matchingLabel == null - ? null - : renderedFragment.Find($"#{matchingLabel.GetAttribute("for")}"); - } - - private static IElement? FindByLabelTextWithWrappedElement(IRenderedFragment renderedFragment, string labelText) - { - var labels = renderedFragment.FindAll("label"); - var matchingLabel = labels.SingleOrDefault(l => l.InnerHtml.StartsWith(labelText)); - - return matchingLabel? - .Children - .SingleOrDefault(n => n.NodeName == "INPUT"); - } - - private static IElement? FindByLabelWithAriaLabelAttribute(IRenderedFragment renderedFragment, string labelText) - { - var results = renderedFragment.FindAll($"[aria-label='{labelText}']"); - - return results.Count == 0 - ? null - : results[0]; - } -} diff --git a/src/bunit.web.query/Labels/LabelQueryExtensions.cs b/src/bunit.web.query/Labels/LabelQueryExtensions.cs new file mode 100644 index 000000000..6d767863a --- /dev/null +++ b/src/bunit.web.query/Labels/LabelQueryExtensions.cs @@ -0,0 +1,39 @@ +using AngleSharp.Dom; + +namespace Bunit; + +public static class LabelQueryExtensions +{ + private static List LabelTextQueryStrategies = new() + { + // This is intentionally in the order of most likely to minimize strategies tried to find the label + new LabelTextUsingForAttributeStrategy(), + new LabelTextUsingAriaLabelStrategy(), + new LabelTextUsingWrappedElementStrategy(), + }; + + /// + /// Returns the first element (i.e. an input, select, textarea, etc. element) associated with the given label text. + /// + /// The rendered fragment to search. + /// The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a ``) + public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText) + { + try + { + foreach (var strategy in LabelTextQueryStrategies) + { + var element = strategy.FindElement(renderedFragment, labelText); + + if (element != null) + return element; + } + } + catch (DomException exception) when (exception.Message == "The string did not match the expected pattern.") + { + throw new ElementNotFoundException(labelText); + } + + throw new ElementNotFoundException(labelText); + } +} diff --git a/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs b/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs new file mode 100644 index 000000000..73b02de96 --- /dev/null +++ b/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs @@ -0,0 +1,8 @@ +using AngleSharp.Dom; + +namespace Bunit; + +internal interface ILabelTextQueryStrategy +{ + IElement? FindElement(IRenderedFragment renderedFragment, string labelText); +} diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs new file mode 100644 index 000000000..4a4c83707 --- /dev/null +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs @@ -0,0 +1,15 @@ +using AngleSharp.Dom; + +namespace Bunit; + +internal class LabelTextUsingAriaLabelStrategy : ILabelTextQueryStrategy +{ + public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) + { + var results = renderedFragment.FindAll($"[aria-label='{labelText}']"); + + return results.Count == 0 + ? null + : results[0]; + } +} diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs new file mode 100644 index 000000000..d0fe94c8e --- /dev/null +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs @@ -0,0 +1,16 @@ +using AngleSharp.Dom; + +namespace Bunit; + +internal class LabelTextUsingForAttributeStrategy : ILabelTextQueryStrategy +{ + public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) + { + var labels = renderedFragment.FindAll("label"); + var matchingLabel = labels.SingleOrDefault(l => l.TextContent == labelText); + + return matchingLabel == null + ? null + : renderedFragment.Find($"#{matchingLabel.GetAttribute("for")}"); + } +} diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs new file mode 100644 index 000000000..3b10f0a91 --- /dev/null +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs @@ -0,0 +1,16 @@ +using AngleSharp.Dom; + +namespace Bunit; + +internal class LabelTextUsingWrappedElementStrategy : ILabelTextQueryStrategy +{ + public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) + { + var labels = renderedFragment.FindAll("label"); + var matchingLabel = labels.SingleOrDefault(l => l.InnerHtml.StartsWith(labelText)); + + return matchingLabel? + .Children + .SingleOrDefault(n => n.NodeName == "INPUT"); + } +} diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensions.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs similarity index 92% rename from tests/bunit.web.query.tests/Labels/LabelQueryExtensions.cs rename to tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index a3aa0e74b..c22c2a0d2 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensions.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -50,12 +50,14 @@ public void Test004() public void Test005() { var cut = RenderComponent(); - + var input = cut.FindByLabelText("Aria Label"); input.ShouldNotBeNull(); input.Id.ShouldBe("input-with-aria-label"); } + // Throw error that says why + // TODO: test with button, input (except for type="hidden" ), meter, output, progress, select and textarea // TODO: get aria-labelledby } From 2a1c102e9cf4e85cf1963d200c8ce3573db89732 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 31 Oct 2023 23:16:08 -0500 Subject: [PATCH 03/42] feat: support for all element types that can have a label --- .../Labels/LabelQueryExtensions.cs | 2 +- .../BlazorE2E/LabelQueryComponent.razor | 24 ++++++++++--- .../Labels/LabelQueryExtensionsTests.cs | 35 +++++++++++++------ 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/bunit.web.query/Labels/LabelQueryExtensions.cs b/src/bunit.web.query/Labels/LabelQueryExtensions.cs index 6d767863a..5869b9e19 100644 --- a/src/bunit.web.query/Labels/LabelQueryExtensions.cs +++ b/src/bunit.web.query/Labels/LabelQueryExtensions.cs @@ -4,7 +4,7 @@ namespace Bunit; public static class LabelQueryExtensions { - private static List LabelTextQueryStrategies = new() + private static readonly List LabelTextQueryStrategies = new() { // This is intentionally in the order of most likely to minimize strategies tried to find the label new LabelTextUsingForAttributeStrategy(), diff --git a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor index 7f91ce22d..a57bd52b3 100644 --- a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor @@ -1,19 +1,35 @@ - - - +@* Testing the for attribute *@ +@foreach (var htmlElement in htmlElementsThatCanHaveALabel) +{ + + @((MarkupString) $"""<{htmlElement} id="{htmlElement}-with-label">""") +} +@* This is ignored but just making sure we don't ever pick this up *@ +@* Testing wrapped label *@ +@* Testing no input *@ +@* Testing aria-label *@ @code { - + List htmlElementsThatCanHaveALabel = new() + { + "input", + "select", + "textarea", + "button", + "meter", + "output", + "progress", + }; } diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index c22c2a0d2..0f220bca3 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -4,15 +4,28 @@ namespace Bunit.Labels; public class LabelQueryExtensionsTests : TestContext { - [Fact(DisplayName = "Should return back input associated with label when using the for attribute")] - public void Test001() + public static IEnumerable HtmlElementsThatCanHaveALabel => + new List + { + new object[] { "input" }, + new object[] { "select" }, + new object[] { "button" }, + new object[] { "meter" }, + new object[] { "output" }, + new object[] { "progress" }, + }; + + [Theory(DisplayName = "Should return back associated element with label when using the for attribute")] + [MemberData(nameof(HtmlElementsThatCanHaveALabel))] + public void Test001(string htmlElementWithLabel) { var cut = RenderComponent(); - - var input = cut.FindByLabelText("Label for Input 1"); - + + var input = cut.FindByLabelText($"Label for {htmlElementWithLabel} 1"); + input.ShouldNotBeNull(); - input.Id.ShouldBe("input-with-label"); + input.NodeName.ShouldBe(htmlElementWithLabel, StringCompareShould.IgnoreCase); + input.Id.ShouldBe($"{htmlElementWithLabel}-with-label"); } [Fact(DisplayName = "Should throw exception when label text does not exist in the DOM")] @@ -20,7 +33,7 @@ public void Test002() { var expectedLabelText = Guid.NewGuid().ToString(); var cut = RenderComponent(); - + Should.Throw(() => cut.FindByLabelText(expectedLabelText)) .CssSelector.ShouldBe(expectedLabelText); } @@ -29,9 +42,9 @@ public void Test002() public void Test003() { var cut = RenderComponent(); - + var input = cut.FindByLabelText("Wrapped Label"); - + input.ShouldNotBeNull(); input.Id.ShouldBe("wrapped-label"); } @@ -45,14 +58,14 @@ public void Test004() Should.Throw(() => cut.FindByLabelText(expectedLabelText)) .CssSelector.ShouldBe(expectedLabelText); } - + [Fact(DisplayName = "Should return back input associated with label when label using the aria-label")] public void Test005() { var cut = RenderComponent(); var input = cut.FindByLabelText("Aria Label"); - + input.ShouldNotBeNull(); input.Id.ShouldBe("input-with-aria-label"); } From a3ea7d370b358f62fe1ef37f0883f90082ade85c Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 31 Oct 2023 23:25:05 -0500 Subject: [PATCH 04/42] feat: support for all element types that can have a wrapped label --- src/bunit.web.query/Labels/LabelQueryConstants.cs | 15 +++++++++++++++ .../LabelTextUsingWrappedElementStrategy.cs | 2 +- .../BlazorE2E/LabelQueryComponent.razor | 9 ++++++--- .../Labels/LabelQueryExtensionsTests.cs | 10 ++++++---- 4 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 src/bunit.web.query/Labels/LabelQueryConstants.cs diff --git a/src/bunit.web.query/Labels/LabelQueryConstants.cs b/src/bunit.web.query/Labels/LabelQueryConstants.cs new file mode 100644 index 000000000..f9b144697 --- /dev/null +++ b/src/bunit.web.query/Labels/LabelQueryConstants.cs @@ -0,0 +1,15 @@ +namespace Bunit; + +internal class LabelQueryConstants +{ + internal static readonly List HtmlElementsThatCanHaveALabel = new() + { + "input", + "select", + "textarea", + "button", + "meter", + "output", + "progress", + }; +} diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs index 3b10f0a91..c27a319be 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs @@ -11,6 +11,6 @@ internal class LabelTextUsingWrappedElementStrategy : ILabelTextQueryStrategy return matchingLabel? .Children - .SingleOrDefault(n => n.NodeName == "INPUT"); + .SingleOrDefault(n => LabelQueryConstants.HtmlElementsThatCanHaveALabel.Contains(n.NodeName, StringComparer.OrdinalIgnoreCase)); } } diff --git a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor index a57bd52b3..b9a2fbdc8 100644 --- a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor @@ -10,9 +10,12 @@ @* Testing wrapped label *@ - +@foreach (var htmlElement in htmlElementsThatCanHaveALabel) +{ + +} @* Testing no input *@ diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index 0f220bca3..d0666de77 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -38,15 +38,17 @@ public void Test002() .CssSelector.ShouldBe(expectedLabelText); } - [Fact(DisplayName = "Should return back input associated with label when label when is wrapped around input")] - public void Test003() + [Theory(DisplayName = "Should return back element associated with label when label when is wrapped around element")] + [MemberData(nameof(HtmlElementsThatCanHaveALabel))] + public void Test003(string htmlElementWithLabel) { var cut = RenderComponent(); - var input = cut.FindByLabelText("Wrapped Label"); + var input = cut.FindByLabelText($"{htmlElementWithLabel} Wrapped Label"); input.ShouldNotBeNull(); - input.Id.ShouldBe("wrapped-label"); + input.NodeName.ShouldBe(htmlElementWithLabel, StringCompareShould.IgnoreCase); + input.Id.ShouldBe($"{htmlElementWithLabel}-wrapped-label"); } [Fact(DisplayName = "Should throw exception when label text exists but is not tied to any input")] From b7fca5fccade9d1ed53278938f7f2241e87d78e9 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 31 Oct 2023 23:29:35 -0500 Subject: [PATCH 05/42] feat: support for all element types that can have an aria-label --- .../BlazorE2E/LabelQueryComponent.razor | 5 ++++- .../Labels/LabelQueryExtensionsTests.cs | 11 ++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor index b9a2fbdc8..1da421f7d 100644 --- a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor @@ -21,7 +21,10 @@ @* Testing aria-label *@ - +@foreach (var htmlElement in htmlElementsThatCanHaveALabel) +{ + @((MarkupString) $"""<{htmlElement} id="{htmlElement}-with-aria-label" aria-label="{htmlElement} Aria Label">""") +} @code { diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index d0666de77..0b094009e 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -61,18 +61,19 @@ public void Test004() .CssSelector.ShouldBe(expectedLabelText); } - [Fact(DisplayName = "Should return back input associated with label when label using the aria-label")] - public void Test005() + [Theory(DisplayName = "Should return back element associated with label when element uses aria-label")] + [MemberData(nameof(HtmlElementsThatCanHaveALabel))] + public void Test005(string htmlElementWithLabel) { var cut = RenderComponent(); - var input = cut.FindByLabelText("Aria Label"); + var input = cut.FindByLabelText($"{htmlElementWithLabel} Aria Label"); input.ShouldNotBeNull(); - input.Id.ShouldBe("input-with-aria-label"); + input.NodeName.ShouldBe(htmlElementWithLabel, StringCompareShould.IgnoreCase); + input.Id.ShouldBe($"{htmlElementWithLabel}-with-aria-label"); } // Throw error that says why - // TODO: test with button, input (except for type="hidden" ), meter, output, progress, select and textarea // TODO: get aria-labelledby } From 49b2c18e85f0bea8587e4bbdd54d64bad503a949 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 31 Oct 2023 23:51:46 -0500 Subject: [PATCH 06/42] feat: support for all element types that can have an aria-labelledby --- .../Labels/LabelQueryExtensions.cs | 1 + .../LabelTextUsingAriaLabelledByStrategy.cs | 20 +++++++++++++++++++ .../BlazorE2E/LabelQueryComponent.razor | 8 +++++++- .../Labels/LabelQueryExtensionsTests.cs | 14 ++++++++++++- 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs diff --git a/src/bunit.web.query/Labels/LabelQueryExtensions.cs b/src/bunit.web.query/Labels/LabelQueryExtensions.cs index 5869b9e19..55e40806e 100644 --- a/src/bunit.web.query/Labels/LabelQueryExtensions.cs +++ b/src/bunit.web.query/Labels/LabelQueryExtensions.cs @@ -10,6 +10,7 @@ public static class LabelQueryExtensions new LabelTextUsingForAttributeStrategy(), new LabelTextUsingAriaLabelStrategy(), new LabelTextUsingWrappedElementStrategy(), + new LabelTextUsingAriaLabelledByStrategy(), }; /// diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs new file mode 100644 index 000000000..ea57c02c7 --- /dev/null +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs @@ -0,0 +1,20 @@ +using AngleSharp.Dom; + +namespace Bunit; + +internal class LabelTextUsingAriaLabelledByStrategy : ILabelTextQueryStrategy +{ + public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) + { + var elementsWithAriaLabelledBy = renderedFragment.FindAll("[aria-labelledby]"); + + foreach (var element in elementsWithAriaLabelledBy) + { + var labelElements = renderedFragment.FindAll($"#{element.GetAttribute("aria-labelledby")}"); + if (labelElements.Count > 0 && labelElements[0].GetInnerText() == labelText) + return element; + } + + return null; + } +} diff --git a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor index 1da421f7d..63021c403 100644 --- a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor @@ -26,9 +26,15 @@ @((MarkupString) $"""<{htmlElement} id="{htmlElement}-with-aria-label" aria-label="{htmlElement} Aria Label">""") } +@* Testing aria-labelledby *@ +@foreach (var htmlElement in htmlElementsThatCanHaveALabel) +{ +

@htmlElement Aria Labelled By

+ @((MarkupString) $"""<{htmlElement} aria-labelledby="{htmlElement}-with-aria-labelledby">""") +} @code { - List htmlElementsThatCanHaveALabel = new() + readonly List htmlElementsThatCanHaveALabel = new() { "input", "select", diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index 0b094009e..cd7d7fac7 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -74,6 +74,18 @@ public void Test005(string htmlElementWithLabel) input.Id.ShouldBe($"{htmlElementWithLabel}-with-aria-label"); } + [Theory(DisplayName = "Should return back element associated with another element when that other element uses aria-labelledby")] + [MemberData(nameof(HtmlElementsThatCanHaveALabel))] + public void Test006(string htmlElementWithLabel) + { + var cut = RenderComponent(); + + var input = cut.FindByLabelText($"{htmlElementWithLabel} Aria Labelled By"); + + input.ShouldNotBeNull(); + input.NodeName.ShouldBe(htmlElementWithLabel, StringCompareShould.IgnoreCase); + input.GetAttribute("aria-labelledby").ShouldBe($"{htmlElementWithLabel}-with-aria-labelledby"); + } + // Throw error that says why - // TODO: get aria-labelledby } From ced93f0b046f1560a088ee9df72bd4aaca4904d4 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 31 Oct 2023 23:55:22 -0500 Subject: [PATCH 07/42] style: remove comment --- tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index cd7d7fac7..261f98544 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -86,6 +86,4 @@ public void Test006(string htmlElementWithLabel) input.NodeName.ShouldBe(htmlElementWithLabel, StringCompareShould.IgnoreCase); input.GetAttribute("aria-labelledby").ShouldBe($"{htmlElementWithLabel}-with-aria-labelledby"); } - - // Throw error that says why } From c80345deb0b1dc71a062adbc3a8390f5980b843b Mon Sep 17 00:00:00 2001 From: scottsauber Date: Wed, 1 Nov 2023 21:26:25 -0500 Subject: [PATCH 08/42] fix: use theorydata --- .../Labels/LabelQueryExtensionsTests.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index 261f98544..44db6a118 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -4,16 +4,15 @@ namespace Bunit.Labels; public class LabelQueryExtensionsTests : TestContext { - public static IEnumerable HtmlElementsThatCanHaveALabel => - new List - { - new object[] { "input" }, - new object[] { "select" }, - new object[] { "button" }, - new object[] { "meter" }, - new object[] { "output" }, - new object[] { "progress" }, - }; + public static TheoryData HtmlElementsThatCanHaveALabel { get; } = new() + { + "input", + "select", + "button", + "meter", + "output", + "progress", + }; [Theory(DisplayName = "Should return back associated element with label when using the for attribute")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] From 13b5d3f8c2f1b23bd68052c62a8a382ea34fc137 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Wed, 1 Nov 2023 21:26:48 -0500 Subject: [PATCH 09/42] fix: use method instead of list --- src/bunit.web.query/Labels/LabelOptions.cs | 18 ++++++++++++++++++ .../Labels/LabelQueryConstants.cs | 15 --------------- .../LabelTextUsingWrappedElementStrategy.cs | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 src/bunit.web.query/Labels/LabelOptions.cs delete mode 100644 src/bunit.web.query/Labels/LabelQueryConstants.cs diff --git a/src/bunit.web.query/Labels/LabelOptions.cs b/src/bunit.web.query/Labels/LabelOptions.cs new file mode 100644 index 000000000..2f5d74093 --- /dev/null +++ b/src/bunit.web.query/Labels/LabelOptions.cs @@ -0,0 +1,18 @@ +using AngleSharp.Dom; + +namespace Bunit; + +internal static class LabelOptions +{ + internal static bool IsHtmlElementThatCanHaveALabel(this IElement element) => element.NodeName switch + { + "INPUT" => true, + "SELECT" => true, + "TEXTAREA" => true, + "BUTTON" => true, + "METER" => true, + "OUTPUT" => true, + "PROGRESS" => true, + _ => false + }; +} diff --git a/src/bunit.web.query/Labels/LabelQueryConstants.cs b/src/bunit.web.query/Labels/LabelQueryConstants.cs deleted file mode 100644 index f9b144697..000000000 --- a/src/bunit.web.query/Labels/LabelQueryConstants.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Bunit; - -internal class LabelQueryConstants -{ - internal static readonly List HtmlElementsThatCanHaveALabel = new() - { - "input", - "select", - "textarea", - "button", - "meter", - "output", - "progress", - }; -} diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs index c27a319be..f2660c851 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs @@ -11,6 +11,6 @@ internal class LabelTextUsingWrappedElementStrategy : ILabelTextQueryStrategy return matchingLabel? .Children - .SingleOrDefault(n => LabelQueryConstants.HtmlElementsThatCanHaveALabel.Contains(n.NodeName, StringComparer.OrdinalIgnoreCase)); + .SingleOrDefault(n => n.IsHtmlElementThatCanHaveALabel()); } } From 3ff3ef6e11cf383bc25f4edb02dd763c2f45de01 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Sun, 12 Nov 2023 20:54:32 -0600 Subject: [PATCH 10/42] failing test to prove the re-rendered element issue --- .../BlazorE2E/LabelQueryComponent.razor | 7 +++++++ .../Labels/LabelQueryExtensionsTests.cs | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor index 63021c403..6299aa7e6 100644 --- a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor @@ -33,6 +33,10 @@ @((MarkupString) $"""<{htmlElement} aria-labelledby="{htmlElement}-with-aria-labelledby">""") } +@* Testing we get back the re-rendered element *@ + + + @code { readonly List htmlElementsThatCanHaveALabel = new() { @@ -44,4 +48,7 @@ "output", "progress", }; + + private int _count; + public void IncrementCount() => _count++; } diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index 44db6a118..ab327504d 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -85,4 +85,16 @@ public void Test006(string htmlElementWithLabel) input.NodeName.ShouldBe(htmlElementWithLabel, StringCompareShould.IgnoreCase); input.GetAttribute("aria-labelledby").ShouldBe($"{htmlElementWithLabel}-with-aria-labelledby"); } + + [Fact(DisplayName = "Should reflect latest value when component re-renders")] + public void Test007() + { + var cut = RenderComponent(); + + var input = cut.FindByLabelText("Re-rendered Element"); + input.GetAttribute("value").ShouldBe("0"); + + cut.Find("#increment-button").Click(); + input.GetAttribute("value").ShouldBe("1"); + } } From 78aaf7b6ddde6bff886951ac454b6a5eaf507ec8 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Sun, 12 Nov 2023 23:12:49 -0600 Subject: [PATCH 11/42] move to element factory to prevent re-renders causing issues --- .../ByLabelTextElementFactory.cs | 35 ++++ src/bunit.web.query/ElementWrapperFactory.cs | 156 ++++++++++++++++++ .../Labels/LabelQueryExtensions.cs | 15 +- .../Strategies/ILabelTextQueryStrategy.cs | 2 +- .../LabelTextUsingAriaLabelStrategy.cs | 11 +- .../LabelTextUsingAriaLabelledByStrategy.cs | 10 +- .../LabelTextUsingForAttributeStrategy.cs | 16 +- .../LabelTextUsingWrappedElementStrategy.cs | 11 +- src/bunit.web.query/bunit.web.query.csproj | 1 + .../BlazorE2E/LabelQueryComponent.razor | 16 +- .../Labels/LabelQueryExtensionsTests.cs | 13 +- 11 files changed, 260 insertions(+), 26 deletions(-) create mode 100644 src/bunit.web.query/ByLabelTextElementFactory.cs create mode 100644 src/bunit.web.query/ElementWrapperFactory.cs diff --git a/src/bunit.web.query/ByLabelTextElementFactory.cs b/src/bunit.web.query/ByLabelTextElementFactory.cs new file mode 100644 index 000000000..5ce967b6d --- /dev/null +++ b/src/bunit.web.query/ByLabelTextElementFactory.cs @@ -0,0 +1,35 @@ +using AngleSharp.Dom; + +namespace Bunit; + +using AngleSharpWrappers; + + +internal sealed class ByLabelTextElementFactory : IElementFactory + where TElement : class, IElement +{ + private readonly IRenderedFragment testTarget; + private readonly string labelText; + private TElement? element; + + public ByLabelTextElementFactory(IRenderedFragment testTarget, TElement initialElement, string labelText) + { + this.testTarget = testTarget; + element = initialElement; + this.labelText = labelText; + testTarget.OnMarkupUpdated += FragmentsMarkupUpdated; + } + + private void FragmentsMarkupUpdated(object? sender, EventArgs args) => element = null; + + TElement IElementFactory.GetElement() + { + if (element is null) + { + var queryResult = testTarget.FindByLabelTextInternal(labelText); + element = queryResult as TElement; + } + + return element ?? throw new ElementRemovedFromDomException(labelText); + } +} diff --git a/src/bunit.web.query/ElementWrapperFactory.cs b/src/bunit.web.query/ElementWrapperFactory.cs new file mode 100644 index 000000000..c05f9b217 --- /dev/null +++ b/src/bunit.web.query/ElementWrapperFactory.cs @@ -0,0 +1,156 @@ +using AngleSharp.Dom; +using AngleSharp.Html.Dom; +using AngleSharp.Svg.Dom; +using AngleSharpWrappers; + +namespace Bunit; + +internal static class ElementWrapperFactory +{ + public static IElement CreateByLabelText(IElement element, IRenderedFragment renderedFragment, string labelText) + { + return element switch + { + IHtmlAnchorElement htmlAnchorElement => new HtmlAnchorElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlAnchorElement, labelText)), + IHtmlAreaElement htmlAreaElement => new HtmlAreaElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlAreaElement, labelText)), + IHtmlAudioElement htmlAudioElement => new HtmlAudioElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlAudioElement, labelText)), + IHtmlBaseElement htmlBaseElement => new HtmlBaseElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlBaseElement, labelText)), + IHtmlBodyElement htmlBodyElement => new HtmlBodyElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlBodyElement, labelText)), + IHtmlBreakRowElement htmlBreakRowElement => new HtmlBreakRowElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlBreakRowElement, labelText)), + IHtmlButtonElement htmlButtonElement => new HtmlButtonElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlButtonElement, labelText)), + IHtmlCanvasElement htmlCanvasElement => new HtmlCanvasElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlCanvasElement, labelText)), + IHtmlCommandElement htmlCommandElement => new HtmlCommandElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlCommandElement, labelText)), + IHtmlDataElement htmlDataElement => new HtmlDataElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlDataElement, labelText)), + IHtmlDataListElement htmlDataListElement => new HtmlDataListElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlDataListElement, labelText)), + IHtmlDetailsElement htmlDetailsElement => new HtmlDetailsElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlDetailsElement, labelText)), + IHtmlDialogElement htmlDialogElement => new HtmlDialogElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlDialogElement, labelText)), + IHtmlDivElement htmlDivElement => new HtmlDivElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlDivElement, labelText)), + IHtmlEmbedElement htmlEmbedElement => new HtmlEmbedElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlEmbedElement, labelText)), + IHtmlFieldSetElement htmlFieldSetElement => new HtmlFieldSetElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlFieldSetElement, labelText)), + IHtmlFormElement htmlFormElement => new HtmlFormElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlFormElement, labelText)), + IHtmlHeadElement htmlHeadElement => new HtmlHeadElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlHeadElement, labelText)), + IHtmlHeadingElement htmlHeadingElement => new HtmlHeadingElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlHeadingElement, labelText)), + IHtmlHrElement htmlHrElement => new HtmlHrElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlHrElement, labelText)), + IHtmlHtmlElement htmlHtmlElement => new HtmlHtmlElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlHtmlElement, labelText)), + IHtmlImageElement htmlImageElement => new HtmlImageElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlImageElement, labelText)), + IHtmlInlineFrameElement htmlInlineFrameElement => new HtmlInlineFrameElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlInlineFrameElement, labelText)), + IHtmlInputElement htmlInputElement => new HtmlInputElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlInputElement, labelText)), + IHtmlKeygenElement htmlKeygenElement => new HtmlKeygenElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlKeygenElement, labelText)), + IHtmlLabelElement htmlLabelElement => new HtmlLabelElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlLabelElement, labelText)), + IHtmlLegendElement htmlLegendElement => new HtmlLegendElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlLegendElement, labelText)), + IHtmlLinkElement htmlLinkElement => new HtmlLinkElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlLinkElement, labelText)), + IHtmlListItemElement htmlListItemElement => new HtmlListItemElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlListItemElement, labelText)), + IHtmlMapElement htmlMapElement => new HtmlMapElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlMapElement, labelText)), + IHtmlMarqueeElement htmlMarqueeElement => new HtmlMarqueeElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlMarqueeElement, labelText)), + IHtmlMenuElement htmlMenuElement => new HtmlMenuElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlMenuElement, labelText)), + IHtmlMenuItemElement htmlMenuItemElement => new HtmlMenuItemElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlMenuItemElement, labelText)), + IHtmlMetaElement htmlMetaElement => new HtmlMetaElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlMetaElement, labelText)), + IHtmlMeterElement htmlMeterElement => new HtmlMeterElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlMeterElement, labelText)), + IHtmlModElement htmlModElement => new HtmlModElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlModElement, labelText)), + IHtmlObjectElement htmlObjectElement => new HtmlObjectElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlObjectElement, labelText)), + IHtmlOrderedListElement htmlOrderedListElement => new HtmlOrderedListElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlOrderedListElement, labelText)), + IHtmlParagraphElement htmlParagraphElement => new HtmlParagraphElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlParagraphElement, labelText)), + IHtmlParamElement htmlParamElement => new HtmlParamElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlParamElement, labelText)), + IHtmlPreElement htmlPreElement => new HtmlPreElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlPreElement, labelText)), + IHtmlProgressElement htmlProgressElement => new HtmlProgressElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlProgressElement, labelText)), + IHtmlQuoteElement htmlQuoteElement => new HtmlQuoteElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlQuoteElement, labelText)), + IHtmlScriptElement htmlScriptElement => new HtmlScriptElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlScriptElement, labelText)), + IHtmlSelectElement htmlSelectElement => new HtmlSelectElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlSelectElement, labelText)), + IHtmlSourceElement htmlSourceElement => new HtmlSourceElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlSourceElement, labelText)), + IHtmlSpanElement htmlSpanElement => new HtmlSpanElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlSpanElement, labelText)), + IHtmlStyleElement htmlStyleElement => new HtmlStyleElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlStyleElement, labelText)), + IHtmlTableCaptionElement htmlTableCaptionElement => new HtmlTableCaptionElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlTableCaptionElement, labelText)), + IHtmlTableCellElement htmlTableCellElement => new HtmlTableCellElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlTableCellElement, labelText)), + IHtmlTableElement htmlTableElement => new HtmlTableElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlTableElement, labelText)), + IHtmlTableRowElement htmlTableRowElement => new HtmlTableRowElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlTableRowElement, labelText)), + IHtmlTableSectionElement htmlTableSectionElement => new HtmlTableSectionElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlTableSectionElement, labelText)), + IHtmlTemplateElement htmlTemplateElement => new HtmlTemplateElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlTemplateElement, labelText)), + IHtmlTextAreaElement htmlTextAreaElement => new HtmlTextAreaElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlTextAreaElement, labelText)), + IHtmlTimeElement htmlTimeElement => new HtmlTimeElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlTimeElement, labelText)), + IHtmlTitleElement htmlTitleElement => new HtmlTitleElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlTitleElement, labelText)), + IHtmlTrackElement htmlTrackElement => new HtmlTrackElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlTrackElement, labelText)), + IHtmlUnknownElement htmlUnknownElement => new HtmlUnknownElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlUnknownElement, labelText)), + IHtmlVideoElement htmlVideoElement => new HtmlVideoElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlVideoElement, labelText)), + IHtmlMediaElement htmlMediaElement => new HtmlMediaElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlMediaElement, labelText)), + IPseudoElement pseudoElement => new PseudoElementWrapper( + new ByLabelTextElementFactory(renderedFragment, pseudoElement, labelText)), + ISvgCircleElement svgCircleElement => new SvgCircleElementWrapper( + new ByLabelTextElementFactory(renderedFragment, svgCircleElement, labelText)), + ISvgDescriptionElement svgDescriptionElement => new SvgDescriptionElementWrapper( + new ByLabelTextElementFactory(renderedFragment, svgDescriptionElement, labelText)), + ISvgForeignObjectElement svgForeignObjectElement => new SvgForeignObjectElementWrapper( + new ByLabelTextElementFactory(renderedFragment, svgForeignObjectElement, labelText)), + ISvgSvgElement svgSvgElement => new SvgSvgElementWrapper( + new ByLabelTextElementFactory(renderedFragment, svgSvgElement, labelText)), + ISvgTitleElement svgTitleElement => new SvgTitleElementWrapper( + new ByLabelTextElementFactory(renderedFragment, svgTitleElement, labelText)), + ISvgElement svgElement => new SvgElementWrapper( + new ByLabelTextElementFactory(renderedFragment, svgElement, labelText)), + IHtmlElement htmlElement => new HtmlElementWrapper( + new ByLabelTextElementFactory(renderedFragment, htmlElement, labelText)), + _ => new ElementWrapper( + new ByLabelTextElementFactory(renderedFragment, element, labelText)), + }; + } +} diff --git a/src/bunit.web.query/Labels/LabelQueryExtensions.cs b/src/bunit.web.query/Labels/LabelQueryExtensions.cs index 55e40806e..682d91d97 100644 --- a/src/bunit.web.query/Labels/LabelQueryExtensions.cs +++ b/src/bunit.web.query/Labels/LabelQueryExtensions.cs @@ -1,4 +1,5 @@ using AngleSharp.Dom; +using Bunit.Labels.Strategies; namespace Bunit; @@ -19,6 +20,16 @@ public static class LabelQueryExtensions /// The rendered fragment to search. /// The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a ``) public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText) + { + return FindByLabelTextInternal(renderedFragment, labelText) ?? throw new ElementNotFoundException(labelText); + } + + /// + /// Returns the first element (i.e. an input, select, textarea, etc. element) associated with the given label text. + /// + /// The rendered fragment to search. + /// The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a ``) + internal static IElement? FindByLabelTextInternal(this IRenderedFragment renderedFragment, string labelText) { try { @@ -32,9 +43,9 @@ public static IElement FindByLabelText(this IRenderedFragment renderedFragment, } catch (DomException exception) when (exception.Message == "The string did not match the expected pattern.") { - throw new ElementNotFoundException(labelText); + return null; } - throw new ElementNotFoundException(labelText); + return null; } } diff --git a/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs b/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs index 73b02de96..98ead62c3 100644 --- a/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs @@ -1,6 +1,6 @@ using AngleSharp.Dom; -namespace Bunit; +namespace Bunit.Labels.Strategies; internal interface ILabelTextQueryStrategy { diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs index 4a4c83707..f7ddcb90b 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs @@ -1,15 +1,16 @@ using AngleSharp.Dom; -namespace Bunit; +namespace Bunit.Labels.Strategies; internal class LabelTextUsingAriaLabelStrategy : ILabelTextQueryStrategy { public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) { - var results = renderedFragment.FindAll($"[aria-label='{labelText}']"); + var element = renderedFragment.Nodes.QuerySelector($"[aria-label='{labelText}']"); - return results.Count == 0 - ? null - : results[0]; + if (element is null) + return null; + + return ElementWrapperFactory.CreateByLabelText(element, renderedFragment, labelText); } } diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs index ea57c02c7..b9c386fb4 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs @@ -1,18 +1,18 @@ using AngleSharp.Dom; -namespace Bunit; +namespace Bunit.Labels.Strategies; internal class LabelTextUsingAriaLabelledByStrategy : ILabelTextQueryStrategy { public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) { - var elementsWithAriaLabelledBy = renderedFragment.FindAll("[aria-labelledby]"); + var elementsWithAriaLabelledBy = renderedFragment.Nodes.QuerySelectorAll("[aria-labelledby]"); foreach (var element in elementsWithAriaLabelledBy) { - var labelElements = renderedFragment.FindAll($"#{element.GetAttribute("aria-labelledby")}"); - if (labelElements.Count > 0 && labelElements[0].GetInnerText() == labelText) - return element; + var labelElement = renderedFragment.Nodes.QuerySelector($"#{element.GetAttribute("aria-labelledby")}"); + if (labelElement.GetInnerText() == labelText) + return ElementWrapperFactory.CreateByLabelText(element, renderedFragment, labelText); } return null; diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs index d0fe94c8e..1900d0d0a 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs @@ -1,16 +1,22 @@ using AngleSharp.Dom; -namespace Bunit; +namespace Bunit.Labels.Strategies; internal class LabelTextUsingForAttributeStrategy : ILabelTextQueryStrategy { public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) { - var labels = renderedFragment.FindAll("label"); + var labels = renderedFragment.Nodes.QuerySelectorAll("label"); var matchingLabel = labels.SingleOrDefault(l => l.TextContent == labelText); - return matchingLabel == null - ? null - : renderedFragment.Find($"#{matchingLabel.GetAttribute("for")}"); + if (matchingLabel is null) + return null; + + var matchingElement = renderedFragment.Nodes.QuerySelector($"#{matchingLabel.GetAttribute("for")}"); + + if (matchingElement is null) + return null; + + return ElementWrapperFactory.CreateByLabelText(matchingElement, renderedFragment, labelText); } } diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs index f2660c851..a5f506da5 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs @@ -1,16 +1,21 @@ using AngleSharp.Dom; -namespace Bunit; +namespace Bunit.Labels.Strategies; internal class LabelTextUsingWrappedElementStrategy : ILabelTextQueryStrategy { public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) { - var labels = renderedFragment.FindAll("label"); + var labels = renderedFragment.Nodes.QuerySelectorAll("label"); var matchingLabel = labels.SingleOrDefault(l => l.InnerHtml.StartsWith(labelText)); - return matchingLabel? + var matchingElement = matchingLabel? .Children .SingleOrDefault(n => n.IsHtmlElementThatCanHaveALabel()); + + if (matchingElement is null) + return null; + + return ElementWrapperFactory.CreateByLabelText(matchingElement, renderedFragment, labelText); } } diff --git a/src/bunit.web.query/bunit.web.query.csproj b/src/bunit.web.query/bunit.web.query.csproj index 254433b64..42fbbe72e 100644 --- a/src/bunit.web.query/bunit.web.query.csproj +++ b/src/bunit.web.query/bunit.web.query.csproj @@ -51,6 +51,7 @@ + diff --git a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor index 6299aa7e6..3d7a24ac0 100644 --- a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor @@ -33,8 +33,20 @@ @((MarkupString) $"""<{htmlElement} aria-labelledby="{htmlElement}-with-aria-labelledby">""") } -@* Testing we get back the re-rendered element *@ - + +@* Testing we get back the re-rendered element for an aria-label *@ + + + + + + + +

Re-rendered input with Aria Labelledby

+ + @code { diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index ab327504d..97b69a874 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -37,6 +37,7 @@ public void Test002() .CssSelector.ShouldBe(expectedLabelText); } + // TODO: fix this for refreshable [Theory(DisplayName = "Should return back element associated with label when label when is wrapped around element")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test003(string htmlElementWithLabel) @@ -60,6 +61,7 @@ public void Test004() .CssSelector.ShouldBe(expectedLabelText); } + // TODO: fix this for refreshable [Theory(DisplayName = "Should return back element associated with label when element uses aria-label")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test005(string htmlElementWithLabel) @@ -73,6 +75,7 @@ public void Test005(string htmlElementWithLabel) input.Id.ShouldBe($"{htmlElementWithLabel}-with-aria-label"); } + // TODO: fix this for refreshable [Theory(DisplayName = "Should return back element associated with another element when that other element uses aria-labelledby")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test006(string htmlElementWithLabel) @@ -86,12 +89,16 @@ public void Test006(string htmlElementWithLabel) input.GetAttribute("aria-labelledby").ShouldBe($"{htmlElementWithLabel}-with-aria-labelledby"); } - [Fact(DisplayName = "Should reflect latest value when component re-renders")] - public void Test007() + [Theory(DisplayName = "Should reflect latest value when element re-renders")] + [InlineData("Re-rendered input with label")] + [InlineData("Re-rendered input with wrapped label")] + [InlineData("Re-rendered input With Aria Label")] + [InlineData("Re-rendered input with Aria Labelledby")] + public void Test007(string labelText) { var cut = RenderComponent(); - var input = cut.FindByLabelText("Re-rendered Element"); + var input = cut.FindByLabelText(labelText); input.GetAttribute("value").ShouldBe("0"); cut.Find("#increment-button").Click(); From 61d53b953c19f1dedbabece972ed6a9d2f979748 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Mon, 13 Nov 2023 09:17:55 -0600 Subject: [PATCH 12/42] fix: switch to array for strategies --- src/bunit.web.query/Labels/LabelQueryExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bunit.web.query/Labels/LabelQueryExtensions.cs b/src/bunit.web.query/Labels/LabelQueryExtensions.cs index 682d91d97..831f299eb 100644 --- a/src/bunit.web.query/Labels/LabelQueryExtensions.cs +++ b/src/bunit.web.query/Labels/LabelQueryExtensions.cs @@ -1,11 +1,12 @@ -using AngleSharp.Dom; +using System.Collections.ObjectModel; +using AngleSharp.Dom; using Bunit.Labels.Strategies; namespace Bunit; public static class LabelQueryExtensions { - private static readonly List LabelTextQueryStrategies = new() + private static readonly IReadOnlyList LabelTextQueryStrategies = new ILabelTextQueryStrategy[] { // This is intentionally in the order of most likely to minimize strategies tried to find the label new LabelTextUsingForAttributeStrategy(), From b0a1535f7d01a3c61ce5b7f33fe6459072d3aeda Mon Sep 17 00:00:00 2001 From: scottsauber Date: Mon, 13 Nov 2023 09:20:55 -0600 Subject: [PATCH 13/42] fix: remove todos --- .../bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index 97b69a874..33456ea1c 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -37,7 +37,6 @@ public void Test002() .CssSelector.ShouldBe(expectedLabelText); } - // TODO: fix this for refreshable [Theory(DisplayName = "Should return back element associated with label when label when is wrapped around element")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test003(string htmlElementWithLabel) @@ -61,7 +60,6 @@ public void Test004() .CssSelector.ShouldBe(expectedLabelText); } - // TODO: fix this for refreshable [Theory(DisplayName = "Should return back element associated with label when element uses aria-label")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test005(string htmlElementWithLabel) @@ -75,7 +73,6 @@ public void Test005(string htmlElementWithLabel) input.Id.ShouldBe($"{htmlElementWithLabel}-with-aria-label"); } - // TODO: fix this for refreshable [Theory(DisplayName = "Should return back element associated with another element when that other element uses aria-labelledby")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test006(string htmlElementWithLabel) From 81f8ecf4a9e1716448cb9c351e1feea571007c12 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Sat, 25 Nov 2023 22:33:16 -0600 Subject: [PATCH 14/42] fix: move to new ielementwrapperfactory --- .../ByLabelTextElementFactory.cs | 26 ++- src/bunit.web.query/ElementWrapperFactory.cs | 156 ------------------ .../LabelTextUsingAriaLabelStrategy.cs | 5 +- .../LabelTextUsingAriaLabelledByStrategy.cs | 5 +- .../LabelTextUsingForAttributeStrategy.cs | 5 +- .../LabelTextUsingWrappedElementStrategy.cs | 5 +- src/bunit.web.query/bunit.web.query.csproj | 2 +- 7 files changed, 23 insertions(+), 181 deletions(-) delete mode 100644 src/bunit.web.query/ElementWrapperFactory.cs diff --git a/src/bunit.web.query/ByLabelTextElementFactory.cs b/src/bunit.web.query/ByLabelTextElementFactory.cs index 5ce967b6d..5c3b85f4f 100644 --- a/src/bunit.web.query/ByLabelTextElementFactory.cs +++ b/src/bunit.web.query/ByLabelTextElementFactory.cs @@ -1,34 +1,28 @@ -using AngleSharp.Dom; +using AngleSharp.Dom; +using Bunit.Web.AngleSharp; namespace Bunit; -using AngleSharpWrappers; - - -internal sealed class ByLabelTextElementFactory : IElementFactory - where TElement : class, IElement +internal sealed class ByLabelTextElementFactory : IElementWrapperFactory { private readonly IRenderedFragment testTarget; private readonly string labelText; - private TElement? element; - public ByLabelTextElementFactory(IRenderedFragment testTarget, TElement initialElement, string labelText) + public Action? OnElementReplaced { get; set; } + + public ByLabelTextElementFactory(IRenderedFragment testTarget, string labelText) { this.testTarget = testTarget; - element = initialElement; this.labelText = labelText; testTarget.OnMarkupUpdated += FragmentsMarkupUpdated; } - private void FragmentsMarkupUpdated(object? sender, EventArgs args) => element = null; + private void FragmentsMarkupUpdated(object? sender, EventArgs args) + => OnElementReplaced?.Invoke(); - TElement IElementFactory.GetElement() + public TElement GetElement() where TElement : class, IElement { - if (element is null) - { - var queryResult = testTarget.FindByLabelTextInternal(labelText); - element = queryResult as TElement; - } + var element = testTarget.FindByLabelTextInternal(labelText) as TElement; return element ?? throw new ElementRemovedFromDomException(labelText); } diff --git a/src/bunit.web.query/ElementWrapperFactory.cs b/src/bunit.web.query/ElementWrapperFactory.cs deleted file mode 100644 index c05f9b217..000000000 --- a/src/bunit.web.query/ElementWrapperFactory.cs +++ /dev/null @@ -1,156 +0,0 @@ -using AngleSharp.Dom; -using AngleSharp.Html.Dom; -using AngleSharp.Svg.Dom; -using AngleSharpWrappers; - -namespace Bunit; - -internal static class ElementWrapperFactory -{ - public static IElement CreateByLabelText(IElement element, IRenderedFragment renderedFragment, string labelText) - { - return element switch - { - IHtmlAnchorElement htmlAnchorElement => new HtmlAnchorElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlAnchorElement, labelText)), - IHtmlAreaElement htmlAreaElement => new HtmlAreaElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlAreaElement, labelText)), - IHtmlAudioElement htmlAudioElement => new HtmlAudioElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlAudioElement, labelText)), - IHtmlBaseElement htmlBaseElement => new HtmlBaseElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlBaseElement, labelText)), - IHtmlBodyElement htmlBodyElement => new HtmlBodyElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlBodyElement, labelText)), - IHtmlBreakRowElement htmlBreakRowElement => new HtmlBreakRowElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlBreakRowElement, labelText)), - IHtmlButtonElement htmlButtonElement => new HtmlButtonElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlButtonElement, labelText)), - IHtmlCanvasElement htmlCanvasElement => new HtmlCanvasElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlCanvasElement, labelText)), - IHtmlCommandElement htmlCommandElement => new HtmlCommandElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlCommandElement, labelText)), - IHtmlDataElement htmlDataElement => new HtmlDataElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlDataElement, labelText)), - IHtmlDataListElement htmlDataListElement => new HtmlDataListElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlDataListElement, labelText)), - IHtmlDetailsElement htmlDetailsElement => new HtmlDetailsElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlDetailsElement, labelText)), - IHtmlDialogElement htmlDialogElement => new HtmlDialogElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlDialogElement, labelText)), - IHtmlDivElement htmlDivElement => new HtmlDivElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlDivElement, labelText)), - IHtmlEmbedElement htmlEmbedElement => new HtmlEmbedElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlEmbedElement, labelText)), - IHtmlFieldSetElement htmlFieldSetElement => new HtmlFieldSetElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlFieldSetElement, labelText)), - IHtmlFormElement htmlFormElement => new HtmlFormElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlFormElement, labelText)), - IHtmlHeadElement htmlHeadElement => new HtmlHeadElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlHeadElement, labelText)), - IHtmlHeadingElement htmlHeadingElement => new HtmlHeadingElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlHeadingElement, labelText)), - IHtmlHrElement htmlHrElement => new HtmlHrElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlHrElement, labelText)), - IHtmlHtmlElement htmlHtmlElement => new HtmlHtmlElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlHtmlElement, labelText)), - IHtmlImageElement htmlImageElement => new HtmlImageElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlImageElement, labelText)), - IHtmlInlineFrameElement htmlInlineFrameElement => new HtmlInlineFrameElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlInlineFrameElement, labelText)), - IHtmlInputElement htmlInputElement => new HtmlInputElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlInputElement, labelText)), - IHtmlKeygenElement htmlKeygenElement => new HtmlKeygenElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlKeygenElement, labelText)), - IHtmlLabelElement htmlLabelElement => new HtmlLabelElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlLabelElement, labelText)), - IHtmlLegendElement htmlLegendElement => new HtmlLegendElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlLegendElement, labelText)), - IHtmlLinkElement htmlLinkElement => new HtmlLinkElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlLinkElement, labelText)), - IHtmlListItemElement htmlListItemElement => new HtmlListItemElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlListItemElement, labelText)), - IHtmlMapElement htmlMapElement => new HtmlMapElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlMapElement, labelText)), - IHtmlMarqueeElement htmlMarqueeElement => new HtmlMarqueeElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlMarqueeElement, labelText)), - IHtmlMenuElement htmlMenuElement => new HtmlMenuElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlMenuElement, labelText)), - IHtmlMenuItemElement htmlMenuItemElement => new HtmlMenuItemElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlMenuItemElement, labelText)), - IHtmlMetaElement htmlMetaElement => new HtmlMetaElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlMetaElement, labelText)), - IHtmlMeterElement htmlMeterElement => new HtmlMeterElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlMeterElement, labelText)), - IHtmlModElement htmlModElement => new HtmlModElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlModElement, labelText)), - IHtmlObjectElement htmlObjectElement => new HtmlObjectElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlObjectElement, labelText)), - IHtmlOrderedListElement htmlOrderedListElement => new HtmlOrderedListElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlOrderedListElement, labelText)), - IHtmlParagraphElement htmlParagraphElement => new HtmlParagraphElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlParagraphElement, labelText)), - IHtmlParamElement htmlParamElement => new HtmlParamElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlParamElement, labelText)), - IHtmlPreElement htmlPreElement => new HtmlPreElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlPreElement, labelText)), - IHtmlProgressElement htmlProgressElement => new HtmlProgressElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlProgressElement, labelText)), - IHtmlQuoteElement htmlQuoteElement => new HtmlQuoteElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlQuoteElement, labelText)), - IHtmlScriptElement htmlScriptElement => new HtmlScriptElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlScriptElement, labelText)), - IHtmlSelectElement htmlSelectElement => new HtmlSelectElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlSelectElement, labelText)), - IHtmlSourceElement htmlSourceElement => new HtmlSourceElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlSourceElement, labelText)), - IHtmlSpanElement htmlSpanElement => new HtmlSpanElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlSpanElement, labelText)), - IHtmlStyleElement htmlStyleElement => new HtmlStyleElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlStyleElement, labelText)), - IHtmlTableCaptionElement htmlTableCaptionElement => new HtmlTableCaptionElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlTableCaptionElement, labelText)), - IHtmlTableCellElement htmlTableCellElement => new HtmlTableCellElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlTableCellElement, labelText)), - IHtmlTableElement htmlTableElement => new HtmlTableElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlTableElement, labelText)), - IHtmlTableRowElement htmlTableRowElement => new HtmlTableRowElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlTableRowElement, labelText)), - IHtmlTableSectionElement htmlTableSectionElement => new HtmlTableSectionElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlTableSectionElement, labelText)), - IHtmlTemplateElement htmlTemplateElement => new HtmlTemplateElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlTemplateElement, labelText)), - IHtmlTextAreaElement htmlTextAreaElement => new HtmlTextAreaElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlTextAreaElement, labelText)), - IHtmlTimeElement htmlTimeElement => new HtmlTimeElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlTimeElement, labelText)), - IHtmlTitleElement htmlTitleElement => new HtmlTitleElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlTitleElement, labelText)), - IHtmlTrackElement htmlTrackElement => new HtmlTrackElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlTrackElement, labelText)), - IHtmlUnknownElement htmlUnknownElement => new HtmlUnknownElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlUnknownElement, labelText)), - IHtmlVideoElement htmlVideoElement => new HtmlVideoElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlVideoElement, labelText)), - IHtmlMediaElement htmlMediaElement => new HtmlMediaElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlMediaElement, labelText)), - IPseudoElement pseudoElement => new PseudoElementWrapper( - new ByLabelTextElementFactory(renderedFragment, pseudoElement, labelText)), - ISvgCircleElement svgCircleElement => new SvgCircleElementWrapper( - new ByLabelTextElementFactory(renderedFragment, svgCircleElement, labelText)), - ISvgDescriptionElement svgDescriptionElement => new SvgDescriptionElementWrapper( - new ByLabelTextElementFactory(renderedFragment, svgDescriptionElement, labelText)), - ISvgForeignObjectElement svgForeignObjectElement => new SvgForeignObjectElementWrapper( - new ByLabelTextElementFactory(renderedFragment, svgForeignObjectElement, labelText)), - ISvgSvgElement svgSvgElement => new SvgSvgElementWrapper( - new ByLabelTextElementFactory(renderedFragment, svgSvgElement, labelText)), - ISvgTitleElement svgTitleElement => new SvgTitleElementWrapper( - new ByLabelTextElementFactory(renderedFragment, svgTitleElement, labelText)), - ISvgElement svgElement => new SvgElementWrapper( - new ByLabelTextElementFactory(renderedFragment, svgElement, labelText)), - IHtmlElement htmlElement => new HtmlElementWrapper( - new ByLabelTextElementFactory(renderedFragment, htmlElement, labelText)), - _ => new ElementWrapper( - new ByLabelTextElementFactory(renderedFragment, element, labelText)), - }; - } -} diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs index f7ddcb90b..b7b32e313 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs @@ -1,4 +1,5 @@ -using AngleSharp.Dom; +using AngleSharp.Dom; +using Bunit.Web.AngleSharp; namespace Bunit.Labels.Strategies; @@ -11,6 +12,6 @@ internal class LabelTextUsingAriaLabelStrategy : ILabelTextQueryStrategy if (element is null) return null; - return ElementWrapperFactory.CreateByLabelText(element, renderedFragment, labelText); + return element.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText)); } } diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs index b9c386fb4..ce6e200cc 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs @@ -1,4 +1,5 @@ -using AngleSharp.Dom; +using AngleSharp.Dom; +using Bunit.Web.AngleSharp; namespace Bunit.Labels.Strategies; @@ -12,7 +13,7 @@ internal class LabelTextUsingAriaLabelledByStrategy : ILabelTextQueryStrategy { var labelElement = renderedFragment.Nodes.QuerySelector($"#{element.GetAttribute("aria-labelledby")}"); if (labelElement.GetInnerText() == labelText) - return ElementWrapperFactory.CreateByLabelText(element, renderedFragment, labelText); + return element.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText)); } return null; diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs index 1900d0d0a..b9567efd7 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs @@ -1,4 +1,5 @@ -using AngleSharp.Dom; +using AngleSharp.Dom; +using Bunit.Web.AngleSharp; namespace Bunit.Labels.Strategies; @@ -17,6 +18,6 @@ internal class LabelTextUsingForAttributeStrategy : ILabelTextQueryStrategy if (matchingElement is null) return null; - return ElementWrapperFactory.CreateByLabelText(matchingElement, renderedFragment, labelText); + return matchingElement.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText)); } } diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs index a5f506da5..258429827 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs @@ -1,4 +1,5 @@ -using AngleSharp.Dom; +using AngleSharp.Dom; +using Bunit.Web.AngleSharp; namespace Bunit.Labels.Strategies; @@ -16,6 +17,6 @@ internal class LabelTextUsingWrappedElementStrategy : ILabelTextQueryStrategy if (matchingElement is null) return null; - return ElementWrapperFactory.CreateByLabelText(matchingElement, renderedFragment, labelText); + return matchingElement.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText)); } } diff --git a/src/bunit.web.query/bunit.web.query.csproj b/src/bunit.web.query/bunit.web.query.csproj index 42fbbe72e..ebce19a7c 100644 --- a/src/bunit.web.query/bunit.web.query.csproj +++ b/src/bunit.web.query/bunit.web.query.csproj @@ -51,8 +51,8 @@ - + From 109cc6b785f57fdba1a1214c35a3c630218aecfd Mon Sep 17 00:00:00 2001 From: scottsauber Date: Sat, 25 Nov 2023 23:04:54 -0600 Subject: [PATCH 15/42] fix: use custom labelnotfoundexception --- .../Labels/LabelNotFoundException.cs | 24 +++++++++++++++++++ .../Labels/LabelQueryExtensions.cs | 4 ++-- .../Labels/LabelQueryExtensionsTests.cs | 10 ++++---- 3 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 src/bunit.web.query/Labels/LabelNotFoundException.cs diff --git a/src/bunit.web.query/Labels/LabelNotFoundException.cs b/src/bunit.web.query/Labels/LabelNotFoundException.cs new file mode 100644 index 000000000..b57d419aa --- /dev/null +++ b/src/bunit.web.query/Labels/LabelNotFoundException.cs @@ -0,0 +1,24 @@ +namespace Bunit; + +/// +/// Represents a failure to find an element in the searched target +/// using the Label's text. +/// +[Serializable] +public class LabelNotFoundException : Exception +{ + /// + /// Gets the Label Text used to search with. + /// + public string LabelText { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + public LabelNotFoundException(string labelText) + : base($"Unable to find a label with the text of '{labelText}'.") + { + LabelText = labelText; + } +} diff --git a/src/bunit.web.query/Labels/LabelQueryExtensions.cs b/src/bunit.web.query/Labels/LabelQueryExtensions.cs index 831f299eb..dd2eb7a5a 100644 --- a/src/bunit.web.query/Labels/LabelQueryExtensions.cs +++ b/src/bunit.web.query/Labels/LabelQueryExtensions.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using AngleSharp.Dom; using Bunit.Labels.Strategies; @@ -22,7 +22,7 @@ public static class LabelQueryExtensions /// The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a ``) public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText) { - return FindByLabelTextInternal(renderedFragment, labelText) ?? throw new ElementNotFoundException(labelText); + return FindByLabelTextInternal(renderedFragment, labelText) ?? throw new LabelNotFoundException(labelText); } /// diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index 33456ea1c..41df22e15 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -1,4 +1,4 @@ -using Bunit.TestAssets.BlazorE2E; +using Bunit.TestAssets.BlazorE2E; namespace Bunit.Labels; @@ -33,8 +33,8 @@ public void Test002() var expectedLabelText = Guid.NewGuid().ToString(); var cut = RenderComponent(); - Should.Throw(() => cut.FindByLabelText(expectedLabelText)) - .CssSelector.ShouldBe(expectedLabelText); + Should.Throw(() => cut.FindByLabelText(expectedLabelText)) + .LabelText.ShouldBe(expectedLabelText); } [Theory(DisplayName = "Should return back element associated with label when label when is wrapped around element")] @@ -56,8 +56,8 @@ public void Test004() var expectedLabelText = "Label With Missing Input"; var cut = RenderComponent(); - Should.Throw(() => cut.FindByLabelText(expectedLabelText)) - .CssSelector.ShouldBe(expectedLabelText); + Should.Throw(() => cut.FindByLabelText(expectedLabelText)) + .LabelText.ShouldBe(expectedLabelText); } [Theory(DisplayName = "Should return back element associated with label when element uses aria-label")] From 1237a3ee4706701d5e9e2deedffbae1a326edd1c Mon Sep 17 00:00:00 2001 From: scottsauber Date: Sun, 26 Nov 2023 23:52:27 -0600 Subject: [PATCH 16/42] feat: support for different casing sensitivity --- .../ByLabelTextElementFactory.cs | 6 +- .../Labels/ByLabelTextOptions.cs | 7 + ...elOptions.cs => LabelElementExtensions.cs} | 4 +- .../Labels/LabelQueryExtensions.cs | 41 +++-- .../Strategies/ILabelTextQueryStrategy.cs | 4 +- .../LabelTextUsingAriaLabelStrategy.cs | 14 +- .../LabelTextUsingAriaLabelledByStrategy.cs | 10 +- .../LabelTextUsingForAttributeStrategy.cs | 10 +- .../LabelTextUsingWrappedElementStrategy.cs | 8 +- src/bunit.web.query/NodeListExtensions.cs | 32 ++++ .../Labels/LabelQueryExtensionsTests.cs | 140 +++++++++++++++++- 11 files changed, 227 insertions(+), 49 deletions(-) create mode 100644 src/bunit.web.query/Labels/ByLabelTextOptions.cs rename src/bunit.web.query/Labels/{LabelOptions.cs => LabelElementExtensions.cs} (81%) create mode 100644 src/bunit.web.query/NodeListExtensions.cs diff --git a/src/bunit.web.query/ByLabelTextElementFactory.cs b/src/bunit.web.query/ByLabelTextElementFactory.cs index 5c3b85f4f..eef786319 100644 --- a/src/bunit.web.query/ByLabelTextElementFactory.cs +++ b/src/bunit.web.query/ByLabelTextElementFactory.cs @@ -7,13 +7,15 @@ internal sealed class ByLabelTextElementFactory : IElementWrapperFactory { private readonly IRenderedFragment testTarget; private readonly string labelText; + private readonly ByLabelTextOptions options; public Action? OnElementReplaced { get; set; } - public ByLabelTextElementFactory(IRenderedFragment testTarget, string labelText) + public ByLabelTextElementFactory(IRenderedFragment testTarget, string labelText, ByLabelTextOptions options) { this.testTarget = testTarget; this.labelText = labelText; + this.options = options; testTarget.OnMarkupUpdated += FragmentsMarkupUpdated; } @@ -22,7 +24,7 @@ private void FragmentsMarkupUpdated(object? sender, EventArgs args) public TElement GetElement() where TElement : class, IElement { - var element = testTarget.FindByLabelTextInternal(labelText) as TElement; + var element = testTarget.FindByLabelTextInternal(labelText, options) as TElement; return element ?? throw new ElementRemovedFromDomException(labelText); } diff --git a/src/bunit.web.query/Labels/ByLabelTextOptions.cs b/src/bunit.web.query/Labels/ByLabelTextOptions.cs new file mode 100644 index 000000000..928c27bee --- /dev/null +++ b/src/bunit.web.query/Labels/ByLabelTextOptions.cs @@ -0,0 +1,7 @@ +namespace Bunit; + +public record ByLabelTextOptions +{ + public static readonly ByLabelTextOptions Default = new(); + public StringComparison ComparisonType { get; set; } = StringComparison.Ordinal; +} diff --git a/src/bunit.web.query/Labels/LabelOptions.cs b/src/bunit.web.query/Labels/LabelElementExtensions.cs similarity index 81% rename from src/bunit.web.query/Labels/LabelOptions.cs rename to src/bunit.web.query/Labels/LabelElementExtensions.cs index 2f5d74093..3575210f1 100644 --- a/src/bunit.web.query/Labels/LabelOptions.cs +++ b/src/bunit.web.query/Labels/LabelElementExtensions.cs @@ -1,8 +1,8 @@ -using AngleSharp.Dom; +using AngleSharp.Dom; namespace Bunit; -internal static class LabelOptions +internal static class LabelElementExtensions { internal static bool IsHtmlElementThatCanHaveALabel(this IElement element) => element.NodeName switch { diff --git a/src/bunit.web.query/Labels/LabelQueryExtensions.cs b/src/bunit.web.query/Labels/LabelQueryExtensions.cs index dd2eb7a5a..4e4a643b4 100644 --- a/src/bunit.web.query/Labels/LabelQueryExtensions.cs +++ b/src/bunit.web.query/Labels/LabelQueryExtensions.cs @@ -1,4 +1,3 @@ -using System.Collections.ObjectModel; using AngleSharp.Dom; using Bunit.Labels.Strategies; @@ -7,22 +6,26 @@ namespace Bunit; public static class LabelQueryExtensions { private static readonly IReadOnlyList LabelTextQueryStrategies = new ILabelTextQueryStrategy[] - { - // This is intentionally in the order of most likely to minimize strategies tried to find the label - new LabelTextUsingForAttributeStrategy(), - new LabelTextUsingAriaLabelStrategy(), - new LabelTextUsingWrappedElementStrategy(), - new LabelTextUsingAriaLabelledByStrategy(), - }; + { + // This is intentionally in the order of most likely to minimize strategies tried to find the label + new LabelTextUsingForAttributeStrategy(), + new LabelTextUsingAriaLabelStrategy(), + new LabelTextUsingWrappedElementStrategy(), + new LabelTextUsingAriaLabelledByStrategy(), + }; /// /// Returns the first element (i.e. an input, select, textarea, etc. element) associated with the given label text. /// /// The rendered fragment to search. /// The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a ``) - public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText) + /// Options to override the default behavior of FindByLabelText + public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText, ByLabelTextOptions? options = null) { - return FindByLabelTextInternal(renderedFragment, labelText) ?? throw new LabelNotFoundException(labelText); + options ??= ByLabelTextOptions.Default; + + return FindByLabelTextInternal(renderedFragment, labelText, options) ?? + throw new LabelNotFoundException(labelText); } /// @@ -30,21 +33,15 @@ public static IElement FindByLabelText(this IRenderedFragment renderedFragment, /// /// The rendered fragment to search. /// The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a ``) - internal static IElement? FindByLabelTextInternal(this IRenderedFragment renderedFragment, string labelText) + /// Options to override the default behavior of FindByLabelText + internal static IElement? FindByLabelTextInternal(this IRenderedFragment renderedFragment, string labelText, ByLabelTextOptions options) { - try + foreach (var strategy in LabelTextQueryStrategies) { - foreach (var strategy in LabelTextQueryStrategies) - { - var element = strategy.FindElement(renderedFragment, labelText); + var element = strategy.FindElement(renderedFragment, labelText, options); - if (element != null) - return element; - } - } - catch (DomException exception) when (exception.Message == "The string did not match the expected pattern.") - { - return null; + if (element is not null) + return element; } return null; diff --git a/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs b/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs index 98ead62c3..3c1ba8ea5 100644 --- a/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/ILabelTextQueryStrategy.cs @@ -1,8 +1,8 @@ -using AngleSharp.Dom; +using AngleSharp.Dom; namespace Bunit.Labels.Strategies; internal interface ILabelTextQueryStrategy { - IElement? FindElement(IRenderedFragment renderedFragment, string labelText); + IElement? FindElement(IRenderedFragment renderedFragment, string labelText, ByLabelTextOptions options); } diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs index b7b32e313..866752206 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs @@ -5,13 +5,21 @@ namespace Bunit.Labels.Strategies; internal class LabelTextUsingAriaLabelStrategy : ILabelTextQueryStrategy { - public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) + public IElement? FindElement(IRenderedFragment renderedFragment, string labelText, ByLabelTextOptions options) { - var element = renderedFragment.Nodes.QuerySelector($"[aria-label='{labelText}']"); + var caseSensitivityQualifier = options.ComparisonType switch + { + StringComparison.OrdinalIgnoreCase => "i", + StringComparison.InvariantCultureIgnoreCase => "i", + StringComparison.CurrentCultureIgnoreCase => "i", + _ => "" + }; + + var element = renderedFragment.Nodes.TryQuerySelector($"[aria-label='{labelText}'{caseSensitivityQualifier}]"); if (element is null) return null; - return element.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText)); + return element.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText, options)); } } diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs index ce6e200cc..480c74c5a 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelledByStrategy.cs @@ -5,15 +5,15 @@ namespace Bunit.Labels.Strategies; internal class LabelTextUsingAriaLabelledByStrategy : ILabelTextQueryStrategy { - public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) + public IElement? FindElement(IRenderedFragment renderedFragment, string labelText, ByLabelTextOptions options) { - var elementsWithAriaLabelledBy = renderedFragment.Nodes.QuerySelectorAll("[aria-labelledby]"); + var elementsWithAriaLabelledBy = renderedFragment.Nodes.TryQuerySelectorAll("[aria-labelledby]"); foreach (var element in elementsWithAriaLabelledBy) { - var labelElement = renderedFragment.Nodes.QuerySelector($"#{element.GetAttribute("aria-labelledby")}"); - if (labelElement.GetInnerText() == labelText) - return element.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText)); + var labelElement = renderedFragment.Nodes.TryQuerySelector($"#{element.GetAttribute("aria-labelledby")}"); + if (labelElement.GetInnerText().Equals(labelText, options.ComparisonType)) + return element.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText, options)); } return null; diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs index b9567efd7..4167be3be 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingForAttributeStrategy.cs @@ -5,19 +5,19 @@ namespace Bunit.Labels.Strategies; internal class LabelTextUsingForAttributeStrategy : ILabelTextQueryStrategy { - public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) + public IElement? FindElement(IRenderedFragment renderedFragment, string labelText, ByLabelTextOptions options) { - var labels = renderedFragment.Nodes.QuerySelectorAll("label"); - var matchingLabel = labels.SingleOrDefault(l => l.TextContent == labelText); + var matchingLabel = renderedFragment.Nodes.TryQuerySelectorAll("label") + .SingleOrDefault(l => l.TextContent.Equals(labelText, options.ComparisonType)); if (matchingLabel is null) return null; - var matchingElement = renderedFragment.Nodes.QuerySelector($"#{matchingLabel.GetAttribute("for")}"); + var matchingElement = renderedFragment.Nodes.TryQuerySelector($"#{matchingLabel.GetAttribute("for")}"); if (matchingElement is null) return null; - return matchingElement.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText)); + return matchingElement.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText, options)); } } diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs index 258429827..78f0f62f6 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingWrappedElementStrategy.cs @@ -5,10 +5,10 @@ namespace Bunit.Labels.Strategies; internal class LabelTextUsingWrappedElementStrategy : ILabelTextQueryStrategy { - public IElement? FindElement(IRenderedFragment renderedFragment, string labelText) + public IElement? FindElement(IRenderedFragment renderedFragment, string labelText, ByLabelTextOptions options) { - var labels = renderedFragment.Nodes.QuerySelectorAll("label"); - var matchingLabel = labels.SingleOrDefault(l => l.InnerHtml.StartsWith(labelText)); + var matchingLabel = renderedFragment.Nodes.TryQuerySelectorAll("label") + .SingleOrDefault(l => l.InnerHtml.StartsWith(labelText, options.ComparisonType)); var matchingElement = matchingLabel? .Children @@ -17,6 +17,6 @@ internal class LabelTextUsingWrappedElementStrategy : ILabelTextQueryStrategy if (matchingElement is null) return null; - return matchingElement.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText)); + return matchingElement.WrapUsing(new ByLabelTextElementFactory(renderedFragment, labelText, options)); } } diff --git a/src/bunit.web.query/NodeListExtensions.cs b/src/bunit.web.query/NodeListExtensions.cs new file mode 100644 index 000000000..405476a20 --- /dev/null +++ b/src/bunit.web.query/NodeListExtensions.cs @@ -0,0 +1,32 @@ +using AngleSharp.Css.Dom; +using AngleSharp.Css.Parser; +using AngleSharp.Dom; + +namespace Bunit; + +internal static class NodeListExtensions +{ + internal static IElement? TryQuerySelector(this INodeList nodes, string cssSelector) + { + if (nodes.Length == 0 || + nodes[0].Owner?.Context.GetService() is not ICssSelectorParser cssParser) + return null; + + if (cssParser.ParseSelector(cssSelector) is not ISelector selector) + return null; + + return nodes.QuerySelector(selector); + } + + internal static IEnumerable TryQuerySelectorAll(this INodeList nodes, string cssSelector) + { + if (nodes.Length == 0 || + nodes[0].Owner?.Context.GetService() is not ICssSelectorParser cssParser) + return Enumerable.Empty(); + + if (cssParser.ParseSelector(cssSelector) is not ISelector selector) + return Enumerable.Empty(); + + return nodes.QuerySelectorAll(selector); + } +} diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index 41df22e15..02c1992e0 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -14,7 +14,7 @@ public class LabelQueryExtensionsTests : TestContext "progress", }; - [Theory(DisplayName = "Should return back associated element with label when using the for attribute")] + [Theory(DisplayName = "Should return back associated element with label when using the for attribute with the correct casing")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test001(string htmlElementWithLabel) { @@ -37,7 +37,7 @@ public void Test002() .LabelText.ShouldBe(expectedLabelText); } - [Theory(DisplayName = "Should return back element associated with label when label when is wrapped around element")] + [Theory(DisplayName = "Should return back element associated with label when label when is wrapped around element with the correct casing")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test003(string htmlElementWithLabel) { @@ -60,7 +60,7 @@ public void Test004() .LabelText.ShouldBe(expectedLabelText); } - [Theory(DisplayName = "Should return back element associated with label when element uses aria-label")] + [Theory(DisplayName = "Should return back element associated with label when element uses aria-label with the correct casing")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test005(string htmlElementWithLabel) { @@ -73,7 +73,7 @@ public void Test005(string htmlElementWithLabel) input.Id.ShouldBe($"{htmlElementWithLabel}-with-aria-label"); } - [Theory(DisplayName = "Should return back element associated with another element when that other element uses aria-labelledby")] + [Theory(DisplayName = "Should return back element associated with another element that uses aria-labelledby with the correct casing")] [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test006(string htmlElementWithLabel) { @@ -101,4 +101,136 @@ public void Test007(string labelText) cut.Find("#increment-button").Click(); input.GetAttribute("value").ShouldBe("1"); } + + [Theory(DisplayName = "Should throw LabelNotFoundException when ComparisonType is case sensitive and incorrect casing is used with for attribute")] + [InlineData(StringComparison.Ordinal)] + [InlineData(StringComparison.InvariantCulture)] + [InlineData(StringComparison.CurrentCulture)] + public void Test009(StringComparison comparison) + { + var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; + var expectedLabelText = "LABEL TEXT"; + var cut = RenderComponent(ps => + ps.AddChildContent("""""")); + + Should.Throw(() => cut.FindByLabelText(expectedLabelText, labelTextOptions)) + .LabelText.ShouldBe(expectedLabelText); + } + + [Theory(DisplayName = "Should return back element associated with label when ComparisonType is case insensitive and incorrect casing is used with for attribute")] + [InlineData(StringComparison.OrdinalIgnoreCase)] + [InlineData(StringComparison.InvariantCultureIgnoreCase)] + [InlineData(StringComparison.CurrentCultureIgnoreCase)] + public void Test010(StringComparison comparison) + { + var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; + var expectedLabelText = "LABEL TEXT"; + var cut = RenderComponent(ps => + ps.AddChildContent("""""")); + + var input = cut.FindByLabelText(expectedLabelText, labelTextOptions); + + input.ShouldNotBeNull(); + input.NodeName.ShouldBe("INPUT"); + input.Id.ShouldBe("input-1"); + } + + [Theory(DisplayName = "Should throw LabelNotFoundException when ComparisonType is case sensitive and incorrect casing is used with wrapped label")] + [InlineData(StringComparison.Ordinal)] + [InlineData(StringComparison.InvariantCulture)] + [InlineData(StringComparison.CurrentCulture)] + public void Test011(StringComparison comparison) + { + var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; + var expectedLabelText = "LABEL TEXT"; + var cut = RenderComponent(ps => + ps.AddChildContent("""""")); + + Should.Throw(() => cut.FindByLabelText(expectedLabelText, labelTextOptions)) + .LabelText.ShouldBe(expectedLabelText); + } + + [Theory(DisplayName = "Should return back element associated with label when ComparisonType is case insensitive and incorrect casing is used with wrapped label")] + [InlineData(StringComparison.OrdinalIgnoreCase)] + [InlineData(StringComparison.InvariantCultureIgnoreCase)] + [InlineData(StringComparison.CurrentCultureIgnoreCase)] + public void Test012(StringComparison comparison) + { + var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; + var expectedLabelText = "LABEL TEXT"; + var cut = RenderComponent(ps => + ps.AddChildContent("""""")); + + var input = cut.FindByLabelText(expectedLabelText, labelTextOptions); + + input.ShouldNotBeNull(); + input.NodeName.ShouldBe("INPUT"); + input.Id.ShouldBe("input-1"); + } + + [Theory(DisplayName = "Should throw LabelNotFoundException when ComparisonType is case sensitive and incorrect casing is used with aria-label")] + [InlineData(StringComparison.Ordinal)] + [InlineData(StringComparison.InvariantCulture)] + [InlineData(StringComparison.CurrentCulture)] + public void Test013(StringComparison comparison) + { + var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; + var expectedLabelText = "LABEL TEXT"; + var cut = RenderComponent(ps => + ps.AddChildContent("""""")); + + Should.Throw(() => cut.FindByLabelText(expectedLabelText, labelTextOptions)) + .LabelText.ShouldBe(expectedLabelText); + } + + [Theory(DisplayName = "Should return back element associated with label when ComparisonType is case insensitive and incorrect casing is used with aria-label")] + [InlineData(StringComparison.OrdinalIgnoreCase)] + [InlineData(StringComparison.InvariantCultureIgnoreCase)] + [InlineData(StringComparison.CurrentCultureIgnoreCase)] + public void Test014(StringComparison comparison) + { + var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; + var expectedLabelText = "LABEL TEXT"; + var cut = RenderComponent(ps => + ps.AddChildContent("""""")); + + var input = cut.FindByLabelText(expectedLabelText, labelTextOptions); + + input.ShouldNotBeNull(); + input.NodeName.ShouldBe("INPUT"); + input.Id.ShouldBe("input-1"); + } + + [Theory(DisplayName = "Should throw LabelNotFoundException when ComparisonType is case insensitive and incorrect casing is used with aria-labelledby")] + [InlineData(StringComparison.Ordinal)] + [InlineData(StringComparison.InvariantCulture)] + [InlineData(StringComparison.CurrentCulture)] + public void Test015(StringComparison comparison) + { + var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; + var expectedLabelText = "LABEL TEXT"; + var cut = RenderComponent(ps => + ps.AddChildContent("""

Label Text

""")); + + Should.Throw(() => cut.FindByLabelText(expectedLabelText, labelTextOptions)) + .LabelText.ShouldBe(expectedLabelText); + } + + [Theory(DisplayName = "Should return back element associated with label when ComparisonType is case insensitive and incorrect casing is used with aria-labelledby")] + [InlineData(StringComparison.OrdinalIgnoreCase)] + [InlineData(StringComparison.InvariantCultureIgnoreCase)] + [InlineData(StringComparison.CurrentCultureIgnoreCase)] + public void Test016(StringComparison comparison) + { + var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; + var expectedLabelText = "LABEL TEXT"; + var cut = RenderComponent(ps => + ps.AddChildContent("""

Label Text

""")); + + var input = cut.FindByLabelText(expectedLabelText, labelTextOptions); + + input.ShouldNotBeNull(); + input.NodeName.ShouldBe("INPUT"); + input.Id.ShouldBe("input-1"); + } } From 7ebab187ad8a015dcc3f20337039dfd7ec39f323 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Mon, 27 Nov 2023 00:05:38 -0600 Subject: [PATCH 17/42] chore: add xml docs to indicate defaults of ByLabelTextOptions --- src/bunit.web.query/Labels/ByLabelTextOptions.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/bunit.web.query/Labels/ByLabelTextOptions.cs b/src/bunit.web.query/Labels/ByLabelTextOptions.cs index 928c27bee..649162ab4 100644 --- a/src/bunit.web.query/Labels/ByLabelTextOptions.cs +++ b/src/bunit.web.query/Labels/ByLabelTextOptions.cs @@ -1,7 +1,17 @@ namespace Bunit; +/// +/// Allows overrides of behavior for FindByLabelText method +/// public record ByLabelTextOptions { + /// + /// The default behavior used by FindByLabelText if no overrides are specified + /// public static readonly ByLabelTextOptions Default = new(); + + /// + /// The StringComparison used for comparing the desired Label Text to the resulting HTML. Defaults to Ordinal (case sensitive). + /// public StringComparison ComparisonType { get; set; } = StringComparison.Ordinal; } From e063ad933082e64196e5b53adc29e5bf4d59aa49 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 12 Mar 2024 11:18:33 -0700 Subject: [PATCH 18/42] fix: make classes add public --- src/bunit.generators.internal/Web.AngleSharp/IElementWrapper.cs | 2 +- .../Web.AngleSharp/IElementWrapperFactory.cs | 2 +- src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bunit.generators.internal/Web.AngleSharp/IElementWrapper.cs b/src/bunit.generators.internal/Web.AngleSharp/IElementWrapper.cs index 13bbc1e2b..de2596bdc 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/IElementWrapper.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/IElementWrapper.cs @@ -8,7 +8,7 @@ namespace Bunit.Web.AngleSharp; /// Represents a wrapper around an . ///
[GeneratedCodeAttribute("Bunit.Web.AngleSharp", "1.0.0.0")] -internal interface IElementWrapper where TElement : class, IElement +public interface IElementWrapper where TElement : class, IElement { /// /// Gets the wrapped element. diff --git a/src/bunit.generators.internal/Web.AngleSharp/IElementWrapperFactory.cs b/src/bunit.generators.internal/Web.AngleSharp/IElementWrapperFactory.cs index 0b5e5ce98..86f13f3c5 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/IElementWrapperFactory.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/IElementWrapperFactory.cs @@ -9,7 +9,7 @@ namespace Bunit.Web.AngleSharp; /// Represents an factory, used by a . /// [GeneratedCodeAttribute("Bunit.Web.AngleSharp", "1.0.0.0")] -internal interface IElementWrapperFactory +public interface IElementWrapperFactory { /// /// A method that returns the latest version of the element to wrap. diff --git a/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs b/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs index f24e9e124..8dd4d5963 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs @@ -11,7 +11,7 @@ namespace Bunit.Web.AngleSharp; /// [DebuggerNonUserCode] [GeneratedCodeAttribute("Bunit.Web.AngleSharp", "1.0.0.0")] -internal abstract class WrapperBase : IElementWrapper +public abstract class WrapperBase : IElementWrapper where TElement : class, IElement { private readonly IElementWrapperFactory elementFactory; From 667e6f75d2045b9dd2c7b42545fd1486398c6fed Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 12 Mar 2024 11:19:10 -0700 Subject: [PATCH 19/42] fix: remove project references --- src/bunit.web.query/bunit.web.query.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bunit.web.query/bunit.web.query.csproj b/src/bunit.web.query/bunit.web.query.csproj index ebce19a7c..254433b64 100644 --- a/src/bunit.web.query/bunit.web.query.csproj +++ b/src/bunit.web.query/bunit.web.query.csproj @@ -52,7 +52,6 @@ - From 8b8b1a93c067e310c9894ea143b694f8b63dcf18 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 12 Mar 2024 11:28:28 -0700 Subject: [PATCH 20/42] fix: move to source generator to public --- .../Web.AngleSharp/WrapperElementsGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs index 8d7f6d921..b53132171 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/WrapperElementsGenerator.cs @@ -66,7 +66,7 @@ private static void GenerateWrapperFactory(StringBuilder source, IEnumerable(this global::AngleSharp.Dom.IElement element, TElementFactory elementFactory) where TElementFactory : Bunit.Web.AngleSharp.IElementWrapperFactory => element switch"); From a4f7a5e2d76bed326ac264328a6057f493b940bd Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 12 Mar 2024 15:05:18 -0700 Subject: [PATCH 21/42] chore: switch to use wrapper component for tests --- .../BlazorE2E/LabelQueryComponent.razor | 22 -------------- .../Labels/LabelQueryExtensionsTests.cs | 29 +++++++++++++++---- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor index 3d7a24ac0..f19268d0e 100644 --- a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor @@ -1,25 +1,3 @@ -@* Testing the for attribute *@ -@foreach (var htmlElement in htmlElementsThatCanHaveALabel) -{ - - @((MarkupString) $"""<{htmlElement} id="{htmlElement}-with-label">""") -} - -@* This is ignored but just making sure we don't ever pick this up *@ - - - -@* Testing wrapped label *@ -@foreach (var htmlElement in htmlElementsThatCanHaveALabel) -{ - -} - -@* Testing no input *@ - - @* Testing aria-label *@ @foreach (var htmlElement in htmlElementsThatCanHaveALabel) { diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index 02c1992e0..be411b4a8 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -1,3 +1,4 @@ +using AngleSharp.Html.Dom; using Bunit.TestAssets.BlazorE2E; namespace Bunit.Labels; @@ -18,7 +19,11 @@ public class LabelQueryExtensionsTests : TestContext [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test001(string htmlElementWithLabel) { - var cut = RenderComponent(); + var cut = RenderComponent(ps => + ps.AddChildContent($""" + + <{htmlElementWithLabel} id="{htmlElementWithLabel}-with-label" /> + """)); var input = cut.FindByLabelText($"Label for {htmlElementWithLabel} 1"); @@ -31,7 +36,10 @@ public void Test001(string htmlElementWithLabel) public void Test002() { var expectedLabelText = Guid.NewGuid().ToString(); - var cut = RenderComponent(); + var cut = RenderComponent(ps => + ps.AddChildContent($""" + {Guid.NewGuid()} + """)); Should.Throw(() => cut.FindByLabelText(expectedLabelText)) .LabelText.ShouldBe(expectedLabelText); @@ -41,7 +49,12 @@ public void Test002() [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test003(string htmlElementWithLabel) { - var cut = RenderComponent(); + var cut = RenderComponent(ps => + ps.AddChildContent($""" + + """)); var input = cut.FindByLabelText($"{htmlElementWithLabel} Wrapped Label"); @@ -54,7 +67,10 @@ public void Test003(string htmlElementWithLabel) public void Test004() { var expectedLabelText = "Label With Missing Input"; - var cut = RenderComponent(); + var cut = RenderComponent(ps => + ps.AddChildContent($""" + + """)); Should.Throw(() => cut.FindByLabelText(expectedLabelText)) .LabelText.ShouldBe(expectedLabelText); @@ -64,7 +80,10 @@ public void Test004() [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test005(string htmlElementWithLabel) { - var cut = RenderComponent(); + var cut = RenderComponent(ps => + ps.AddChildContent($""" + <{htmlElementWithLabel} id="{htmlElementWithLabel}-with-aria-label" aria-label="{htmlElementWithLabel} Aria Label" /> + """)); var input = cut.FindByLabelText($"{htmlElementWithLabel} Aria Label"); From f8e98b0c3e0b3099d32cd84ff44795c6c42b39ba Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 12 Mar 2024 15:07:36 -0700 Subject: [PATCH 22/42] chore: switch to use wrapper component for tests --- tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor | 6 ------ .../Labels/LabelQueryExtensionsTests.cs | 6 +++++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor index f19268d0e..f28dce8db 100644 --- a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor @@ -1,9 +1,3 @@ -@* Testing aria-label *@ -@foreach (var htmlElement in htmlElementsThatCanHaveALabel) -{ - @((MarkupString) $"""<{htmlElement} id="{htmlElement}-with-aria-label" aria-label="{htmlElement} Aria Label">""") -} - @* Testing aria-labelledby *@ @foreach (var htmlElement in htmlElementsThatCanHaveALabel) { diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index be411b4a8..fdc8aa0f9 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -96,7 +96,11 @@ public void Test005(string htmlElementWithLabel) [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test006(string htmlElementWithLabel) { - var cut = RenderComponent(); + var cut = RenderComponent(ps => + ps.AddChildContent($""" +

{htmlElementWithLabel} Aria Labelled By

+ <{htmlElementWithLabel} aria-labelledby="{htmlElementWithLabel}-with-aria-labelledby" /> + """)); var input = cut.FindByLabelText($"{htmlElementWithLabel} Aria Labelled By"); From a8daf3cba678f758d975536a60bd7ec1e15580b3 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Tue, 12 Mar 2024 15:11:15 -0700 Subject: [PATCH 23/42] chore: rename to labelquerycounter for re-rendering test --- ...omponent.razor => LabelQueryCounter.razor} | 19 ------------------- .../Labels/LabelQueryExtensionsTests.cs | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) rename tests/bunit.testassets/BlazorE2E/{LabelQueryComponent.razor => LabelQueryCounter.razor} (60%) diff --git a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor b/tests/bunit.testassets/BlazorE2E/LabelQueryCounter.razor similarity index 60% rename from tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor rename to tests/bunit.testassets/BlazorE2E/LabelQueryCounter.razor index f28dce8db..65adc4754 100644 --- a/tests/bunit.testassets/BlazorE2E/LabelQueryComponent.razor +++ b/tests/bunit.testassets/BlazorE2E/LabelQueryCounter.razor @@ -1,11 +1,3 @@ -@* Testing aria-labelledby *@ -@foreach (var htmlElement in htmlElementsThatCanHaveALabel) -{ -

@htmlElement Aria Labelled By

- @((MarkupString) $"""<{htmlElement} aria-labelledby="{htmlElement}-with-aria-labelledby">""") -} - - @* Testing we get back the re-rendered element for an aria-label *@ @@ -22,17 +14,6 @@ @code { - readonly List htmlElementsThatCanHaveALabel = new() - { - "input", - "select", - "textarea", - "button", - "meter", - "output", - "progress", - }; - private int _count; public void IncrementCount() => _count++; } diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index fdc8aa0f9..1a6656da9 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -116,7 +116,7 @@ public void Test006(string htmlElementWithLabel) [InlineData("Re-rendered input with Aria Labelledby")] public void Test007(string labelText) { - var cut = RenderComponent(); + var cut = RenderComponent(); var input = cut.FindByLabelText(labelText); input.GetAttribute("value").ShouldBe("0"); From ae78a6d4de237052455c21b80f7c670456e261ff Mon Sep 17 00:00:00 2001 From: scottsauber Date: Wed, 13 Mar 2024 13:20:34 -0700 Subject: [PATCH 24/42] fix: remove warnings --- src/bunit.web.query/Labels/LabelNotFoundException.cs | 7 +++++++ src/bunit.web.query/Labels/LabelQueryExtensions.cs | 3 +++ .../Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/bunit.web.query/Labels/LabelNotFoundException.cs b/src/bunit.web.query/Labels/LabelNotFoundException.cs index b57d419aa..2d941f62b 100644 --- a/src/bunit.web.query/Labels/LabelNotFoundException.cs +++ b/src/bunit.web.query/Labels/LabelNotFoundException.cs @@ -21,4 +21,11 @@ public LabelNotFoundException(string labelText) { LabelText = labelText; } + + + /// + /// Initializes a new instance of the class. + /// + protected LabelNotFoundException(SerializationInfo info, StreamingContext context) + : base(info, context) { } } diff --git a/src/bunit.web.query/Labels/LabelQueryExtensions.cs b/src/bunit.web.query/Labels/LabelQueryExtensions.cs index 4e4a643b4..c8c8f5abc 100644 --- a/src/bunit.web.query/Labels/LabelQueryExtensions.cs +++ b/src/bunit.web.query/Labels/LabelQueryExtensions.cs @@ -3,6 +3,9 @@ namespace Bunit; +/// +/// Extension methods for querying IRenderedFragments by Label +/// public static class LabelQueryExtensions { private static readonly IReadOnlyList LabelTextQueryStrategies = new ILabelTextQueryStrategy[] diff --git a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs index 866752206..2ef029809 100644 --- a/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs +++ b/src/bunit.web.query/Labels/Strategies/LabelTextUsingAriaLabelStrategy.cs @@ -3,7 +3,7 @@ namespace Bunit.Labels.Strategies; -internal class LabelTextUsingAriaLabelStrategy : ILabelTextQueryStrategy +internal sealed class LabelTextUsingAriaLabelStrategy : ILabelTextQueryStrategy { public IElement? FindElement(IRenderedFragment renderedFragment, string labelText, ByLabelTextOptions options) { From ad176c5188c77f480ab13d08b9c07847a8976027 Mon Sep 17 00:00:00 2001 From: scottsauber Date: Wed, 13 Mar 2024 13:21:34 -0700 Subject: [PATCH 25/42] refactor: remove string duplication in tests --- .../Labels/LabelQueryExtensionsTests.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index 1a6656da9..fa1365bcf 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -19,13 +19,14 @@ public class LabelQueryExtensionsTests : TestContext [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test001(string htmlElementWithLabel) { + var labelText = $"Label for {htmlElementWithLabel} 1"; var cut = RenderComponent(ps => ps.AddChildContent($""" - + <{htmlElementWithLabel} id="{htmlElementWithLabel}-with-label" /> """)); - var input = cut.FindByLabelText($"Label for {htmlElementWithLabel} 1"); + var input = cut.FindByLabelText(labelText); input.ShouldNotBeNull(); input.NodeName.ShouldBe(htmlElementWithLabel, StringCompareShould.IgnoreCase); @@ -49,14 +50,15 @@ public void Test002() [MemberData(nameof(HtmlElementsThatCanHaveALabel))] public void Test003(string htmlElementWithLabel) { + var labelText = $"{htmlElementWithLabel} Wrapped Label"; var cut = RenderComponent(ps => ps.AddChildContent($""" -
public static class LabelQueryExtensions { - private static readonly IReadOnlyList LabelTextQueryStrategies = new ILabelTextQueryStrategy[] - { + private static readonly IReadOnlyList LabelTextQueryStrategies = + [ // This is intentionally in the order of most likely to minimize strategies tried to find the label new LabelTextUsingForAttributeStrategy(), new LabelTextUsingAriaLabelStrategy(), new LabelTextUsingWrappedElementStrategy(), new LabelTextUsingAriaLabelledByStrategy(), - }; + ]; /// /// Returns the first element (i.e. an input, select, textarea, etc. element) associated with the given label text. /// /// The rendered fragment to search. /// The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a ``) - /// Options to override the default behavior of FindByLabelText - public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText, ByLabelTextOptions? options = null) + /// Method used to override the default behavior of FindByLabelText. + public static IElement FindByLabelText(this IRenderedFragment renderedFragment, string labelText, Action? configureOptions = null) { - options ??= ByLabelTextOptions.Default; + var options = ByLabelTextOptions.Default; + if (configureOptions is not null) + { + options = options with { }; + configureOptions.Invoke(options); + } - return FindByLabelTextInternal(renderedFragment, labelText, options) ?? - throw new LabelNotFoundException(labelText); + return FindByLabelTextInternal(renderedFragment, labelText, options) ?? throw new LabelNotFoundException(labelText); } - /// - /// Returns the first element (i.e. an input, select, textarea, etc. element) associated with the given label text. - /// - /// The rendered fragment to search. - /// The text of the label to search (i.e. the InnerText of the Label, such as "First Name" for a ``) - /// Options to override the default behavior of FindByLabelText internal static IElement? FindByLabelTextInternal(this IRenderedFragment renderedFragment, string labelText, ByLabelTextOptions options) { foreach (var strategy in LabelTextQueryStrategies) diff --git a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs index 347540c3f..9ee2c2e6c 100644 --- a/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs +++ b/tests/bunit.web.query.tests/Labels/LabelQueryExtensionsTests.cs @@ -134,12 +134,11 @@ public void Test007(string labelText) [InlineData(StringComparison.CurrentCulture)] public void Test009(StringComparison comparison) { - var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; var expectedLabelText = "LABEL TEXT"; var cut = RenderComponent(ps => ps.AddChildContent("""""")); - Should.Throw(() => cut.FindByLabelText(expectedLabelText, labelTextOptions)) + Should.Throw(() => cut.FindByLabelText(expectedLabelText, o => o.ComparisonType = comparison)) .LabelText.ShouldBe(expectedLabelText); } @@ -149,12 +148,11 @@ public void Test009(StringComparison comparison) [InlineData(StringComparison.CurrentCultureIgnoreCase)] public void Test010(StringComparison comparison) { - var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; var expectedLabelText = "LABEL TEXT"; var cut = RenderComponent(ps => ps.AddChildContent("""""")); - var input = cut.FindByLabelText(expectedLabelText, labelTextOptions); + var input = cut.FindByLabelText(expectedLabelText, o => o.ComparisonType = comparison); input.ShouldNotBeNull(); input.NodeName.ShouldBe("INPUT"); @@ -167,12 +165,11 @@ public void Test010(StringComparison comparison) [InlineData(StringComparison.CurrentCulture)] public void Test011(StringComparison comparison) { - var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; var expectedLabelText = "LABEL TEXT"; var cut = RenderComponent(ps => ps.AddChildContent("""""")); - Should.Throw(() => cut.FindByLabelText(expectedLabelText, labelTextOptions)) + Should.Throw(() => cut.FindByLabelText(expectedLabelText, o => o.ComparisonType = comparison)) .LabelText.ShouldBe(expectedLabelText); } @@ -182,12 +179,11 @@ public void Test011(StringComparison comparison) [InlineData(StringComparison.CurrentCultureIgnoreCase)] public void Test012(StringComparison comparison) { - var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; var expectedLabelText = "LABEL TEXT"; var cut = RenderComponent(ps => ps.AddChildContent("""""")); - var input = cut.FindByLabelText(expectedLabelText, labelTextOptions); + var input = cut.FindByLabelText(expectedLabelText, o => o.ComparisonType = comparison); input.ShouldNotBeNull(); input.NodeName.ShouldBe("INPUT"); @@ -200,12 +196,11 @@ public void Test012(StringComparison comparison) [InlineData(StringComparison.CurrentCulture)] public void Test013(StringComparison comparison) { - var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; var expectedLabelText = "LABEL TEXT"; var cut = RenderComponent(ps => ps.AddChildContent("""""")); - Should.Throw(() => cut.FindByLabelText(expectedLabelText, labelTextOptions)) + Should.Throw(() => cut.FindByLabelText(expectedLabelText, o => o.ComparisonType = comparison)) .LabelText.ShouldBe(expectedLabelText); } @@ -215,12 +210,11 @@ public void Test013(StringComparison comparison) [InlineData(StringComparison.CurrentCultureIgnoreCase)] public void Test014(StringComparison comparison) { - var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; var expectedLabelText = "LABEL TEXT"; var cut = RenderComponent(ps => ps.AddChildContent("""""")); - var input = cut.FindByLabelText(expectedLabelText, labelTextOptions); + var input = cut.FindByLabelText(expectedLabelText, o => o.ComparisonType = comparison); input.ShouldNotBeNull(); input.NodeName.ShouldBe("INPUT"); @@ -233,12 +227,11 @@ public void Test014(StringComparison comparison) [InlineData(StringComparison.CurrentCulture)] public void Test015(StringComparison comparison) { - var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; var expectedLabelText = "LABEL TEXT"; var cut = RenderComponent(ps => ps.AddChildContent("""

Label Text

""")); - Should.Throw(() => cut.FindByLabelText(expectedLabelText, labelTextOptions)) + Should.Throw(() => cut.FindByLabelText(expectedLabelText, o => o.ComparisonType = comparison)) .LabelText.ShouldBe(expectedLabelText); } @@ -248,12 +241,11 @@ public void Test015(StringComparison comparison) [InlineData(StringComparison.CurrentCultureIgnoreCase)] public void Test016(StringComparison comparison) { - var labelTextOptions = new ByLabelTextOptions { ComparisonType = comparison }; var expectedLabelText = "LABEL TEXT"; var cut = RenderComponent(ps => ps.AddChildContent("""

Label Text

""")); - var input = cut.FindByLabelText(expectedLabelText, labelTextOptions); + var input = cut.FindByLabelText(expectedLabelText, o => o.ComparisonType = comparison); input.ShouldNotBeNull(); input.NodeName.ShouldBe("INPUT");