1
0
Fork 0
firefox/accessible/tests/browser/text/browser_highlights.js
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

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);
},
}
);