tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 });