From ceefbd85386047472a99e7a9d188b1972afb0c32 Mon Sep 17 00:00:00 2001 From: James Sampica Date: Wed, 31 Dec 2025 00:43:42 -0600 Subject: [PATCH 1/3] feat: added FindByTestId --- .../TestIds/TestIdNotFoundException.cs | 23 ++++ src/bunit.web.query/TestIds/TestIdOptions.cs | 19 ++++ .../TestIds/TestIdQueryExtensions.cs | 42 ++++++++ .../TestIds/TestIdQueryExtensionsTests.cs | 102 ++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 src/bunit.web.query/TestIds/TestIdNotFoundException.cs create mode 100644 src/bunit.web.query/TestIds/TestIdOptions.cs create mode 100644 src/bunit.web.query/TestIds/TestIdQueryExtensions.cs create mode 100644 tests/bunit.web.query.tests/TestIds/TestIdQueryExtensionsTests.cs diff --git a/src/bunit.web.query/TestIds/TestIdNotFoundException.cs b/src/bunit.web.query/TestIds/TestIdNotFoundException.cs new file mode 100644 index 000000000..422449d72 --- /dev/null +++ b/src/bunit.web.query/TestIds/TestIdNotFoundException.cs @@ -0,0 +1,23 @@ +namespace Bunit.TestIds; + +/// +/// Represents a failure to find an element in the searched target +/// using the specified test id. +/// +public sealed class TestIdNotFoundException : Exception +{ + /// + /// Gets the test id used to search with. + /// + public string? TestId { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The test id that was searched for. + public TestIdNotFoundException(string? testId = null) + : base($"Unable to find an element with the Test ID '{testId}'.") + { + TestId = testId; + } +} diff --git a/src/bunit.web.query/TestIds/TestIdOptions.cs b/src/bunit.web.query/TestIds/TestIdOptions.cs new file mode 100644 index 000000000..8d4d79a78 --- /dev/null +++ b/src/bunit.web.query/TestIds/TestIdOptions.cs @@ -0,0 +1,19 @@ +namespace Bunit.TestIds; + +/// +/// Allows overrides of behavior for FindByTestId method +/// +public record class ByTestIdOptions +{ + internal static readonly ByTestIdOptions Default = new(); + + /// + /// The StringComparison used for comparing the desired Test ID to the resulting HTML. Defaults to Ordinal (case sensitive). + /// + public StringComparison ComparisonType { get; set; } = StringComparison.Ordinal; + + /// + /// The name of the attribute used for finding Test IDs. Defaults to "data-testid". + /// + public string TestIdAttribute { get; set; } = "data-testid"; +} diff --git a/src/bunit.web.query/TestIds/TestIdQueryExtensions.cs b/src/bunit.web.query/TestIds/TestIdQueryExtensions.cs new file mode 100644 index 000000000..c37623d2b --- /dev/null +++ b/src/bunit.web.query/TestIds/TestIdQueryExtensions.cs @@ -0,0 +1,42 @@ +using AngleSharp.Dom; +using Bunit.TestIds; + +namespace Bunit; + +/// +/// Extension methods for querying by Test ID +/// +public static class TestIdQueryExtensions +{ + /// + /// Returns the first element with the specified Test ID. + /// + /// The rendered fragment to search. + /// The Test ID to search for (e.g. "myTestId" in <span data-testid="myTestId">). + /// Method used to override the default behavior of FindByTestId. + /// The first element matching the specified role and options. + /// Thrown when no element matching the provided testId is found. + public static IElement FindByTestId(this IRenderedComponent renderedComponent, string testId, Action? configureOptions = null) + { + ArgumentNullException.ThrowIfNull(renderedComponent); + ArgumentNullException.ThrowIfNull(testId); + + var options = ByTestIdOptions.Default; + if (configureOptions is not null) + { + options = options with { }; + configureOptions.Invoke(options); + } + + var elems = renderedComponent.Nodes.TryQuerySelectorAll($"[{options.TestIdAttribute}]"); + + foreach (var elem in elems) + { + var attr = elem.GetAttribute(options.TestIdAttribute); + if (attr is not null && attr.Equals(testId, options.ComparisonType)) + return elem; + } + + throw new TestIdNotFoundException(testId); + } +} diff --git a/tests/bunit.web.query.tests/TestIds/TestIdQueryExtensionsTests.cs b/tests/bunit.web.query.tests/TestIds/TestIdQueryExtensionsTests.cs new file mode 100644 index 000000000..c14af52ad --- /dev/null +++ b/tests/bunit.web.query.tests/TestIds/TestIdQueryExtensionsTests.cs @@ -0,0 +1,102 @@ +namespace Bunit.TestIds; + +public class TestIdQueryExtensionsTests : BunitContext +{ + [Fact(DisplayName = "Should find span element with matching testid value")] + public void Test001() + { + var cut = Render(ps => ps.AddChildContent($"""""")); + + var elem = cut.FindByTestId("myTestId"); + + elem.ShouldNotBeNull(); + elem.NodeName.ShouldBe("SPAN", StringCompareShould.IgnoreCase); + elem.GetAttribute("data-testid").ShouldBe("myTestId"); + } + + [Fact(DisplayName = "Should throw exception when testid does not exist in the DOM")] + public void Test002() + { + var cut = Render(ps => ps.AddChildContent("""""")); + + Should.Throw(() => cut.FindByTestId("myTestId")).TestId.ShouldBe("myTestId"); + } + + [Fact(DisplayName = "Should throw exception when testid casing is different from DOM")] + public void Test003() + { + var cut = Render(ps => ps.AddChildContent("""""")); + + Should.Throw(() => cut.FindByTestId("MYTESTID")).TestId.ShouldBe("MYTESTID"); + } + + [Fact(DisplayName = "Should find first div element with matching testid value")] + public void Test004() + { + var cut = Render(ps => ps.AddChildContent($""" +
+ + """)); + + var elem = cut.FindByTestId("myTestId"); + + elem.ShouldNotBeNull(); + elem.NodeName.ShouldBe("DIV", StringCompareShould.IgnoreCase); + elem.GetAttribute("data-testid").ShouldBe("myTestId"); + } + + [Fact(DisplayName = "Should find first non-child div element with matching testid value")] + public void Test005() + { + var cut = Render(ps => ps.AddChildContent($""" +
+ +
+ """)); + + var elem = cut.FindByTestId("myTestId"); + + elem.ShouldNotBeNull(); + elem.NodeName.ShouldBe("DIV", StringCompareShould.IgnoreCase); + elem.GetAttribute("data-testid").ShouldBe("myTestId"); + } + + [Fact(DisplayName = "Should find span element with matching testid attribute name and value")] + public void Test006() + { + var cut = Render(ps => ps.AddChildContent($"""""")); + + var elem = cut.FindByTestId("myTestId", opts => opts.TestIdAttribute = "data-testidattr"); + + elem.ShouldNotBeNull(); + elem.NodeName.ShouldBe("SPAN", StringCompareShould.IgnoreCase); + elem.GetAttribute("data-testidattr").ShouldBe("myTestId"); + } + + [Fact(DisplayName = "Should find span element with equivalent case-insensitive testid value")] + public void Test007() + { + var cut = Render(ps => ps.AddChildContent("""""")); + + var elem = cut.FindByTestId("MYTESTID", opts => opts.ComparisonType = StringComparison.OrdinalIgnoreCase); + + elem.ShouldNotBeNull(); + elem.NodeName.ShouldBe("SPAN", StringCompareShould.IgnoreCase); + elem.GetAttribute("data-testid").ShouldBe("myTestId"); + } + + [Fact(DisplayName = "Should find span element with equivalent case-sensitive testid value")] + public void Test008() + { + var cut = Render(ps => ps.AddChildContent(""" + + + """)); + + var elem = cut.FindByTestId("MYTESTID"); + + elem.ShouldNotBeNull(); + elem.NodeName.ShouldBe("SPAN", StringCompareShould.IgnoreCase); + elem.GetAttribute("data-testid").ShouldBe("MYTESTID"); + } +} From acf1c044dd04d84d8d534514c6ad68aa03ab5924 Mon Sep 17 00:00:00 2001 From: James Sampica Date: Wed, 31 Dec 2025 08:27:37 -0600 Subject: [PATCH 2/3] remove unnecessary nullability --- src/bunit.web.query/TestIds/TestIdNotFoundException.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bunit.web.query/TestIds/TestIdNotFoundException.cs b/src/bunit.web.query/TestIds/TestIdNotFoundException.cs index 422449d72..258b483d1 100644 --- a/src/bunit.web.query/TestIds/TestIdNotFoundException.cs +++ b/src/bunit.web.query/TestIds/TestIdNotFoundException.cs @@ -9,13 +9,13 @@ public sealed class TestIdNotFoundException : Exception /// /// Gets the test id used to search with. /// - public string? TestId { get; } + public string TestId { get; } /// /// Initializes a new instance of the class. /// /// The test id that was searched for. - public TestIdNotFoundException(string? testId = null) + public TestIdNotFoundException(string testId) : base($"Unable to find an element with the Test ID '{testId}'.") { TestId = testId; From 4446e6097145113f547a27793c5061dfe3632da1 Mon Sep 17 00:00:00 2001 From: James Sampica Date: Wed, 31 Dec 2025 08:33:44 -0600 Subject: [PATCH 3/3] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d3a18812..35eac1447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to **bUnit** will be documented in this file. The project ad ## [Unreleased] +### Added + +- `FindByTestId` to `bunit.web.query` to gather elements by a given test id. By [@jimSampica](https://github.com/jimSampica) + ## [2.4.2] - 2025-12-21 ### Fixed