/* 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"; requestLongerTimeout(3); /* import-globals-from ../../mochitest/layout.js */ loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR }); // Note that testTextNode, testChar and testTextRange currently don't handle // white space in the code that doesn't get rendered on screen. To work around // this, ensure that containers you want to test are all on a single line in the // test snippet. async function testTextNode(accDoc, browser, id) { await testTextRange(accDoc, browser, id, 0, -1); } async function testChar(accDoc, browser, id, idx) { await testTextRange(accDoc, browser, id, idx, idx + 1); } async function testTextRange(accDoc, browser, id, start, end) { const r = await invokeContentTask( browser, [id, start, end], (_id, _start, _end) => { const htNode = content.document.getElementById(_id); let [eX, eY, eW, eH] = [ Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, 0, 0, ]; let traversed = 0; let localStart = _start; let endTraversal = false; for (let element of htNode.childNodes) { // ignore whitespace, but not embedded elements let isEmbeddedElement = false; if (element.length == undefined) { let potentialTextContainer = element; while ( potentialTextContainer && potentialTextContainer.length == undefined ) { potentialTextContainer = element.firstChild; } if (potentialTextContainer && potentialTextContainer.length) { // If we can reach some text from this container, use that as part // of our range. This is important when testing with intervening inline // elements. ie.
ab%0acd
element = potentialTextContainer;
} else if (element.firstChild) {
isEmbeddedElement = true;
} else {
continue;
}
}
if (element.length + traversed < _start) {
// If our start index is not within this
// node, keep looking.
traversed += element.length;
localStart -= element.length;
continue;
}
let rect;
if (isEmbeddedElement) {
rect = element.getBoundingClientRect();
} else {
const range = content.document.createRange();
range.setStart(element, localStart);
if (_end != -1 && _end - traversed <= element.length) {
// If the current node contains
// our end index, stop here.
endTraversal = true;
range.setEnd(element, _end - traversed);
} else {
range.setEnd(element, element.length);
}
rect = range.getBoundingClientRect();
}
const oldX = eX == Number.MAX_SAFE_INTEGER ? 0 : eX;
const oldY = eY == Number.MAX_SAFE_INTEGER ? 0 : eY;
eX = Math.min(eX, rect.x);
eY = Math.min(eY, rect.y);
eW = Math.abs(Math.max(oldX + eW, rect.x + rect.width) - eX);
eH = Math.abs(Math.max(oldY + eH, rect.y + rect.height) - eY);
if (endTraversal) {
break;
}
localStart = 0;
traversed += element.length;
}
return [Math.round(eX), Math.round(eY), Math.round(eW), Math.round(eH)];
}
);
let hyperTextNode = findAccessibleChildByID(accDoc, id);
// Add in the doc's screen coords because getBoundingClientRect
// is relative to the document, not the screen. This assumes the doc's
// screen coords are correct. We use getBoundsInCSSPixels to avoid factoring
// in the DPR ourselves.
let x = {};
let y = {};
let w = {};
let h = {};
accDoc.getBoundsInCSSPixels(x, y, w, h);
r[0] += x.value;
r[1] += y.value;
if (end != -1 && end - start == 1) {
// If we're only testing a character, use this function because it calls
// CharBounds() directly instead of TextBounds().
testTextPos(hyperTextNode, start, [r[0], r[1]], COORDTYPE_SCREEN_RELATIVE);
} else {
testTextBounds(hyperTextNode, start, end, r, COORDTYPE_SCREEN_RELATIVE);
}
}
/**
* Test the text range boundary for simple LtR text
*/
addAccessibleTask(
`
Tilimilitryamdiya
ل
Привіт Світ
a%0abcdef
`,
async function (browser, accDoc) {
info("Testing simple LtR text");
await testTextNode(accDoc, browser, "p1");
await testTextNode(accDoc, browser, "p2");
await testTextNode(accDoc, browser, "p3");
await testTextNode(accDoc, browser, "p4");
},
{
iframe: true,
}
);
/**
* Test the partial text range boundary for LtR text
*/
addAccessibleTask(
`
Tilimilitryamdiya
Привіт Світ
`,
async function (browser, accDoc) {
info("Testing partial ranges in LtR text");
await testTextRange(accDoc, browser, "p1", 0, 4);
await testTextRange(accDoc, browser, "p1", 2, 8);
await testTextRange(accDoc, browser, "p1", 12, 17);
await testTextRange(accDoc, browser, "p2", 0, 4);
await testTextRange(accDoc, browser, "p2", 2, 8);
await testTextRange(accDoc, browser, "p2", 6, 11);
},
{
topLevel: true,
iframe: true,
}
);
/**
* Test the text boundary for multiline LtR text
*/
addAccessibleTask(
`
Привіт Світ
Привіт Світ
Привіт Світ
Я ще трохи тексту в другому рядку
hello world I'm on line one
and I'm a separate line two with slightly more text
hello world
hello world
`,
async function (browser, accDoc) {
info("Testing multiline LtR text");
await testTextNode(accDoc, browser, "p4");
await testTextNode(accDoc, browser, "p5");
// await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache wrong w, h in iframe (line wrapping)
await testTextNode(accDoc, browser, "p7");
},
{
topLevel: true,
iframe: true,
}
);
/**
* Test the text boundary for simple RtL text
*/
addAccessibleTask(
`
Tilimilitryamdiya
ل
لل لللل لل
a%0abcdef
`,
async function (browser, accDoc) {
info("Testing simple RtL text");
await testTextNode(accDoc, browser, "p1");
await testTextNode(accDoc, browser, "p2");
await testTextNode(accDoc, browser, "p3");
await testTextNode(accDoc, browser, "p4");
},
{
topLevel: true,
iframe: true,
}
);
/**
* Test the text boundary for multiline RtL text
*/
addAccessibleTask(
`
لل لللل لل
لل لللل لل
لل لللل لل
لل لل لل لل ل لل لل لل
hello world I'm on line one
and I'm a separate line two with slightly more text
hello world
hello world
`,
async function (browser, accDoc) {
info("Testing multiline RtL text");
await testTextNode(accDoc, browser, "p4");
//await testTextNode(accDoc, browser, "p5"); // w/ cache fails x, w - off by one char
// await testTextNode(accDoc, browser, "p6"); // w/o cache, fails width (a 259, e 250), w/ cache fails w, h in iframe (line wrapping)
await testTextNode(accDoc, browser, "p7");
},
{
topLevel: true,
iframe: true,
}
);
/**
* Test the partial text range boundary for RtL text
*/
addAccessibleTask(
`
Tilimilitryamdiya
لل لللل لل
`,
async function (browser, accDoc) {
info("Testing partial ranges in RtL text");
await testTextRange(accDoc, browser, "p1", 0, 4);
await testTextRange(accDoc, browser, "p1", 2, 8);
await testTextRange(accDoc, browser, "p1", 12, 17);
await testTextRange(accDoc, browser, "p2", 0, 4);
await testTextRange(accDoc, browser, "p2", 2, 8);
await testTextRange(accDoc, browser, "p2", 6, 10);
},
{
topLevel: true,
iframe: true,
}
);
/**
* Test simple vertical text in rl and lr layouts
*/
addAccessibleTask(
`
你好世界
hello world
こんにちは世界
你好世界
hello world
こんにちは世界
`,
async function (browser, accDoc) {
info("Testing vertical-rl");
await testTextNode(accDoc, browser, "p1");
await testTextNode(accDoc, browser, "p2");
await testTextNode(accDoc, browser, "p3");
info("Testing vertical-lr");
await testTextNode(accDoc, browser, "p4");
await testTextNode(accDoc, browser, "p5");
await testTextNode(accDoc, browser, "p6");
},
{
topLevel: true,
iframe: true,
}
);
/**
* Test multiline vertical-rl text
*/
addAccessibleTask(
`
你好世界
你好世界
hello world
hello world
你好世界
你好世界 你好世界
hello world
hello world hello world
`,
async function (browser, accDoc) {
info("Testing vertical-rl multiline");
await testTextNode(accDoc, browser, "p1");
await testTextNode(accDoc, browser, "p2");
await testTextNode(accDoc, browser, "p3");
// await testTextNode(accDoc, browser, "p4"); // off by 4 with caching, iframe
},
{
topLevel: true,
iframe: true,
}
);
/**
* Test text with embedded chars
*/
addAccessibleTask(
`hello world
hello
world
hello world
hello world
ohhello world`,
async function (browser, accDoc) {
info("Testing embedded chars");
await testTextNode(accDoc, browser, "p1");
await testTextNode(accDoc, browser, "p2");
await testTextNode(accDoc, browser, "d3");
await testTextNode(accDoc, browser, "d4");
await testTextNode(accDoc, browser, "d5");
},
{
topLevel: true,
iframe: true,
}
);
/**
* Test bounds after text mutations.
*/
addAccessibleTask(
`a
`,
async function (browser, docAcc) {
await testTextNode(docAcc, browser, "p");
const p = findAccessibleChildByID(docAcc, "p");
info("Appending a character to text leaf");
let textInserted = waitForEvent(EVENT_TEXT_INSERTED, p);
await invokeContentTask(browser, [], () => {
content.document.getElementById("p").firstChild.data = "ab";
});
await textInserted;
await testTextNode(docAcc, browser, "p");
},
{
chrome: true,
topLevel: true,
iframe: true,
}
);
/**
* Test character bounds on the insertion point at the end of a text box.
*/
addAccessibleTask(
``,
async function (browser, docAcc) {
const input = findAccessibleChildByID(docAcc, "input");
testTextPos(input, 1, [0, 0], COORDTYPE_SCREEN_RELATIVE);
},
{
chrome: true,
topLevel: true,
iframe: true,
}
);
/**
* Test character bounds after non-br line break.
*/
addAccessibleTask(
`
XX
XXX
`,
async function (browser, docAcc) {
await testChar(docAcc, browser, "t", 3);
},
{
chrome: true,
topLevel: true,
iframe: true,
}
);
/**
* Test character bounds in a pre with padding.
*/
addAccessibleTask(
`
XX
XXX
`,
async function (browser, docAcc) {
await testTextNode(docAcc, browser, "t");
await testChar(docAcc, browser, "t", 3);
},
{
chrome: true,
topLevel: true,
iframe: true,
}
);
/**
* Test text bounds with an invalid end offset.
*/
addAccessibleTask(
`a
`,
async function (browser, docAcc) {
const p = findAccessibleChildByID(docAcc, "p");
testTextBounds(p, 0, 2, [0, 0, 0, 0], COORDTYPE_SCREEN_RELATIVE);
},
{ chrome: true, topLevel: !true }
);
/**
* Test character bounds in an intervening inline element with non-br line breaks
*/
addAccessibleTask(
`
XX
XXX
XX
X
`,
async function (browser, docAcc) {
await testChar(docAcc, browser, "t", 0);
await testChar(docAcc, browser, "t", 3);
await testChar(docAcc, browser, "t", 7);
await testChar(docAcc, browser, "t", 10);
},
{
chrome: true,
topLevel: true,
iframe: true,
}
);
/**
* Test character bounds in an intervening inline element with margins
* and with non-br line breaks
*/
addAccessibleTask(
`
helloXX
XXX
XX
X
`,
async function (browser, docAcc) {
await testChar(docAcc, browser, "t", 0);
await testChar(docAcc, browser, "t", 3);
await testChar(docAcc, browser, "t", 7);
await testChar(docAcc, browser, "t", 10);
},
{
chrome: true,
topLevel: true,
iframe: true,
}
);
/**
* Test text bounds in a textarea after scrolling.
*/
addAccessibleTask(
`
`,
async function (browser, docAcc) {
// We can't use testChar because Range.getBoundingClientRect isn't supported
// inside textareas.
const textarea = findAccessibleChildByID(docAcc, "textarea");
textarea.QueryInterface(nsIAccessibleText);
const oldY = {};
textarea.getCharacterExtents(
4,
{},
oldY,
{},
{},
COORDTYPE_SCREEN_RELATIVE
);
info("Moving textarea caret to c");
await invokeContentTask(browser, [], () => {
const textareaDom = content.document.getElementById("textarea");
textareaDom.focus();
textareaDom.selectionStart = 4;
});
await waitForContentPaint(browser);
const newY = {};
textarea.getCharacterExtents(
4,
{},
newY,
{},
{},
COORDTYPE_SCREEN_RELATIVE
);
ok(newY.value < oldY.value, "y coordinate smaller after scrolling down");
},
{ chrome: true, topLevel: true, iframe: !true }
);
/**
* Test magic offsets with GetCharacter/RangeExtents.
*/
addAccessibleTask(
``,
async function (browser, docAcc) {
const input = findAccessibleChildByID(docAcc, "input", [nsIAccessibleText]);
info("Setting caret and focusing input");
let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input);
await invokeContentTask(browser, [], () => {
const inputDom = content.document.getElementById("input");
inputDom.selectionStart = inputDom.selectionEnd = 1;
inputDom.focus();
});
await caretMoved;
is(input.caretOffset, 1, "input caretOffset is 1");
let expectedX = {};
let expectedY = {};
let expectedW = {};
let expectedH = {};
let magicX = {};
let magicY = {};
let magicW = {};
let magicH = {};
input.getCharacterExtents(
1,
expectedX,
expectedY,
expectedW,
expectedH,
COORDTYPE_SCREEN_RELATIVE
);
input.getCharacterExtents(
nsIAccessibleText.TEXT_OFFSET_CARET,
magicX,
magicY,
magicW,
magicH,
COORDTYPE_SCREEN_RELATIVE
);
Assert.deepEqual(
[magicX.value, magicY.value, magicW.value, magicH.value],
[expectedX.value, expectedY.value, expectedW.value, expectedH.value],
"GetCharacterExtents correct with TEXT_OFFSET_CARET"
);
input.getRangeExtents(
1,
3,
expectedX,
expectedY,
expectedW,
expectedH,
COORDTYPE_SCREEN_RELATIVE
);
input.getRangeExtents(
nsIAccessibleText.TEXT_OFFSET_CARET,
nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT,
magicX,
magicY,
magicW,
magicH,
COORDTYPE_SCREEN_RELATIVE
);
Assert.deepEqual(
[magicX.value, magicY.value, magicW.value, magicH.value],
[expectedX.value, expectedY.value, expectedW.value, expectedH.value],
"GetRangeExtents correct with TEXT_OFFSET_CARET/END_OF_TEXT"
);
},
{ chrome: true, topLevel: true, remoteIframe: !true }
);
/**
* Test wrapped text and pre-formatted text beginning with an empty line.
*/
addAccessibleTask(
`
ab cd
foo
`,
async function (browser, docAcc) {
await testChar(docAcc, browser, "wrappedText", 0);
await testChar(docAcc, browser, "wrappedText", 1);
await testChar(docAcc, browser, "wrappedText", 2);
await testChar(docAcc, browser, "wrappedText", 3);
await testChar(docAcc, browser, "wrappedText", 4);
// We can't use testChar for emptyFirstLine because it doesn't handle white
// space properly. Instead, verify that the first character is at the top
// left of the text leaf.
const emptyFirstLine = findAccessibleChildByID(docAcc, "emptyFirstLine", [
nsIAccessibleText,
]);
const emptyFirstLineLeaf = emptyFirstLine.firstChild;
const leafX = {};
const leafY = {};
emptyFirstLineLeaf.getBounds(leafX, leafY, {}, {});
testTextPos(
emptyFirstLine,
0,
[leafX.value, leafY.value],
COORDTYPE_SCREEN_RELATIVE
);
},
{ chrome: true, topLevel: true, remoteIframe: !true }
);
/**
* Test character bounds in an intervening inline element with non-br line breaks
*/
addAccessibleTask(
`
XX
XXX
XX
X
`,
async function (browser, docAcc) {
await testChar(docAcc, browser, "t", 0);
await testChar(docAcc, browser, "t", 3);
await testChar(docAcc, browser, "t", 7);
await testChar(docAcc, browser, "t", 10);
},
{
chrome: true,
topLevel: true,
iframe: true,
}
);