diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d3a18812..69b43ac27 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,11 @@ All notable changes to **bUnit** will be documented in this file. The project ad
## [Unreleased]
+### Added
+
+- `Render(RenderFragment)` is preferred via the `OverloadResolutionAttribute`. Reported by [@ScarletKuro](https://github.com/ScarletKuro) in #1800. Fixed by [@linkdotnet](https://github.com/linkdotnet).
+- `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
diff --git a/src/bunit.web.query/TestIds/TestIdNotFoundException.cs b/src/bunit.web.query/TestIds/TestIdNotFoundException.cs
new file mode 100644
index 000000000..258b483d1
--- /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)
+ : 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/src/bunit/BunitContext.cs b/src/bunit/BunitContext.cs
index 948d44b15..cb3aea4d9 100644
--- a/src/bunit/BunitContext.cs
+++ b/src/bunit/BunitContext.cs
@@ -1,3 +1,6 @@
+#if NET9_0_OR_GREATER
+using System.Runtime.CompilerServices;
+#endif
using Bunit.Extensions;
using Bunit.Rendering;
using Microsoft.Extensions.Logging;
@@ -158,9 +161,12 @@ public virtual IRenderedComponent Render(ActionThe type of component to find in the render tree.
/// The render fragment to render.
/// The .
+#if NET9_0_OR_GREATER
+ [OverloadResolutionPriority(1)]
+#endif
public virtual IRenderedComponent Render(RenderFragment renderFragment)
where TComponent : IComponent
- => this.RenderInsideRenderTree(renderFragment);
+ => RenderInsideRenderTree(renderFragment);
///
/// Renders the and returns it as a .
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");
+ }
+}
diff --git a/version.json b/version.json
index 1fe82ff13..2543d4d9d 100644
--- a/version.json
+++ b/version.json
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
- "version": "2.4",
+ "version": "2.5",
"assemblyVersion": {
"precision": "revision"
},