Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
"use strict";
function getCaretRect(docAcc, id) {
const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]);
const caretX = {};
const caretY = {};
const caretW = {};
const caretH = {};
acc.getCaretRect(caretX, caretY, caretW, caretH);
const caretBounds = [caretX.value, caretY.value, caretW.value, caretH.value];
info(`Caret bounds: ${caretBounds}`);
return caretBounds;
}
function getCollapsedRangeExtents(acc, offset) {
const rangeX = {};
const rangeY = {};
const rangeW = {};
const rangeH = {};
acc.getRangeExtents(
offset,
offset,
rangeX,
rangeY,
rangeW,
rangeH,
COORDTYPE_SCREEN_RELATIVE
);
const rangeBounds = [rangeX.value, rangeY.value, rangeW.value, rangeH.value];
info(`Range ${offset}-${offset} bounds: ${rangeBounds}`);
return rangeBounds;
}
async function fetchCollapsedRangeBounds(docAcc, acc) {
const state = {};
acc.getState(state, {});
if (state.value & nsIAccessibleStates.STATE_FOCUSABLE) {
// This pre-scrolls the accessible into view as a focus would do.
let focused = waitForEvent(EVENT_FOCUS, acc);
acc.takeFocus();
await focused;
// We need to blur the accessible so the caret does not interfere with
// the bounds fetch.
focused = waitForEvent(EVENT_FOCUS, docAcc);
docAcc.takeFocus();
await focused;
}
acc.QueryInterface(nsIAccessibleHyperText);
// If this is a 0 length text accessible, we need to ensure that
// characterCount is at least 1.
const characterCount = acc.characterCount ? acc.characterCount : 1;
const bounds = [];
for (let offset = 0; offset < characterCount; offset++) {
let linkIndex = acc.getLinkIndexAtOffset(offset);
if (linkIndex != -1) {
const link = acc
.getLinkAt(linkIndex)
.QueryInterface(nsIAccessibleText)
.QueryInterface(nsIAccessible);
bounds.push(...(await fetchCollapsedRangeBounds(docAcc, link)));
} else {
let rect = getCollapsedRangeExtents(acc, offset);
bounds.push(rect);
}
}
return bounds;
}
function testCaretRect(
docAcc,
id,
offset,
fetchedBounds,
atLineEnd = false,
isVertical = false
) {
const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]);
is(acc.caretOffset, offset, `Caret at offset ${offset}`);
const atEnd = offset == acc.characterCount;
const empty = offset == 0 && atEnd;
let queryOffset = atEnd && !empty ? offset - 1 : offset;
const atEndInNewLine = atEnd && acc.getCharacterAtOffset(queryOffset) == "\n";
const [rangeX, rangeY, , rangeH] =
fetchedBounds.length > queryOffset
? fetchedBounds[queryOffset]
: [0, 0, 0, 0];
const [caretX, caretY, caretW, caretH] = getCaretRect(docAcc, id);
if (!empty) {
// In case of an empty input `getRangeExtents()` will return the full accessible bounds.
let [currRangeX, currRangeY, currRangeW] = getCollapsedRangeExtents(
acc,
acc.caretOffset
);
Assert.deepEqual(
[caretX, caretY, caretW],
[currRangeX, currRangeY, currRangeW],
"Collapsed range extents at caret position should be identical caret rect"
);
}
if (atEndInNewLine) {
Assert.lessOrEqual(caretX, rangeX, "Caret x before range x");
} else if (atEnd || atLineEnd) {
Assert.greater(caretX, rangeX, "Caret x after last range x");
} else {
// Caret width changes depending on device pixel ratio. In RTL
// text that would change the x where the caret is drawn by a pixel or two.
isWithin(caretX, rangeX, 3, "Caret x similar to range x");
}
if (isVertical && atEnd) {
Assert.greaterOrEqual(caretY, rangeY, "Caret y below range y");
} else if (atEndInNewLine) {
Assert.greater(caretY, rangeY, "Caret y below range y");
} else if (atLineEnd) {
Assert.less(caretY, rangeY, "Caret y above start line range.");
} else {
isWithin(caretY, rangeY, 3, "Caret y similar to range y");
}
ok(caretW, "Caret width is greater than 0");
if (!empty) {
// Depending on glyph, the range can be taller.
isWithin(caretH, rangeH, 2, "Caret height similar to range height");
}
}
function getAccBounds(acc) {
const x = {};
const y = {};
const w = {};
const h = {};
acc.getBounds(x, y, w, h);
return [x.value, y.value, w.value, h.value];
}
/**
* Test the caret rect in content documents.
*/
addAccessibleTask(
`
<input id="input" value="ab">
<input id="emptyInput">
`,
async function (browser, docAcc) {
async function runTests() {
const input = findAccessibleChildByID(docAcc, "input", [
nsIAccessibleText,
]);
let fetchedBounds = await fetchCollapsedRangeBounds(docAcc, input);
info("Focusing input");
let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
input.takeFocus();
await caretMoved;
testCaretRect(docAcc, "input", 0, fetchedBounds);
info("Setting caretOffset to 1");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
input.caretOffset = 1;
await caretMoved;
testCaretRect(docAcc, "input", 1, fetchedBounds);
info("Setting caretOffset to 2");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
input.caretOffset = 2;
await caretMoved;
testCaretRect(docAcc, "input", 2, fetchedBounds);
info("Resetting caretOffset to 0");
input.caretOffset = 0;
const emptyInput = findAccessibleChildByID(docAcc, "emptyInput", [
nsIAccessibleText,
]);
fetchedBounds = await fetchCollapsedRangeBounds(docAcc, emptyInput);
info("Focusing emptyInput");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, emptyInput);
emptyInput.takeFocus();
await caretMoved;
testCaretRect(docAcc, "emptyInput", 0, fetchedBounds);
}
await runTests();
// Check that the caret rect is correct when the title bar is shown.
if (LINUX || Services.env.get("MOZ_HEADLESS")) {
// Disabling tabs in title bar doesn't change the bounds on Linux or in
// headless mode.
info("Skipping title bar tests");
return;
}
const [, origDocY] = getAccBounds(docAcc);
info("Showing title bar");
let titleBarChanged = BrowserTestUtils.waitForMutationCondition(
document.documentElement,
{ attributes: true, attributeFilter: ["customtitlebar"] },
() => !document.documentElement.hasAttribute("customtitlebar")
);
await SpecialPowers.pushPrefEnv({
set: [["browser.tabs.inTitlebar", false]],
});
await titleBarChanged;
const [, newDocY] = getAccBounds(docAcc);
Assert.greater(
newDocY,
origDocY,
"Doc has larger y after title bar change"
);
await runTests();
await SpecialPowers.popPrefEnv();
},
{ chrome: true, topLevel: true }
);
/**
* Test the caret rect in multiline content.
*/
addAccessibleTask(
`<style>
@font-face {
font-family: Ahem;
src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
}
textarea {
font: 10px/10px Ahem;
width: 30px;
height: 80px;
}
</style>
<textarea id="textarea">1234
56789
</textarea>
`,
async function testMultiline(browser, docAcc) {
async function moveCaret(key, keyopts = {}) {
let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, "textarea");
if (key) {
EventUtils.synthesizeKey(key, keyopts);
} else {
// If no key is provided, just focus the textarea.
findAccessibleChildByID(docAcc, "textarea").takeFocus();
}
let evt = await caretMoved;
evt.QueryInterface(nsIAccessibleCaretMoveEvent);
return [evt.caretOffset, evt.isAtEndOfLine];
}
const textarea = findAccessibleChildByID(docAcc, "textarea", [
nsIAccessibleText,
]);
let fetchedBounds = await fetchCollapsedRangeBounds(docAcc, textarea);
info("Focusing textarea");
let [offset, isAtLineEnd] = await moveCaret();
is(offset, 0, "Caret at offset 0");
is(isAtLineEnd, false, "Caret not at end of line");
testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
info("Moving caret right");
[offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
is(offset, 1, "Caret at offset 1");
is(isAtLineEnd, false, "Caret not at end of line");
testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
info("Moving caret right again");
[offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
is(offset, 2, "Caret at offset 2");
is(isAtLineEnd, false, "Caret not at end of line");
testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
info("Moving caret right again again");
[offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
is(offset, 3, "Caret at offset 3");
is(isAtLineEnd, true, "Caret at end of line");
testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
info("Moving caret right stays at same offset, but on new line");
[offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
is(offset, 3, "Caret at offset 3");
is(isAtLineEnd, false, "Caret not at end of line");
testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
info("Moving caret right in second line");
[offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
is(offset, 4, "Caret at offset 4");
is(isAtLineEnd, false, "Caret not at end of line");
testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
info("Move caret right to new line");
[offset, isAtLineEnd] = await moveCaret("KEY_ArrowRight");
is(offset, 5, "Caret at offset 5");
is(isAtLineEnd, false, "Caret not at end of line");
testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
info("Move caret to end of previous line");
[offset, isAtLineEnd] = await moveCaret("KEY_ArrowLeft");
is(offset, 4, "Caret at offset 4");
is(isAtLineEnd, false, "Caret at end line break");
testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
info("Move caret to end of text");
if (AppConstants.platform == "macosx") {
[offset, isAtLineEnd] = await moveCaret("KEY_PageDown", {
altKey: true,
});
} else {
[offset, isAtLineEnd] = await moveCaret("KEY_End", {
ctrlKey: true,
});
}
is(offset, 11, "Caret at offset 11");
is(isAtLineEnd, false, "Caret at end line break");
testCaretRect(docAcc, "textarea", offset, fetchedBounds, isAtLineEnd);
}
);
function todoIsWithin(expected, got, within, msg) {
if (Math.abs(got - expected) <= within) {
todo(true, `${msg} - Got ${got}`);
} else {
todo(
false,
`${msg} - Got ${got}, expected ${expected} with error of ${within}`
);
}
}
/**
* Test the caret rect and collapsed range in bidi text
*/
addAccessibleTask(
`
<input id="input1" value="hello שלום world">
<input id="input2" dir="rtl" value="שלום hello עולם">
<textarea id="textarea" dir="rtl">שלום
עולם</textarea>
`,
async function testRTL(browser, docAcc) {
const input1 = findAccessibleChildByID(docAcc, "input1", [
nsIAccessibleText,
]);
let fetchedBounds = await fetchCollapsedRangeBounds(docAcc, input1);
info("Focusing input1");
let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input1);
input1.takeFocus();
await caretMoved;
testCaretRect(docAcc, "input1", 0, fetchedBounds);
info("Setting caretOffset to 1");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input1);
input1.caretOffset = 1;
await caretMoved;
testCaretRect(docAcc, "input1", 1, fetchedBounds);
info("Setting caretOffset to 6 (first in embedded RTL)");
// Retrieving rangeX before caret goes there
let [rangeX] = getCollapsedRangeExtents(input1, 6);
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input1);
input1.caretOffset = 6;
await caretMoved;
let [caretX] = getCaretRect(docAcc, "input1");
todoIsWithin(caretX, rangeX, 2, "Caret x similar to range x");
info("Setting caretOffset to 7 (in embedded RTL)");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input1);
input1.caretOffset = 7;
await caretMoved;
testCaretRect(docAcc, "input1", 7, fetchedBounds);
info("Resetting caretOffset to 0");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input1);
input1.caretOffset = 0;
await caretMoved;
const input2 = findAccessibleChildByID(docAcc, "input2", [
nsIAccessibleText,
]);
fetchedBounds = await fetchCollapsedRangeBounds(docAcc, input2);
info("Focusing input2");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input2);
input2.takeFocus();
await caretMoved;
testCaretRect(docAcc, "input2", 0, fetchedBounds);
info("Setting caretOffset to 1");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input2);
input2.caretOffset = 1;
await caretMoved;
testCaretRect(docAcc, "input2", 1, fetchedBounds);
info("Setting caretOffset to 5 (first in embedded LTR)");
// Retrieving rangeX before caret goes there
[rangeX] = getCollapsedRangeExtents(input2, 5);
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input2);
input2.caretOffset = 5;
await caretMoved;
[caretX] = getCaretRect(docAcc, "input1");
todoIsWithin(caretX, rangeX, 2, "Caret x similar to range x");
info("Setting caretOffset to 7 (in embedded LTR)");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input2);
input2.caretOffset = 7;
await caretMoved;
testCaretRect(docAcc, "input2", 7, fetchedBounds);
const textarea = findAccessibleChildByID(docAcc, "textarea", [
nsIAccessibleText,
]);
fetchedBounds = await fetchCollapsedRangeBounds(docAcc, textarea);
info("Focusing textarea");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
textarea.takeFocus();
await caretMoved;
testCaretRect(docAcc, "textarea", 0, fetchedBounds);
info("Setting caretOffset to 1");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
textarea.caretOffset = 1;
await caretMoved;
testCaretRect(docAcc, "textarea", 1, fetchedBounds);
info("Setting caretOffset to 4 (before newline)");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
textarea.caretOffset = 4;
await caretMoved;
testCaretRect(docAcc, "textarea", 4, fetchedBounds);
info("Setting caretOffset to 5 (after newline)");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, textarea);
textarea.caretOffset = 5;
await caretMoved;
testCaretRect(docAcc, "textarea", 5, fetchedBounds);
},
{ chrome: true, topLevel: true }
);
/**
* Test the caret rect in vertical text.
*/
addAccessibleTask(
`
<input id="input" value="ab" style="writing-mode: vertical-lr;">
<input id="emptyInput" style="writing-mode: vertical-lr;">
`,
async function testVerticalInputs(browser, docAcc) {
const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
let fetchedBounds = await fetchCollapsedRangeBounds(docAcc, input);
info("Focusing input");
let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
input.takeFocus();
await caretMoved;
testCaretRect(docAcc, "input", 0, fetchedBounds, false, true);
info("Setting caretOffset to 1");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
input.caretOffset = 1;
await caretMoved;
testCaretRect(docAcc, "input", 1, fetchedBounds, false, true);
info("Setting caretOffset to 2");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
input.caretOffset = 2;
await caretMoved;
testCaretRect(docAcc, "input", 2, fetchedBounds, false, true);
info("Resetting caretOffset to 0");
input.caretOffset = 0;
const emptyInput = findAccessibleChildByID(docAcc, "emptyInput", [
nsIAccessibleText,
]);
fetchedBounds = await fetchCollapsedRangeBounds(docAcc, emptyInput);
info("Focusing emptyInput");
caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, emptyInput);
emptyInput.takeFocus();
await caretMoved;
testCaretRect(docAcc, "emptyInput", 0, fetchedBounds, false, true);
}
);
/**
* Test contenteditable caret
*/
addAccessibleTask(
`
<div contenteditable="true" id="input" role="textbox">one <strong>two</strong> <i>three</i></div>
`,
async function testRichTextEdit(browser, docAcc) {
const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
let fetchedBounds = await fetchCollapsedRangeBounds(docAcc, input);
let focused = waitForEvent(EVENT_FOCUS, input);
input.takeFocus();
info("Focusing input");
await focused;
testCaretRect(docAcc, "input", 0, fetchedBounds);
for (let offset = 1; offset < fetchedBounds.length - 1; offset++) {
info("Pressing ArrowRight to move caret to index " + offset);
let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED);
EventUtils.synthesizeKey("KEY_ArrowRight");
let evt = (await caretMoved).QueryInterface(nsIAccessibleCaretMoveEvent);
let acc = evt.accessible.QueryInterface(nsIAccessibleHyperText);
let caretOffset = evt.caretOffset;
let linkIndex = acc.getLinkIndexAtOffset(evt.caretOffset);
if (linkIndex != -1) {
acc = acc
.getLinkAt(linkIndex)
.QueryInterface(nsIAccessibleText)
.QueryInterface(nsIAccessible);
caretOffset = 0;
}
const [rangeX, rangeY, rangeW, rangeH] =
fetchedBounds.length > offset ? fetchedBounds[offset] : [0, 0, 0, 0];
const [caretX, caretY, caretW, caretH] = getCollapsedRangeExtents(
acc,
caretOffset
);
info(
`${offset}: Caret rect: ${caretX}, ${caretY}, ${caretW}, ${caretH}, Prefetched rect: ${rangeX}, ${rangeY}, ${rangeW}, ${rangeH}`
);
isWithin(rangeX, caretX, 2, "Caret x similar to range x");
isWithin(rangeY, caretY, 2, "Caret y similar to range y");
isWithin(rangeW, caretW, 2, "Caret width similar to range width");
}
}
);