Skip to content

Commit 68599cd

Browse files
Release of new minor version v2.5 (#1803)
* Set version to '2.5-preview' * feat: Added FindByTestId (#1802) * fix: Add OverloadResolutionAttribute for Render method in .NET 9.0+ (#1801) * Set version to '2.5' --------- Co-authored-by: Jim Sampica <jtsampica@gmail.com> Co-authored-by: Steven Giesel <stgiesel35@gmail.com>
2 parents 683d56d + 2f597e0 commit 68599cd

File tree

7 files changed

+199
-2
lines changed

7 files changed

+199
-2
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ All notable changes to **bUnit** will be documented in this file. The project ad
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- `Render(RenderFragment)` is preferred via the `OverloadResolutionAttribute`. Reported by [@ScarletKuro](https://github.com/ScarletKuro) in #1800. Fixed by [@linkdotnet](https://github.com/linkdotnet).
12+
- `FindByTestId` to `bunit.web.query` to gather elements by a given test id. By [@jimSampica](https://github.com/jimSampica)
13+
914
## [2.4.2] - 2025-12-21
1015

1116
### Fixed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace Bunit.TestIds;
2+
3+
/// <summary>
4+
/// Represents a failure to find an element in the searched target
5+
/// using the specified test id.
6+
/// </summary>
7+
public sealed class TestIdNotFoundException : Exception
8+
{
9+
/// <summary>
10+
/// Gets the test id used to search with.
11+
/// </summary>
12+
public string TestId { get; }
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="TestIdNotFoundException"/> class.
16+
/// </summary>
17+
/// <param name="testId">The test id that was searched for.</param>
18+
public TestIdNotFoundException(string testId)
19+
: base($"Unable to find an element with the Test ID '{testId}'.")
20+
{
21+
TestId = testId;
22+
}
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace Bunit.TestIds;
2+
3+
/// <summary>
4+
/// Allows overrides of behavior for FindByTestId method
5+
/// </summary>
6+
public record class ByTestIdOptions
7+
{
8+
internal static readonly ByTestIdOptions Default = new();
9+
10+
/// <summary>
11+
/// The StringComparison used for comparing the desired Test ID to the resulting HTML. Defaults to Ordinal (case sensitive).
12+
/// </summary>
13+
public StringComparison ComparisonType { get; set; } = StringComparison.Ordinal;
14+
15+
/// <summary>
16+
/// The name of the attribute used for finding Test IDs. Defaults to "data-testid".
17+
/// </summary>
18+
public string TestIdAttribute { get; set; } = "data-testid";
19+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using AngleSharp.Dom;
2+
using Bunit.TestIds;
3+
4+
namespace Bunit;
5+
6+
/// <summary>
7+
/// Extension methods for querying <see cref="IRenderedComponent{TComponent}" /> by Test ID
8+
/// </summary>
9+
public static class TestIdQueryExtensions
10+
{
11+
/// <summary>
12+
/// Returns the first element with the specified Test ID.
13+
/// </summary>
14+
/// <param name="renderedComponent">The rendered fragment to search.</param>
15+
/// <param name="testId">The Test ID to search for (e.g. "myTestId" in &lt;span data-testid="myTestId"&gt;).</param>
16+
/// <param name="configureOptions">Method used to override the default behavior of FindByTestId.</param>
17+
/// <returns>The first element matching the specified role and options.</returns>
18+
/// <exception cref="TestIdNotFoundException">Thrown when no element matching the provided testId is found.</exception>
19+
public static IElement FindByTestId(this IRenderedComponent<IComponent> renderedComponent, string testId, Action<ByTestIdOptions>? configureOptions = null)
20+
{
21+
ArgumentNullException.ThrowIfNull(renderedComponent);
22+
ArgumentNullException.ThrowIfNull(testId);
23+
24+
var options = ByTestIdOptions.Default;
25+
if (configureOptions is not null)
26+
{
27+
options = options with { };
28+
configureOptions.Invoke(options);
29+
}
30+
31+
var elems = renderedComponent.Nodes.TryQuerySelectorAll($"[{options.TestIdAttribute}]");
32+
33+
foreach (var elem in elems)
34+
{
35+
var attr = elem.GetAttribute(options.TestIdAttribute);
36+
if (attr is not null && attr.Equals(testId, options.ComparisonType))
37+
return elem;
38+
}
39+
40+
throw new TestIdNotFoundException(testId);
41+
}
42+
}

src/bunit/BunitContext.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#if NET9_0_OR_GREATER
2+
using System.Runtime.CompilerServices;
3+
#endif
14
using Bunit.Extensions;
25
using Bunit.Rendering;
36
using Microsoft.Extensions.Logging;
@@ -158,9 +161,12 @@ public virtual IRenderedComponent<TComponent> Render<TComponent>(Action<Componen
158161
/// <typeparam name="TComponent">The type of component to find in the render tree.</typeparam>
159162
/// <param name="renderFragment">The render fragment to render.</param>
160163
/// <returns>The <see cref="RenderedComponent{TComponent}"/>.</returns>
164+
#if NET9_0_OR_GREATER
165+
[OverloadResolutionPriority(1)]
166+
#endif
161167
public virtual IRenderedComponent<TComponent> Render<TComponent>(RenderFragment renderFragment)
162168
where TComponent : IComponent
163-
=> this.RenderInsideRenderTree<TComponent>(renderFragment);
169+
=> RenderInsideRenderTree<TComponent>(renderFragment);
164170

165171
/// <summary>
166172
/// Renders the <paramref name="renderFragment"/> and returns it as a <see cref="IRenderedComponent{TComponent}"/>.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
namespace Bunit.TestIds;
2+
3+
public class TestIdQueryExtensionsTests : BunitContext
4+
{
5+
[Fact(DisplayName = "Should find span element with matching testid value")]
6+
public void Test001()
7+
{
8+
var cut = Render<Wrapper>(ps => ps.AddChildContent($"""<span data-testid="myTestId"><span>"""));
9+
10+
var elem = cut.FindByTestId("myTestId");
11+
12+
elem.ShouldNotBeNull();
13+
elem.NodeName.ShouldBe("SPAN", StringCompareShould.IgnoreCase);
14+
elem.GetAttribute("data-testid").ShouldBe("myTestId");
15+
}
16+
17+
[Fact(DisplayName = "Should throw exception when testid does not exist in the DOM")]
18+
public void Test002()
19+
{
20+
var cut = Render<Wrapper>(ps => ps.AddChildContent("""<span data-testid="testId"><span>"""));
21+
22+
Should.Throw<TestIdNotFoundException>(() => cut.FindByTestId("myTestId")).TestId.ShouldBe("myTestId");
23+
}
24+
25+
[Fact(DisplayName = "Should throw exception when testid casing is different from DOM")]
26+
public void Test003()
27+
{
28+
var cut = Render<Wrapper>(ps => ps.AddChildContent("""<span data-testid="testId"><span>"""));
29+
30+
Should.Throw<TestIdNotFoundException>(() => cut.FindByTestId("MYTESTID")).TestId.ShouldBe("MYTESTID");
31+
}
32+
33+
[Fact(DisplayName = "Should find first div element with matching testid value")]
34+
public void Test004()
35+
{
36+
var cut = Render<Wrapper>(ps => ps.AddChildContent($"""
37+
<div data-testid="myTestId"></div>
38+
<span data-testid="myTestId"><span>
39+
"""));
40+
41+
var elem = cut.FindByTestId("myTestId");
42+
43+
elem.ShouldNotBeNull();
44+
elem.NodeName.ShouldBe("DIV", StringCompareShould.IgnoreCase);
45+
elem.GetAttribute("data-testid").ShouldBe("myTestId");
46+
}
47+
48+
[Fact(DisplayName = "Should find first non-child div element with matching testid value")]
49+
public void Test005()
50+
{
51+
var cut = Render<Wrapper>(ps => ps.AddChildContent($"""
52+
<div data-testid="myTestId">
53+
<span data-testid="myTestId"><span>
54+
</div>
55+
"""));
56+
57+
var elem = cut.FindByTestId("myTestId");
58+
59+
elem.ShouldNotBeNull();
60+
elem.NodeName.ShouldBe("DIV", StringCompareShould.IgnoreCase);
61+
elem.GetAttribute("data-testid").ShouldBe("myTestId");
62+
}
63+
64+
[Fact(DisplayName = "Should find span element with matching testid attribute name and value")]
65+
public void Test006()
66+
{
67+
var cut = Render<Wrapper>(ps => ps.AddChildContent($"""<span data-testidattr="myTestId"><span>"""));
68+
69+
var elem = cut.FindByTestId("myTestId", opts => opts.TestIdAttribute = "data-testidattr");
70+
71+
elem.ShouldNotBeNull();
72+
elem.NodeName.ShouldBe("SPAN", StringCompareShould.IgnoreCase);
73+
elem.GetAttribute("data-testidattr").ShouldBe("myTestId");
74+
}
75+
76+
[Fact(DisplayName = "Should find span element with equivalent case-insensitive testid value")]
77+
public void Test007()
78+
{
79+
var cut = Render<Wrapper>(ps => ps.AddChildContent("""<span data-testid="myTestId"><span>"""));
80+
81+
var elem = cut.FindByTestId("MYTESTID", opts => opts.ComparisonType = StringComparison.OrdinalIgnoreCase);
82+
83+
elem.ShouldNotBeNull();
84+
elem.NodeName.ShouldBe("SPAN", StringCompareShould.IgnoreCase);
85+
elem.GetAttribute("data-testid").ShouldBe("myTestId");
86+
}
87+
88+
[Fact(DisplayName = "Should find span element with equivalent case-sensitive testid value")]
89+
public void Test008()
90+
{
91+
var cut = Render<Wrapper>(ps => ps.AddChildContent("""
92+
<span data-testid="myTestId"><span>
93+
<span data-testid="MYTESTID"><span>
94+
"""));
95+
96+
var elem = cut.FindByTestId("MYTESTID");
97+
98+
elem.ShouldNotBeNull();
99+
elem.NodeName.ShouldBe("SPAN", StringCompareShould.IgnoreCase);
100+
elem.GetAttribute("data-testid").ShouldBe("MYTESTID");
101+
}
102+
}

version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
3-
"version": "2.4",
3+
"version": "2.5",
44
"assemblyVersion": {
55
"precision": "revision"
66
},

0 commit comments

Comments
 (0)