/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint-disable mozilla/no-arbitrary-setTimeout */
// Context menu links prepend `http`, so this is what
// is being tested for rather than an `https` prefix.
/* eslint-disable @microsoft/sdl/no-insecure-url */
function testExpected(expected, msg) {
is(
!document.getElementById("context-openlinkincurrent").hidden,
expected,
msg
);
}
function testLinkExpected(expected, msg) {
is(gContextMenu.linkURL, expected, msg);
}
add_task(async function () {
const TEST_HTML_STRING = `
http://192.168.0.1/
http://192.168.0.1/hello
example.com/
example.com/hello
http://cheese/hello
hello:password@google.com
exam4ple.com
example.com:8080
hello@google.com
cheese
hello:this
hello@this
hello/1
hello/cheese
hello/%
hello/?
hello/#
hello/
example.com4
example.4com
example.co4m
example.com4:8080
`;
const TESTS = [
// ---- URL selections that should show context menu link options ----
{
id: "http-url-across-spans",
selection: {
startNode: "prefix",
startIndex: 0,
endNode: "hostTwice",
endIndex: "example.com".length,
},
expectLinks: true,
expectedLink: "http://www.example.com/",
message: "Link options should show for http://www.example.com",
},
{
id: "url-across-spans-without-http",
selection: {
startNode: "prefix",
startIndex: "http://".length,
endNode: "hostTwice",
endIndex: "example.com".length,
},
expectLinks: true,
expectedLink: "http://www.example.com/",
message: "Link options should show for www.example.com",
},
{
id: "example-com-without-www",
selection: {
startNode: "hostTwice",
startIndex: "example.com ".length,
endNode: "hostTwice",
endIndex: "example.com example.com".length,
},
expectLinks: true,
expectedLink: "http://example.com/",
message:
"Link options should show for 'example.com' (without prepending 'www').",
},
{
id: "ftp-example",
selection: {
startNode: "nonLinks",
startIndex: "mailto:test.com ".length,
endNode: "nonLinks",
endIndex: "mailto:test.com ftp.example.com".length,
},
expectLinks: true,
expectedLink: "http://ftp.example.com/",
message: "ftp.example.com should be linkified with http://",
},
{
id: "example-trailing-dash",
selection: {
startNode: "trailing",
startIndex: 0,
endNode: "trailing",
endIndex: "example.com".length,
},
expectLinks: true,
expectedLink: "http://example.com/",
message: "Link options should show for \"'example.com' -\"",
},
{
id: "triple-click-main-domain",
selection: {
startNode: "mainDomain",
startIndex: 0,
endNode: "mainDomain",
endIndex: "main.example.com".length,
},
expectLinks: true,
expectedLink: "http://main.example.com/",
message:
"Link options should show for triple-click selection of main.example.com",
},
{
id: "anchor-element",
selection: {
startNode: "anchor",
startIndex: 0,
endNode: "anchor",
endIndex: "http://www.example.com/example".length,
},
expectLinks: false, // Is URL due to anchor element, not plaintext.
expectedLink: "http://www.example.com/",
message: "Context menu should use anchor href, not raw text",
},
{
id: "open-suse",
customHTML: "open-suse.ru
",
selection: {
startNode: "os",
startIndex: 0,
endNode: "os",
endIndex: "open-suse.ru".length,
},
expectLinks: true,
expectedLink: "http://open-suse.ru/",
message: "Link options should show for open-suse.ru",
},
{
id: "ip-address-only",
selection: {
startNode: "ipURL",
startIndex: "http://".length,
endNode: "ipURL",
endIndex: "http://192.168.0.1".length,
},
expectLinks: true,
expectedLink: "http://192.168.0.1/",
message: "Link options should show for ip 192.168.0.1",
},
{
id: "ip-with-protocol",
selection: {
startNode: "ipURL",
startIndex: 0,
endNode: "ipURL",
endIndex: "http://192.168.0.1".length,
},
expectLinks: true,
expectedLink: "http://192.168.0.1/",
message: "Link options should show for ip http://192.168.0.1",
},
{
id: "ip-with-protocol-and-slash",
selection: {
startNode: "ipURL",
startIndex: 0,
endNode: "ipURL",
endIndex: "http://192.168.0.1/".length,
},
expectLinks: true,
expectedLink: "http://192.168.0.1/",
message: "Link options should show for ip http://192.168.0.1/",
},
{
id: "ip-with-path",
selection: {
startNode: "ipWithPath",
startIndex: "http://".length,
endNode: "ipWithPath",
endIndex: "http://192.168.0.1/hello".length,
},
expectLinks: true,
expectedLink: "http://192.168.0.1/hello",
message: "Link options should show for ip 192.168.0.1/hello",
},
{
id: "ip-with-protocol-and-path",
selection: {
startNode: "ipWithPath",
startIndex: 0,
endNode: "ipWithPath",
endIndex: "http://192.168.0.1/hello".length,
},
expectLinks: true,
expectedLink: "http://192.168.0.1/hello",
message: "Link options should show for ip http://192.168.0.1/hello",
},
{
id: "host-with-slash-only",
selection: {
startNode: "hostWithSlash",
startIndex: 0,
endNode: "hostWithSlash",
endIndex: "example.com/".length,
},
expectLinks: true,
expectedLink: "http://example.com/",
message: "Link options should show for example.com/",
},
{
id: "host-with-path",
selection: {
startNode: "hostWithPath",
startIndex: 0,
endNode: "hostWithPath",
endIndex: "example.com/hello".length,
},
expectLinks: true,
expectedLink: "http://example.com/hello",
message: "Link options should show for example.com/hello",
},
{
id: "looks-like-URL",
selection: {
startNode: "looksLikeURL",
startIndex: 0,
endNode: "looksLikeURL",
endIndex: "http://cheese/hello".length,
},
expectLinks: true,
expectedLink: "http://cheese/hello",
message:
"Link options should show for malformed but plausible url http://cheese/hello",
},
{
id: "username-password-URL",
selection: {
startNode: "usernamePasswordURL",
startIndex: 0,
endNode: "usernamePasswordURL",
endIndex: "hello:password@google.com".length,
},
expectLinks: true,
expectedLink: "http://hello:password@google.com/",
message: "Link options should show for hello:password@google.com",
},
{
id: "host-with-number-in-name",
selection: {
startNode: "numberInHost",
startIndex: 0,
endNode: "numberInHost",
endIndex: "exam4ple.com".length,
},
expectLinks: true,
expectedLink: "http://exam4ple.com/",
message: "Link options should show for exam4ple.com",
},
{
id: "host-with-port",
selection: {
startNode: "hostWithPort",
startIndex: 0,
endNode: "hostWithPort",
endIndex: "example.com:8080".length,
},
expectLinks: true,
expectedLink: "http://example.com:8080/",
message: "Link options should show for example.com:8080",
},
// ---- Non-URL selections ----
{
id: "selection-not-at-word-boundary",
selection: {
startNode: "hostTwice",
startIndex: 1,
endNode: "hostTwice",
endIndex: "www.example.com".length,
},
expectLinks: false,
message: "Link options should not show for \"w'ww.example.com'\"",
},
{
id: "selection-includes-non-url-text",
selection: {
startNode: "hostTwice",
startIndex: "example.com ".length,
endNode: "suffix",
endIndex: " - Test".length,
},
expectLinks: false,
message:
"Link options should not show when crossing non-URL text ('example.com - Test')",
},
{
id: "whitespace-in-selection",
selection: {
startNode: "hostTwice",
startIndex: 12,
endNode: "hostTwice",
endIndex: 19,
},
expectLinks: false,
message:
"Link options should not show for selection with whitespace (' example.com')",
},
{
id: "mailto-link",
selection: {
startNode: "nonLinks",
startIndex: 0,
endNode: "nonLinks",
endIndex: "mailto:test.com".length,
},
expectLinks: false,
message: "Link options should not show for mailto: links",
},
{
id: "selection-includes-parentheses",
customHTML: "(open-suse.ru)
",
selection: {
startNode: "osparens",
startIndex: 1,
endNode: "osparens",
endIndex: "(open-suse.ru)".length,
},
expectLinks: false,
message: "Link options should not show for 'open-suse.ru)'",
},
{
id: "email", // Emails are intentionally handled differently from URLs.
selection: {
startNode: "email",
startIndex: 0,
endNode: "email",
endIndex: "hello@google.com".length,
},
expectLinks: false,
message: "Link options should not show for hello@google.com",
},
{
id: "just-a-word",
selection: {
startNode: "justCheese",
startIndex: 0,
endNode: "justCheese",
endIndex: "cheese".length,
},
expectLinks: false,
message: "Link options should not show for cheese",
},
{
id: "colon-separated-words",
selection: {
startNode: "colonSeparated",
startIndex: 0,
endNode: "colonSeparated",
endIndex: "hello:this".length,
},
expectLinks: false,
message: "Link options should not show for hello:this",
},
{
id: "at-symbol-separated-words",
selection: {
startNode: "atSymbolSeparated",
startIndex: 0,
endNode: "atSymbolSeparated",
endIndex: "hello@this".length,
},
expectLinks: false,
message: "Link options should not show for hello@this",
},
{
id: "host-with-number-after-slash",
selection: {
startNode: "noHostWithNumber",
startIndex: 0,
endNode: "noHostWithNumber",
endIndex: "hello/1".length,
},
expectLinks: false,
message: "Link options should not show for hello/1",
},
{
id: "host-with-word-after-slash",
selection: {
startNode: "noHostWithText",
startIndex: 0,
endNode: "noHostWithText",
endIndex: "hello/cheese".length,
},
expectLinks: false,
message: "Link options should not show for hello/cheese",
},
{
id: "host-with-modulo-symbol-after-slash",
selection: {
startNode: "noHostWithModulo",
startIndex: 0,
endNode: "noHostWithModulo",
endIndex: "hello/%".length,
},
expectLinks: false,
message: "Link options should not show for hello/%",
},
{
id: "host-with-question-mark-after-slash",
selection: {
startNode: "noHostWithQuestion",
startIndex: 0,
endNode: "noHostWithQuestion",
endIndex: "hello/?".length,
},
expectLinks: false,
message: "Link options should not show for hello/?",
},
{
id: "host-with-pound-sign-after-slash",
selection: {
startNode: "noHostWithPound",
startIndex: 0,
endNode: "noHostWithPound",
endIndex: "hello/#".length,
},
expectLinks: false,
message: "Link options should not show for hello/#",
},
{
id: "host-with-nothing-after-slash",
selection: {
startNode: "noHostWithSlashOnly",
startIndex: 0,
endNode: "noHostWithSlashOnly",
endIndex: "hello/".length,
},
expectLinks: false,
message: "Link options should not show for hello/",
},
{
id: "top-level-domain-with-illegal-number-at-end",
selection: {
startNode: "topDomainIllegalNumberEnd",
startIndex: 1,
endNode: "topDomainIllegalNumberEnd",
endIndex: "example.com4".length,
},
expectLinks: false,
message: "Link options should not show for example.com4",
},
{
id: "top-level-domain-with-illegal-number-at-start",
selection: {
startNode: "topDomainIllegalNumberStart",
startIndex: 1,
endNode: "topDomainIllegalNumberStart",
endIndex: "example.4com".length,
},
expectLinks: false,
message: "Link options should not show for example.4com",
},
{
id: "top-level-domain-with-illegal-number-in-middle",
selection: {
startNode: "topDomainIllegalNumberMid",
startIndex: 1,
endNode: "topDomainIllegalNumberMid",
endIndex: "example.co4m".length,
},
expectLinks: false,
message: "Link options should not show for example.co4m",
},
{
id: "top-level-domain-with-illegal-number-before-port",
selection: {
startNode: "illegalNumberBeforePort",
startIndex: 1,
endNode: "illegalNumberBeforePort",
endIndex: "example.com4:8080".length,
},
expectLinks: false,
message: "Link options should not show for example.com4:8080",
},
];
await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"data:text/html,"
);
await SimpleTest.promiseFocus(gBrowser.selectedBrowser);
for (let test of TESTS) {
info("Running test: " + test.id);
let menuPosition = await SpecialPowers.spawn(
gBrowser.selectedBrowser,
[TEST_HTML_STRING, test.selection, test.customHTML],
async function (html, testSelection, customHTML) {
// Reset HTML to test template or custom HTML for specific tests
if (customHTML) {
content.document.body.innerHTML = customHTML;
} else {
content.document.body.innerHTML = "";
let parser = new content.DOMParser();
let doc = parser.parseFromString(html, "text/html");
let node = content.document.importNode(doc.body.firstChild, true);
content.document.body.appendChild(node);
}
// Build selection range from start/end nodes/indices
let selection = content.getSelection();
selection.removeAllRanges();
let range = content.document.createRange();
let startNode = content.document.getElementById(
testSelection.startNode
);
while (startNode.nodeType != startNode.TEXT_NODE) {
startNode = startNode.firstChild;
}
let endNode = content.document.getElementById(testSelection.endNode);
while (endNode.nodeType != endNode.TEXT_NODE) {
endNode = endNode.firstChild;
}
range.setStart(startNode, testSelection.startIndex);
range.setEnd(endNode, testSelection.endIndex);
selection.addRange(range);
range.startContainer.parentElement.scrollIntoView();
// Get the range of the selection and determine its coordinates. These
// coordinates will be returned to the parent process and the context menu
// will be opened at that location.
let rangeRect = range.getBoundingClientRect();
return [rangeRect.x + 3, rangeRect.y + 3];
}
);
let contentAreaContextMenu = document.getElementById(
"contentAreaContextMenu"
);
// Trigger a mouse event until we receive the popupshown event.
let sawPopup = false;
let popupShownPromise = BrowserTestUtils.waitForEvent(
contentAreaContextMenu,
"popupshown",
false,
() => {
sawPopup = true;
return true;
}
);
while (!sawPopup) {
await BrowserTestUtils.synthesizeMouseAtPoint(
menuPosition[0],
menuPosition[1],
{ type: "contextmenu", button: 2 },
gBrowser.selectedBrowser
);
if (!sawPopup) {
await new Promise(r => setTimeout(r, 100));
}
}
await popupShownPromise;
// Run the tests.
testExpected(test.expectLinks, test.message);
if (test.expectedLink) {
testLinkExpected(
test.expectedLink,
`Expected link URL for ${test.id} selection: ${test.expectedLink}`
);
}
// On Linux non-e10s it's possible the menu was closed by a focus-out event
// on the window. Work around this by calling hidePopup only if the menu
// hasn't been closed yet. See bug 1352709 comment 36.
if (contentAreaContextMenu.state === "closed") {
continue;
}
let popupHiddenPromise = BrowserTestUtils.waitForEvent(
contentAreaContextMenu,
"popuphidden"
);
contentAreaContextMenu.hidePopup();
await popupHiddenPromise;
}
gBrowser.removeCurrentTab();
});