Skip to content

Commit ceefbd8

Browse files
committed
feat: added FindByTestId
1 parent 55e2ef2 commit ceefbd8

File tree

4 files changed

+186
-0
lines changed

4 files changed

+186
-0
lines changed
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 = null)
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+
}
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+
}

0 commit comments

Comments
 (0)