/* 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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"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,
nsIAccessibleHyperText,
]);
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, rangeW, rangeH] =
fetchedBounds.length > queryOffset
? fetchedBounds[queryOffset]
: [
0,
0,
0,
0];
const [caretX, caretY, caretW, caretH] = getCaretRect(docAcc, id);
// In case of an empty input `getRangeExtents()` will return the full accessible bounds.
// When called on an embedded object, `getRangeExtents()` will return the
// bounds for the content inside it, so it isn't a collapsed range and thus it
// won't match the caret.
if (!empty && acc.getLinkIndexAtOffset(offset) == -
1) {
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"
);
}
let [caretInlinePos, caretBlockPos, rangeInlinePos, rangeBlockPos] =
isVertical
? [caretY, caretX, rangeY, rangeX]
: [caretX, caretY, rangeX, rangeY];
// Compare inline-axis position for caret and text range
if (atEndInNewLine) {
Assert.lessOrEqual(
caretInlinePos,
rangeInlinePos,
"Caret inline before range inline"
);
}
else if (atEnd || atLineEnd) {
Assert.greaterOrEqual(
caretInlinePos,
rangeInlinePos,
"Caret inline after last range inline"
);
}
else {
// Caret inline size 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(
caretInlinePos,
rangeInlinePos,
3,
"Caret inline similar to range inline"
);
}
// Compare block-axis position for caret and text range
if (atEndInNewLine) {
Assert.greater(
caretBlockPos,
rangeBlockPos,
"Caret block below range block"
);
}
else if (atLineEnd) {
Assert.less(
caretBlockPos,
rangeBlockPos,
"Caret block above start range block"
);
}
else {
isWithin(
caretBlockPos,
rangeBlockPos,
3,
"Caret block similar to range block"
);
}
let [caretInlineSize, caretBlockSize, rangeBlockSize] = isVertical
? [caretH, caretW, rangeW]
: [caretW, caretH, rangeH];
ok(caretInlineSize,
"Caret inline size is greater than 0");
if (!empty) {
// Depending on glyph, the range can be taller.
isWithin(
caretBlockSize,
rangeBlockSize,
2,
"Caret block size similar to range block size"
);
}
}
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.
*
* Padding is removed in order to make the text range bounds
* and the caret bounds consistent for the empty text input.
* See Bug 2038670.
*/
addAccessibleTask(
`
<input id=
"input" value=
"ab" style=
"padding: 0;">
<input id=
"emptyInput" style=
"padding: 0;">
`,
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.
*
* Padding is removed in order to make the text range bounds
* and the caret bounds consistent for the empty text input.
* See Bug 2038670.
*/
addAccessibleTask(
`
<input id=
"input" value=
"ab" style=
"writing-mode: vertical-lr; padding: 0;">
<input id=
"emptyInput" style=
"writing-mode: vertical-lr; padding: 0;">
`,
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: ${ra
ngeX}, ${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");
}
}
);
/**
* Test a contentEditable with a paragraph containing a line break.
*/
addAccessibleTask(
`
<style>
@font-face {
font-family: Ahem;
src: url(${CURRENT_CONTENT_DIR}e10s/fonts/Ahem.sjs);
}
#editable {
font: 10px/10px Ahem;
}
</style>
<div id="editable" contenteditable>a<p><br></p></div>
`,
async function testEditablePBr(browser, docAcc) {
const editable = findAccessibleChildByID(docAcc, "editable", [
nsIAccessibleText,
]);
const fetchedBounds = await fetchCollapsedRangeBounds(docAcc, editable);
info("Focusing editable");
let moved = waitForEvent(EVENT_FOCUS, editable);
editable.takeFocus();
await moved;
testCaretRect(docAcc, "editable", 0, fetchedBounds);
info("Pressing ArrowRight");
moved = waitForEvent(EVENT_TEXT_CARET_MOVED);
EventUtils.synthesizeKey("KEY_ArrowRight");
await moved;
testCaretRect(docAcc, "editable", 1, fetchedBounds, true);
},
{ chrome: true, topLevel: true }
);