blob: 4097ff87f48b5c1c56fe62717dca9c1bce1eed95 [file] [edit]
<!DOCTYPE HTML>
<html>
<head>
<script src="../../resources/accessibility-helper.js"></script>
<script src="../../resources/js-test.js"></script>
</head>
<body>
<!-- A text control whose value ends in a line break renders an empty final line, whose newline is not
a character of the value — innerTextValueFrom() strips one trailing newline "that's collapsed out
by rendering" — so the control's character count and its line ranges must not count it. Editing
represents that newline in shapes setInnerTextValue never produces, which is what these cases
cover; values set in markup are in textarea-line-range-with-trailing-newline.html. -->
<!-- The caret's line range is checked here rather than in the cross-platform character-count test
because it needs AXSelectedTextMarkerRange, which the iOS test runner doesn't implement (see
AccessibilityUIElement::selectedTextMarkerRange, which only mac overrides). -->
<textarea id="warmup" style="width: 200px; height: 60px"></textarea>
<!-- Typed into after the accessibility tree exists, because editing represents the newline that
rendering adds for the empty final line as a text node holding just that newline, where
setInnerTextValue appends a placeholder <br>. The caret's line must come out the same either way. -->
<textarea id="typed" style="width: 200px; height: 60px"></textarea>
<!-- Built through AXReplaceRangeWithText, the way VoiceOver inserts text. That path leaves the whole
value in one text node with the newline rendering adds for the empty final line at its end, so the
collapsed newline is neither a <br> nor a node of its own.
This isn't an accessibility-only shape: AXReplaceRangeWithText is one of many callers of
Editor::replaceSelectionWithText, so pasting a value that ends in a line break produces the same
text node and reproduced the same bug. The insertion is driven through the accessibility API here
only because that needs no pasteboard. -->
<textarea id="inserted" style="width: 200px; height: 60px"></textarea>
<script>
var output = "Asserts the line APIs for text control values edited after the accessibility tree was built:\n"
+ "the caret's line range, and, where a case names one, the last line's character range and the line\n"
+ "of the index one past the end of the value.\n\n";
var cases = [
{ id: "typed", label: "trailing newline typed, caret on the blank final line", value: "one two \n", caret: 9, lineString: "", lineLength: 0, lastLine: 1, lastLineRange: "{9, 0}", typed: true },
// Inserting "\n" and then more text is where VoiceOver's own insertions used to leave the count and
// the line one character long: the value ends without a line break, but the control still renders
// one, and it sits at the end of the same text node as the rest of the value.
{ id: "inserted", label: "inserted through AXReplaceRangeWithText, caret at the end", value: "one two \nthree", caret: 14, lineString: "three", lineLength: 5, lastLine: 1, lastLineRange: "{9, 5}", inserted: [[0, 0, "one "], [4, 0, "two "], [8, 0, "\n"], [9, 0, "three"]] },
];
// expect() evaluates its expressions in its own scope, so the values it reads are globals.
var axField, caret, lineRange, lineEnd, webArea, lastLine, indexPastEnd;
// The id of the text control the document's accessibility selection currently sits in, or null.
function idOfFieldHoldingAccessibilitySelection() {
var selection = axField.selectedTextMarkerRange();
if (!selection)
return null;
var element = axField.accessibilityElementForTextMarker(axField.startTextMarkerForTextMarkerRange(selection));
// The marker points into the control's inner text, so walk out to the control itself.
while (element && !element.domIdentifier)
element = element.parentElement();
return element ? element.domIdentifier : null;
}
// Types |value| one character at a time, the way a user would, leaving the caret somewhere other than
// |caret| so that placing it there below is a real change and posts the notification the gate waits for.
function typeValue(field, value, caret) {
field.focus();
for (var character of value) {
if (character === "\n")
document.execCommand("insertLineBreak");
else
document.execCommand("insertText", false, character);
}
var elsewhere = caret ? 0 : value.length;
field.setSelectionRange(elsewhere, elsewhere);
}
async function checkCase(testCase) {
var field = document.getElementById(testCase.id);
if (testCase.typed)
typeValue(field, testCase.value, testCase.caret);
if (testCase.inserted) {
// AXReplaceRangeWithText needs the field focused, the way VoiceOver leaves it.
field.focus();
var axFieldForInsertion = await waitForElementById(testCase.id);
for (var [location, length, text] of testCase.inserted)
axFieldForInsertion.replaceTextInRange(text, location, length);
// Leave the caret somewhere else so placing it below posts the notification the gate waits for.
field.setSelectionRange(0, 0);
}
axField = await waitForElementById(testCase.id);
if (!axField) {
output += `FAIL: ${testCase.label}: no accessibility element for #${testCase.id}\n`;
return;
}
// The accessibility layer learns of the selection through a notification, so place the caret
// inside waitForNotification: that notification is the point at which the accessibility
// selection — which the text marker below is read from — has caught up. Waiting on this field's
// AXSelectedTextRange instead would be too weak, because that reflects the control's own
// selection, which setSelectionRange updates synchronously, so it can be satisfied while the
// accessibility selection still points into the field measured before this one.
await waitForNotification(webArea, "AXSelectedTextChanged", () => {
field.focus();
field.setSelectionRange(testCase.caret, testCase.caret);
});
var selectionFieldId = idOfFieldHoldingAccessibilitySelection();
if (selectionFieldId !== testCase.id) {
output += `FAIL: ${testCase.label}: the accessibility selection reached #${selectionFieldId}, not #${testCase.id}\n`;
return;
}
caret = axField.startTextMarkerForTextMarkerRange(axField.selectedTextMarkerRange());
lineRange = axField.lineTextMarkerRangeForTextMarker(caret);
lineEnd = axField.endTextMarkerForTextMarkerRange(lineRange);
output += `${testCase.label}: value ${JSON.stringify(testCase.value)}, caret at ${testCase.caret}\n`;
output += expect("axField.numberOfCharacters", `${testCase.value.length}`);
output += expect("axField.stringForTextMarkerRange(lineRange)", JSON.stringify(testCase.lineString));
output += expect("axField.textMarkerRangeLength(lineRange)", `${testCase.lineLength}`);
// A caret on the blank final line has nothing after it, so the line it reports must end where
// the caret is. Otherwise the line names a character past the end of the value.
if (!testCase.lineLength)
output += expect("lineEnd.isEqual(caret)", "true");
if (testCase.lastLineRange) {
lastLine = testCase.lastLine;
indexPastEnd = testCase.value.length;
output += expect("axField.rangeForLine(lastLine)", JSON.stringify(testCase.lastLineRange));
output += expect("axField.lineForIndex(indexPastEnd)", "-1");
}
output += "\n";
}
if (window.accessibilityController) {
window.jsTestIsAsync = true;
setTimeout(async function() {
webArea = accessibilityController.rootElement.childAtIndex(0);
// Warm up the selection machinery on a field this test doesn't measure, so that the first
// measured case doesn't race the accessibility tree's first selection update.
var warmUpField = document.getElementById("warmup");
await waitForElementById("warmup");
await waitForNotification(webArea, "AXSelectedTextChanged", () => {
warmUpField.focus();
warmUpField.setSelectionRange(0, 0);
});
for (var testCase of cases)
await checkCase(testCase);
debugEscaped(output);
finishJSTest();
}, 0);
}
</script>
</body>
</html>