Skip to content

Commit 0336aa7

Browse files
committed
feat: added FindByRole
1 parent ca409ec commit 0336aa7

File tree

9 files changed

+2129
-1
lines changed

9 files changed

+2129
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ All notable changes to **bUnit** will be documented in this file. The project ad
66

77
## [Unreleased]
88

9-
## Added
9+
### Added
1010
- Added `FindByAllByLabel` to `bunit.web.query` package. By [@linkdotnet](https://github.com/linkdotnet).
11+
- Added `FindByRole` and `FindAllByRole` to `bunit.web.query` package. By [@linkdotnet](https://github.com/linkdotnet).
1112

1213
## [2.1.1] - 2025-11-21
1314

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using AngleSharp.Dom;
2+
3+
namespace Bunit;
4+
5+
/// <summary>
6+
/// Provides methods to compute the accessible name of an element.
7+
/// </summary>
8+
/// <remarks>
9+
/// This is a simplified implementation of the Accessible Name and Description Computation algorithm.
10+
/// See https://www.w3.org/TR/accname-1.1/ for the full specification.
11+
/// </remarks>
12+
[SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Using lowercase for comparison with lowercase constant values.")]
13+
internal static class AccessibleNameComputation
14+
{
15+
/// <summary>
16+
/// Computes the accessible name of an element.
17+
/// </summary>
18+
public static string? GetAccessibleName(IElement element)
19+
{
20+
return GetNameFromAriaLabelledBy(element)
21+
?? GetNameFromAriaLabel(element)
22+
?? GetNameFromAssociatedLabelOrNativeContent(element)
23+
?? GetNameFromTitleAttribute(element)
24+
?? GetNameFromPlaceholder(element);
25+
}
26+
27+
private static string? GetNameFromAriaLabelledBy(IElement element)
28+
{
29+
var labelledBy = element.GetAttribute("aria-labelledby");
30+
if (string.IsNullOrWhiteSpace(labelledBy))
31+
return null;
32+
33+
return GetTextFromReferencedElements(element, labelledBy);
34+
}
35+
36+
private static string? GetNameFromAriaLabel(IElement element)
37+
{
38+
var ariaLabel = element.GetAttribute("aria-label");
39+
return string.IsNullOrWhiteSpace(ariaLabel) ? null : ariaLabel;
40+
}
41+
42+
private static string? GetNameFromAssociatedLabelOrNativeContent(IElement element)
43+
{
44+
var tagName = element.TagName.ToUpperInvariant();
45+
46+
if (tagName is "INPUT" or "SELECT" or "TEXTAREA")
47+
{
48+
return GetNameFromLinkedLabel(element)
49+
?? GetNameFromInputButtonValue(element);
50+
}
51+
52+
if (tagName == "IMG")
53+
{
54+
return GetNameFromAltAttribute(element);
55+
}
56+
57+
if (tagName is "BUTTON" or "A" or "H1" or "H2" or "H3" or "H4" or "H5" or "H6")
58+
{
59+
return GetNonEmptyTextContent(element);
60+
}
61+
62+
return null;
63+
}
64+
65+
private static string? GetNameFromTitleAttribute(IElement element)
66+
{
67+
var title = element.GetAttribute("title");
68+
return string.IsNullOrWhiteSpace(title) ? null : title;
69+
}
70+
71+
private static string? GetNameFromPlaceholder(IElement element)
72+
{
73+
var tagName = element.TagName.ToUpperInvariant();
74+
if (tagName is not ("INPUT" or "TEXTAREA"))
75+
return null;
76+
77+
var placeholder = element.GetAttribute("placeholder");
78+
return string.IsNullOrWhiteSpace(placeholder) ? null : placeholder;
79+
}
80+
81+
private static string? GetTextFromReferencedElements(IElement element, string spaceDelimitedIds)
82+
{
83+
var ids = spaceDelimitedIds.Split(' ', StringSplitOptions.RemoveEmptyEntries);
84+
var texts = new List<string>();
85+
var root = GetRootElement(element);
86+
87+
foreach (var id in ids)
88+
{
89+
var referencedElement = element.Owner?.GetElementById(id) ?? root?.QuerySelector($"#{id}");
90+
if (referencedElement == null)
91+
continue;
92+
93+
var text = referencedElement.TextContent.Trim();
94+
if (!string.IsNullOrWhiteSpace(text))
95+
texts.Add(text);
96+
}
97+
98+
return texts.Count > 0 ? string.Join(" ", texts) : null;
99+
}
100+
101+
private static IElement? GetRootElement(IElement element)
102+
{
103+
var current = element;
104+
while (current.ParentElement != null)
105+
{
106+
current = current.ParentElement;
107+
}
108+
return current;
109+
}
110+
111+
private static string? GetNameFromLinkedLabel(IElement element)
112+
{
113+
var id = element.GetAttribute("id");
114+
if (!string.IsNullOrWhiteSpace(id))
115+
{
116+
var linkedLabel = FindLabelWithForAttribute(element, id);
117+
if (linkedLabel != null)
118+
{
119+
return linkedLabel.TextContent.Trim();
120+
}
121+
}
122+
123+
var wrappingLabel = element.Closest("label");
124+
if (wrappingLabel != null)
125+
{
126+
return GetTextContentExcludingElement(wrappingLabel, element);
127+
}
128+
129+
return null;
130+
}
131+
132+
private static IElement? FindLabelWithForAttribute(IElement element, string id)
133+
{
134+
var label = element.Owner?.QuerySelector($"label[for='{id}']");
135+
if (label != null)
136+
return label;
137+
138+
var root = GetRootElement(element);
139+
return root?.QuerySelector($"label[for='{id}']");
140+
}
141+
142+
private static string? GetNameFromInputButtonValue(IElement element)
143+
{
144+
if (!element.TagName.Equals("INPUT", StringComparison.OrdinalIgnoreCase))
145+
return null;
146+
147+
var inputType = element.GetAttribute("type")?.ToLowerInvariant();
148+
if (inputType is not ("button" or "submit" or "reset"))
149+
return null;
150+
151+
var value = element.GetAttribute("value");
152+
return string.IsNullOrWhiteSpace(value) ? null : value;
153+
}
154+
155+
private static string? GetNameFromAltAttribute(IElement element)
156+
{
157+
var alt = element.GetAttribute("alt");
158+
return string.IsNullOrWhiteSpace(alt) ? null : alt;
159+
}
160+
161+
private static string? GetNonEmptyTextContent(IElement element)
162+
{
163+
var textContent = element.TextContent.Trim();
164+
return string.IsNullOrWhiteSpace(textContent) ? null : textContent;
165+
}
166+
167+
private static string GetTextContentExcludingElement(IElement container, IElement excludeElement)
168+
{
169+
var texts = new List<string>();
170+
CollectTextNodesExcluding(container, excludeElement, texts);
171+
return string.Join(" ", texts).Trim();
172+
}
173+
174+
private static void CollectTextNodesExcluding(INode node, IElement excludeElement, List<string> texts)
175+
{
176+
foreach (var child in node.ChildNodes)
177+
{
178+
if (child == excludeElement)
179+
continue;
180+
181+
if (child.NodeType == NodeType.Text)
182+
{
183+
var text = child.TextContent.Trim();
184+
if (!string.IsNullOrWhiteSpace(text))
185+
texts.Add(text);
186+
}
187+
else if (child.NodeType == NodeType.Element)
188+
{
189+
CollectTextNodesExcluding(child, excludeElement, texts);
190+
}
191+
}
192+
}
193+
}

0 commit comments

Comments
 (0)