browser_contextmenu_plaintextlinks.js (18734B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* eslint-disable mozilla/no-arbitrary-setTimeout */ 5 6 // Context menu links prepend `http`, so this is what 7 // is being tested for rather than an `https` prefix. 8 /* eslint-disable @microsoft/sdl/no-insecure-url */ 9 10 function testExpected(expected, msg) { 11 is( 12 !document.getElementById("context-openlinkincurrent").hidden, 13 expected, 14 msg 15 ); 16 } 17 18 function testLinkExpected(expected, msg) { 19 is(gContextMenu.linkURL, expected, msg); 20 } 21 22 add_task(async function () { 23 const TEST_HTML_STRING = ` 24 <div id="test-root"> 25 <div id="block1"> 26 <span id="prefix">http://www.</span><span id="hostTwice">example.com example.com</span> 27 <span id="suffix"> - Test</span> 28 <span id="anchor"><a href="http://www.example.com">http://www.example.com/example</a></span> 29 <p id="nonLinks">mailto:test.com ftp.example.com</p> 30 <p id="trailing">example.com -</p> 31 </div> 32 <div id="block2"> 33 <p id="mainDomain">main.example.com</p> 34 </div> 35 <div id="block3"> 36 <p id="ipURL">http://192.168.0.1/</p> 37 <p id="ipWithPath">http://192.168.0.1/hello</p> 38 <p id="hostWithSlash">example.com/</p> 39 <p id="hostWithPath">example.com/hello</p> 40 <p id="looksLikeURL">http://cheese/hello</p> 41 <p id="usernamePasswordURL">hello:password@google.com</p> 42 <p id="numberInHost">exam4ple.com</p> 43 <p id="hostWithPort">example.com:8080</p> 44 </div> 45 <div id="block4"> 46 <p id="email">hello@google.com</p> 47 <p id="justCheese">cheese</p> 48 <p id="colonSeparated">hello:this</p> 49 <p id="atSymbolSeparated">hello@this</p> 50 <p id="noHostWithNumber">hello/1</p> 51 <p id="noHostWithText">hello/cheese</p> 52 <p id="noHostWithModulo">hello/%</p> 53 <p id="noHostWithQuestion">hello/?</p> 54 <p id="noHostWithPound">hello/#</p> 55 <p id="noHostWithSlashOnly">hello/</p> 56 <p id="topDomainIllegalNumberEnd">example.com4</p> 57 <p id="topDomainIllegalNumberStart">example.4com</p> 58 <p id="topDomainIllegalNumberMid">example.co4m</p> 59 <p id="illegalNumberBeforePort">example.com4:8080</p> 60 </div> 61 </div> 62 `; 63 64 const TESTS = [ 65 // ---- URL selections that should show context menu link options ---- 66 { 67 id: "http-url-across-spans", 68 selection: { 69 startNode: "prefix", 70 startIndex: 0, 71 endNode: "hostTwice", 72 endIndex: "example.com".length, 73 }, 74 expectLinks: true, 75 expectedLink: "http://www.example.com/", 76 message: "Link options should show for http://www.example.com", 77 }, 78 { 79 id: "url-across-spans-without-http", 80 selection: { 81 startNode: "prefix", 82 startIndex: "http://".length, 83 endNode: "hostTwice", 84 endIndex: "example.com".length, 85 }, 86 expectLinks: true, 87 expectedLink: "http://www.example.com/", 88 message: "Link options should show for www.example.com", 89 }, 90 { 91 id: "example-com-without-www", 92 selection: { 93 startNode: "hostTwice", 94 startIndex: "example.com ".length, 95 endNode: "hostTwice", 96 endIndex: "example.com example.com".length, 97 }, 98 expectLinks: true, 99 expectedLink: "http://example.com/", 100 message: 101 "Link options should show for 'example.com' (without prepending 'www').", 102 }, 103 { 104 id: "ftp-example", 105 selection: { 106 startNode: "nonLinks", 107 startIndex: "mailto:test.com ".length, 108 endNode: "nonLinks", 109 endIndex: "mailto:test.com ftp.example.com".length, 110 }, 111 expectLinks: true, 112 expectedLink: "http://ftp.example.com/", 113 message: "ftp.example.com should be linkified with http://", 114 }, 115 { 116 id: "example-trailing-dash", 117 selection: { 118 startNode: "trailing", 119 startIndex: 0, 120 endNode: "trailing", 121 endIndex: "example.com".length, 122 }, 123 expectLinks: true, 124 expectedLink: "http://example.com/", 125 message: "Link options should show for \"'example.com' -\"", 126 }, 127 { 128 id: "triple-click-main-domain", 129 selection: { 130 startNode: "mainDomain", 131 startIndex: 0, 132 endNode: "mainDomain", 133 endIndex: "main.example.com".length, 134 }, 135 expectLinks: true, 136 expectedLink: "http://main.example.com/", 137 message: 138 "Link options should show for triple-click selection of main.example.com", 139 }, 140 { 141 id: "anchor-element", 142 selection: { 143 startNode: "anchor", 144 startIndex: 0, 145 endNode: "anchor", 146 endIndex: "http://www.example.com/example".length, 147 }, 148 expectLinks: false, // Is URL due to anchor element, not plaintext. 149 expectedLink: "http://www.example.com/", 150 message: "Context menu should use anchor href, not raw text", 151 }, 152 { 153 id: "open-suse", 154 customHTML: "<div id='os'>open-suse.ru</div>", 155 selection: { 156 startNode: "os", 157 startIndex: 0, 158 endNode: "os", 159 endIndex: "open-suse.ru".length, 160 }, 161 expectLinks: true, 162 expectedLink: "http://open-suse.ru/", 163 message: "Link options should show for open-suse.ru", 164 }, 165 { 166 id: "ip-address-only", 167 selection: { 168 startNode: "ipURL", 169 startIndex: "http://".length, 170 endNode: "ipURL", 171 endIndex: "http://192.168.0.1".length, 172 }, 173 expectLinks: true, 174 expectedLink: "http://192.168.0.1/", 175 message: "Link options should show for ip 192.168.0.1", 176 }, 177 { 178 id: "ip-with-protocol", 179 selection: { 180 startNode: "ipURL", 181 startIndex: 0, 182 endNode: "ipURL", 183 endIndex: "http://192.168.0.1".length, 184 }, 185 expectLinks: true, 186 expectedLink: "http://192.168.0.1/", 187 message: "Link options should show for ip http://192.168.0.1", 188 }, 189 { 190 id: "ip-with-protocol-and-slash", 191 selection: { 192 startNode: "ipURL", 193 startIndex: 0, 194 endNode: "ipURL", 195 endIndex: "http://192.168.0.1/".length, 196 }, 197 expectLinks: true, 198 expectedLink: "http://192.168.0.1/", 199 message: "Link options should show for ip http://192.168.0.1/", 200 }, 201 { 202 id: "ip-with-path", 203 selection: { 204 startNode: "ipWithPath", 205 startIndex: "http://".length, 206 endNode: "ipWithPath", 207 endIndex: "http://192.168.0.1/hello".length, 208 }, 209 expectLinks: true, 210 expectedLink: "http://192.168.0.1/hello", 211 message: "Link options should show for ip 192.168.0.1/hello", 212 }, 213 { 214 id: "ip-with-protocol-and-path", 215 selection: { 216 startNode: "ipWithPath", 217 startIndex: 0, 218 endNode: "ipWithPath", 219 endIndex: "http://192.168.0.1/hello".length, 220 }, 221 expectLinks: true, 222 expectedLink: "http://192.168.0.1/hello", 223 message: "Link options should show for ip http://192.168.0.1/hello", 224 }, 225 { 226 id: "host-with-slash-only", 227 selection: { 228 startNode: "hostWithSlash", 229 startIndex: 0, 230 endNode: "hostWithSlash", 231 endIndex: "example.com/".length, 232 }, 233 expectLinks: true, 234 expectedLink: "http://example.com/", 235 message: "Link options should show for example.com/", 236 }, 237 { 238 id: "host-with-path", 239 selection: { 240 startNode: "hostWithPath", 241 startIndex: 0, 242 endNode: "hostWithPath", 243 endIndex: "example.com/hello".length, 244 }, 245 expectLinks: true, 246 expectedLink: "http://example.com/hello", 247 message: "Link options should show for example.com/hello", 248 }, 249 { 250 id: "looks-like-URL", 251 selection: { 252 startNode: "looksLikeURL", 253 startIndex: 0, 254 endNode: "looksLikeURL", 255 endIndex: "http://cheese/hello".length, 256 }, 257 expectLinks: true, 258 expectedLink: "http://cheese/hello", 259 message: 260 "Link options should show for malformed but plausible url http://cheese/hello", 261 }, 262 { 263 id: "username-password-URL", 264 selection: { 265 startNode: "usernamePasswordURL", 266 startIndex: 0, 267 endNode: "usernamePasswordURL", 268 endIndex: "hello:password@google.com".length, 269 }, 270 expectLinks: true, 271 expectedLink: "http://hello:password@google.com/", 272 message: "Link options should show for hello:password@google.com", 273 }, 274 { 275 id: "host-with-number-in-name", 276 selection: { 277 startNode: "numberInHost", 278 startIndex: 0, 279 endNode: "numberInHost", 280 endIndex: "exam4ple.com".length, 281 }, 282 expectLinks: true, 283 expectedLink: "http://exam4ple.com/", 284 message: "Link options should show for exam4ple.com", 285 }, 286 { 287 id: "host-with-port", 288 selection: { 289 startNode: "hostWithPort", 290 startIndex: 0, 291 endNode: "hostWithPort", 292 endIndex: "example.com:8080".length, 293 }, 294 expectLinks: true, 295 expectedLink: "http://example.com:8080/", 296 message: "Link options should show for example.com:8080", 297 }, 298 299 // ---- Non-URL selections ---- 300 { 301 id: "selection-not-at-word-boundary", 302 selection: { 303 startNode: "hostTwice", 304 startIndex: 1, 305 endNode: "hostTwice", 306 endIndex: "www.example.com".length, 307 }, 308 expectLinks: false, 309 message: "Link options should not show for \"w'ww.example.com'\"", 310 }, 311 { 312 id: "selection-includes-non-url-text", 313 selection: { 314 startNode: "hostTwice", 315 startIndex: "example.com ".length, 316 endNode: "suffix", 317 endIndex: " - Test".length, 318 }, 319 expectLinks: false, 320 message: 321 "Link options should not show when crossing non-URL text ('example.com - Test')", 322 }, 323 { 324 id: "whitespace-in-selection", 325 selection: { 326 startNode: "hostTwice", 327 startIndex: 12, 328 endNode: "hostTwice", 329 endIndex: 19, 330 }, 331 expectLinks: false, 332 message: 333 "Link options should not show for selection with whitespace (' example.com')", 334 }, 335 { 336 id: "mailto-link", 337 selection: { 338 startNode: "nonLinks", 339 startIndex: 0, 340 endNode: "nonLinks", 341 endIndex: "mailto:test.com".length, 342 }, 343 expectLinks: false, 344 message: "Link options should not show for mailto: links", 345 }, 346 { 347 id: "selection-includes-parentheses", 348 customHTML: "<div id='osparens'>(open-suse.ru)</div>", 349 selection: { 350 startNode: "osparens", 351 startIndex: 1, 352 endNode: "osparens", 353 endIndex: "(open-suse.ru)".length, 354 }, 355 expectLinks: false, 356 message: "Link options should not show for 'open-suse.ru)'", 357 }, 358 { 359 id: "email", // Emails are intentionally handled differently from URLs. 360 selection: { 361 startNode: "email", 362 startIndex: 0, 363 endNode: "email", 364 endIndex: "hello@google.com".length, 365 }, 366 expectLinks: false, 367 message: "Link options should not show for hello@google.com", 368 }, 369 { 370 id: "just-a-word", 371 selection: { 372 startNode: "justCheese", 373 startIndex: 0, 374 endNode: "justCheese", 375 endIndex: "cheese".length, 376 }, 377 expectLinks: false, 378 message: "Link options should not show for cheese", 379 }, 380 { 381 id: "colon-separated-words", 382 selection: { 383 startNode: "colonSeparated", 384 startIndex: 0, 385 endNode: "colonSeparated", 386 endIndex: "hello:this".length, 387 }, 388 expectLinks: false, 389 message: "Link options should not show for hello:this", 390 }, 391 { 392 id: "at-symbol-separated-words", 393 selection: { 394 startNode: "atSymbolSeparated", 395 startIndex: 0, 396 endNode: "atSymbolSeparated", 397 endIndex: "hello@this".length, 398 }, 399 expectLinks: false, 400 message: "Link options should not show for hello@this", 401 }, 402 { 403 id: "host-with-number-after-slash", 404 selection: { 405 startNode: "noHostWithNumber", 406 startIndex: 0, 407 endNode: "noHostWithNumber", 408 endIndex: "hello/1".length, 409 }, 410 expectLinks: false, 411 message: "Link options should not show for hello/1", 412 }, 413 { 414 id: "host-with-word-after-slash", 415 selection: { 416 startNode: "noHostWithText", 417 startIndex: 0, 418 endNode: "noHostWithText", 419 endIndex: "hello/cheese".length, 420 }, 421 expectLinks: false, 422 message: "Link options should not show for hello/cheese", 423 }, 424 { 425 id: "host-with-modulo-symbol-after-slash", 426 selection: { 427 startNode: "noHostWithModulo", 428 startIndex: 0, 429 endNode: "noHostWithModulo", 430 endIndex: "hello/%".length, 431 }, 432 expectLinks: false, 433 message: "Link options should not show for hello/%", 434 }, 435 { 436 id: "host-with-question-mark-after-slash", 437 selection: { 438 startNode: "noHostWithQuestion", 439 startIndex: 0, 440 endNode: "noHostWithQuestion", 441 endIndex: "hello/?".length, 442 }, 443 expectLinks: false, 444 message: "Link options should not show for hello/?", 445 }, 446 { 447 id: "host-with-pound-sign-after-slash", 448 selection: { 449 startNode: "noHostWithPound", 450 startIndex: 0, 451 endNode: "noHostWithPound", 452 endIndex: "hello/#".length, 453 }, 454 expectLinks: false, 455 message: "Link options should not show for hello/#", 456 }, 457 { 458 id: "host-with-nothing-after-slash", 459 selection: { 460 startNode: "noHostWithSlashOnly", 461 startIndex: 0, 462 endNode: "noHostWithSlashOnly", 463 endIndex: "hello/".length, 464 }, 465 expectLinks: false, 466 message: "Link options should not show for hello/", 467 }, 468 { 469 id: "top-level-domain-with-illegal-number-at-end", 470 selection: { 471 startNode: "topDomainIllegalNumberEnd", 472 startIndex: 1, 473 endNode: "topDomainIllegalNumberEnd", 474 endIndex: "example.com4".length, 475 }, 476 expectLinks: false, 477 message: "Link options should not show for example.com4", 478 }, 479 { 480 id: "top-level-domain-with-illegal-number-at-start", 481 selection: { 482 startNode: "topDomainIllegalNumberStart", 483 startIndex: 1, 484 endNode: "topDomainIllegalNumberStart", 485 endIndex: "example.4com".length, 486 }, 487 expectLinks: false, 488 message: "Link options should not show for example.4com", 489 }, 490 { 491 id: "top-level-domain-with-illegal-number-in-middle", 492 selection: { 493 startNode: "topDomainIllegalNumberMid", 494 startIndex: 1, 495 endNode: "topDomainIllegalNumberMid", 496 endIndex: "example.co4m".length, 497 }, 498 expectLinks: false, 499 message: "Link options should not show for example.co4m", 500 }, 501 { 502 id: "top-level-domain-with-illegal-number-before-port", 503 selection: { 504 startNode: "illegalNumberBeforePort", 505 startIndex: 1, 506 endNode: "illegalNumberBeforePort", 507 endIndex: "example.com4:8080".length, 508 }, 509 expectLinks: false, 510 message: "Link options should not show for example.com4:8080", 511 }, 512 ]; 513 514 await BrowserTestUtils.openNewForegroundTab( 515 gBrowser, 516 "data:text/html,<html><body></body></html>" 517 ); 518 519 await SimpleTest.promiseFocus(gBrowser.selectedBrowser); 520 521 for (let test of TESTS) { 522 info("Running test: " + test.id); 523 524 let menuPosition = await SpecialPowers.spawn( 525 gBrowser.selectedBrowser, 526 [TEST_HTML_STRING, test.selection, test.customHTML], 527 async function (html, testSelection, customHTML) { 528 // Reset HTML to test template or custom HTML for specific tests 529 if (customHTML) { 530 content.document.body.innerHTML = customHTML; 531 } else { 532 content.document.body.innerHTML = ""; 533 let parser = new content.DOMParser(); 534 let doc = parser.parseFromString(html, "text/html"); 535 let node = content.document.importNode(doc.body.firstChild, true); 536 content.document.body.appendChild(node); 537 } 538 539 // Build selection range from start/end nodes/indices 540 let selection = content.getSelection(); 541 selection.removeAllRanges(); 542 let range = content.document.createRange(); 543 544 let startNode = content.document.getElementById( 545 testSelection.startNode 546 ); 547 548 while (startNode.nodeType != startNode.TEXT_NODE) { 549 startNode = startNode.firstChild; 550 } 551 552 let endNode = content.document.getElementById(testSelection.endNode); 553 554 while (endNode.nodeType != endNode.TEXT_NODE) { 555 endNode = endNode.firstChild; 556 } 557 558 range.setStart(startNode, testSelection.startIndex); 559 range.setEnd(endNode, testSelection.endIndex); 560 selection.addRange(range); 561 562 range.startContainer.parentElement.scrollIntoView(); 563 564 // Get the range of the selection and determine its coordinates. These 565 // coordinates will be returned to the parent process and the context menu 566 // will be opened at that location. 567 let rangeRect = range.getBoundingClientRect(); 568 return [rangeRect.x + 3, rangeRect.y + 3]; 569 } 570 ); 571 572 let contentAreaContextMenu = document.getElementById( 573 "contentAreaContextMenu" 574 ); 575 576 // Trigger a mouse event until we receive the popupshown event. 577 let sawPopup = false; 578 let popupShownPromise = BrowserTestUtils.waitForEvent( 579 contentAreaContextMenu, 580 "popupshown", 581 false, 582 () => { 583 sawPopup = true; 584 return true; 585 } 586 ); 587 while (!sawPopup) { 588 await BrowserTestUtils.synthesizeMouseAtPoint( 589 menuPosition[0], 590 menuPosition[1], 591 { type: "contextmenu", button: 2 }, 592 gBrowser.selectedBrowser 593 ); 594 if (!sawPopup) { 595 await new Promise(r => setTimeout(r, 100)); 596 } 597 } 598 await popupShownPromise; 599 600 // Run the tests. 601 testExpected(test.expectLinks, test.message); 602 if (test.expectedLink) { 603 testLinkExpected( 604 test.expectedLink, 605 `Expected link URL for ${test.id} selection: ${test.expectedLink}` 606 ); 607 } 608 609 // On Linux non-e10s it's possible the menu was closed by a focus-out event 610 // on the window. Work around this by calling hidePopup only if the menu 611 // hasn't been closed yet. See bug 1352709 comment 36. 612 if (contentAreaContextMenu.state === "closed") { 613 continue; 614 } 615 616 let popupHiddenPromise = BrowserTestUtils.waitForEvent( 617 contentAreaContextMenu, 618 "popuphidden" 619 ); 620 contentAreaContextMenu.hidePopup(); 621 await popupHiddenPromise; 622 } 623 624 gBrowser.removeCurrentTab(); 625 });