Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Errors

<!DOCTYPE html>
<html>
<head>
<title>Text Fragment Chrome-only API Test</title>
<meta charset="UTF-8">
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/GleanTest.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>
<!-- TODO: Add test case with `visibility: hidden` as soon as it's supported (bug 1950707) -->
<p id="textWithInvisibleContent">Text with <span style="display: none">display:none </span>content</p>
<p id="wordBoundaryAtInlineBoundary">WordEnd <span>should not be part of text directive</span></p>
<p id="matchAtEndOfBlock">EndOfBlock</p>
<p id="prevMatchAtEndOfBlock">EndOfBlock</p>
<div id="emptyBlockAtBeginning"><div></div>AfterEmptyBlock</div>
<div id="emptyBlockAtEnd">BeforeEmptyBlock<div></div></div>
<div id="rangeBasedWithMultipleEndMatchesBegin">Begin and begin or begin with begin</div>
<div id="rangeBasedWithMultipleEndMatchesEnd">end or end and end at end</div>
<div>HereStartsATableAndThisIsHereToMakeSureTheTestResultsAreIndependentOfSurroundingContent</div>
<table>
<tr>
<td id="firstRow">first row</td></tr> <tr><td id="secondRow">second row</td>
</tr>
</table>
<div>HereEndsATableAndThisIsHereToMakeSureTheTestResultsAreIndependentOfSurroundingContent</div>
<span id="longWord">1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111</span>
<!-- Test case for Bug 1979588: Long URL with unbreakable segment at the end -->
<p id="loremIpsum">Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source.</p>
<p>
<span id="beforeEmptyTextNode">before empty text node</span>
<!-- empty text node will be inserted right here -->
<span id="afterEmptyTextNode">after empty text node</span>
</p>
<div id="nbsp">&nbsp;</div>
<div id="punctuationStartsRangeBasedTextDirective">"</div>
<div id="punctuationEndsRangeBasedTextDirective">text preceded by punctuation in a different block</div>
<!-- Test case for Bug 1976869. -->
<div>Foo Bar</div>
<div id="rangeBasedWithPrefixAtBlockBoundaries">Some text Foo Bar more text</div>
<div id="rangeBasedWithPrefixAtBlockBoundariesEnd">End of Range</div>
<!-- Test case for Bug 1976480. -->
<div id="quotationMark">"Quotation mark"</div>
LongSpacerWord
<!-- Test case for Bug 1979474 -->
<table>
<tr id="trWithEmptyTdInside">
<td id="tdFollowedByEmptyTd">}</td>
<td></td>
<td></td>
</tr>
<tr>
<td>foo</td>
</tr>
</table>
<!-- (crash)Test cases for Bug 1987845; example paragraph found on https://en.wikipedia.org/wiki/Nuclear_weapon-->
<p>Nuclear<!-- This is needed so that there is an additional match to consider --></p>
<p id="rangeBasedWihSpaceInTheMiddle">Nuclear weapons have only twice been used in warfare, both times by the United States against Japan at the end of World War II. On August 6, 1945, the United States Army Air Forces (USAAF) detonated a uranium gun-type fission bomb nicknamed "Little Boy" over the Japanese city of Hiroshima; three days later, on August 9, the USAAF[4] detonated a plutonium implosion-type fission bomb nicknamed "Fat Man" over the Japanese city of Nagasaki. These bombings caused injuries that resulted in the deaths of approximately 200,000 civilians and military personnel.[5] The ethics of these bombings and their role in Japan's surrender are to this day, still subjects of debate</p>
<!-- note that "text" must already exist in the page to trigger this. -->
<p id="rangeBasedWithPunctuationAfterFirstWordStart">Text: the colon</p>
<p id="rangeBasedWithPunctuationAfterFirstWordEnd">is important here</p>
<!-- (crash)Test case for bug 1989891 -->
<p>1 1111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
<span id="prefixAndSuffixHitMaxLength">test</span>
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
1111111111111 1
</p>
<script>
document.addEventListener("DOMContentLoaded", () => {
const empty = document.createTextNode("");
const afterEmptyTextNode = document.getElementById("afterEmptyTextNode");
afterEmptyTextNode.parentNode.insertBefore(empty, afterEmptyTextNode);
});
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() {
const createDirectiveTimeStart = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0};
let lastCreateTimeCount = createDirectiveTimeStart.count;
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-,%F0%9F%98%8D",
},
{
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"
},
{
name: "Text directive contains search-invisible content",
startContainer: textWithInvisibleContent.firstChild,
startOffset: 0,
endContainer: textWithInvisibleContent.lastChild,
endOffset: textWithInvisibleContent.lastChild.length,
content: "Text with display:none content",
textDirective: "text=Text%20with%20content"
},
{
name: "Range-based text directive needs prefix",
startContainer: abc.firstChild,
startOffset: 12,
endContainer: foo.firstChild,
endOffset: 3,
content: "abc def ghi jkl abc. def ghi\n foo",
textDirective: "text=ghi-,abc,foo"
},
{
name: "Range-based text directive, end term is at inline boundary",
startContainer: textWithInvisibleContent.firstChild,
startOffset: 0,
endContainer: wordBoundaryAtInlineBoundary.firstChild,
endOffset: 7,
content: "Text with display:none content\n WordEnd",
textDirective: "text=Text,WordEnd"
},
{
name: "Finding text directive matches must correctly deal with block boundaries",
startContainer: prevMatchAtEndOfBlock.firstChild,
startOffset: 0,
endContainer: prevMatchAtEndOfBlock.firstChild,
endOffset: 10,
content: "EndOfBlock",
textDirective: "text=EndOfBlock-,EndOfBlock"
},
{
name: "Finding text directive across table rows",
startContainer: firstRow.firstChild,
startOffset: 0,
endContainer: secondRow.firstChild,
endOffset: secondRow.firstChild.length,
content: "first row second row",
textDirective: "text=first%20row,second%20row",
},
{
name: "Finding prefix in the correct order",
startContainer: loremIpsum.firstChild,
startOffset: 308,
endContainer: loremIpsum.firstChild,
endOffset: 313,
content: "Lorem",
textDirective: "text=a-,Lorem"
},
{
name: "Range-based text directive with multiple end matches 1 (first match)",
startContainer: rangeBasedWithMultipleEndMatchesBegin.firstChild,
startOffset: 0,
endContainer: rangeBasedWithMultipleEndMatchesEnd.firstChild,
endOffset: 3,
content: "Begin and begin or begin with begin\n end",
textDirective: "text=Begin,end"
},
{
name: "Range-based text directive with multiple end matches 2 (use end because it's shorter)",
startContainer: rangeBasedWithMultipleEndMatchesBegin.firstChild,
startOffset: 0,
endContainer: rangeBasedWithMultipleEndMatchesEnd.firstChild,
endOffset: 10,
content: "Begin and begin or begin with begin\n end or end",
textDirective: "text=Begin,or%20end"
},
{
name: "Range-based text directive with multiple end matches 3 (use suffix because it's shorter)",
startContainer: rangeBasedWithMultipleEndMatchesBegin.firstChild,
startOffset: 0,
endContainer: rangeBasedWithMultipleEndMatchesEnd.firstChild,
endOffset: 18,
content: "Begin and begin or begin with begin\n end or end and end",
textDirective: "text=Begin,end,-at"
},
{
name: "Range-based text directive with multiple start matches 1 (use start because it's shorter)",
startContainer: rangeBasedWithMultipleEndMatchesBegin.firstChild,
startOffset: 10,
endContainer: rangeBasedWithMultipleEndMatchesEnd.firstChild,
endOffset: 18,
content: "begin or begin with begin\n end or end and end",
textDirective: "text=begin%20or,end,-at"
},
{
name: "Range-based text directive with multiple start matches 2 (use prefix because it's shorter)",
startContainer: rangeBasedWithMultipleEndMatchesBegin.firstChild,
startOffset: 19,
endContainer: rangeBasedWithMultipleEndMatchesEnd.firstChild,
endOffset: 18,
content: "begin with begin\n end or end and end",
textDirective: "text=or-,begin,end,-at"
},
{
name: "Empty text nodes need to be handled correctly when finding word boundary looking left",
startContainer: afterEmptyTextNode.firstChild,
startOffset: 0,
endContainer: afterEmptyTextNode.firstChild,
endOffset: 5,
content: "after",
textDirective: "text=after"
},
{
name: "Empty text nodes need to be handled correctly when finding word boundary looking right",
startContainer: beforeEmptyTextNode.firstChild,
startOffset: 18,
endContainer: beforeEmptyTextNode.firstChild,
endOffset: 22,
content: "node",
textDirective: "text=node"
},
{
name: "Range-based text directive with punctuation as start",
startContainer: punctuationStartsRangeBasedTextDirective.firstChild,
startOffset: 0,
endContainer: punctuationEndsRangeBasedTextDirective.firstChild,
endOffset: 4,
content: '"\n text',
textDirective: "text=%22,text"
},
{
name: "Test for Bug 1976869 (edge cases for common string length computation)",
startContainer: rangeBasedWithPrefixAtBlockBoundaries.firstChild,
startOffset: 14,
endContainer: rangeBasedWithPrefixAtBlockBoundariesEnd.firstChild,
endOffset: 3,
content: "Bar more text\n End",
textDirective: "text=Bar%20more,End"
},
{
name: "Test for Bug 1976480: Selected quotation mark should not be extended to the next word",
startContainer: quotationMark.firstChild,
startOffset: 0,
endContainer: quotationMark.firstChild,
endOffset: 1,
content: "\"",
textDirective: "text=Range-,%22"
},
{
name: "Test for Bug 1976480: Selected quotation mark should not be extended to the previous word",
startContainer: quotationMark.firstChild,
startOffset: 15,
endContainer: quotationMark.firstChild,
endOffset: 16,
content: "\"",
textDirective: "text=mark-,%22"
},
{
name: "Test for Bug 1976480: Text inside quotation marks should not have the selection extended to the quotation marks",
startContainer: quotationMark.firstChild,
startOffset: 1,
endContainer: quotationMark.firstChild,
endOffset: 15,
content: "Quotation mark",
textDirective: "text=Quotation%20mark"
},
{
name: "Test for Bug 1979474: Text directive with empty table cell",
startContainer: tdFollowedByEmptyTd.firstChild,
startOffset: 0,
endContainer: tdFollowedByEmptyTd.nextSibling,
endOffset: 0,
content: "}",
textDirective: "text=%7D",
dontCompareRangeBoundaries: true,
},
{
name: "Test for Bug 1979588: Long URL with unbreakable segment should not crash",
startContainer: longUrlForBug1979588.firstChild,
startOffset: 0,
endContainer: longUrlForBug1979588.firstChild,
endOffset: longUrlForBug1979588.firstChild.length,
textDirective: "text=https,verylongnonbreakableurlsegmentthatcannotbebrokenintowordsbecauseiturlsegmentsdonotcontainspacesorothertraditionwordbreakingcharacterswhichcausesproblemsintextdirectivecreation",
},
{
name: "Test for Bug 1987845: Long text with spaces in the middle",
startContainer: rangeBasedWihSpaceInTheMiddle.firstChild,
startOffset: 0,
endContainer: rangeBasedWihSpaceInTheMiddle.firstChild,
endOffset: rangeBasedWihSpaceInTheMiddle.firstChild.length,
content: "Nuclear weapons have only twice been used in warfare, both times by the United States against Japan at the end of World War II. On August 6, 1945, the United States Army Air Forces (USAAF) detonated a uranium gun-type fission bomb nicknamed \"Little Boy\" over the Japanese city of Hiroshima; three days later, on August 9, the USAAF[4] detonated a plutonium implosion-type fission bomb nicknamed \"Fat Man\" over the Japanese city of Nagasaki. These bombings caused injuries that resulted in the deaths of approximately 200,000 civilians and military personnel.[5] The ethics of these bombings and their role in Japan's surrender are to this day, still subjects of debate",
textDirective: "text=Nuclear%20weapons,debate",
},
{
name: "Test for Bug 1987845: Punctuation after first word of range",
startContainer: rangeBasedWithPunctuationAfterFirstWordStart.firstChild,
startOffset: 0,
endContainer: rangeBasedWithPunctuationAfterFirstWordEnd.firstChild,
endOffset: rangeBasedWithPunctuationAfterFirstWordEnd.firstChild.length,
content: "Text: the colon\n is important here",
textDirective: "text=Text%3A%20the,here",
},
{
name: "Prefix and suffix hit max length at whitespace",
startContainer: prefixAndSuffixHitMaxLength.firstChild,
startOffset: 0,
endContainer: prefixAndSuffixHitMaxLength.firstChild,
endOffset: prefixAndSuffixHitMaxLength.firstChild.length,
content: "test",
textDirective: "text=test,-1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
},
]) {
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.createTextDirectiveForRanges([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(
ranges[0].toString(), range.toString(),
`${testCase.name}: Ranges have the same content`
);
if(!testCase.dontCompareRangeBoundaries) {
// For some test cases it's necessary that the range boundaries in the test case
// and the range boundaries of the created match are _not_ the same,
// e.g. if the test requires that the range contains empty nodes.
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();
const createDirectiveTime = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0};
is(createDirectiveTime.count, lastCreateTimeCount + 1, `${testCase.name}: Glean should have recorded the time it took to create the text directive`);
lastCreateTimeCount = createDirectiveTime.count;
}
}
/**
* Calling the API with an empty / collapsed range should
* return an empty string, not an error.
*/
async function testEmptyRange() {
let textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([]);
is(
textDirective, null,
"Empty input range: Produces empty text directive"
);
const range = document.createRange();
range.selectNode(abc);
range.collapse(true);
textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]);
is(
textDirective, null,
"Collapsed input range: Produces empty text directive"
);
range.selectNode(image);
textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]);
is(
textDirective, null,
"Input range contains image only: Produces empty text directive"
);
range.selectNode(nbsp);
textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]);
is(
textDirective, null,
"Input range contains nbsp only: Produces empty text directive"
);
}
async function testExpandRangeToWordBoundaries() {
const createDirectiveTimeStart = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0};
let lastCreateTimeCount = createDirectiveTimeStart.count;
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.createTextDirectiveForRanges([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 = "";
const createDirectiveTime = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0};
is(createDirectiveTime.count, lastCreateTimeCount + 1, `${testCase.name}: Glean should have recorded the time it took to create the text directive`);
lastCreateTimeCount = createDirectiveTime.count;
}
}
async function testNonUniqueTestSurroundedByBlockBoundaries() {
const createDirectiveTimeStart = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0};
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.createTextDirectiveForRanges([range]);
is(textDirective, null, `${testCase}: It's not possible to create a unique text directive`);
const createDirectiveTime = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0};
is(createDirectiveTime.count, createDirectiveTimeStart.count + 1, `${testCase}: Glean should have recorded the time it took to create the text directive`);
}
async function testRangeBasedWithEmptySurroundingBlocks() {
const testCase = "Empty blocks at beginning or end of range must be ignored";
const range = document.createRange();
range.setStart(emptyBlockAtBeginning, 0);
range.setEnd(emptyBlockAtEnd.nextSibling ,0);
const content = "AfterEmptyBlock\n BeforeEmptyBlock";
const textDirectiveString = "text=AfterEmptyBlock,BeforeEmptyBlock";
is(range.toString(), content, `${testCase}: Range has expected value`);
const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]);
is(textDirective, textDirectiveString, `${testCase}: text directive has expected value '${textDirectiveString}'`);
location.hash = `:~:${textDirective}`;
await rAF();
ranges = SpecialPowers.wrap(document).fragmentDirective.getTextDirectiveRanges();
is(ranges.length, 1, `${testCase}: There is one text fragment range on the document`);
is(ranges[0].toString(), range.toString(), `${testCase}: Ranges have the same content`);
}
async function testRangeBasedWithNonBreakableWord() {
const testCase = "Range-based text directive with non-breakable word";
const range = document.createRange();
range.setStart(longWord.firstChild, 0);
range.setEnd(longWord.firstChild, longWord.firstChild.length);
const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]);
is(textDirective, null, `${testCase}: This test case should not produce a text directive`);
}
async function testMultipleRanges() {
let testCase = "Two single-word text directives";
const range1 = document.createRange();
range1.setStart(abc.firstChild, 0);
range1.setEnd(abc.firstChild, 3);
const range2 = document.createRange();
range2.setStart(foo.firstChild, 0);
range2.setEnd(foo.firstChild, 3);
const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range1, range2]);
is(textDirective, "text=abc&text=foo", `${testCase}: The text fragment has the expected value`);
}
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();
await testRangeBasedWithEmptySurroundingBlocks();
await testRangeBasedWithNonBreakableWord();
await testMultipleRanges();
}
finally {
SimpleTest.finish();
}
}
document.body.onload = runTests;
</script>
</body>
</html>