Skip to content

Commit 6375671

Browse files
authored
Snapshot the accessibility tree (#763)
1 parent e55dc2e commit 6375671

File tree

8 files changed

+934
-1
lines changed

8 files changed

+934
-1
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
using System.Threading.Tasks;
2+
using PuppeteerSharp.PageAccessibility;
3+
using Xunit;
4+
using Xunit.Abstractions;
5+
6+
namespace PuppeteerSharp.Tests.AccesibilityTests
7+
{
8+
[Collection("PuppeteerLoaderFixture collection")]
9+
public class AccesibilityTests : PuppeteerPageBaseTest
10+
{
11+
public AccesibilityTests(ITestOutputHelper output) : base(output)
12+
{
13+
}
14+
15+
[Fact]
16+
public async Task ShouldWork()
17+
{
18+
await Page.SetContentAsync(@"
19+
<head>
20+
<title>Accessibility Test</title>
21+
</head>
22+
<body>
23+
<div>Hello World</div>
24+
<h1>Inputs</h1>
25+
<input placeholder='Empty input' autofocus />
26+
<input placeholder='readonly input' readonly />
27+
<input placeholder='disabled input' disabled />
28+
<input aria-label='Input with whitespace' value=' ' />
29+
<input value='value only' />
30+
<input aria-placeholder='placeholder' value='and a value' />
31+
<div aria-hidden='true' id='desc'>This is a description!</div>
32+
<input aria-placeholder='placeholder' value='and a value' aria-describedby='desc' />
33+
<select>
34+
<option>First Option</option>
35+
<option>Second Option</option>
36+
</select>
37+
</body>");
38+
Assert.Equal(
39+
new SerializedAXNode
40+
{
41+
Role = "WebArea",
42+
Name = "Accessibility Test",
43+
Children = new SerializedAXNode[]
44+
{
45+
new SerializedAXNode
46+
{
47+
Role = "text",
48+
Name = "Hello World"
49+
},
50+
new SerializedAXNode
51+
{
52+
Role = "heading",
53+
Name = "Inputs",
54+
Level = 1
55+
},
56+
new SerializedAXNode{
57+
Role = "textbox",
58+
Name = "Empty input",
59+
Focused = true
60+
},
61+
new SerializedAXNode{
62+
Role = "textbox",
63+
Name = "readonly input",
64+
Readonly = true
65+
},
66+
new SerializedAXNode{
67+
Role = "textbox",
68+
Name = "disabled input",
69+
Disabled= true
70+
},
71+
new SerializedAXNode{
72+
Role = "textbox",
73+
Name = "Input with whitespace",
74+
Value= " "
75+
},
76+
new SerializedAXNode{
77+
Role = "textbox",
78+
Name = "",
79+
Value= "value only"
80+
},
81+
new SerializedAXNode{
82+
Role = "textbox",
83+
Name = "placeholder",
84+
Value= "and a value"
85+
},
86+
new SerializedAXNode{
87+
Role = "textbox",
88+
Name = "placeholder",
89+
Value= "and a value",
90+
Description= "This is a description!"},
91+
new SerializedAXNode{
92+
Role= "combobox",
93+
Name= "",
94+
Value= "First Option",
95+
Children= new SerializedAXNode[]{
96+
new SerializedAXNode
97+
{
98+
Role = "menuitem",
99+
Name = "First Option",
100+
Selected= true
101+
},
102+
new SerializedAXNode
103+
{
104+
Role = "menuitem",
105+
Name = "Second Option"
106+
}
107+
}
108+
}
109+
}
110+
},
111+
await Page.Accessibility.SnapshotAsync());
112+
}
113+
114+
[Fact]
115+
public async Task ShouldReportUninterestingNodes()
116+
{
117+
await Page.SetContentAsync("<textarea autofocus>hi</textarea>");
118+
Assert.Equal(
119+
new SerializedAXNode
120+
{
121+
Role = "textbox",
122+
Name = "",
123+
Value = "hi",
124+
Focused = true,
125+
Multiline = true,
126+
Children = new SerializedAXNode[]
127+
{
128+
new SerializedAXNode
129+
{
130+
Role = "GenericContainer",
131+
Name = "",
132+
Children = new SerializedAXNode[]
133+
{
134+
new SerializedAXNode
135+
{
136+
Role = "text",
137+
Name = "hi"
138+
}
139+
}
140+
}
141+
}
142+
},
143+
FindFocusedNode(await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions
144+
{
145+
InterestingOnly = false
146+
})));
147+
}
148+
149+
[Fact]
150+
public async Task ShouldNotReportTextNodesInsideControls()
151+
{
152+
await Page.SetContentAsync(@"
153+
<div role='tablist'>
154+
<div role='tab' aria-selected='true'><b>Tab1</b></div>
155+
<div role='tab'>Tab2</div>
156+
</div>");
157+
Assert.Equal(
158+
new SerializedAXNode
159+
{
160+
Role = "WebArea",
161+
Name = "",
162+
Children = new SerializedAXNode[]
163+
{
164+
new SerializedAXNode
165+
{
166+
Role = "tab",
167+
Name = "Tab1",
168+
Selected = true
169+
},
170+
new SerializedAXNode
171+
{
172+
Role = "tab",
173+
Name = "Tab2"
174+
}
175+
}
176+
},
177+
await Page.Accessibility.SnapshotAsync());
178+
}
179+
180+
[Fact]
181+
public async Task RichTextEditableFieldsShouldHaveChildren()
182+
{
183+
await Page.SetContentAsync(@"
184+
<div contenteditable='true'>
185+
Edit this image: <img src='fakeimage.png' alt='my fake image'>
186+
</div>");
187+
Assert.Equal(
188+
new SerializedAXNode
189+
{
190+
Role = "GenericContainer",
191+
Name = "",
192+
Value = "Edit this image: ",
193+
Children = new SerializedAXNode[]
194+
{
195+
new SerializedAXNode
196+
{
197+
Role = "text",
198+
Name = "Edit this image:"
199+
},
200+
new SerializedAXNode
201+
{
202+
Role = "img",
203+
Name = "my fake image"
204+
}
205+
}
206+
},
207+
(await Page.Accessibility.SnapshotAsync()).Children[0]);
208+
}
209+
210+
[Fact]
211+
public async Task RichTextEditableFieldsWithRoleShouldHaveChildren()
212+
{
213+
await Page.SetContentAsync(@"
214+
<div contenteditable='true' role='textbox'>
215+
Edit this image: <img src='fakeimage.png' alt='my fake image'>
216+
</div>");
217+
Assert.Equal(
218+
new SerializedAXNode
219+
{
220+
Role = "textbox",
221+
Name = "",
222+
Value = "Edit this image: ",
223+
Children = new SerializedAXNode[]
224+
{
225+
new SerializedAXNode
226+
{
227+
Role = "text",
228+
Name = "Edit this image:"
229+
},
230+
new SerializedAXNode
231+
{
232+
Role = "img",
233+
Name = "my fake image"
234+
}
235+
}
236+
},
237+
(await Page.Accessibility.SnapshotAsync()).Children[0]);
238+
}
239+
240+
[Fact]
241+
public async Task PlainTextFieldWithRoleShouldNotHaveChildren()
242+
{
243+
await Page.SetContentAsync("<div contenteditable='plaintext-only' role='textbox'>Edit this image:<img src='fakeimage.png' alt='my fake image'></div>");
244+
Assert.Equal(
245+
new SerializedAXNode
246+
{
247+
Role = "textbox",
248+
Name = "",
249+
Value = "Edit this image:"
250+
},
251+
(await Page.Accessibility.SnapshotAsync()).Children[0]);
252+
}
253+
254+
[Fact]
255+
public async Task PlainTextFieldWithTabindexAndWithoutRoleShouldNotHaveContent()
256+
{
257+
await Page.SetContentAsync("<div contenteditable='plaintext-only' role='textbox' tabIndex=0>Edit this image:<img src='fakeimage.png' alt='my fake image'></div>");
258+
Assert.Equal(
259+
new SerializedAXNode
260+
{
261+
Role = "textbox",
262+
Name = "",
263+
Value = "Edit this image:"
264+
},
265+
(await Page.Accessibility.SnapshotAsync()).Children[0]);
266+
}
267+
268+
[Fact]
269+
public async Task NonEditableTextboxWithRoleAndTabIndexAndLabelShouldNotHaveChildren()
270+
{
271+
await Page.SetContentAsync(@"
272+
<div role='textbox' tabIndex=0 aria-checked='true' aria-label='my favorite textbox'>
273+
this is the inner content
274+
<img alt='yo' src='fakeimg.png'>
275+
</div>");
276+
Assert.Equal(
277+
new SerializedAXNode
278+
{
279+
Role = "textbox",
280+
Name = "my favorite textbox",
281+
Value = "this is the inner content "
282+
},
283+
(await Page.Accessibility.SnapshotAsync()).Children[0]);
284+
}
285+
286+
[Fact]
287+
public async Task CheckboxWithAndTabIndexAndLabelShouldNotHaveChildren()
288+
{
289+
await Page.SetContentAsync(@"
290+
<div role='checkbox' tabIndex=0 aria-checked='true' aria-label='my favorite checkbox'>
291+
this is the inner content
292+
<img alt='yo' src='fakeimg.png'>
293+
</div>");
294+
Assert.Equal(
295+
new SerializedAXNode
296+
{
297+
Role = "checkbox",
298+
Name = "my favorite checkbox",
299+
Checked = CheckedState.True
300+
},
301+
(await Page.Accessibility.SnapshotAsync()).Children[0]);
302+
}
303+
304+
[Fact]
305+
public async Task CheckboxWithoutLabelShouldNotHaveChildren()
306+
{
307+
await Page.SetContentAsync(@"
308+
<div role='checkbox' aria-checked='true'>
309+
this is the inner content
310+
<img alt='yo' src='fakeimg.png'>
311+
</div>");
312+
Assert.Equal(
313+
new SerializedAXNode
314+
{
315+
Role = "checkbox",
316+
Name = "this is the inner content yo",
317+
Checked = CheckedState.True
318+
},
319+
(await Page.Accessibility.SnapshotAsync()).Children[0]);
320+
}
321+
322+
private SerializedAXNode FindFocusedNode(SerializedAXNode serializedAXNode)
323+
{
324+
if (serializedAXNode.Focused)
325+
{
326+
return serializedAXNode;
327+
}
328+
foreach (var item in serializedAXNode.Children)
329+
{
330+
var focusedChild = FindFocusedNode(item);
331+
if (focusedChild != null)
332+
{
333+
return focusedChild;
334+
}
335+
}
336+
337+
return null;
338+
}
339+
}
340+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Newtonsoft.Json;
4+
using Newtonsoft.Json.Linq;
5+
6+
namespace PuppeteerSharp.Messaging
7+
{
8+
internal class AccessibilityGetFullAXTreeResponse
9+
{
10+
[JsonProperty("nodes")]
11+
public IEnumerable<AXTreeNode> Nodes { get; set; }
12+
13+
public class AXTreeNode
14+
{
15+
[JsonProperty("nodeId")]
16+
public string NodeId { get; set; }
17+
[JsonProperty("childIds")]
18+
public IEnumerable<string> ChildIds { get; set; }
19+
[JsonProperty("name")]
20+
public AXTreePropertyValue Name { get; set; }
21+
[JsonProperty("value")]
22+
public AXTreePropertyValue Value { get; set; }
23+
[JsonProperty("description")]
24+
public AXTreePropertyValue Description { get; set; }
25+
[JsonProperty("role")]
26+
public AXTreePropertyValue Role { get; set; }
27+
[JsonProperty("properties")]
28+
public IEnumerable<AXTreeProperty> Properties { get; set; }
29+
}
30+
31+
public class AXTreeProperty
32+
{
33+
[JsonProperty("name")]
34+
public string Name { get; internal set; }
35+
[JsonProperty("value")]
36+
public AXTreePropertyValue Value { get; set; }
37+
}
38+
39+
public class AXTreePropertyValue
40+
{
41+
[JsonProperty("type")]
42+
public string Type { get; set; }
43+
[JsonProperty("value")]
44+
public JToken Value { get; set; }
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)