Skip to content

Commit

Permalink
AX: display:contents elements are sometimes missing their children
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=259608
rdar://problem/113044333

Reviewed by Chris Fleizach.

This happened because AccessibilityObject::insertChild detects and bails when an object is trying to insert a child
that belongs to a display:contents object that is not `this`. But if the correct parent of that child does not have
it’s dirty-children bit set, we never actually insert the child, resulting in it being missing from the accessibility
tree. With this patch, we set that bit, ensuring the accessibility tree is updated correctly.

* LayoutTests/accessibility/display-contents/table-dynamic-expected.txt: Added.
* LayoutTests/accessibility/display-contents/table-dynamic.html: Added.
* LayoutTests/platform/ios/accessibility/display-contents/table-dynamic-expected.txt: Added.
* Source/WebCore/accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::insertChild):

Canonical link: https://commits.webkit.org/266407@main
  • Loading branch information
twilco committed Jul 28, 2023
1 parent 5b4bb30 commit 45ff67f
Show file tree
Hide file tree
Showing 5 changed files with 474 additions and 1 deletion.
184 changes: 184 additions & 0 deletions LayoutTests/accessibility/display-contents/table-dynamic-expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
This test ensures that display:contents tables with dynamic content are accessible.

Dumping table #t0 (expecting row count 3, column count 2)
PASS: table.rowCount === 3
PASS: table.columnCount === 2
PASS: table.cellForColumnAndRow(0, 0).domIdentifier === "r0c0-t0"
PASS: table.cellForColumnAndRow(1, 0).domIdentifier === "r0c1-t0"
PASS: table.cellForColumnAndRow(0, 1).domIdentifier === "r1c0-t0"
PASS: table.cellForColumnAndRow(1, 1).domIdentifier === "r1c1-t0"
PASS: table.cellForColumnAndRow(0, 2).domIdentifier === "r2c0-t0"
PASS: table.cellForColumnAndRow(1, 2).domIdentifier === "r2c1-t0"
Dumping table #t1 (expecting row count 3, column count 2)
PASS: table.rowCount === 3
PASS: table.columnCount === 2
PASS: table.cellForColumnAndRow(0, 0).domIdentifier === "r0c0-t1"
PASS: table.cellForColumnAndRow(1, 0).domIdentifier === "r0c1-t1"
PASS: table.cellForColumnAndRow(0, 1).domIdentifier === "r1c0-t1"
PASS: table.cellForColumnAndRow(1, 1).domIdentifier === "r1c1-t1"
PASS: table.cellForColumnAndRow(0, 2).domIdentifier === "r2c0-t1"
PASS: table.cellForColumnAndRow(1, 2).domIdentifier === "r2c1-t1"

Performing search traversal of body.

{#t0 AXRole: AXTable} (parent: {#body AXRole: AXGroup})

{#r0-t0 AXRole: AXRow} (parent: {#t0 AXRole: AXTable})

{#r0c0-t0 AXRole: AXCell} (parent: {#r0-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) Author} (parent: {#r0c0-t0 AXRole: AXCell})

{#r0c1-t0 AXRole: AXCell} (parent: {#r0-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) Title} (parent: {#r0c1-t0 AXRole: AXCell})

{#r1-t0 AXRole: AXRow} (parent: {#t0 AXRole: AXTable})

{#r1c0-t0 AXRole: AXCell} (parent: {#r1-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) Stephen Hawking} (parent: {#r1c0-t0 AXRole: AXCell})

{#r1c1-t0 AXRole: AXCell} (parent: {#r1-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) A Brief History of Time} (parent: {#r1c1-t0 AXRole: AXCell})

{#r2-t0 AXRole: AXRow} (parent: {#t0 AXRole: AXTable})

{#r2c0-t0 AXRole: AXCell} (parent: {#r2-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) Carl Sagan} (parent: {#r2c0-t0 AXRole: AXCell})

{#r2c1-t0 AXRole: AXCell} (parent: {#r2-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) Cosmos} (parent: {#r2c1-t0 AXRole: AXCell})

{#t1 AXRole: AXTable} (parent: {#body AXRole: AXGroup})

{#r0-t1 AXRole: AXRow} (parent: {#t1 AXRole: AXTable})

{#r0c0-t1 AXRole: AXCell} (parent: {#r0-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) Author} (parent: {#r0c0-t1 AXRole: AXCell})

{#r0c1-t1 AXRole: AXCell} (parent: {#r0-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) Title} (parent: {#r0c1-t1 AXRole: AXCell})

{#r1-t1 AXRole: AXRow} (parent: {#t1 AXRole: AXTable})

{#r1c0-t1 AXRole: AXCell} (parent: {#r1-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) Stephen Hawking} (parent: {#r1c0-t1 AXRole: AXCell})

{#r1c1-t1 AXRole: AXCell} (parent: {#r1-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) A Brief History of Time} (parent: {#r1c1-t1 AXRole: AXCell})

{#r2-t1 AXRole: AXRow} (parent: {#t1 AXRole: AXTable})

{#r2c0-t1 AXRole: AXCell} (parent: {#r2-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) Carl Sagan} (parent: {#r2c0-t1 AXRole: AXCell})

{#r2c1-t1 AXRole: AXCell} (parent: {#r2-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) Cosmos} (parent: {#r2c1-t1 AXRole: AXCell})

Moving #r2-t1 from table one to table zero.
Dumping table #t0 (expecting row count 4, column count 2)
PASS: table.rowCount === 4
PASS: table.columnCount === 2
table.cellForColumnAndRow(0, 0).domIdentifier is r0c0-t0
table.cellForColumnAndRow(1, 0).domIdentifier is r0c1-t0
table.cellForColumnAndRow(0, 1).domIdentifier is r1c0-t0
table.cellForColumnAndRow(1, 1).domIdentifier is r1c1-t0
table.cellForColumnAndRow(0, 2).domIdentifier is r2c0-t0
table.cellForColumnAndRow(1, 2).domIdentifier is r2c1-t0
table.cellForColumnAndRow(0, 3).domIdentifier is r2c0-t1
table.cellForColumnAndRow(1, 3).domIdentifier is r2c1-t1
Dumping table #t1 (expecting row count 2, column count 2)
PASS: table.rowCount === 2
PASS: table.columnCount === 2
PASS: table.cellForColumnAndRow(0, 0).domIdentifier === "r0c0-t1"
PASS: table.cellForColumnAndRow(1, 0).domIdentifier === "r0c1-t1"
PASS: table.cellForColumnAndRow(0, 1).domIdentifier === "r1c0-t1"
PASS: table.cellForColumnAndRow(1, 1).domIdentifier === "r1c1-t1"

Performing search traversal of body.

{#t0 AXRole: AXTable} (parent: {#body AXRole: AXGroup})

{#r0-t0 AXRole: AXRow} (parent: {#t0 AXRole: AXTable})

{#r0c0-t0 AXRole: AXCell} (parent: {#r0-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) Author} (parent: {#r0c0-t0 AXRole: AXCell})

{#r0c1-t0 AXRole: AXCell} (parent: {#r0-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) Title} (parent: {#r0c1-t0 AXRole: AXCell})

{#r1-t0 AXRole: AXRow} (parent: {#t0 AXRole: AXTable})

{#r1c0-t0 AXRole: AXCell} (parent: {#r1-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) Stephen Hawking} (parent: {#r1c0-t0 AXRole: AXCell})

{#r1c1-t0 AXRole: AXCell} (parent: {#r1-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) A Brief History of Time} (parent: {#r1c1-t0 AXRole: AXCell})

{#r2-t0 AXRole: AXRow} (parent: {#t0 AXRole: AXTable})

{#r2c0-t0 AXRole: AXCell} (parent: {#r2-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) Carl Sagan} (parent: {#r2c0-t0 AXRole: AXCell})

{#r2c1-t0 AXRole: AXCell} (parent: {#r2-t0 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T0) Cosmos} (parent: {#r2c1-t0 AXRole: AXCell})

{#r2-t1 AXRole: AXRow} (parent: {#t0 AXRole: AXTable})

{#r2c0-t1 AXRole: AXCell} (parent: {#r2-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) Carl Sagan} (parent: {#r2c0-t1 AXRole: AXCell})

{#r2c1-t1 AXRole: AXCell} (parent: {#r2-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) Cosmos} (parent: {#r2c1-t1 AXRole: AXCell})

{#t1 AXRole: AXTable} (parent: {#body AXRole: AXGroup})

{#r0-t1 AXRole: AXRow} (parent: {#t1 AXRole: AXTable})

{#r0c0-t1 AXRole: AXCell} (parent: {#r0-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) Author} (parent: {#r0c0-t1 AXRole: AXCell})

{#r0c1-t1 AXRole: AXCell} (parent: {#r0-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) Title} (parent: {#r0c1-t1 AXRole: AXCell})

{#r1-t1 AXRole: AXRow} (parent: {#t1 AXRole: AXTable})

{#r1c0-t1 AXRole: AXCell} (parent: {#r1-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) Stephen Hawking} (parent: {#r1c0-t1 AXRole: AXCell})

{#r1c1-t1 AXRole: AXCell} (parent: {#r1-t1 AXRole: AXRow})

{AXRole: AXStaticText AXValue: (T1) A Brief History of Time} (parent: {#r1c1-t1 AXRole: AXCell})

PASS successfullyParsed is true

TEST COMPLETE
Table zero caption
(T0) Author (T0) Title
(T0) Stephen Hawking (T0) A Brief History of Time
(T0) Carl Sagan (T0) Cosmos
(T1) Carl Sagan (T1) Cosmos
Table one caption
(T1) Author (T1) Title
(T1) Stephen Hawking (T1) A Brief History of Time
126 changes: 126 additions & 0 deletions LayoutTests/accessibility/display-contents/table-dynamic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<script src="../../resources/accessibility-helper.js"></script>
<script src="../../resources/js-test.js"></script>
<style>
td, th { display: contents; }
</style>
</head>
<body role="group" id="body">

<table id="t0">
<caption>Table zero caption</caption>
<thead>
<tr id="r0-t0">
<th id="r0c0-t0">(T0) Author</th>
<th id="r0c1-t0">(T0) Title</th>
</tr>
</thead>
<tbody id="tbody-t0">
<tr id="r1-t0">
<td id="r1c0-t0">(T0) Stephen Hawking</td>
<td id="r1c1-t0">(T0) A Brief History of Time</td>
</tr>
<tr id="r2-t0">
<td id="r2c0-t0">(T0) Carl Sagan</td>
<td id="r2c1-t0">(T0) Cosmos</td>
</tr>
</tbody>
</table>

<table id="t1">
<caption>Table one caption</caption>
<thead>
<tr id="r0-t1">
<th id="r0c0-t1">(T1) Author</th>
<th id="r0c1-t1">(T1) Title</th>
</tr>
</thead>
<tbody id="tbody-t1">
<tr id="r1-t1">
<td id="r1c0-t1">(T1) Stephen Hawking</td>
<td id="r1c1-t1">(T1) A Brief History of Time</td>
</tr>
<tr id="r2-t1">
<td id="r2c0-t1">(T1) Carl Sagan</td>
<td id="r2c1-t1">(T1) Cosmos</td>
</tr>
</tbody>
</table>

<script>
var output = "This test ensures that display:contents tables with dynamic content are accessible.\n\n";

var table;
function dumpTable(tableID, expectedRowCount, expectedColumnCount, useExpect = true) {
output += `Dumping table #${tableID} (expecting row count ${expectedRowCount}, column count ${expectedColumnCount})\n`;
table = accessibilityController.accessibleElementById(tableID);
output += expect("table.rowCount", expectedRowCount);
output += expect("table.columnCount", expectedColumnCount);

for (let row = 0; row < expectedRowCount; row++) {
for (let column = 0; column < expectedColumnCount; column++) {
if (useExpect)
output += expect(`table.cellForColumnAndRow(${column}, ${row}).domIdentifier`, `"r${row}c${column}-${tableID}"`);
else
output += `table.cellForColumnAndRow(${column}, ${row}).domIdentifier is ${table.cellForColumnAndRow(column, row).domIdentifier}\n`;
}
}
}

function traverseBody() {
output += `\nPerforming search traversal of body.\n`;
function elementDescription(axElement) {
if (!axElement)
return "null";

const role = axElement.role;
const id = axElement.domIdentifier;
let result = `${id ? `#${id} ` : ""}${role}`;
if (role.includes("StaticText"))
result += ` ${accessibilityController.platformName === "ios" ? axElement.description : axElement.stringValue}`;
return result;
}

const container = accessibilityController.accessibleElementById("body");
let searchResult = null;
while (true) {
searchResult = container.uiElementForSearchPredicate(searchResult, true, "AXAnyTypeSearchKey", "", false);
if (!searchResult)
break;
const parentOutput = accessibilityController.platformName === "ios" ? "" : ` (parent: {${elementDescription(searchResult.parentElement())}})`;
output += `\n{${elementDescription(searchResult)}}${parentOutput}\n`;
}
}

if (window.accessibilityController) {
window.jsTestIsAsync = true;

dumpTable("t0", 3, 2);
dumpTable("t1", 3, 2);
traverseBody();

output += "\nMoving #r2-t1 from table one to table zero.\n";
document.getElementById("tbody-t0").appendChild(document.getElementById("r2-t1"));
setTimeout(async () => {
await waitFor(() => {
var tableZero = accessibilityController.accessibleElementById("t0");
var tableOne = accessibilityController.accessibleElementById("t1");
if (!tableZero || !tableOne)
return false;
return tableZero.rowCount >= 4 && tableOne.rowCount === 2;
});

dumpTable("t0", 4, 2, false /* useExpect */);
dumpTable("t1", 2, 2);
traverseBody();

debug(output);
finishJSTest();
});
}
</script>
</body>
</html>

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
This test ensures that display:contents tables with dynamic content are accessible.

Dumping table #t0 (expecting row count 3, column count 2)
PASS: table.rowCount === 3
PASS: table.columnCount === 2
PASS: table.cellForColumnAndRow(0, 0).domIdentifier === "r0c0-t0"
PASS: table.cellForColumnAndRow(1, 0).domIdentifier === "r0c1-t0"
PASS: table.cellForColumnAndRow(0, 1).domIdentifier === "r1c0-t0"
PASS: table.cellForColumnAndRow(1, 1).domIdentifier === "r1c1-t0"
PASS: table.cellForColumnAndRow(0, 2).domIdentifier === "r2c0-t0"
PASS: table.cellForColumnAndRow(1, 2).domIdentifier === "r2c1-t0"
Dumping table #t1 (expecting row count 3, column count 2)
PASS: table.rowCount === 3
PASS: table.columnCount === 2
PASS: table.cellForColumnAndRow(0, 0).domIdentifier === "r0c0-t1"
PASS: table.cellForColumnAndRow(1, 0).domIdentifier === "r0c1-t1"
PASS: table.cellForColumnAndRow(0, 1).domIdentifier === "r1c0-t1"
PASS: table.cellForColumnAndRow(1, 1).domIdentifier === "r1c1-t1"
PASS: table.cellForColumnAndRow(0, 2).domIdentifier === "r2c0-t1"
PASS: table.cellForColumnAndRow(1, 2).domIdentifier === "r2c1-t1"

Performing search traversal of body.

Moving #r2-t1 from table one to table zero.
Dumping table #t0 (expecting row count 4, column count 2)
PASS: table.rowCount === 4
PASS: table.columnCount === 2
table.cellForColumnAndRow(0, 0).domIdentifier is r0c0-t0
table.cellForColumnAndRow(1, 0).domIdentifier is r0c1-t0
table.cellForColumnAndRow(0, 1).domIdentifier is r1c0-t0
table.cellForColumnAndRow(1, 1).domIdentifier is r1c1-t0
table.cellForColumnAndRow(0, 2).domIdentifier is r2c0-t0
table.cellForColumnAndRow(1, 2).domIdentifier is r2c1-t0
table.cellForColumnAndRow(0, 3).domIdentifier is r2c0-t1
table.cellForColumnAndRow(1, 3).domIdentifier is r2c1-t1
Dumping table #t1 (expecting row count 2, column count 2)
PASS: table.rowCount === 2
PASS: table.columnCount === 2
PASS: table.cellForColumnAndRow(0, 0).domIdentifier === "r0c0-t1"
PASS: table.cellForColumnAndRow(1, 0).domIdentifier === "r0c1-t1"
PASS: table.cellForColumnAndRow(0, 1).domIdentifier === "r1c0-t1"
PASS: table.cellForColumnAndRow(1, 1).domIdentifier === "r1c1-t1"

Performing search traversal of body.

PASS successfullyParsed is true

TEST COMPLETE
Table zero caption
(T0) Author (T0) Title
(T0) Stephen Hawking (T0) A Brief History of Time
(T0) Carl Sagan (T0) Cosmos
(T1) Carl Sagan (T1) Cosmos
Table one caption
(T1) Author (T1) Title
(T1) Stephen Hawking (T1) A Brief History of Time
Loading

0 comments on commit 45ff67f

Please sign in to comment.