blob: 46af7b9f2e9aed4f593a2e84bd8ab247b76af560 [file] [edit]
<!DOCTYPE html><!-- webkit-test-runner [ runSingly=true ] -->
<html>
<head>
<script src="../../resources/js-test.js"></script>
<script src="../../resources/accessibility-helper.js"></script>
</head>
<body>
<textarea id="textarea" rows="5" cols="40"></textarea>
<p id="description"></p>
<div id="console"></div>
<script>
description("This tests that when an AXValueChanged notification fires for a textarea after inserting text, the accessibility selection (caret position) reported at that moment is consistent with the newly-reported value. Regression test for an isolated-tree bug where value updates and selection updates are delivered to the isolated tree on separate channels, leaving it temporarily inconsistent (value updated, selection stale) at the time the value-changed notification is delivered.");
var textareaElement = document.getElementById("textarea");
var axTextarea = 0;
var axWebArea = 0;
// The number of characters we type. Each keystroke should produce an AXValueChanged.
var charactersToType = 5;
var valueChangedCount = 0;
// Records any inconsistency seen at AXValueChanged-delivery time. We keep the
// details in a string (rather than debug()-ing immediately) so the committed
// expected result stays stable in the passing case; on failure the details are
// surfaced through testFailed().
var inconsistencies = [];
// Returns the length of the accessibility value string (stripping "AXValue: ").
function currentValueLength() {
var value = axTextarea.stringValue;
if (!value)
return -1;
return value.replace(/^AXValue: /, "").length;
}
// Caret offset from the node-level AXSelectedTextRange ("{location, length}").
function caretOffsetFromSelectedTextRange() {
var range = axTextarea.selectedTextRange;
if (!range)
return -1;
var match = /\{(\d+), (\d+)\}/.exec(range);
return match ? parseInt(match[1], 10) : -1;
}
// Caret offset from the tree-level AXSelectedTextMarkerRange.
function caretOffsetFromSelectedMarkerRange() {
var markerRange = axWebArea.selectedTextMarkerRange();
if (!markerRange)
return -1;
var startMarker = axWebArea.startTextMarkerForTextMarkerRange(markerRange);
if (!startMarker)
return -1;
return axWebArea.indexForTextMarker(startMarker);
}
function notificationCallback(notification) {
if (notification != "AXValueChanged")
return;
valueChangedCount++;
// At the moment the value-changed notification is delivered, the caret should
// sit at the end of the inserted text, i.e. the selection should report an
// offset equal to the current value length. AXSelectedTextRange is the node-level
// selection an assistive technology reads in response to AXValueChanged; if it
// lags behind the value, the isolated tree is in an inconsistent state.
var valueLength = currentValueLength();
var caretFromRange = caretOffsetFromSelectedTextRange();
// AXSelectedTextMarkerRange is a second, tree-level selection representation,
// reported here for diagnostics only.
var caretFromMarker = caretOffsetFromSelectedMarkerRange();
var detail = `AXValueChanged #${valueChangedCount}: valueLength=${valueLength}, ` +
`AXSelectedTextRange caret=${caretFromRange}, AXSelectedTextMarkerRange caret=${caretFromMarker}`;
if (caretFromRange != valueLength)
inconsistencies.push(detail + " (INCONSISTENT: selection is stale relative to value)");
}
// Returns a promise that wiggles the mouse over the textarea. Doing this concurrently with typing
// interleaves extra main-thread and AX-thread work, which makes the value-vs-selection race far more
// likely to reproduce in isolated tree mode.
async function wigglePromise() {
if (!window.eventSender)
return;
var rect = textareaElement.getBoundingClientRect();
for (var i = 0; i < 6; i++) {
eventSender.mouseMoveTo(rect.left + (i * 3), rect.top + (i * 2));
await new Promise(requestAnimationFrame);
}
}
if (window.accessibilityController) {
window.jsTestIsAsync = true;
textareaElement.focus();
axTextarea = accessibilityController.accessibleElementById("textarea");
axWebArea = accessibilityController.rootElement.childAtIndex(0);
axTextarea.addNotificationListener(notificationCallback);
setTimeout(async function() {
for (var i = 0; i < charactersToType; i++) {
var ch = String.fromCharCode("a".charCodeAt(0) + i);
// Kick off mouse wiggling concurrently with the keystroke to exercise the race.
var wiggle = wigglePromise();
if (window.eventSender)
eventSender.keyDown(ch);
else {
// Fallback for environments without eventSender: mutate the DOM directly.
textareaElement.value += ch;
textareaElement.setSelectionRange(textareaElement.value.length, textareaElement.value.length);
}
await wiggle;
// Wait until the AXValueChanged for this keystroke has been observed.
await waitFor(() => valueChangedCount > i);
}
if (!inconsistencies.length)
testPassed("Value and selection were consistent at every AXValueChanged notification.");
else {
for (var i = 0; i < inconsistencies.length; i++)
testFailed(inconsistencies[i]);
}
axTextarea.removeNotificationListener(notificationCallback);
finishJSTest();
}, 0);
}
</script>
</body>
</html>