blob: a3fd258931f8ebde419b99e8be54348312f7aa5e [file] [log] [blame] [edit]
<!DOCTYPE html>
<html>
<head>
<script src="../../resources/accessibility-helper.js"></script>
<script src="../../resources/js-test.js"></script>
</head>
<body>
<main id="main">
<div>
on <a id="link" title="Foo bar baz" href="#url1">date</a>
</div>
<table id="table">
<thead>
<tr>
<th><button id="button1">A</button></th>
<th><button id="button2">B</button></th>
</tr>
</thead>
<tbody>
<tr>
<td>ThirdCell</td>
<td>FourthCell</td>
</tr>
</tbody>
</table>
<div id="container"></div>
<div id="foo" style="display: none">Last text</div>
</main>
<script>
var output = "This test ensures that the accessiblity tree doesn't become broken when accessible children change in the middle of servicing an AT request.\n\n";
// This test must *never* be flaky. If there is a bug in the implementation, it will fail only sometimes. If this test
// becomes flaky, that should be taken very seriously. It's possible to very reliably reproduce the bug this test covers
// with VoiceOver by using the tipsy jQuery library (https://github.com/CreativeDream/jquery.tipsy). Add that JS to a
// <script src="tipsy.js">, import jQuery in a <script src="jquery.js">, add this to the webpage:
//
// $("#link").tipsy({ duration: 10, trigger: "focus" });
//
// and then move VoiceOver focus back and forth between the link and the table ("Foo bar baz" should appear and disappear
// on the webpage as you do). If the implementation bug is present, the table content will become completely inaccessible,
// because the accessibility tree becomes broken.
// FIXME: In the future, we should consider adding this VoiceOver test to a manual-tests folder (https://bugs.webkit.org/show_bug.cgi?id=277821)
setInterval(() => {
let randomString = "";
const characters = "ABCDEPXYZabcdef89";
while (randomString.length < 3)
randomString += characters.charAt(Math.floor(Math.random() * characters.length));
// Cause constant children changed events to ensure the accessibility tree is always processing updates.
document.getElementById("container").innerHTML = `<div>${randomString}</div>`;
}, 0);
var axTable = window.accessibilityController ? accessibilityController.accessibleElementById("table") : null;
setInterval(() => {
if (window.accessibilityController) {
// `scrollToMakeVisible()` is intentionally used here, as it is processed asynchronously (it uses
// AccessibilityUIElement::executeOnAXThread and not executeOnAXThreadAndWait), making it possible
// for the accessibility thread to process queued tree updates (`AXIsolatedTree::applyPendingChanges`) while the
// main-thread is in the middle of queuing tree updates (`AXIsolatedTree::updateChildren`).
axTable.scrollToMakeVisible();
}
}, 0);
var root;
var searchResult;
var showTextContainer = true;
var traversalCounter = 0;
function traverse() {
traversalCounter++;
output += `TRAVERSAL ${traversalCounter}\n\n`;
let searchOutput = "";
let tableCount = 0;
let cellCount = 0;
let rowCount = 0;
let buttonCount = 0;
let foundThirdCellText = false;
let foundFourthCellText = false;
while (true) {
searchResult = root.uiElementForSearchPredicate(searchResult, true, "AXAnyTypeSearchKey", "", false);
if (!searchResult)
break;
const role = searchResult.role;
const roleLowerCase = role.toLowerCase();
if (roleLowerCase.includes("table"))
tableCount++;
else if (roleLowerCase.includes("cell"))
cellCount++;
else if (roleLowerCase.includes("row"))
rowCount++;
else if (roleLowerCase.includes("button"))
buttonCount++;
const id = searchResult.domIdentifier;
let resultDescription = `${id ? `#${id} ` : ""}${role}`;
if (role.includes("StaticText")) {
const textValue = ` ${accessibilityController.platformName === "ios" ? searchResult.description : searchResult.stringValue}`;
if (textValue.includes("ThirdCell"))
foundThirdCellText = true;
else if (textValue.includes("FourthCell"))
foundFourthCellText = true;
resultDescription += textValue;
}
searchOutput += `\n{${resultDescription}}\n`;
}
// Every traversal must include these elements. If not, it's very likely the accessibility tree is broken.
if (tableCount != 1 || cellCount != 4 || rowCount != 2 || buttonCount != 2 || !foundThirdCellText || !foundFourthCellText) {
output += `FAIL: Did not find expected elements. Table count: ${tableCount}, cell count: ${cellCount}, rowCount: ${rowCount}, buttonCount: ${buttonCount}, foundThirdCellText: ${foundThirdCellText}, foundFourthCellText: ${foundFourthCellText}`;
output += searchOutput;
}
// Trigger some more children changed events.
if (showTextContainer)
document.getElementById("foo").style.display = "block";
else
document.getElementById("foo").style.display = "none";
showTextContainer = !showTextContainer;
}
if (window.accessibilityController) {
window.jsTestIsAsync = true;
root = accessibilityController.rootElement.childAtIndex(0);
setTimeout(async function() {
for (let i = 0; i < 6; i++) {
traverse();
await sleep(5);
}
document.getElementById("main").style.display = "none";
debug(output);
finishJSTest();
}, 0);
}
</script>
</body>
</html>