browser_highlights.js (13891B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /* import-globals-from ../../mochitest/text.js */ 8 /* import-globals-from ../../mochitest/attributes.js */ 9 loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR }); 10 11 const boldAttrs = { "font-weight": "700" }; 12 const highlightAttrs = { mark: "true" }; 13 const fragmentAttrs = highlightAttrs; 14 const spellingAttrs = { invalid: "spelling" }; 15 const grammarAttrs = { invalid: "grammar" }; 16 const snippet = ` 17 <p id="first">The first phrase.</p> 18 <p id="second">The <i>second <b>phrase.</b></i></p> 19 `; 20 21 /** 22 * Returns a promise that resolves once the attribute ranges match. If 23 * shouldWaitForEvent is true, we first wait for a text attribute change event. 24 */ 25 async function waitForTextAttrRanges( 26 acc, 27 ranges, 28 attrs, 29 shouldWaitForEvent = true 30 ) { 31 if (shouldWaitForEvent) { 32 await waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED); 33 } 34 await untilCacheOk( 35 () => textAttrRangesMatch(acc, ranges, attrs), 36 `Attr ranges match: ${JSON.stringify(ranges)}` 37 ); 38 } 39 40 /** 41 * Test a text fragment within a single node. 42 */ 43 addAccessibleTask( 44 snippet, 45 async function testTextFragmentSingleNode(browser, docAcc) { 46 const first = findAccessibleChildByID(docAcc, "first"); 47 ok( 48 textAttrRangesMatch( 49 first, 50 [ 51 [4, 16], // "first phrase" 52 ], 53 fragmentAttrs 54 ), 55 "first attr ranges correct" 56 ); 57 const second = findAccessibleChildByID(docAcc, "second"); 58 ok( 59 textAttrRangesMatch(second, [], fragmentAttrs), 60 "second attr ranges correct" 61 ); 62 }, 63 { chrome: true, topLevel: true, urlSuffix: "#:~:text=first%20phrase" } 64 ); 65 66 /** 67 * Test a text fragment crossing nodes. 68 */ 69 addAccessibleTask( 70 snippet, 71 async function testTextFragmentCrossNode(browser, docAcc) { 72 const first = findAccessibleChildByID(docAcc, "first"); 73 ok( 74 textAttrRangesMatch(first, [], fragmentAttrs), 75 "first attr ranges correct" 76 ); 77 const second = findAccessibleChildByID(docAcc, "second"); 78 ok( 79 textAttrRangesMatch( 80 second, 81 [ 82 // This run is split because of the bolded word. 83 [4, 11], // "second " 84 [11, 17], // "phrase" 85 ], 86 fragmentAttrs 87 ), 88 "second attr ranges correct" 89 ); 90 // Ensure bold is still exposed in the presence of a fragment. 91 testTextAttrs( 92 second, 93 11, 94 { ...fragmentAttrs, ...boldAttrs }, 95 {}, 96 11, 97 17, 98 true 99 ); // "phrase" 100 testTextAttrs(second, 17, boldAttrs, {}, 17, 18, true); // "." 101 }, 102 { chrome: true, topLevel: true, urlSuffix: "#:~:text=second%20phrase" } 103 ); 104 105 /** 106 * Test scrolling to a text fragment on the same page. This also tests that the 107 * scrolling start event is fired. 108 */ 109 add_task(async function testTextFragmentSamePage() { 110 // We use add_task here because we need to verify that an 111 // event is fired, but it might be fired before document load complete, so we 112 // could miss it if we used addAccessibleTask. 113 const docUrl = snippetToURL(snippet); 114 const initialUrl = docUrl + "#:~:text=first%20phrase"; 115 let scrolled = waitForEvent( 116 EVENT_SCROLLING_START, 117 event => 118 event.accessible.role == ROLE_TEXT_LEAF && 119 getAccessibleDOMNodeID(event.accessible.parent) == "first" 120 ); 121 await BrowserTestUtils.withNewTab(initialUrl, async function (browser) { 122 info("Waiting for scroll to first"); 123 const first = (await scrolled).accessible.parent; 124 info("Checking ranges"); 125 await waitForTextAttrRanges( 126 first, 127 [ 128 [4, 16], // "first phrase" 129 ], 130 fragmentAttrs, 131 false 132 ); 133 const second = first.nextSibling; 134 await waitForTextAttrRanges(second, [], fragmentAttrs, false); 135 136 info("Navigating to second"); 137 // The text fragment begins with the text "second", which is the second 138 // child of the `second` Accessible. 139 scrolled = waitForEvent(EVENT_SCROLLING_START, second.getChildAt(1)); 140 let rangeCheck = waitForTextAttrRanges( 141 second, 142 [ 143 [4, 11], // "second " 144 [11, 17], // "phrase" 145 ], 146 fragmentAttrs, 147 true 148 ); 149 await invokeContentTask(browser, [], () => { 150 content.location.hash = "#:~:text=second%20phrase"; 151 }); 152 await scrolled; 153 info("Checking ranges"); 154 await rangeCheck; 155 // XXX DOM should probably remove the highlight from "first phrase" since 156 // we've navigated to "second phrase". For now, this test expects the 157 // current DOM behaviour: "first" is still highlighted. 158 await waitForTextAttrRanges( 159 first, 160 [ 161 [4, 16], // "first phrase" 162 ], 163 fragmentAttrs, 164 false 165 ); 166 }); 167 }); 168 169 /** 170 * Test custom highlight mutations. 171 */ 172 addAccessibleTask( 173 snippet, 174 async function testCustomHighlightMutations(browser, docAcc) { 175 info("Checking initial highlight"); 176 const first = findAccessibleChildByID(docAcc, "first"); 177 ok( 178 textAttrRangesMatch( 179 first, 180 [ 181 [4, 9], // "first" 182 ], 183 highlightAttrs 184 ), 185 "first attr ranges correct" 186 ); 187 const second = findAccessibleChildByID(docAcc, "second"); 188 ok( 189 textAttrRangesMatch(second, [], highlightAttrs), 190 "second attr ranges correct" 191 ); 192 193 info("Adding range2 to highlight1"); 194 let rangeCheck = waitForTextAttrRanges( 195 first, 196 [ 197 [0, 3], // "The " 198 [4, 9], // "first" 199 ], 200 highlightAttrs, 201 true 202 ); 203 await invokeContentTask(browser, [], () => { 204 content.firstText = content.document.getElementById("first").firstChild; 205 // Highlight the word "The". 206 content.range2 = new content.Range(); 207 content.range2.setStart(content.firstText, 0); 208 content.range2.setEnd(content.firstText, 3); 209 content.highlight1 = content.CSS.highlights.get("highlight1"); 210 content.highlight1.add(content.range2); 211 }); 212 await rangeCheck; 213 214 info("Adding highlight2"); 215 rangeCheck = waitForTextAttrRanges( 216 first, 217 [ 218 [0, 3], // "The " 219 [4, 9], // "first" 220 [10, 16], // "phrase" 221 ], 222 highlightAttrs, 223 true 224 ); 225 await invokeContentTask(browser, [], () => { 226 // Highlight the word "phrase". 227 const range3 = new content.Range(); 228 range3.setStart(content.firstText, 10); 229 range3.setEnd(content.firstText, 16); 230 const highlight2 = new content.Highlight(range3); 231 content.CSS.highlights.set("highlight2", highlight2); 232 }); 233 await rangeCheck; 234 235 info("Removing range2"); 236 rangeCheck = waitForTextAttrRanges( 237 first, 238 [ 239 [4, 9], // "first" 240 [10, 16], // "phrase" 241 ], 242 highlightAttrs, 243 true 244 ); 245 await invokeContentTask(browser, [], () => { 246 content.highlight1.delete(content.range2); 247 }); 248 await rangeCheck; 249 250 info("Removing highlight1"); 251 rangeCheck = waitForTextAttrRanges( 252 first, 253 [ 254 [10, 16], // "phrase" 255 ], 256 highlightAttrs, 257 true 258 ); 259 await invokeContentTask(browser, [], () => { 260 content.CSS.highlights.delete("highlight1"); 261 }); 262 await rangeCheck; 263 }, 264 { 265 chrome: true, 266 topLevel: true, 267 contentSetup: async function contentSetup() { 268 const firstText = content.document.getElementById("first").firstChild; 269 // Highlight the word "first". 270 const range1 = new content.Range(); 271 range1.setStart(firstText, 4); 272 range1.setEnd(firstText, 9); 273 const highlight1 = new content.Highlight(range1); 274 content.CSS.highlights.set("highlight1", highlight1); 275 }, 276 } 277 ); 278 279 /** 280 * Test custom highlight types. 281 */ 282 addAccessibleTask( 283 snippet, 284 async function testCustomHighlightTypes(browser, docAcc) { 285 const first = findAccessibleChildByID(docAcc, "first"); 286 ok( 287 textAttrRangesMatch( 288 first, 289 [ 290 [0, 3], // "the" 291 ], 292 highlightAttrs 293 ), 294 "first highlight ranges correct" 295 ); 296 ok( 297 textAttrRangesMatch( 298 first, 299 [ 300 [4, 9], // "first" 301 ], 302 spellingAttrs 303 ), 304 "first spelling ranges correct" 305 ); 306 ok( 307 textAttrRangesMatch( 308 first, 309 [ 310 [10, 16], // "phrase" 311 ], 312 grammarAttrs 313 ), 314 "first grammar ranges correct" 315 ); 316 const second = findAccessibleChildByID(docAcc, "second"); 317 ok( 318 textAttrRangesMatch(second, [], highlightAttrs), 319 "second highlight ranges correct" 320 ); 321 }, 322 { 323 chrome: true, 324 topLevel: true, 325 contentSetup: async function contentSetup() { 326 const firstText = content.document.getElementById("first").firstChild; 327 // Highlight the word "The". 328 const range1 = new content.Range(); 329 range1.setStart(firstText, 0); 330 range1.setEnd(firstText, 3); 331 const highlight = new content.Highlight(range1); 332 content.CSS.highlights.set("highlight", highlight); 333 334 // Make the word "first" a spelling error. 335 const range2 = new content.Range(); 336 range2.setStart(firstText, 4); 337 range2.setEnd(firstText, 9); 338 const spelling = new content.Highlight(range2); 339 spelling.type = "spelling-error"; 340 content.CSS.highlights.set("spelling", spelling); 341 342 // Make the word "phrase" a grammar error. 343 const range3 = new content.Range(); 344 range3.setStart(firstText, 10); 345 range3.setEnd(firstText, 16); 346 const grammar = new content.Highlight(range3); 347 grammar.type = "grammar-error"; 348 content.CSS.highlights.set("grammar", grammar); 349 }, 350 } 351 ); 352 353 /** 354 * Test overlapping custom highlights. 355 */ 356 addAccessibleTask( 357 snippet, 358 async function testCustomHighlightOverlapping(browser, docAcc) { 359 const first = findAccessibleChildByID(docAcc, "first"); 360 ok( 361 textAttrRangesMatch( 362 first, 363 [ 364 [0, 3], // "the" 365 [4, 6], // "fi" 366 [6, 7], // "r" 367 [7, 9], // "st" 368 [10, 12], // "ph" 369 [12, 15], // "ras" 370 [15, 16], // "e" 371 ], 372 highlightAttrs 373 ), 374 "first highlight ranges correct" 375 ); 376 ok( 377 textAttrRangesMatch( 378 first, 379 [ 380 [0, 3], // "the" 381 [4, 6], // "fi" 382 [6, 7], // "r" 383 [7, 9], // "st" 384 [12, 15], // "ras" 385 ], 386 spellingAttrs 387 ), 388 "first spelling ranges correct" 389 ); 390 const second = findAccessibleChildByID(docAcc, "second"); 391 ok( 392 textAttrRangesMatch( 393 second, 394 [ 395 [4, 7], // "sec" 396 [7, 8], // "o" 397 [8, 10], // "nd" 398 [11, 13], // "ph" 399 [13, 16], // "ras" 400 [16, 17], // "e" 401 ], 402 highlightAttrs 403 ), 404 "second highlight ranges correct" 405 ); 406 ok( 407 textAttrRangesMatch( 408 second, 409 [ 410 [4, 7], // "sec" 411 [8, 10], // "nd" 412 ], 413 spellingAttrs 414 ), 415 "second spelling ranges correct" 416 ); 417 }, 418 { 419 chrome: true, 420 topLevel: true, 421 contentSetup: async function contentSetup() { 422 const firstText = content.document.getElementById("first").firstChild; 423 // Make the word "The" both a highlight and a spelling error. 424 const range1 = new content.Range(); 425 range1.setStart(firstText, 0); 426 range1.setEnd(firstText, 3); 427 const highlight1 = new content.Highlight(range1); 428 content.CSS.highlights.set("highlight1", highlight1); 429 const spelling = new content.Highlight(range1); 430 spelling.type = "spelling-error"; 431 content.CSS.highlights.set("spelling", spelling); 432 433 // Highlight the word "first". 434 const range2 = new content.Range(); 435 range2.setStart(firstText, 4); 436 range2.setEnd(firstText, 9); 437 highlight1.add(range2); 438 // Make "fir" a spelling error. 439 const range3 = new content.Range(); 440 range3.setStart(firstText, 4); 441 range3.setEnd(firstText, 7); 442 spelling.add(range3); 443 // Make "rst" a spelling error. 444 const range4 = new content.Range(); 445 range4.setStart(firstText, 6); 446 range4.setEnd(firstText, 9); 447 spelling.add(range4); 448 449 // Highlight the word "phrase". 450 const range5 = new content.Range(); 451 range5.setStart(firstText, 10); 452 range5.setEnd(firstText, 16); 453 highlight1.add(range5); 454 // Make "ras" a spelling error. 455 const range6 = new content.Range(); 456 range6.setStart(firstText, 12); 457 range6.setEnd(firstText, 15); 458 spelling.add(range6); 459 460 const secondText = content.document.querySelector("#second i").firstChild; 461 // Highlight the word "second". 462 const range7 = new content.Range(); 463 range7.setStart(secondText, 0); 464 range7.setEnd(secondText, 6); 465 highlight1.add(range7); 466 // Make "sec" a spelling error. 467 const range8 = new content.Range(); 468 range8.setStart(secondText, 0); 469 range8.setEnd(secondText, 3); 470 spelling.add(range8); 471 // Make "nd" a spelling error. 472 const range9 = new content.Range(); 473 range9.setStart(secondText, 4); 474 range9.setEnd(secondText, 6); 475 spelling.add(range9); 476 477 const phrase2Text = 478 content.document.querySelector("#second b").firstChild; 479 // Highlight the word "phrase". 480 const range10 = new content.Range(); 481 range10.setStart(phrase2Text, 0); 482 range10.setEnd(phrase2Text, 6); 483 highlight1.add(range10); 484 // Highlight "ras" using a different Highlight. 485 const range11 = new content.Range(); 486 range11.setStart(phrase2Text, 2); 487 range11.setEnd(phrase2Text, 5); 488 const highlight2 = new content.Highlight(range11); 489 content.CSS.highlights.set("highlight2", highlight2); 490 }, 491 } 492 );