test_text-fragments-create-text-directive.html (36235B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>Text Fragment Chrome-only API Test</title> 5 <meta charset="UTF-8"> 6 <script src="/tests/SimpleTest/SimpleTest.js"></script> 7 <script src="/tests/SimpleTest/GleanTest.js"></script> 8 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 9 </head> 10 <body> 11 <div id="abc">abc def ghi abc def ghi jkl abc. def <span>ghi</span></div> 12 <div id="foo">foo</div> 13 <p id="block">p<span id="inlinespan">sp<span id="nestedinlinespan">a</span>n</span>p</p><span id="afterblockboundary">afterblockboundary</span> 14 <div id="image"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="></div> 15 <p>placeholder with block boundary</p> 16 <p id="surroundedByBlockBoundaries">abc def ghi</p> 17 <p>placeholder with block boundary</p> 18 <ul> 19 <li>First element</li> 20 <li>Second element</li> 21 </ul> 22 <ul> 23 <li>First element, but different</li> 24 <li id="secondListElement">Second Element</li> 25 </ul> 26 <p id="textWithEmoji"><span>😍😍</span> abc def 😍😍</p> 27 <p id="textWithDifferentEmoji"><span>😍😏😍</span> abc def 😍😏</p> 28 <!-- TODO: Add test case with `visibility: hidden` as soon as it's supported (bug 1950707) --> 29 <p id="textWithInvisibleContent">Text with <span style="display: none">display:none </span>content</p> 30 <p id="wordBoundaryAtInlineBoundary">WordEnd <span>should not be part of text directive</span></p> 31 <p id="matchAtEndOfBlock">EndOfBlock</p> 32 <p id="prevMatchAtEndOfBlock">EndOfBlock</p> 33 <div id="emptyBlockAtBeginning"><div></div>AfterEmptyBlock</div> 34 <div id="emptyBlockAtEnd">BeforeEmptyBlock<div></div></div> 35 <div id="rangeBasedWithMultipleEndMatchesBegin">Begin and begin or begin with begin</div> 36 <div id="rangeBasedWithMultipleEndMatchesEnd">end or end and end at end</div> 37 <div>HereStartsATableAndThisIsHereToMakeSureTheTestResultsAreIndependentOfSurroundingContent</div> 38 <table> 39 <tr> 40 <td id="firstRow">first row</td></tr> <tr><td id="secondRow">second row</td> 41 </tr> 42 </table> 43 <div>HereEndsATableAndThisIsHereToMakeSureTheTestResultsAreIndependentOfSurroundingContent</div> 44 45 <span id="longWord">1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111</span> 46 <!-- Test case for Bug 1979588: Long URL with unbreakable segment at the end --> 47 <div id="longUrlForBug1979588">https://example.com/test?foo=bar#verylongnonbreakableurlsegmentthatcannotbebrokenintowordsbecauseiturlsegmentsdonotcontainspacesorothertraditionwordbreakingcharacterswhichcausesproblemsintextdirectivecreation</div> 48 <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> 49 <p> 50 <span id="beforeEmptyTextNode">before empty text node</span> 51 <!-- empty text node will be inserted right here --> 52 <span id="afterEmptyTextNode">after empty text node</span> 53 </p> 54 <div id="nbsp"> </div> 55 <div id="punctuationStartsRangeBasedTextDirective">"</div> 56 <div id="punctuationEndsRangeBasedTextDirective">text preceded by punctuation in a different block</div> 57 <!-- Test case for Bug 1976869. --> 58 <div>Foo Bar</div> 59 <div id="rangeBasedWithPrefixAtBlockBoundaries">Some text Foo Bar more text</div> 60 <div id="rangeBasedWithPrefixAtBlockBoundariesEnd">End of Range</div> 61 <!-- Test case for Bug 1976480. --> 62 <div id="quotationMark">"Quotation mark"</div> 63 LongSpacerWord 64 <!-- Test case for Bug 1979474 --> 65 <table> 66 <tr id="trWithEmptyTdInside"> 67 <td id="tdFollowedByEmptyTd">}</td> 68 <td></td> 69 <td></td> 70 </tr> 71 <tr> 72 <td>foo</td> 73 </tr> 74 </table> 75 <!-- (crash)Test cases for Bug 1987845; example paragraph found on https://en.wikipedia.org/wiki/Nuclear_weapon--> 76 <p>Nuclear<!-- This is needed so that there is an additional match to consider --></p> 77 <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> 78 <!-- note that "text" must already exist in the page to trigger this. --> 79 <p id="rangeBasedWithPunctuationAfterFirstWordStart">Text: the colon</p> 80 <p id="rangeBasedWithPunctuationAfterFirstWordEnd">is important here</p> 81 <!-- (crash)Test case for bug 1989891 --> 82 <p>1 1111111111111 83 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 84 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 85 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 86 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 87 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 88 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 89 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 90 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 91 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 92 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 93 <span id="prefixAndSuffixHitMaxLength">test</span> 94 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 95 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 96 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 97 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 98 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 99 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 100 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 101 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 102 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 103 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 104 1111111111111 1 105 </p> 106 <script> 107 document.addEventListener("DOMContentLoaded", () => { 108 const empty = document.createTextNode(""); 109 const afterEmptyTextNode = document.getElementById("afterEmptyTextNode"); 110 afterEmptyTextNode.parentNode.insertBefore(empty, afterEmptyTextNode); 111 }); 112 113 SimpleTest.waitForExplicitFinish(); 114 function rAF() { 115 return new Promise(resolve => { 116 requestAnimationFrame(resolve); 117 }); 118 } 119 /** 120 * Helper function which is used by assertions below. 121 * Returns true if the two ranges have the exact same start and end points. 122 */ 123 function rangeBoundariesAreEqual(range1, range2) { 124 const startContainerIsEqual = range1.startContainer.textContent === range2.startContainer.textContent; 125 const endContainerIsEqual = range1.endContainer.textContent === range2.endContainer.textContent; 126 127 const startOffsetIsEqual = range1.startOffset == range2.startOffset; 128 const endOffsetIsEqual = range1.endOffset == range2.endOffset; 129 return ( 130 startContainerIsEqual 131 && endContainerIsEqual 132 && startOffsetIsEqual 133 && endOffsetIsEqual 134 ); 135 } 136 137 /** 138 * Test for the `rangeBoundariesAreEqual()` helper function. 139 */ 140 function testRangeBoundariesAreEqual() { 141 const range1 = document.createRange(); 142 range1.setStart(abc.firstChild, 0); 143 range1.setEnd(abc.firstChild, 3); 144 const range2 = document.createRange(); 145 range2.setStart(abc.firstChild, 0); 146 range2.setEnd(abc.firstChild, 3); 147 148 ok( 149 rangeBoundariesAreEqual(range1, range2), 150 "Ranges should have the same boundary points when containers are text nodes" 151 ); 152 153 const range3 = document.createRange(); 154 range3.selectNode(abc); 155 const range4 = document.createRange(); 156 range4.selectNode(abc); 157 ok( 158 rangeBoundariesAreEqual(range3, range4), 159 "Ranges should have the same boundary points when containers are nodes" 160 ); 161 } 162 163 async function basicTests() { 164 const createDirectiveTimeStart = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0}; 165 let lastCreateTimeCount = createDirectiveTimeStart.count; 166 for(let testCase of [ 167 { 168 name: "Text directive is first string in the document", 169 startContainer: abc.firstChild, 170 startOffset: 0, 171 endContainer: abc.firstChild, 172 endOffset: 3, 173 content: "abc", 174 textDirective: "text=abc" 175 }, 176 { 177 name: "Text directive is the second word in the document", 178 startContainer: abc.firstChild, 179 startOffset: 4, 180 endContainer: abc.firstChild, 181 endOffset: 7, 182 content: "def", 183 textDirective: "text=def" 184 }, 185 { 186 name: "Text directive spans two words", 187 startContainer: abc.firstChild, 188 startOffset: 0, 189 endContainer: abc.firstChild, 190 endOffset: 7, 191 content: "abc def", 192 textDirective: "text=abc%20def" 193 }, 194 { 195 name: "Text directive is second occurrence of content", 196 startContainer: abc.firstChild, 197 startOffset: 12, 198 endContainer: abc.firstChild, 199 endOffset: 15, 200 content: "abc", 201 textDirective: "text=ghi-,abc" 202 }, 203 { 204 name: "Text directive is second occurrence, suffix is shorter", 205 startContainer: abc.firstChild, 206 startOffset: 20, 207 endContainer: abc.firstChild, 208 endOffset: 23, 209 content: "ghi", 210 textDirective: "text=ghi,-jkl" 211 }, 212 { 213 name: "Shortest text directive crosses block boundary", 214 startContainer: abc.lastChild.firstChild, 215 startOffset: 0, 216 endContainer: abc.lastChild.firstChild, 217 endOffset: 3, 218 content: "ghi", 219 textDirective: "text=ghi,-foo" 220 }, 221 { 222 name: "Text directive crosses block boundary", 223 startContainer: block.firstChild, 224 startOffset: 0, 225 endContainer: afterblockboundary.firstChild, 226 endOffset: 18, 227 content: "pspanpafterblockboundary", 228 textDirective: "text=pspanp,afterblockboundary", 229 }, 230 { 231 name: "Text directive crosses several block boundaries", 232 startContainer: foo.firstChild, 233 startOffset: 0, 234 endContainer: afterblockboundary.firstChild, 235 endOffset: 18, 236 content: "foo\n pspanpafterblockboundary", 237 textDirective: "text=foo,afterblockboundary", 238 }, 239 { 240 name: "Text directive needs to check before previous block boundary", 241 startContainer: secondListElement.firstChild, 242 startOffset: 0, 243 endContainer: secondListElement.firstChild, 244 endOffset: 6, 245 content: "Second", 246 textDirective: "text=different-,Second" 247 }, 248 { 249 name: "Text directive contains emoji as start", 250 startContainer: textWithEmoji.firstChild.firstChild, 251 startOffset: 0, 252 endContainer: textWithEmoji.firstChild.firstChild, 253 endOffset: 2, 254 content: "😍", 255 textDirective: "text=%F0%9F%98%8D", 256 }, 257 { 258 name: "Text directive contains emoji as start and prefix", 259 startContainer: textWithEmoji.firstChild.firstChild, 260 startOffset: 2, 261 endContainer: textWithEmoji.firstChild.firstChild, 262 endOffset: 4, 263 content: "😍", 264 textDirective: "text=%F0%9F%98%8D-,%F0%9F%98%8D", 265 }, 266 { 267 name: "Text directive contains emoji as prefix", 268 startContainer: textWithEmoji.firstChild.nextSibling, 269 startOffset: 1, 270 endContainer: textWithEmoji.firstChild.nextSibling, 271 endOffset: 4, 272 content: "abc", 273 textDirective: "text=%F0%9F%98%8D-,abc", 274 }, 275 { 276 name: "Text directive contains slightly different emoji as prefix", 277 startContainer: textWithDifferentEmoji.firstChild.nextSibling, 278 startOffset: 1, 279 endContainer: textWithDifferentEmoji.firstChild.nextSibling, 280 endOffset: 4, 281 content: "abc", 282 textDirective: "text=%F0%9F%98%8F%F0%9F%98%8D-,abc", 283 }, 284 { 285 name: "Text directive needs to compare emojis for suffix", 286 startContainer: textWithDifferentEmoji.firstChild.nextSibling, 287 startOffset: 5, 288 endContainer: textWithDifferentEmoji.firstChild.nextSibling, 289 endOffset: 8, 290 content: "def", 291 textDirective: "text=def,-%F0%9F%98%8D%F0%9F%98%8F" 292 }, 293 { 294 name: "Text directive contains search-invisible content", 295 startContainer: textWithInvisibleContent.firstChild, 296 startOffset: 0, 297 endContainer: textWithInvisibleContent.lastChild, 298 endOffset: textWithInvisibleContent.lastChild.length, 299 content: "Text with display:none content", 300 textDirective: "text=Text%20with%20content" 301 }, 302 { 303 name: "Range-based text directive needs prefix", 304 startContainer: abc.firstChild, 305 startOffset: 12, 306 endContainer: foo.firstChild, 307 endOffset: 3, 308 content: "abc def ghi jkl abc. def ghi\n foo", 309 textDirective: "text=ghi-,abc,foo" 310 }, 311 { 312 name: "Range-based text directive, end term is at inline boundary", 313 startContainer: textWithInvisibleContent.firstChild, 314 startOffset: 0, 315 endContainer: wordBoundaryAtInlineBoundary.firstChild, 316 endOffset: 7, 317 content: "Text with display:none content\n WordEnd", 318 textDirective: "text=Text,WordEnd" 319 }, 320 { 321 name: "Finding text directive matches must correctly deal with block boundaries", 322 startContainer: prevMatchAtEndOfBlock.firstChild, 323 startOffset: 0, 324 endContainer: prevMatchAtEndOfBlock.firstChild, 325 endOffset: 10, 326 content: "EndOfBlock", 327 textDirective: "text=EndOfBlock-,EndOfBlock" 328 }, 329 { 330 name: "Finding text directive across table rows", 331 startContainer: firstRow.firstChild, 332 startOffset: 0, 333 endContainer: secondRow.firstChild, 334 endOffset: secondRow.firstChild.length, 335 content: "first row second row", 336 textDirective: "text=first%20row,second%20row", 337 }, 338 { 339 name: "Finding prefix in the correct order", 340 startContainer: loremIpsum.firstChild, 341 startOffset: 308, 342 endContainer: loremIpsum.firstChild, 343 endOffset: 313, 344 content: "Lorem", 345 textDirective: "text=a-,Lorem" 346 }, 347 { 348 name: "Range-based text directive with multiple end matches 1 (first match)", 349 startContainer: rangeBasedWithMultipleEndMatchesBegin.firstChild, 350 startOffset: 0, 351 endContainer: rangeBasedWithMultipleEndMatchesEnd.firstChild, 352 endOffset: 3, 353 content: "Begin and begin or begin with begin\n end", 354 textDirective: "text=Begin,end" 355 }, 356 { 357 name: "Range-based text directive with multiple end matches 2 (use end because it's shorter)", 358 startContainer: rangeBasedWithMultipleEndMatchesBegin.firstChild, 359 startOffset: 0, 360 endContainer: rangeBasedWithMultipleEndMatchesEnd.firstChild, 361 endOffset: 10, 362 content: "Begin and begin or begin with begin\n end or end", 363 textDirective: "text=Begin,or%20end" 364 }, 365 { 366 name: "Range-based text directive with multiple end matches 3 (use suffix because it's shorter)", 367 startContainer: rangeBasedWithMultipleEndMatchesBegin.firstChild, 368 startOffset: 0, 369 endContainer: rangeBasedWithMultipleEndMatchesEnd.firstChild, 370 endOffset: 18, 371 content: "Begin and begin or begin with begin\n end or end and end", 372 textDirective: "text=Begin,end,-at" 373 }, 374 { 375 name: "Range-based text directive with multiple start matches 1 (use start because it's shorter)", 376 startContainer: rangeBasedWithMultipleEndMatchesBegin.firstChild, 377 startOffset: 10, 378 endContainer: rangeBasedWithMultipleEndMatchesEnd.firstChild, 379 endOffset: 18, 380 content: "begin or begin with begin\n end or end and end", 381 textDirective: "text=begin%20or,end,-at" 382 }, 383 { 384 name: "Range-based text directive with multiple start matches 2 (use prefix because it's shorter)", 385 startContainer: rangeBasedWithMultipleEndMatchesBegin.firstChild, 386 startOffset: 19, 387 endContainer: rangeBasedWithMultipleEndMatchesEnd.firstChild, 388 endOffset: 18, 389 content: "begin with begin\n end or end and end", 390 textDirective: "text=or-,begin,end,-at" 391 }, 392 { 393 name: "Empty text nodes need to be handled correctly when finding word boundary looking left", 394 startContainer: afterEmptyTextNode.firstChild, 395 startOffset: 0, 396 endContainer: afterEmptyTextNode.firstChild, 397 endOffset: 5, 398 content: "after", 399 textDirective: "text=after" 400 }, 401 { 402 name: "Empty text nodes need to be handled correctly when finding word boundary looking right", 403 startContainer: beforeEmptyTextNode.firstChild, 404 startOffset: 18, 405 endContainer: beforeEmptyTextNode.firstChild, 406 endOffset: 22, 407 content: "node", 408 textDirective: "text=node" 409 }, 410 { 411 name: "Range-based text directive with punctuation as start", 412 startContainer: punctuationStartsRangeBasedTextDirective.firstChild, 413 startOffset: 0, 414 endContainer: punctuationEndsRangeBasedTextDirective.firstChild, 415 endOffset: 4, 416 content: '"\n text', 417 textDirective: "text=%22,text" 418 }, 419 { 420 name: "Test for Bug 1976869 (edge cases for common string length computation)", 421 startContainer: rangeBasedWithPrefixAtBlockBoundaries.firstChild, 422 startOffset: 14, 423 endContainer: rangeBasedWithPrefixAtBlockBoundariesEnd.firstChild, 424 endOffset: 3, 425 content: "Bar more text\n End", 426 textDirective: "text=Bar%20more,End" 427 }, 428 { 429 name: "Test for Bug 1976480: Selected quotation mark should not be extended to the next word", 430 startContainer: quotationMark.firstChild, 431 startOffset: 0, 432 endContainer: quotationMark.firstChild, 433 endOffset: 1, 434 content: "\"", 435 textDirective: "text=Range-,%22" 436 }, 437 { 438 name: "Test for Bug 1976480: Selected quotation mark should not be extended to the previous word", 439 startContainer: quotationMark.firstChild, 440 startOffset: 15, 441 endContainer: quotationMark.firstChild, 442 endOffset: 16, 443 content: "\"", 444 textDirective: "text=mark-,%22" 445 }, 446 { 447 name: "Test for Bug 1976480: Text inside quotation marks should not have the selection extended to the quotation marks", 448 startContainer: quotationMark.firstChild, 449 startOffset: 1, 450 endContainer: quotationMark.firstChild, 451 endOffset: 15, 452 content: "Quotation mark", 453 textDirective: "text=Quotation%20mark" 454 }, 455 { 456 name: "Test for Bug 1979474: Text directive with empty table cell", 457 startContainer: tdFollowedByEmptyTd.firstChild, 458 startOffset: 0, 459 endContainer: tdFollowedByEmptyTd.nextSibling, 460 endOffset: 0, 461 content: "}", 462 textDirective: "text=%7D", 463 dontCompareRangeBoundaries: true, 464 }, 465 { 466 name: "Test for Bug 1979588: Long URL with unbreakable segment should not crash", 467 startContainer: longUrlForBug1979588.firstChild, 468 startOffset: 0, 469 endContainer: longUrlForBug1979588.firstChild, 470 endOffset: longUrlForBug1979588.firstChild.length, 471 content: "https://example.com/test?foo=bar#verylongnonbreakableurlsegmentthatcannotbebrokenintowordsbecauseiturlsegmentsdonotcontainspacesorothertraditionwordbreakingcharacterswhichcausesproblemsintextdirectivecreation", 472 textDirective: "text=https,verylongnonbreakableurlsegmentthatcannotbebrokenintowordsbecauseiturlsegmentsdonotcontainspacesorothertraditionwordbreakingcharacterswhichcausesproblemsintextdirectivecreation", 473 }, 474 { 475 name: "Test for Bug 1987845: Long text with spaces in the middle", 476 startContainer: rangeBasedWihSpaceInTheMiddle.firstChild, 477 startOffset: 0, 478 endContainer: rangeBasedWihSpaceInTheMiddle.firstChild, 479 endOffset: rangeBasedWihSpaceInTheMiddle.firstChild.length, 480 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", 481 textDirective: "text=Nuclear%20weapons,debate", 482 }, 483 { 484 name: "Test for Bug 1987845: Punctuation after first word of range", 485 startContainer: rangeBasedWithPunctuationAfterFirstWordStart.firstChild, 486 startOffset: 0, 487 endContainer: rangeBasedWithPunctuationAfterFirstWordEnd.firstChild, 488 endOffset: rangeBasedWithPunctuationAfterFirstWordEnd.firstChild.length, 489 content: "Text: the colon\n is important here", 490 textDirective: "text=Text%3A%20the,here", 491 }, 492 { 493 name: "Prefix and suffix hit max length at whitespace", 494 startContainer: prefixAndSuffixHitMaxLength.firstChild, 495 startOffset: 0, 496 endContainer: prefixAndSuffixHitMaxLength.firstChild, 497 endOffset: prefixAndSuffixHitMaxLength.firstChild.length, 498 content: "test", 499 textDirective: "text=test,-1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", 500 }, 501 ]) { 502 const range = document.createRange(); 503 range.setStart(testCase.startContainer, testCase.startOffset); 504 range.setEnd(testCase.endContainer, testCase.endOffset); 505 is( 506 range.toString(), testCase.content, 507 `${testCase.name}: Precondition - Range has expected value` 508 ); 509 510 const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]); 511 is( 512 textDirective, testCase.textDirective, 513 `${testCase.name}: text directive has expected value '${testCase.textDirective}'` 514 ); 515 // load the page with the given text directive 516 location.hash = `#:~:${textDirective}`; 517 await rAF(); 518 519 // access the range from the loaded text directive and compare the boundary points 520 ranges = SpecialPowers.wrap(document).fragmentDirective.getTextDirectiveRanges(); 521 is( 522 ranges.length, 1, 523 `${testCase.name}: There is one text fragment range on the document` 524 ); 525 526 is( 527 ranges[0].toString(), range.toString(), 528 `${testCase.name}: Ranges have the same content` 529 ); 530 if(!testCase.dontCompareRangeBoundaries) { 531 // For some test cases it's necessary that the range boundaries in the test case 532 // and the range boundaries of the created match are _not_ the same, 533 // e.g. if the test requires that the range contains empty nodes. 534 ok( 535 rangeBoundariesAreEqual(range, ranges[0]), 536 `${testCase.name}: Ranges have the same boundary points` 537 ); 538 } 539 540 // finally, remove all text directives to clean up for the next test. 541 SpecialPowers.wrap(document).fragmentDirective.removeAllTextDirectives(); 542 543 const createDirectiveTime = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0}; 544 is(createDirectiveTime.count, lastCreateTimeCount + 1, `${testCase.name}: Glean should have recorded the time it took to create the text directive`); 545 lastCreateTimeCount = createDirectiveTime.count; 546 } 547 } 548 549 /** 550 * Calling the API with an empty / collapsed range should 551 * return an empty string, not an error. 552 */ 553 async function testEmptyRange() { 554 555 let textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([]); 556 is( 557 textDirective, null, 558 "Empty input range: Produces empty text directive" 559 ); 560 const range = document.createRange(); 561 range.selectNode(abc); 562 range.collapse(true); 563 textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]); 564 is( 565 textDirective, null, 566 "Collapsed input range: Produces empty text directive" 567 ); 568 range.selectNode(image); 569 textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]); 570 is( 571 textDirective, null, 572 "Input range contains image only: Produces empty text directive" 573 ); 574 575 range.selectNode(nbsp); 576 textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]); 577 is( 578 textDirective, null, 579 "Input range contains nbsp only: Produces empty text directive" 580 ); 581 } 582 583 async function testExpandRangeToWordBoundaries() { 584 const createDirectiveTimeStart = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0}; 585 let lastCreateTimeCount = createDirectiveTimeStart.count; 586 587 for (testCase of [ 588 { 589 name: "Expanding single-word range to word boundaries (input is inside word)", 590 startContainer: abc.firstChild, 591 startOffset: 5, 592 endContainer: abc.firstChild, 593 endOffset: 6, 594 outputStartContainer: abc.firstChild, 595 outputStartOffset: 4, 596 outputEndContainer: abc.firstChild, 597 outputEndOffset: 7, 598 content: "e", 599 outputContent: "def", 600 textDirective: "text=def" 601 }, 602 { 603 name: "Expanding single-word range to word boundaries (input is start of word)", 604 startContainer: abc.firstChild, 605 startOffset: 4, 606 endContainer: abc.firstChild, 607 endOffset: 5, 608 outputStartContainer: abc.firstChild, 609 outputStartOffset: 4, 610 outputEndContainer: abc.firstChild, 611 outputEndOffset: 7, 612 content: "d", 613 outputContent: "def", 614 textDirective: "text=def" 615 }, 616 { 617 name: "Expanding single-word range to word boundaries (input is end of word)", 618 startContainer: abc.firstChild, 619 startOffset: 6, 620 endContainer: abc.firstChild, 621 endOffset: 7, 622 outputStartContainer: abc.firstChild, 623 outputStartOffset: 4, 624 outputEndContainer: abc.firstChild, 625 outputEndOffset: 7, 626 content: "f", 627 outputContent: "def", 628 textDirective: "text=def" 629 }, 630 { 631 name: "Expanding multi-word range to word boundaries", 632 startContainer: abc.firstChild, 633 startOffset: 5, 634 endContainer: abc.firstChild, 635 endOffset: 9, 636 outputStartContainer: abc.firstChild, 637 outputStartOffset: 4, 638 outputEndContainer: abc.firstChild, 639 outputEndOffset: 11, 640 content: "ef g", 641 outputContent: "def ghi", 642 textDirective: "text=def%20ghi" 643 }, 644 { 645 name: "Expanding inline-boundary word range to word boundaries", 646 startContainer: nestedinlinespan.firstChild, 647 startOffset: 0, 648 endContainer: nestedinlinespan.firstChild, 649 endOffset: 1, 650 outputStartContainer: block.firstChild, 651 outputStartOffset: 0, 652 outputEndContainer: block.lastChild, 653 outputEndOffset: 1, 654 content: "a", 655 outputContent: "pspanp", 656 textDirective: "text=pspanp" 657 }, 658 ]) { 659 660 const range = document.createRange(); 661 range.setStart(testCase.startContainer, testCase.startOffset); 662 range.setEnd(testCase.endContainer, testCase.endOffset); 663 is( 664 range.toString().trim(), testCase.content, 665 `${testCase.name}: Precondition - Range has expected value` 666 ); 667 668 const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]); 669 is( 670 textDirective, testCase.textDirective, 671 `${testCase.name}: text directive has expected value '${textDirective}'` 672 ); 673 // load the page with the given text directive 674 location.hash = `#:~:${textDirective}`; 675 await rAF(); 676 677 // access the range from the loaded text directive and compare the boundary points 678 ranges = SpecialPowers.wrap(document).fragmentDirective.getTextDirectiveRanges(); 679 const expectedRange = document.createRange(); 680 expectedRange.setStart(testCase.outputStartContainer, testCase.outputStartOffset); 681 expectedRange.setEnd(testCase.outputEndContainer, testCase.outputEndOffset); 682 is( 683 expectedRange.toString(), ranges[0].toString(), 684 `${testCase.name}: Ranges have the same content '${ranges[0].toString()}'` 685 ); 686 ok( 687 rangeBoundariesAreEqual(expectedRange, ranges[0]), 688 `${testCase.name}: Ranges have the same boundary points` 689 ); 690 691 // finally, remove all text directives to clean up for the next test. 692 SpecialPowers.wrap(document).fragmentDirective.removeAllTextDirectives(); 693 location.hash = ""; 694 695 const createDirectiveTime = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0}; 696 is(createDirectiveTime.count, lastCreateTimeCount + 1, `${testCase.name}: Glean should have recorded the time it took to create the text directive`); 697 lastCreateTimeCount = createDirectiveTime.count; 698 699 } 700 } 701 702 async function testNonUniqueTestSurroundedByBlockBoundaries() { 703 const createDirectiveTimeStart = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0}; 704 705 const testCase = "Creating a text directive which cannot be found" 706 const range = document.createRange(); 707 range.setStart(surroundedByBlockBoundaries.firstChild, 4); 708 range.setEnd(surroundedByBlockBoundaries.firstChild, 7); 709 is(range.toString(), "def", `${testCase}: Range has expected value`); 710 const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]); 711 is(textDirective, null, `${testCase}: It's not possible to create a unique text directive`); 712 713 const createDirectiveTime = await GleanTest.domTextfragment.createDirective.testGetValue() ?? {count: 0}; 714 is(createDirectiveTime.count, createDirectiveTimeStart.count + 1, `${testCase}: Glean should have recorded the time it took to create the text directive`); 715 } 716 717 async function testRangeBasedWithEmptySurroundingBlocks() { 718 const testCase = "Empty blocks at beginning or end of range must be ignored"; 719 const range = document.createRange(); 720 range.setStart(emptyBlockAtBeginning, 0); 721 range.setEnd(emptyBlockAtEnd.nextSibling ,0); 722 const content = "AfterEmptyBlock\n BeforeEmptyBlock"; 723 const textDirectiveString = "text=AfterEmptyBlock,BeforeEmptyBlock"; 724 is(range.toString(), content, `${testCase}: Range has expected value`); 725 const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]); 726 is(textDirective, textDirectiveString, `${testCase}: text directive has expected value '${textDirectiveString}'`); 727 location.hash = `:~:${textDirective}`; 728 await rAF(); 729 ranges = SpecialPowers.wrap(document).fragmentDirective.getTextDirectiveRanges(); 730 is(ranges.length, 1, `${testCase}: There is one text fragment range on the document`); 731 is(ranges[0].toString(), range.toString(), `${testCase}: Ranges have the same content`); 732 } 733 734 async function testRangeBasedWithNonBreakableWord() { 735 const testCase = "Range-based text directive with non-breakable word"; 736 const range = document.createRange(); 737 range.setStart(longWord.firstChild, 0); 738 range.setEnd(longWord.firstChild, longWord.firstChild.length); 739 const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range]); 740 is(textDirective, null, `${testCase}: This test case should not produce a text directive`); 741 } 742 743 async function testMultipleRanges() { 744 let testCase = "Two single-word text directives"; 745 const range1 = document.createRange(); 746 range1.setStart(abc.firstChild, 0); 747 range1.setEnd(abc.firstChild, 3); 748 const range2 = document.createRange(); 749 range2.setStart(foo.firstChild, 0); 750 range2.setEnd(foo.firstChild, 3); 751 const textDirective = await SpecialPowers.wrap(document).fragmentDirective.createTextDirectiveForRanges([range1, range2]); 752 is(textDirective, "text=abc&text=foo", `${testCase}: The text fragment has the expected value`); 753 } 754 755 async function runTests() { 756 try { 757 testRangeBoundariesAreEqual(); 758 await basicTests(); 759 await testEmptyRange(); 760 await testExpandRangeToWordBoundaries(); 761 await testNonUniqueTestSurroundedByBlockBoundaries(); 762 await testRangeBasedWithEmptySurroundingBlocks(); 763 await testRangeBasedWithNonBreakableWord(); 764 await testMultipleRanges(); 765 766 } 767 finally { 768 SimpleTest.finish(); 769 } 770 } 771 772 document.body.onload = runTests; 773 </script> 774 </body> 775 </html>