492 lines
14 KiB
JavaScript
492 lines
14 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";
|
|
|
|
/* import-globals-from ../../mochitest/text.js */
|
|
/* import-globals-from ../../mochitest/attributes.js */
|
|
loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR });
|
|
|
|
const boldAttrs = { "font-weight": "700" };
|
|
const highlightAttrs = { mark: "true" };
|
|
const fragmentAttrs = highlightAttrs;
|
|
const spellingAttrs = { invalid: "spelling" };
|
|
const grammarAttrs = { invalid: "grammar" };
|
|
const snippet = `
|
|
<p id="first">The first phrase.</p>
|
|
<p id="second">The <i>second <b>phrase.</b></i></p>
|
|
`;
|
|
|
|
/**
|
|
* Returns a promise that resolves once the attribute ranges match. If
|
|
* shouldWaitForEvent is true, we first wait for a text attribute change event.
|
|
*/
|
|
async function waitForTextAttrRanges(
|
|
acc,
|
|
ranges,
|
|
attrs,
|
|
shouldWaitForEvent = true
|
|
) {
|
|
if (shouldWaitForEvent) {
|
|
await waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED);
|
|
}
|
|
await untilCacheOk(
|
|
() => textAttrRangesMatch(acc, ranges, attrs),
|
|
`Attr ranges match: ${JSON.stringify(ranges)}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Test a text fragment within a single node.
|
|
*/
|
|
addAccessibleTask(
|
|
snippet,
|
|
async function testTextFragmentSingleNode(browser, docAcc) {
|
|
const first = findAccessibleChildByID(docAcc, "first");
|
|
ok(
|
|
textAttrRangesMatch(
|
|
first,
|
|
[
|
|
[4, 16], // "first phrase"
|
|
],
|
|
fragmentAttrs
|
|
),
|
|
"first attr ranges correct"
|
|
);
|
|
const second = findAccessibleChildByID(docAcc, "second");
|
|
ok(
|
|
textAttrRangesMatch(second, [], fragmentAttrs),
|
|
"second attr ranges correct"
|
|
);
|
|
},
|
|
{ chrome: true, topLevel: true, urlSuffix: "#:~:text=first%20phrase" }
|
|
);
|
|
|
|
/**
|
|
* Test a text fragment crossing nodes.
|
|
*/
|
|
addAccessibleTask(
|
|
snippet,
|
|
async function testTextFragmentCrossNode(browser, docAcc) {
|
|
const first = findAccessibleChildByID(docAcc, "first");
|
|
ok(
|
|
textAttrRangesMatch(first, [], fragmentAttrs),
|
|
"first attr ranges correct"
|
|
);
|
|
const second = findAccessibleChildByID(docAcc, "second");
|
|
ok(
|
|
textAttrRangesMatch(
|
|
second,
|
|
[
|
|
// This run is split because of the bolded word.
|
|
[4, 11], // "second "
|
|
[11, 17], // "phrase"
|
|
],
|
|
fragmentAttrs
|
|
),
|
|
"second attr ranges correct"
|
|
);
|
|
// Ensure bold is still exposed in the presence of a fragment.
|
|
testTextAttrs(
|
|
second,
|
|
11,
|
|
{ ...fragmentAttrs, ...boldAttrs },
|
|
{},
|
|
11,
|
|
17,
|
|
true
|
|
); // "phrase"
|
|
testTextAttrs(second, 17, boldAttrs, {}, 17, 18, true); // "."
|
|
},
|
|
{ chrome: true, topLevel: true, urlSuffix: "#:~:text=second%20phrase" }
|
|
);
|
|
|
|
/**
|
|
* Test scrolling to a text fragment on the same page. This also tests that the
|
|
* scrolling start event is fired.
|
|
*/
|
|
add_task(async function testTextFragmentSamePage() {
|
|
// We use add_task here because we need to verify that an
|
|
// event is fired, but it might be fired before document load complete, so we
|
|
// could miss it if we used addAccessibleTask.
|
|
const docUrl = snippetToURL(snippet);
|
|
const initialUrl = docUrl + "#:~:text=first%20phrase";
|
|
let scrolled = waitForEvent(
|
|
EVENT_SCROLLING_START,
|
|
event =>
|
|
event.accessible.role == ROLE_TEXT_LEAF &&
|
|
getAccessibleDOMNodeID(event.accessible.parent) == "first"
|
|
);
|
|
await BrowserTestUtils.withNewTab(initialUrl, async function (browser) {
|
|
info("Waiting for scroll to first");
|
|
const first = (await scrolled).accessible.parent;
|
|
info("Checking ranges");
|
|
await waitForTextAttrRanges(
|
|
first,
|
|
[
|
|
[4, 16], // "first phrase"
|
|
],
|
|
fragmentAttrs,
|
|
false
|
|
);
|
|
const second = first.nextSibling;
|
|
await waitForTextAttrRanges(second, [], fragmentAttrs, false);
|
|
|
|
info("Navigating to second");
|
|
// The text fragment begins with the text "second", which is the second
|
|
// child of the `second` Accessible.
|
|
scrolled = waitForEvent(EVENT_SCROLLING_START, second.getChildAt(1));
|
|
let rangeCheck = waitForTextAttrRanges(
|
|
second,
|
|
[
|
|
[4, 11], // "second "
|
|
[11, 17], // "phrase"
|
|
],
|
|
fragmentAttrs,
|
|
true
|
|
);
|
|
await invokeContentTask(browser, [], () => {
|
|
content.location.hash = "#:~:text=second%20phrase";
|
|
});
|
|
await scrolled;
|
|
info("Checking ranges");
|
|
await rangeCheck;
|
|
// XXX DOM should probably remove the highlight from "first phrase" since
|
|
// we've navigated to "second phrase". For now, this test expects the
|
|
// current DOM behaviour: "first" is still highlighted.
|
|
await waitForTextAttrRanges(
|
|
first,
|
|
[
|
|
[4, 16], // "first phrase"
|
|
],
|
|
fragmentAttrs,
|
|
false
|
|
);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Test custom highlight mutations.
|
|
*/
|
|
addAccessibleTask(
|
|
snippet,
|
|
async function testCustomHighlightMutations(browser, docAcc) {
|
|
info("Checking initial highlight");
|
|
const first = findAccessibleChildByID(docAcc, "first");
|
|
ok(
|
|
textAttrRangesMatch(
|
|
first,
|
|
[
|
|
[4, 9], // "first"
|
|
],
|
|
highlightAttrs
|
|
),
|
|
"first attr ranges correct"
|
|
);
|
|
const second = findAccessibleChildByID(docAcc, "second");
|
|
ok(
|
|
textAttrRangesMatch(second, [], highlightAttrs),
|
|
"second attr ranges correct"
|
|
);
|
|
|
|
info("Adding range2 to highlight1");
|
|
let rangeCheck = waitForTextAttrRanges(
|
|
first,
|
|
[
|
|
[0, 3], // "The "
|
|
[4, 9], // "first"
|
|
],
|
|
highlightAttrs,
|
|
true
|
|
);
|
|
await invokeContentTask(browser, [], () => {
|
|
content.firstText = content.document.getElementById("first").firstChild;
|
|
// Highlight the word "The".
|
|
content.range2 = new content.Range();
|
|
content.range2.setStart(content.firstText, 0);
|
|
content.range2.setEnd(content.firstText, 3);
|
|
content.highlight1 = content.CSS.highlights.get("highlight1");
|
|
content.highlight1.add(content.range2);
|
|
});
|
|
await rangeCheck;
|
|
|
|
info("Adding highlight2");
|
|
rangeCheck = waitForTextAttrRanges(
|
|
first,
|
|
[
|
|
[0, 3], // "The "
|
|
[4, 9], // "first"
|
|
[10, 16], // "phrase"
|
|
],
|
|
highlightAttrs,
|
|
true
|
|
);
|
|
await invokeContentTask(browser, [], () => {
|
|
// Highlight the word "phrase".
|
|
const range3 = new content.Range();
|
|
range3.setStart(content.firstText, 10);
|
|
range3.setEnd(content.firstText, 16);
|
|
const highlight2 = new content.Highlight(range3);
|
|
content.CSS.highlights.set("highlight2", highlight2);
|
|
});
|
|
await rangeCheck;
|
|
|
|
info("Removing range2");
|
|
rangeCheck = waitForTextAttrRanges(
|
|
first,
|
|
[
|
|
[4, 9], // "first"
|
|
[10, 16], // "phrase"
|
|
],
|
|
highlightAttrs,
|
|
true
|
|
);
|
|
await invokeContentTask(browser, [], () => {
|
|
content.highlight1.delete(content.range2);
|
|
});
|
|
await rangeCheck;
|
|
|
|
info("Removing highlight1");
|
|
rangeCheck = waitForTextAttrRanges(
|
|
first,
|
|
[
|
|
[10, 16], // "phrase"
|
|
],
|
|
highlightAttrs,
|
|
true
|
|
);
|
|
await invokeContentTask(browser, [], () => {
|
|
content.CSS.highlights.delete("highlight1");
|
|
});
|
|
await rangeCheck;
|
|
},
|
|
{
|
|
chrome: true,
|
|
topLevel: true,
|
|
contentSetup: async function contentSetup() {
|
|
const firstText = content.document.getElementById("first").firstChild;
|
|
// Highlight the word "first".
|
|
const range1 = new content.Range();
|
|
range1.setStart(firstText, 4);
|
|
range1.setEnd(firstText, 9);
|
|
const highlight1 = new content.Highlight(range1);
|
|
content.CSS.highlights.set("highlight1", highlight1);
|
|
},
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Test custom highlight types.
|
|
*/
|
|
addAccessibleTask(
|
|
snippet,
|
|
async function testCustomHighlightTypes(browser, docAcc) {
|
|
const first = findAccessibleChildByID(docAcc, "first");
|
|
ok(
|
|
textAttrRangesMatch(
|
|
first,
|
|
[
|
|
[0, 3], // "the"
|
|
],
|
|
highlightAttrs
|
|
),
|
|
"first highlight ranges correct"
|
|
);
|
|
ok(
|
|
textAttrRangesMatch(
|
|
first,
|
|
[
|
|
[4, 9], // "first"
|
|
],
|
|
spellingAttrs
|
|
),
|
|
"first spelling ranges correct"
|
|
);
|
|
ok(
|
|
textAttrRangesMatch(
|
|
first,
|
|
[
|
|
[10, 16], // "phrase"
|
|
],
|
|
grammarAttrs
|
|
),
|
|
"first grammar ranges correct"
|
|
);
|
|
const second = findAccessibleChildByID(docAcc, "second");
|
|
ok(
|
|
textAttrRangesMatch(second, [], highlightAttrs),
|
|
"second highlight ranges correct"
|
|
);
|
|
},
|
|
{
|
|
chrome: true,
|
|
topLevel: true,
|
|
contentSetup: async function contentSetup() {
|
|
const firstText = content.document.getElementById("first").firstChild;
|
|
// Highlight the word "The".
|
|
const range1 = new content.Range();
|
|
range1.setStart(firstText, 0);
|
|
range1.setEnd(firstText, 3);
|
|
const highlight = new content.Highlight(range1);
|
|
content.CSS.highlights.set("highlight", highlight);
|
|
|
|
// Make the word "first" a spelling error.
|
|
const range2 = new content.Range();
|
|
range2.setStart(firstText, 4);
|
|
range2.setEnd(firstText, 9);
|
|
const spelling = new content.Highlight(range2);
|
|
spelling.type = "spelling-error";
|
|
content.CSS.highlights.set("spelling", spelling);
|
|
|
|
// Make the word "phrase" a grammar error.
|
|
const range3 = new content.Range();
|
|
range3.setStart(firstText, 10);
|
|
range3.setEnd(firstText, 16);
|
|
const grammar = new content.Highlight(range3);
|
|
grammar.type = "grammar-error";
|
|
content.CSS.highlights.set("grammar", grammar);
|
|
},
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Test overlapping custom highlights.
|
|
*/
|
|
addAccessibleTask(
|
|
snippet,
|
|
async function testCustomHighlightOverlapping(browser, docAcc) {
|
|
const first = findAccessibleChildByID(docAcc, "first");
|
|
ok(
|
|
textAttrRangesMatch(
|
|
first,
|
|
[
|
|
[0, 3], // "the"
|
|
[4, 6], // "fi"
|
|
[6, 7], // "r"
|
|
[7, 9], // "st"
|
|
[10, 12], // "ph"
|
|
[12, 15], // "ras"
|
|
[15, 16], // "e"
|
|
],
|
|
highlightAttrs
|
|
),
|
|
"first highlight ranges correct"
|
|
);
|
|
ok(
|
|
textAttrRangesMatch(
|
|
first,
|
|
[
|
|
[0, 3], // "the"
|
|
[4, 6], // "fi"
|
|
[6, 7], // "r"
|
|
[7, 9], // "st"
|
|
[12, 15], // "ras"
|
|
],
|
|
spellingAttrs
|
|
),
|
|
"first spelling ranges correct"
|
|
);
|
|
const second = findAccessibleChildByID(docAcc, "second");
|
|
ok(
|
|
textAttrRangesMatch(
|
|
second,
|
|
[
|
|
[4, 7], // "sec"
|
|
[7, 8], // "o"
|
|
[8, 10], // "nd"
|
|
[11, 13], // "ph"
|
|
[13, 16], // "ras"
|
|
[16, 17], // "e"
|
|
],
|
|
highlightAttrs
|
|
),
|
|
"second highlight ranges correct"
|
|
);
|
|
ok(
|
|
textAttrRangesMatch(
|
|
second,
|
|
[
|
|
[4, 7], // "sec"
|
|
[8, 10], // "nd"
|
|
],
|
|
spellingAttrs
|
|
),
|
|
"second spelling ranges correct"
|
|
);
|
|
},
|
|
{
|
|
chrome: true,
|
|
topLevel: true,
|
|
contentSetup: async function contentSetup() {
|
|
const firstText = content.document.getElementById("first").firstChild;
|
|
// Make the word "The" both a highlight and a spelling error.
|
|
const range1 = new content.Range();
|
|
range1.setStart(firstText, 0);
|
|
range1.setEnd(firstText, 3);
|
|
const highlight1 = new content.Highlight(range1);
|
|
content.CSS.highlights.set("highlight1", highlight1);
|
|
const spelling = new content.Highlight(range1);
|
|
spelling.type = "spelling-error";
|
|
content.CSS.highlights.set("spelling", spelling);
|
|
|
|
// Highlight the word "first".
|
|
const range2 = new content.Range();
|
|
range2.setStart(firstText, 4);
|
|
range2.setEnd(firstText, 9);
|
|
highlight1.add(range2);
|
|
// Make "fir" a spelling error.
|
|
const range3 = new content.Range();
|
|
range3.setStart(firstText, 4);
|
|
range3.setEnd(firstText, 7);
|
|
spelling.add(range3);
|
|
// Make "rst" a spelling error.
|
|
const range4 = new content.Range();
|
|
range4.setStart(firstText, 6);
|
|
range4.setEnd(firstText, 9);
|
|
spelling.add(range4);
|
|
|
|
// Highlight the word "phrase".
|
|
const range5 = new content.Range();
|
|
range5.setStart(firstText, 10);
|
|
range5.setEnd(firstText, 16);
|
|
highlight1.add(range5);
|
|
// Make "ras" a spelling error.
|
|
const range6 = new content.Range();
|
|
range6.setStart(firstText, 12);
|
|
range6.setEnd(firstText, 15);
|
|
spelling.add(range6);
|
|
|
|
const secondText = content.document.querySelector("#second i").firstChild;
|
|
// Highlight the word "second".
|
|
const range7 = new content.Range();
|
|
range7.setStart(secondText, 0);
|
|
range7.setEnd(secondText, 6);
|
|
highlight1.add(range7);
|
|
// Make "sec" a spelling error.
|
|
const range8 = new content.Range();
|
|
range8.setStart(secondText, 0);
|
|
range8.setEnd(secondText, 3);
|
|
spelling.add(range8);
|
|
// Make "nd" a spelling error.
|
|
const range9 = new content.Range();
|
|
range9.setStart(secondText, 4);
|
|
range9.setEnd(secondText, 6);
|
|
spelling.add(range9);
|
|
|
|
const phrase2Text =
|
|
content.document.querySelector("#second b").firstChild;
|
|
// Highlight the word "phrase".
|
|
const range10 = new content.Range();
|
|
range10.setStart(phrase2Text, 0);
|
|
range10.setEnd(phrase2Text, 6);
|
|
highlight1.add(range10);
|
|
// Highlight "ras" using a different Highlight.
|
|
const range11 = new content.Range();
|
|
range11.setStart(phrase2Text, 2);
|
|
range11.setEnd(phrase2Text, 5);
|
|
const highlight2 = new content.Highlight(range11);
|
|
content.CSS.highlights.set("highlight2", highlight2);
|
|
},
|
|
}
|
|
);
|