Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Snapshot the accessibility tree (#763)
- Loading branch information
Showing
8 changed files
with
934 additions
and
1 deletion.
There are no files selected for viewing
340 changes: 340 additions & 0 deletions
340
lib/PuppeteerSharp.Tests/AccessibilityTests/AccessibilityTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,340 @@ | ||
using System.Threading.Tasks; | ||
using PuppeteerSharp.PageAccessibility; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
||
namespace PuppeteerSharp.Tests.AccesibilityTests | ||
{ | ||
[Collection("PuppeteerLoaderFixture collection")] | ||
public class AccesibilityTests : PuppeteerPageBaseTest | ||
{ | ||
public AccesibilityTests(ITestOutputHelper output) : base(output) | ||
{ | ||
} | ||
|
||
[Fact] | ||
public async Task ShouldWork() | ||
{ | ||
await Page.SetContentAsync(@" | ||
<head> | ||
<title>Accessibility Test</title> | ||
</head> | ||
<body> | ||
<div>Hello World</div> | ||
<h1>Inputs</h1> | ||
<input placeholder='Empty input' autofocus /> | ||
<input placeholder='readonly input' readonly /> | ||
<input placeholder='disabled input' disabled /> | ||
<input aria-label='Input with whitespace' value=' ' /> | ||
<input value='value only' /> | ||
<input aria-placeholder='placeholder' value='and a value' /> | ||
<div aria-hidden='true' id='desc'>This is a description!</div> | ||
<input aria-placeholder='placeholder' value='and a value' aria-describedby='desc' /> | ||
<select> | ||
<option>First Option</option> | ||
<option>Second Option</option> | ||
</select> | ||
</body>"); | ||
Assert.Equal( | ||
new SerializedAXNode | ||
{ | ||
Role = "WebArea", | ||
Name = "Accessibility Test", | ||
Children = new SerializedAXNode[] | ||
{ | ||
new SerializedAXNode | ||
{ | ||
Role = "text", | ||
Name = "Hello World" | ||
}, | ||
new SerializedAXNode | ||
{ | ||
Role = "heading", | ||
Name = "Inputs", | ||
Level = 1 | ||
}, | ||
new SerializedAXNode{ | ||
Role = "textbox", | ||
Name = "Empty input", | ||
Focused = true | ||
}, | ||
new SerializedAXNode{ | ||
Role = "textbox", | ||
Name = "readonly input", | ||
Readonly = true | ||
}, | ||
new SerializedAXNode{ | ||
Role = "textbox", | ||
Name = "disabled input", | ||
Disabled= true | ||
}, | ||
new SerializedAXNode{ | ||
Role = "textbox", | ||
Name = "Input with whitespace", | ||
Value= " " | ||
}, | ||
new SerializedAXNode{ | ||
Role = "textbox", | ||
Name = "", | ||
Value= "value only" | ||
}, | ||
new SerializedAXNode{ | ||
Role = "textbox", | ||
Name = "placeholder", | ||
Value= "and a value" | ||
}, | ||
new SerializedAXNode{ | ||
Role = "textbox", | ||
Name = "placeholder", | ||
Value= "and a value", | ||
Description= "This is a description!"}, | ||
new SerializedAXNode{ | ||
Role= "combobox", | ||
Name= "", | ||
Value= "First Option", | ||
Children= new SerializedAXNode[]{ | ||
new SerializedAXNode | ||
{ | ||
Role = "menuitem", | ||
Name = "First Option", | ||
Selected= true | ||
}, | ||
new SerializedAXNode | ||
{ | ||
Role = "menuitem", | ||
Name = "Second Option" | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
await Page.Accessibility.SnapshotAsync()); | ||
} | ||
|
||
[Fact] | ||
public async Task ShouldReportUninterestingNodes() | ||
{ | ||
await Page.SetContentAsync("<textarea autofocus>hi</textarea>"); | ||
Assert.Equal( | ||
new SerializedAXNode | ||
{ | ||
Role = "textbox", | ||
Name = "", | ||
Value = "hi", | ||
Focused = true, | ||
Multiline = true, | ||
Children = new SerializedAXNode[] | ||
{ | ||
new SerializedAXNode | ||
{ | ||
Role = "GenericContainer", | ||
Name = "", | ||
Children = new SerializedAXNode[] | ||
{ | ||
new SerializedAXNode | ||
{ | ||
Role = "text", | ||
Name = "hi" | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
FindFocusedNode(await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions | ||
{ | ||
InterestingOnly = false | ||
}))); | ||
} | ||
|
||
[Fact] | ||
public async Task ShouldNotReportTextNodesInsideControls() | ||
{ | ||
await Page.SetContentAsync(@" | ||
<div role='tablist'> | ||
<div role='tab' aria-selected='true'><b>Tab1</b></div> | ||
<div role='tab'>Tab2</div> | ||
</div>"); | ||
Assert.Equal( | ||
new SerializedAXNode | ||
{ | ||
Role = "WebArea", | ||
Name = "", | ||
Children = new SerializedAXNode[] | ||
{ | ||
new SerializedAXNode | ||
{ | ||
Role = "tab", | ||
Name = "Tab1", | ||
Selected = true | ||
}, | ||
new SerializedAXNode | ||
{ | ||
Role = "tab", | ||
Name = "Tab2" | ||
} | ||
} | ||
}, | ||
await Page.Accessibility.SnapshotAsync()); | ||
} | ||
|
||
[Fact] | ||
public async Task RichTextEditableFieldsShouldHaveChildren() | ||
{ | ||
await Page.SetContentAsync(@" | ||
<div contenteditable='true'> | ||
Edit this image: <img src='fakeimage.png' alt='my fake image'> | ||
</div>"); | ||
Assert.Equal( | ||
new SerializedAXNode | ||
{ | ||
Role = "GenericContainer", | ||
Name = "", | ||
Value = "Edit this image: ", | ||
Children = new SerializedAXNode[] | ||
{ | ||
new SerializedAXNode | ||
{ | ||
Role = "text", | ||
Name = "Edit this image:" | ||
}, | ||
new SerializedAXNode | ||
{ | ||
Role = "img", | ||
Name = "my fake image" | ||
} | ||
} | ||
}, | ||
(await Page.Accessibility.SnapshotAsync()).Children[0]); | ||
} | ||
|
||
[Fact] | ||
public async Task RichTextEditableFieldsWithRoleShouldHaveChildren() | ||
{ | ||
await Page.SetContentAsync(@" | ||
<div contenteditable='true' role='textbox'> | ||
Edit this image: <img src='fakeimage.png' alt='my fake image'> | ||
</div>"); | ||
Assert.Equal( | ||
new SerializedAXNode | ||
{ | ||
Role = "textbox", | ||
Name = "", | ||
Value = "Edit this image: ", | ||
Children = new SerializedAXNode[] | ||
{ | ||
new SerializedAXNode | ||
{ | ||
Role = "text", | ||
Name = "Edit this image:" | ||
}, | ||
new SerializedAXNode | ||
{ | ||
Role = "img", | ||
Name = "my fake image" | ||
} | ||
} | ||
}, | ||
(await Page.Accessibility.SnapshotAsync()).Children[0]); | ||
} | ||
|
||
[Fact] | ||
public async Task PlainTextFieldWithRoleShouldNotHaveChildren() | ||
{ | ||
await Page.SetContentAsync("<div contenteditable='plaintext-only' role='textbox'>Edit this image:<img src='fakeimage.png' alt='my fake image'></div>"); | ||
Assert.Equal( | ||
new SerializedAXNode | ||
{ | ||
Role = "textbox", | ||
Name = "", | ||
Value = "Edit this image:" | ||
}, | ||
(await Page.Accessibility.SnapshotAsync()).Children[0]); | ||
} | ||
|
||
[Fact] | ||
public async Task PlainTextFieldWithTabindexAndWithoutRoleShouldNotHaveContent() | ||
{ | ||
await Page.SetContentAsync("<div contenteditable='plaintext-only' role='textbox' tabIndex=0>Edit this image:<img src='fakeimage.png' alt='my fake image'></div>"); | ||
Assert.Equal( | ||
new SerializedAXNode | ||
{ | ||
Role = "textbox", | ||
Name = "", | ||
Value = "Edit this image:" | ||
}, | ||
(await Page.Accessibility.SnapshotAsync()).Children[0]); | ||
} | ||
|
||
[Fact] | ||
public async Task NonEditableTextboxWithRoleAndTabIndexAndLabelShouldNotHaveChildren() | ||
{ | ||
await Page.SetContentAsync(@" | ||
<div role='textbox' tabIndex=0 aria-checked='true' aria-label='my favorite textbox'> | ||
this is the inner content | ||
<img alt='yo' src='fakeimg.png'> | ||
</div>"); | ||
Assert.Equal( | ||
new SerializedAXNode | ||
{ | ||
Role = "textbox", | ||
Name = "my favorite textbox", | ||
Value = "this is the inner content " | ||
}, | ||
(await Page.Accessibility.SnapshotAsync()).Children[0]); | ||
} | ||
|
||
[Fact] | ||
public async Task CheckboxWithAndTabIndexAndLabelShouldNotHaveChildren() | ||
{ | ||
await Page.SetContentAsync(@" | ||
<div role='checkbox' tabIndex=0 aria-checked='true' aria-label='my favorite checkbox'> | ||
this is the inner content | ||
<img alt='yo' src='fakeimg.png'> | ||
</div>"); | ||
Assert.Equal( | ||
new SerializedAXNode | ||
{ | ||
Role = "checkbox", | ||
Name = "my favorite checkbox", | ||
Checked = CheckedState.True | ||
}, | ||
(await Page.Accessibility.SnapshotAsync()).Children[0]); | ||
} | ||
|
||
[Fact] | ||
public async Task CheckboxWithoutLabelShouldNotHaveChildren() | ||
{ | ||
await Page.SetContentAsync(@" | ||
<div role='checkbox' aria-checked='true'> | ||
this is the inner content | ||
<img alt='yo' src='fakeimg.png'> | ||
</div>"); | ||
Assert.Equal( | ||
new SerializedAXNode | ||
{ | ||
Role = "checkbox", | ||
Name = "this is the inner content yo", | ||
Checked = CheckedState.True | ||
}, | ||
(await Page.Accessibility.SnapshotAsync()).Children[0]); | ||
} | ||
|
||
private SerializedAXNode FindFocusedNode(SerializedAXNode serializedAXNode) | ||
{ | ||
if (serializedAXNode.Focused) | ||
{ | ||
return serializedAXNode; | ||
} | ||
foreach (var item in serializedAXNode.Children) | ||
{ | ||
var focusedChild = FindFocusedNode(item); | ||
if (focusedChild != null) | ||
{ | ||
return focusedChild; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
lib/PuppeteerSharp/Messaging/AccessibilityGetFullAXTreeResponse.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using Newtonsoft.Json; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace PuppeteerSharp.Messaging | ||
{ | ||
internal class AccessibilityGetFullAXTreeResponse | ||
{ | ||
[JsonProperty("nodes")] | ||
public IEnumerable<AXTreeNode> Nodes { get; set; } | ||
|
||
public class AXTreeNode | ||
{ | ||
[JsonProperty("nodeId")] | ||
public string NodeId { get; set; } | ||
[JsonProperty("childIds")] | ||
public IEnumerable<string> ChildIds { get; set; } | ||
[JsonProperty("name")] | ||
public AXTreePropertyValue Name { get; set; } | ||
[JsonProperty("value")] | ||
public AXTreePropertyValue Value { get; set; } | ||
[JsonProperty("description")] | ||
public AXTreePropertyValue Description { get; set; } | ||
[JsonProperty("role")] | ||
public AXTreePropertyValue Role { get; set; } | ||
[JsonProperty("properties")] | ||
public IEnumerable<AXTreeProperty> Properties { get; set; } | ||
} | ||
|
||
public class AXTreeProperty | ||
{ | ||
[JsonProperty("name")] | ||
public string Name { get; internal set; } | ||
[JsonProperty("value")] | ||
public AXTreePropertyValue Value { get; set; } | ||
} | ||
|
||
public class AXTreePropertyValue | ||
{ | ||
[JsonProperty("type")] | ||
public string Type { get; set; } | ||
[JsonProperty("value")] | ||
public JToken Value { get; set; } | ||
} | ||
} | ||
} |
Oops, something went wrong.