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