315 lines
9 KiB
JavaScript
315 lines
9 KiB
JavaScript
/* 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";
|
|
|
|
/* exported createTextLeafPoint, DIRECTION_NEXT, DIRECTION_PREVIOUS,
|
|
BOUNDARY_FLAG_DEFAULT, BOUNDARY_FLAG_INCLUDE_ORIGIN,
|
|
BOUNDARY_FLAG_STOP_IN_EDITABLE, BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER,
|
|
readablePoint, testPointEqual, textBoundaryGenerator, testBoundarySequence,
|
|
isFinalValueCorrect, isFinalValueCorrect, testInsertText, testDeleteText,
|
|
testCopyText, testPasteText, testCutText, testSetTextContents,
|
|
textAttrRangesMatch */
|
|
|
|
// Load the shared-head file first.
|
|
Services.scriptloader.loadSubScript(
|
|
"chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js",
|
|
this
|
|
);
|
|
|
|
// Loading and common.js from accessible/tests/mochitest/ for all tests, as
|
|
// well as promisified-events.js.
|
|
|
|
/* import-globals-from ../../mochitest/role.js */
|
|
|
|
loadScripts(
|
|
{ name: "common.js", dir: MOCHITESTS_DIR },
|
|
{ name: "text.js", dir: MOCHITESTS_DIR },
|
|
{ name: "role.js", dir: MOCHITESTS_DIR },
|
|
{ name: "promisified-events.js", dir: MOCHITESTS_DIR }
|
|
);
|
|
|
|
const DIRECTION_NEXT = Ci.nsIAccessibleTextLeafPoint.DIRECTION_NEXT;
|
|
const DIRECTION_PREVIOUS = Ci.nsIAccessibleTextLeafPoint.DIRECTION_PREVIOUS;
|
|
|
|
const BOUNDARY_FLAG_DEFAULT =
|
|
Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_DEFAULT;
|
|
const BOUNDARY_FLAG_INCLUDE_ORIGIN =
|
|
Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_INCLUDE_ORIGIN;
|
|
const BOUNDARY_FLAG_STOP_IN_EDITABLE =
|
|
Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_STOP_IN_EDITABLE;
|
|
const BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER =
|
|
Ci.nsIAccessibleTextLeafPoint.BOUNDARY_FLAG_SKIP_LIST_ITEM_MARKER;
|
|
|
|
function createTextLeafPoint(acc, offset) {
|
|
let accService = Cc["@mozilla.org/accessibilityService;1"].getService(
|
|
nsIAccessibilityService
|
|
);
|
|
|
|
return accService.createTextLeafPoint(acc, offset);
|
|
}
|
|
|
|
// Converts an nsIAccessibleTextLeafPoint into a human/machine
|
|
// readable tuple with a readable accessible and the offset within it.
|
|
// For a point text leaf it would look like this: ["hello", 2],
|
|
// For a point in an empty input it would look like this ["input#name", 0]
|
|
function readablePoint(point) {
|
|
const readableLeaf = acc => {
|
|
let tagName = getAccessibleTagName(acc);
|
|
if (tagName && !tagName.startsWith("_moz_generated")) {
|
|
let domNodeID = getAccessibleDOMNodeID(acc);
|
|
if (domNodeID) {
|
|
return `${tagName}#${domNodeID}`;
|
|
}
|
|
return tagName;
|
|
}
|
|
|
|
return acc.name;
|
|
};
|
|
|
|
return [readableLeaf(point.accessible), point.offset];
|
|
}
|
|
|
|
function sequenceEqual(val, expected, msg) {
|
|
Assert.deepEqual(val, expected, msg);
|
|
}
|
|
|
|
// eslint-disable-next-line camelcase
|
|
function sequenceEqualTodo(val, expected, msg) {
|
|
todo_is(JSON.stringify(val), JSON.stringify(expected), msg);
|
|
}
|
|
|
|
function pointsEqual(pointA, pointB) {
|
|
return (
|
|
pointA.offset == pointB.offset && pointA.accessible == pointB.accessible
|
|
);
|
|
}
|
|
|
|
function testPointEqual(pointA, pointB, msg) {
|
|
is(pointA.offset, pointB.offset, `Offset mismatch - ${msg}`);
|
|
is(pointA.accessible, pointB.accessible, `Accessible mismatch - ${msg}`);
|
|
}
|
|
|
|
function* textBoundaryGenerator(
|
|
firstPoint,
|
|
boundaryType,
|
|
direction,
|
|
flags = BOUNDARY_FLAG_DEFAULT
|
|
) {
|
|
// Our start point should be inclusive of the given point.
|
|
let nextLeafPoint = firstPoint.findBoundary(
|
|
boundaryType,
|
|
direction,
|
|
flags | BOUNDARY_FLAG_INCLUDE_ORIGIN
|
|
);
|
|
let textLeafPoint = null;
|
|
|
|
do {
|
|
textLeafPoint = nextLeafPoint;
|
|
yield textLeafPoint;
|
|
nextLeafPoint = textLeafPoint.findBoundary(boundaryType, direction, flags);
|
|
} while (!pointsEqual(textLeafPoint, nextLeafPoint));
|
|
}
|
|
|
|
// This function takes FindBoundary arguments and an expected sequence
|
|
// of boundary points formatted with readablePoint.
|
|
// For example, word starts would look like this:
|
|
// [["one two", 0], ["one two", 4], ["one two", 7]]
|
|
function testBoundarySequence(
|
|
startPoint,
|
|
boundaryType,
|
|
direction,
|
|
expectedSequence,
|
|
msg,
|
|
options = {}
|
|
) {
|
|
let sequence = [
|
|
...textBoundaryGenerator(
|
|
startPoint,
|
|
boundaryType,
|
|
direction,
|
|
options.flags ? options.flags : BOUNDARY_FLAG_DEFAULT
|
|
),
|
|
];
|
|
(options.todo ? sequenceEqualTodo : sequenceEqual)(
|
|
sequence.map(readablePoint),
|
|
expectedSequence,
|
|
msg
|
|
);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// Editable text
|
|
|
|
async function waitForCopy(browser) {
|
|
await BrowserTestUtils.waitForContentEvent(browser, "copy", false, () => {
|
|
return true;
|
|
});
|
|
|
|
let clipboardData = await invokeContentTask(browser, [], async () => {
|
|
let text = await content.navigator.clipboard.readText();
|
|
return text;
|
|
});
|
|
|
|
return clipboardData;
|
|
}
|
|
|
|
async function isFinalValueCorrect(
|
|
browser,
|
|
acc,
|
|
expectedTextLeafs,
|
|
msg = "Value is correct"
|
|
) {
|
|
let value =
|
|
acc.role == ROLE_ENTRY
|
|
? acc.value
|
|
: await invokeContentTask(browser, [], () => {
|
|
return content.document.body.textContent;
|
|
});
|
|
|
|
let [before, text, after] = expectedTextLeafs;
|
|
let finalValue =
|
|
before && after && !text
|
|
? [before, after].join(" ")
|
|
: [before, text, after].join("");
|
|
|
|
is(value.replace("\xa0", " "), finalValue, msg);
|
|
}
|
|
|
|
function waitForTextChangeEvents(acc, eventSeq) {
|
|
let events = eventSeq.map(eventType => {
|
|
return [eventType, acc];
|
|
});
|
|
|
|
if (acc.role == ROLE_ENTRY) {
|
|
events.push([EVENT_TEXT_VALUE_CHANGE, acc]);
|
|
}
|
|
|
|
return waitForEvents(events);
|
|
}
|
|
|
|
async function testSetTextContents(acc, text, staticContentOffset, events) {
|
|
acc.QueryInterface(nsIAccessibleEditableText);
|
|
let evtPromise = waitForTextChangeEvents(acc, events);
|
|
acc.setTextContents(text);
|
|
let evt = (await evtPromise)[0];
|
|
evt.QueryInterface(nsIAccessibleTextChangeEvent);
|
|
is(evt.start, staticContentOffset);
|
|
}
|
|
|
|
async function testInsertText(
|
|
acc,
|
|
textToInsert,
|
|
insertOffset,
|
|
staticContentOffset
|
|
) {
|
|
acc.QueryInterface(nsIAccessibleEditableText);
|
|
|
|
let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]);
|
|
acc.insertText(textToInsert, staticContentOffset + insertOffset);
|
|
let evt = (await evtPromise)[0];
|
|
evt.QueryInterface(nsIAccessibleTextChangeEvent);
|
|
is(evt.start, staticContentOffset + insertOffset);
|
|
}
|
|
|
|
async function testDeleteText(
|
|
acc,
|
|
startOffset,
|
|
endOffset,
|
|
staticContentOffset
|
|
) {
|
|
acc.QueryInterface(nsIAccessibleEditableText);
|
|
|
|
let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]);
|
|
acc.deleteText(
|
|
staticContentOffset + startOffset,
|
|
staticContentOffset + endOffset
|
|
);
|
|
let evt = (await evtPromise)[0];
|
|
evt.QueryInterface(nsIAccessibleTextChangeEvent);
|
|
is(evt.start, staticContentOffset + startOffset);
|
|
}
|
|
|
|
async function testCopyText(
|
|
acc,
|
|
startOffset,
|
|
endOffset,
|
|
staticContentOffset,
|
|
browser,
|
|
aExpectedClipboard = null
|
|
) {
|
|
acc.QueryInterface(nsIAccessibleEditableText);
|
|
let copied = waitForCopy(browser);
|
|
acc.copyText(
|
|
staticContentOffset + startOffset,
|
|
staticContentOffset + endOffset
|
|
);
|
|
let clipboardText = await copied;
|
|
if (aExpectedClipboard != null) {
|
|
is(clipboardText, aExpectedClipboard, "Correct text in clipboard");
|
|
}
|
|
}
|
|
|
|
async function testPasteText(acc, insertOffset, staticContentOffset) {
|
|
acc.QueryInterface(nsIAccessibleEditableText);
|
|
let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_INSERTED]);
|
|
acc.pasteText(staticContentOffset + insertOffset);
|
|
|
|
let evt = (await evtPromise)[0];
|
|
evt.QueryInterface(nsIAccessibleTextChangeEvent);
|
|
// XXX: In non-headless mode pasting text produces several text leaves
|
|
// and the offset is not what we expect.
|
|
// is(evt.start, staticContentOffset + insertOffset);
|
|
}
|
|
|
|
async function testCutText(acc, startOffset, endOffset, staticContentOffset) {
|
|
acc.QueryInterface(nsIAccessibleEditableText);
|
|
let evtPromise = waitForTextChangeEvents(acc, [EVENT_TEXT_REMOVED]);
|
|
acc.cutText(
|
|
staticContentOffset + startOffset,
|
|
staticContentOffset + endOffset
|
|
);
|
|
|
|
let evt = (await evtPromise)[0];
|
|
evt.QueryInterface(nsIAccessibleTextChangeEvent);
|
|
is(evt.start, staticContentOffset + startOffset);
|
|
}
|
|
|
|
/**
|
|
* Check if the given ranges match the ranges in the given text accessible which
|
|
* expose the given attribute names/values.
|
|
*/
|
|
function textAttrRangesMatch(acc, ranges, attrs) {
|
|
acc.QueryInterface(nsIAccessibleText);
|
|
let offset = 0;
|
|
let expectedRanges = [...ranges];
|
|
let charCount = acc.characterCount;
|
|
while (offset < charCount) {
|
|
let start = {};
|
|
let end = {};
|
|
let attributes = acc.getTextAttributes(false, offset, start, end);
|
|
offset = end.value;
|
|
let matched = true;
|
|
for (const attr in attrs) {
|
|
try {
|
|
if (attributes.getStringProperty(attr) != attrs[attr]) {
|
|
// Attribute value doesn't match.
|
|
matched = false;
|
|
}
|
|
} catch (err) {
|
|
// Attribute doesn't exist.
|
|
matched = false;
|
|
}
|
|
}
|
|
if (!matched) {
|
|
continue;
|
|
}
|
|
let expected = expectedRanges.shift();
|
|
if (!expected || expected[0] != start.value || expected[1] != end.value) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return !expectedRanges.length;
|
|
}
|