Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
- Manifest: dom/base/test/mochitest.toml
<!DOCTYPE html>
<html>
<head>
<title>Text Fragment Chrome-only API Test</title>
<meta charset="UTF-8">
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<div id="abc">abc def ghi abc def ghi jkl abc. def <span>ghi</span></div>
<div id="foo">foo</div>
<p id="block">p<span id="inlinespan">sp<span id="nestedinlinespan">a</span>n</span>p</p><span id="afterblockboundary">afterblockboundary</span>
<div id="image"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="></div>
<p>placeholder with block boundary</p>
<p id="surroundedByBlockBoundaries">abc def ghi</p>
<p>placeholder with block boundary</p>
<ul>
<li>First element</li>
<li>Second element</li>
</ul>
<ul>
<li>First element, but different</li>
<li id="secondListElement">Second Element</li>
</ul>
<p id="textWithEmoji"><span>😍😍</span> abc def 😍😍</p>
<p id="textWithDifferentEmoji"><span>😍😏😍</span> abc def 😍😏</p>
<script>
SimpleTest.waitForExplicitFinish();
function rAF() {
return new Promise(resolve => {
requestAnimationFrame(resolve);
});
}
/**
* Helper function which is used by assertions below.
* Returns true if the two ranges have the exact same start and end points.
*/
function rangeBoundariesAreEqual(range1, range2) {
const startContainerIsEqual = range1.startContainer.textContent === range2.startContainer.textContent;
const endContainerIsEqual = range1.endContainer.textContent === range2.endContainer.textContent;
const startOffsetIsEqual = range1.startOffset == range2.startOffset;
const endOffsetIsEqual = range1.endOffset == range2.endOffset;
return (
startContainerIsEqual
&& endContainerIsEqual
&& startOffsetIsEqual
&& endOffsetIsEqual
);
}
/**
* Test for the `rangeBoundariesAreEqual()` helper function.
*/
function testRangeBoundariesAreEqual() {
const range1 = document.createRange();
range1.setStart(abc.firstChild, 0);
range1.setEnd(abc.firstChild, 3);
const range2 = document.createRange();
range2.setStart(abc.firstChild, 0);
range2.setEnd(abc.firstChild, 3);
ok(
rangeBoundariesAreEqual(range1, range2),
"Ranges should have the same boundary points when containers are text nodes"
);
const range3 = document.createRange();
range3.selectNode(abc);
const range4 = document.createRange();
range4.selectNode(abc);
ok(
rangeBoundariesAreEqual(range3, range4),
"Ranges should have the same boundary points when containers are nodes"
);
}
async function basicTests() {
for(let testCase of [
{
name: "Text directive is first string in the document",
startContainer: abc.firstChild,
startOffset: 0,
endContainer: abc.firstChild,
endOffset: 3,
content: "abc",
textDirective: "text=abc"
},
{
name: "Text directive is the second word in the document",
startContainer: abc.firstChild,
startOffset: 4,
endContainer: abc.firstChild,
endOffset: 7,
content: "def",
textDirective: "text=def"
},
{
name: "Text directive spans two words",
startContainer: abc.firstChild,
startOffset: 0,
endContainer: abc.firstChild,
endOffset: 7,
content: "abc def",
textDirective: "text=abc%20def"
},
{
name: "Text directive is second occurrence of content",
startContainer: abc.firstChild,
startOffset: 12,
endContainer: abc.firstChild,
endOffset: 15,
content: "abc",
textDirective: "text=ghi-,abc"
},
{
name: "Text directive is second occurrence, suffix is shorter",
startContainer: abc.firstChild,
startOffset: 20,
endContainer: abc.firstChild,
endOffset: 23,
content: "ghi",
textDirective: "text=ghi,-jkl"
},
{
name: "Shortest text directive crosses block boundary",
startContainer: abc.lastChild.firstChild,
startOffset: 0,
endContainer: abc.lastChild.firstChild,
endOffset: 3,
content: "ghi",
textDirective: "text=ghi,-foo"
},
{
name: "Text directive crosses block boundary",
startContainer: block.firstChild,
startOffset: 0,
endContainer: afterblockboundary.firstChild,
endOffset: 18,
content: "pspanpafterblockboundary",
textDirective: "text=pspanp,afterblockboundary",
},
{
name: "Text directive crosses several block boundaries",
startContainer: foo.firstChild,
startOffset: 0,
endContainer: afterblockboundary.firstChild,
endOffset: 18,
content: "foo\n pspanpafterblockboundary",
textDirective: "text=foo,afterblockboundary",
},
{
name: "Text directive needs to check before previous block boundary",
startContainer: secondListElement.firstChild,
startOffset: 0,
endContainer: secondListElement.firstChild,
endOffset: 6,
content: "Second",
textDirective: "text=different-,Second"
},
{
name: "Text directive contains emoji as start",
startContainer: textWithEmoji.firstChild.firstChild,
startOffset: 0,
endContainer: textWithEmoji.firstChild.firstChild,
endOffset: 2,
content: "😍",
textDirective: "text=%F0%9F%98%8D",
},
{
name: "Text directive contains emoji as start and prefix",
startContainer: textWithEmoji.firstChild.firstChild,
startOffset: 2,
endContainer: textWithEmoji.firstChild.firstChild,
endOffset: 4,
content: "😍",
textDirective: "text=%F0%9F%98%8D,-abc",
},
{
name: "Text directive contains emoji as prefix",
startContainer: textWithEmoji.firstChild.nextSibling,
startOffset: 1,
endContainer: textWithEmoji.firstChild.nextSibling,
endOffset: 4,
content: "abc",
textDirective: "text=%F0%9F%98%8D-,abc",
},
{
name: "Text directive contains slightly different emoji as prefix",
startContainer: textWithDifferentEmoji.firstChild.nextSibling,
startOffset: 1,
endContainer: textWithDifferentEmoji.firstChild.nextSibling,
endOffset: 4,
content: "abc",
textDirective: "text=%F0%9F%98%8F%F0%9F%98%8D-,abc",
},
{
name: "Text directive needs to compare emojis for suffix",
startContainer: textWithDifferentEmoji.firstChild.nextSibling,
startOffset: 5,
endContainer: textWithDifferentEmoji.firstChild.nextSibling,
endOffset: 8,
content: "def",
textDirective: "text=def,-%F0%9F%98%8D%F0%9F%98%8F"
}
]) {
const range = document.createRange();
range.setStart(testCase.startContainer, testCase.startOffset);
range.setEnd(testCase.endContainer, testCase.endOffset);
is(
range.toString(), testCase.content,
`${testCase.name}: Precondition - Range has expected value`
);
const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirective(range);
is(
textDirective, testCase.textDirective,
`${testCase.name}: text directive has expected value '${testCase.textDirective}'`
);
// load the page with the given text directive
location.hash = `#:~:${textDirective}`;
await rAF();
// access the range from the loaded text directive and compare the boundary points
ranges = SpecialPowers.wrap(document).fragmentDirective.getTextDirectiveRanges();
is(
ranges.length, 1,
`${testCase.name}: There is one text fragment range on the document`
);
is(
range.toString(), ranges[0].toString(),
`${testCase.name}: Ranges have the same content '${ranges[0].toString()}'`
);
ok(
rangeBoundariesAreEqual(range, ranges[0]),
`${testCase.name}: Ranges have the same boundary points`
);
// finally, remove all text directives to clean up for the next test.
SpecialPowers.wrap(document).fragmentDirective.removeAllTextDirectives();
}
}
/**
* Calling the API with an empty / collapsed range should
* return an empty string, not an error.
*/
async function testEmptyRange() {
const range = document.createRange();
let textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirective(range);
is(
textDirective, null,
"Empty input range: Produces empty text directive"
);
range.selectNode(abc);
range.collapse(true);
textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirective(range);
is(
textDirective, null,
"Collapsed input range: Produces empty text directive"
);
range.selectNode(image);
textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirective(range);
is(
textDirective, null,
"Input range contains image only: Produces empty text directive"
);
}
async function testExpandRangeToWordBoundaries() {
for (testCase of [
{
name: "Expanding single-word range to word boundaries (input is inside word)",
startContainer: abc.firstChild,
startOffset: 5,
endContainer: abc.firstChild,
endOffset: 6,
outputStartContainer: abc.firstChild,
outputStartOffset: 4,
outputEndContainer: abc.firstChild,
outputEndOffset: 7,
content: "e",
outputContent: "def",
textDirective: "text=def"
},
{
name: "Expanding single-word range to word boundaries (input is start of word)",
startContainer: abc.firstChild,
startOffset: 4,
endContainer: abc.firstChild,
endOffset: 5,
outputStartContainer: abc.firstChild,
outputStartOffset: 4,
outputEndContainer: abc.firstChild,
outputEndOffset: 7,
content: "d",
outputContent: "def",
textDirective: "text=def"
},
{
name: "Expanding single-word range to word boundaries (input is end of word)",
startContainer: abc.firstChild,
startOffset: 6,
endContainer: abc.firstChild,
endOffset: 7,
outputStartContainer: abc.firstChild,
outputStartOffset: 4,
outputEndContainer: abc.firstChild,
outputEndOffset: 7,
content: "f",
outputContent: "def",
textDirective: "text=def"
},
{
name: "Expanding multi-word range to word boundaries",
startContainer: abc.firstChild,
startOffset: 5,
endContainer: abc.firstChild,
endOffset: 9,
outputStartContainer: abc.firstChild,
outputStartOffset: 4,
outputEndContainer: abc.firstChild,
outputEndOffset: 11,
content: "ef g",
outputContent: "def ghi",
textDirective: "text=def%20ghi"
},
{
name: "Expanding inline-boundary word range to word boundaries",
startContainer: nestedinlinespan.firstChild,
startOffset: 0,
endContainer: nestedinlinespan.firstChild,
endOffset: 1,
outputStartContainer: block.firstChild,
outputStartOffset: 0,
outputEndContainer: block.lastChild,
outputEndOffset: 1,
content: "a",
outputContent: "pspanp",
textDirective: "text=pspanp"
},
]) {
const range = document.createRange();
range.setStart(testCase.startContainer, testCase.startOffset);
range.setEnd(testCase.endContainer, testCase.endOffset);
is(
range.toString().trim(), testCase.content,
`${testCase.name}: Precondition - Range has expected value`
);
const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirective(range);
is(
textDirective, testCase.textDirective,
`${testCase.name}: text directive has expected value '${textDirective}'`
);
// load the page with the given text directive
location.hash = `#:~:${textDirective}`;
await rAF();
// access the range from the loaded text directive and compare the boundary points
ranges = SpecialPowers.wrap(document).fragmentDirective.getTextDirectiveRanges();
const expectedRange = document.createRange();
expectedRange.setStart(testCase.outputStartContainer, testCase.outputStartOffset);
expectedRange.setEnd(testCase.outputEndContainer, testCase.outputEndOffset);
is(
expectedRange.toString(), ranges[0].toString(),
`${testCase.name}: Ranges have the same content '${ranges[0].toString()}'`
);
ok(
rangeBoundariesAreEqual(expectedRange, ranges[0]),
`${testCase.name}: Ranges have the same boundary points`
);
// finally, remove all text directives to clean up for the next test.
SpecialPowers.wrap(document).fragmentDirective.removeAllTextDirectives();
location.hash = "";
}
}
async function testNonUniqueTestSurroundedByBlockBoundaries() {
const testCase = "Creating a text directive which cannot be found"
const range = document.createRange();
range.setStart(surroundedByBlockBoundaries.firstChild, 4);
range.setEnd(surroundedByBlockBoundaries.firstChild, 7);
is(range.toString(), "def", `${testCase}: Range has expected value`);
const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirective(range);
is(textDirective, null, `${testCase}: It's not possible to create a unique text directive`);
}
async function runTests() {
try {
await SpecialPowers.pushPrefEnv({"set": [
["dom.text_fragments.enabled", true],
["dom.text_fragments.create_text_fragment.enabled", true],
]});
testRangeBoundariesAreEqual();
await basicTests();
await testEmptyRange();
await testExpandRangeToWordBoundaries();
await testNonUniqueTestSurroundedByBlockBoundaries();
}
finally {
SimpleTest.finish();
}
}
document.body.onload = runTests;
</script>
</body>
</html>