diff options
Diffstat (limited to 'mobile/android/components/extensions/test')
39 files changed, 4043 insertions, 0 deletions
diff --git a/mobile/android/components/extensions/test/mochitest/.eslintrc.js b/mobile/android/components/extensions/test/mochitest/.eslintrc.js new file mode 100644 index 0000000000..7d6fe2eb1a --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + extends: + "../../../../../../toolkit/components/extensions/test/mochitest/.eslintrc.js", +}; diff --git a/mobile/android/components/extensions/test/mochitest/chrome.toml b/mobile/android/components/extensions/test/mochitest/chrome.toml new file mode 100644 index 0000000000..5a93353448 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/chrome.toml @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = [ + "head.js", + "../../../../../../toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js", +] +tags = "webextensions" + +["test_ext_options_ui.html"] diff --git a/mobile/android/components/extensions/test/mochitest/context.html b/mobile/android/components/extensions/test/mochitest/context.html new file mode 100644 index 0000000000..1e25c6e851 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/context.html @@ -0,0 +1,24 @@ +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + just some text 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 + <img src="ctxmenu-image.png" id="img1"> + + <p> + <a href="some-link" id="link1">Some link</a> + </p> + + <p> + <a href="image-around-some-link"> + <img src="ctxmenu-image.png" id="img-wrapped-in-link"> + </a> + </p> + + <p> + <input type="text" id="edit-me"><br> + <input type="password" id="password"> + </p> + </body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_iframe.html b/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_iframe.html new file mode 100644 index 0000000000..1e2afec6fa --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_iframe.html @@ -0,0 +1,22 @@ +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h3>test iframe</h3> + <script> + "use strict"; + + window.onload = function() { + window.onhashchange = function() { + window.parent.postMessage("updated-iframe-url", "*"); + }; + // NOTE: without the this setTimeout the location change is not fired + // even without the "fire only for top level windows" fix + setTimeout(function() { + window.location.hash = "updated-iframe-url"; + }, 0); + }; + </script> + </body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html b/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html new file mode 100644 index 0000000000..3fa93979fa --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html @@ -0,0 +1,21 @@ +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h3>test page</h3> + <iframe src="about:blank"></iframe> + <script> + "use strict"; + + window.onmessage = function(evt) { + if (evt.data === "updated-iframe-url") { + window.postMessage("frame-updated", "*"); + } + }; + window.onload = function() { + document.querySelector("iframe").setAttribute("src", "context_tabs_onUpdated_iframe.html"); + }; + </script> + </body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/file_bypass_cache.sjs b/mobile/android/components/extensions/test/mochitest/file_bypass_cache.sjs new file mode 100644 index 0000000000..eed8a6ef49 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/file_bypass_cache.sjs @@ -0,0 +1,13 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain; charset=UTF-8", false); + + if (request.hasHeader("pragma") && request.hasHeader("cache-control")) { + response.write( + `${request.getHeader("pragma")}:${request.getHeader("cache-control")}` + ); + } +} diff --git a/mobile/android/components/extensions/test/mochitest/file_dummy.html b/mobile/android/components/extensions/test/mochitest/file_dummy.html new file mode 100644 index 0000000000..49ad37128d --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/file_dummy.html @@ -0,0 +1,10 @@ +<html> +<head> +<meta charset="utf-8"> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Dummy test page</p> +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/file_iframe_document.html b/mobile/android/components/extensions/test/mochitest/file_iframe_document.html new file mode 100644 index 0000000000..3bb2bd5dcf --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/file_iframe_document.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title></title> +</head> +<body> + <iframe src="/"></iframe> + <iframe src="about:blank"></iframe> +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/file_slowed_document.sjs b/mobile/android/components/extensions/test/mochitest/file_slowed_document.sjs new file mode 100644 index 0000000000..3816cf045b --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/file_slowed_document.sjs @@ -0,0 +1,49 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +// This script slows the load of an HTML document so that we can reliably test +// all phases of the load cycle supported by the extension API. + +/* eslint-disable no-unused-vars */ + +const URL = "file_slowed_document.sjs"; + +const DELAY = 2 * 1000; // Delay one second before completing the request. + +const nsTimer = Components.Constructor( + "@mozilla.org/timer;1", + "nsITimer", + "initWithCallback" +); + +let timer; + +function handleRequest(request, response) { + response.processAsync(); + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + response.write(`<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <title></title> + </head> + <body> + `); + + // Note: We need to store a reference to the timer to prevent it from being + // canceled when it's GCed. + timer = new nsTimer( + () => { + if (request.queryString.includes("with-iframe")) { + response.write(`<iframe src="${URL}?r=${Math.random()}"></iframe>`); + } + response.write(`</body></html>`); + response.finish(); + }, + DELAY, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/mobile/android/components/extensions/test/mochitest/head.js b/mobile/android/components/extensions/test/mochitest/head.js new file mode 100644 index 0000000000..c240edd765 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/head.js @@ -0,0 +1,73 @@ +"use strict"; + +/* exported assertPersistentListeners, AppConstants, TEST_ICON_ARRAYBUFFER */ + +var { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +var TEST_ICON_DATA = + "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC"; + +var TEST_ICON_ARRAYBUFFER = Uint8Array.from(atob(TEST_ICON_DATA), byte => + byte.charCodeAt(0) +).buffer; + +async function assertPersistentListeners( + extWrapper, + apiNs, + apiEvents, + expected +) { + const stringErr = await SpecialPowers.spawnChrome( + [extWrapper.id, apiNs, apiEvents, expected], + async (id, apiNs, apiEvents, expected) => { + try { + const { ExtensionTestCommon } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionTestCommon.sys.mjs" + ); + const ext = { id }; + for (const event of apiEvents) { + ExtensionTestCommon.testAssertions.assertPersistentListeners( + ext, + apiNs, + event, + { + primed: expected.primed, + persisted: expected.persisted, + primedListenersCount: expected.primedListenersCount, + } + ); + } + } catch (err) { + return String(err); + } + } + ); + ok( + stringErr == undefined, + stringErr ? stringErr : `Found expected primed and persistent listeners` + ); +} + +{ + const chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("chrome_cleanup_script.js") + ); + + SimpleTest.registerCleanupFunction(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + + chromeScript.sendAsyncMessage("check-cleanup"); + + const results = await chromeScript.promiseOneMessage("cleanup-results"); + chromeScript.destroy(); + + if (results.extraWindows.length || results.extraTabs.length) { + ok( + false, + `Test left extra windows or tabs: ${JSON.stringify(results)}\n` + ); + } + }); +} diff --git a/mobile/android/components/extensions/test/mochitest/mochitest.toml b/mobile/android/components/extensions/test/mochitest/mochitest.toml new file mode 100644 index 0000000000..f3e5922cc4 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/mochitest.toml @@ -0,0 +1,62 @@ +[DEFAULT] +support-files = [ + "../../../../../../toolkit/components/extensions/test/mochitest/test_ext_all_apis.js", + "../../../../../../toolkit/components/extensions/test/mochitest/file_sample.html", + "../../../../../../toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js", + "context.html", + "context_tabs_onUpdated_iframe.html", + "context_tabs_onUpdated_page.html", + "file_bypass_cache.sjs", + "file_dummy.html", + "file_iframe_document.html", + "file_slowed_document.sjs", + "head.js", +] +tags = "webextensions" +prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"] + +["test_ext_all_apis.html"] + +["test_ext_downloads_event_page.html"] + +["test_ext_tab_runtimeConnect.html"] + +["test_ext_tabs_autoDiscardable.html"] + +["test_ext_tabs_create.html"] + +["test_ext_tabs_events.html"] +skip-if = ["fission"] # Bug 1827754 + +["test_ext_tabs_executeScript.html"] + +["test_ext_tabs_executeScript_bad.html"] + +["test_ext_tabs_executeScript_no_create.html"] + +["test_ext_tabs_executeScript_runAt.html"] + +["test_ext_tabs_get.html"] + +["test_ext_tabs_getCurrent.html"] + +["test_ext_tabs_goBack_goForward.html"] + +["test_ext_tabs_insertCSS.html"] + +["test_ext_tabs_lastAccessed.html"] +skip-if = ["true"] # tab.lastAccessed not implemented + +["test_ext_tabs_onUpdated.html"] + +["test_ext_tabs_query.html"] + +["test_ext_tabs_reload.html"] + +["test_ext_tabs_reload_bypass_cache.html"] + +["test_ext_tabs_sendMessage.html"] + +["test_ext_tabs_update_url.html"] + +["test_ext_webNavigation_onCommitted.html"] diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html b/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html new file mode 100644 index 0000000000..3ad239b093 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>WebExtension test</title> + <meta charset="utf-8"> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> +"use strict"; +/* exported expectedContentApisTargetSpecific, expectedBackgroundApisTargetSpecific */ +let expectedContentApisTargetSpecific = []; + +let expectedBackgroundApisTargetSpecific = [ + "tabs.MutedInfoReason", + "tabs.TAB_ID_NONE", + "tabs.TabStatus", + "tabs.WindowType", + "tabs.ZoomSettingsMode", + "tabs.ZoomSettingsScope", + "tabs.connect", + "tabs.create", + "tabs.detectLanguage", + "tabs.executeScript", + "tabs.get", + "tabs.getCurrent", + "tabs.goBack", + "tabs.goForward", + "tabs.insertCSS", + "tabs.onActivated", + "tabs.onAttached", + "tabs.onCreated", + "tabs.onDetached", + "tabs.onHighlighted", + "tabs.onMoved", + "tabs.onRemoved", + "tabs.onReplaced", + "tabs.onUpdated", + "tabs.query", + "tabs.reload", + "tabs.remove", + "tabs.removeCSS", + "tabs.sendMessage", + "tabs.update", +]; +</script> +<script src="test_ext_all_apis.js"></script> +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_downloads_event_page.html b/mobile/android/components/extensions/test/mochitest/test_ext_downloads_event_page.html new file mode 100644 index 0000000000..78691114df --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_downloads_event_page.html @@ -0,0 +1,102 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Downloads Events Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_downloads_event_page() { + const apiEvents = ["onChanged"]; + const apiNs = "downloads"; + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "permanent", + manifest: { + browser_specific_settings: { gecko: { id: "eventpage@downloads" } }, + permissions: ["downloads"], + background: { persistent: false }, + }, + background() { + browser.downloads.onChanged.addListener(() => { + browser.test.sendMessage("onChanged"); + browser.test.notifyPass("downloads-events"); + }); + browser.test.sendMessage("ready"); + }, + }); + + // on startup, onChanged event listener should not be primed + await extension.startup(); + info("Wait for event page to be started"); + await extension.awaitMessage("ready"); + await assertPersistentListeners(extension, apiNs, apiEvents, { primed: false }); + + // when the extension is killed, onChanged event listener should be primed + info("Terminate event page"); + await extension.terminateBackground(); + await assertPersistentListeners(extension, apiNs, apiEvents, { primed: true }); + + // fire download-changed event and onChanged event listener should not be primed + info("Wait for download-changed to be emitted"); + await SpecialPowers.spawnChrome([], async () => { + const { DownloadTracker } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewWebExtension.sys.mjs" + ); + + const delta = { + filename: "test.gif", + id: 4, + mime: "image/gif", + totalBytes: 5, + }; + + // Mocks DownloadItem from mobile/android/components/extensions/ext-downloads.js + const downloadItem = { + byExtensionId: "download-onChanged@tests.mozilla.org", + byExtensionName: "Download", + bytesReceived: 0, + canResume: false, + danger: "safe", + exists: false, + fileSize: -1, + filename: "test.gif", + id: 4, + incognito: false, + mime: "image/gif", + paused: false, + referrer: "", + startTime: 1680818149350, + state: "in_progress", + totalBytes: 5, + url: "http://localhost:4245/assets/www/images/test.gif", + }; + + // WebExtension.DownloadDelegate has not been overridden in + // TestRunnerActivity (used by mochitests), so the downloads API + // does not actually work. In this test, we are only interested in + // whether or not dispatching an event would wake up the event page, + // so we artificially trigger a fake onChanged event to test that. + DownloadTracker.emit("download-changed", { delta, downloadItem }); + }); + + info("Triggered download change, expecting downloads.onChanged event"); + + await extension.awaitMessage("ready"); + await extension.awaitMessage("onChanged"); + await extension.awaitFinish("downloads-events"); + await assertPersistentListeners(extension, apiNs, apiEvents, { primed: false }); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html b/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html new file mode 100644 index 0000000000..138bb054a9 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html @@ -0,0 +1,498 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>PageAction Test</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +async function waitAboutAddonsRendered(addonId) { + await ContentTaskUtils.waitForCondition(() => { + return content.document.querySelector(`div.addon-item[addonID="${addonId}"]`); + }, `wait Addon Item for ${addonId} to be rendered`); +} + +async function navigateToAddonDetails(addonId) { + const item = content.document.querySelector(`div.addon-item[addonID="${addonId}"]`); + const rect = item.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + const domWinUtils = content.window.windowUtils; + + domWinUtils.sendMouseEventToWindow("mousedown", x, y, 0, 1, 0); + domWinUtils.sendMouseEventToWindow("mouseup", x, y, 0, 1, 0); +} + +async function waitAddonOptionsPage([addonId, expectedText]) { + await ContentTaskUtils.waitForCondition(() => { + const optionsIframe = content.document.querySelector(`#addon-options`); + return optionsIframe && optionsIframe.contentDocument.readyState === "complete" && + optionsIframe.contentDocument.body.innerText.includes(expectedText); + }, `wait Addon Options ${expectedText} for ${addonId} to be loaded`); + + const optionsIframe = content.document.querySelector(`#addon-options`); + + return { + iframeHeight: optionsIframe.style.height, + documentHeight: optionsIframe.contentDocument.documentElement.scrollHeight, + bodyHeight: optionsIframe.contentDocument.body.scrollHeight, + }; +} + +async function clickOnLinkInOptionsPage(selector) { + const optionsIframe = content.document.querySelector(`#addon-options`); + optionsIframe.contentDocument.querySelector(selector).click(); +} + +async function clickAddonOptionButton() { + content.document.querySelector(`button#open-addon-options`).click(); +} + +async function navigateBack() { + content.window.history.back(); +} + +function waitDOMContentLoaded(checkUrlCb) { + const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser"); + + return new Promise(resolve => { + const listener = (event) => { + if (checkUrlCb(event.target.defaultView.location.href)) { + BrowserApp.deck.removeEventListener("DOMContentLoaded", listener); + resolve(); + } + }; + + BrowserApp.deck.addEventListener("DOMContentLoaded", listener); + }); +} + +function waitAboutAddonsLoaded() { + return waitDOMContentLoaded(url => url === "about:addons"); +} + +function clickAddonDisable() { + content.document.querySelector("#disable-btn").click(); +} + +function clickAddonEnable() { + content.document.querySelector("#enable-btn").click(); +} + +add_task(async function test_options_ui_iframe_height() { + const addonID = "test-options-ui@mozilla.org"; + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + browser_specific_settings: { + gecko: {id: addonID}, + }, + name: "Options UI Extension", + description: "Longer addon description", + options_ui: { + page: "options.html", + }, + }, + files: { + // An option page with the document element bigger than the body. + "options.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <style> + html { height: 500px; border: 1px solid black; } + body { height: 200px; } + </style> + </head> + <body> + <h1>Options page 1</h1> + <a href="options2.html">go to page 2</a> + </body> + </html> + `, + // A second option page with the body element bigger than the document. + "options2.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + <style> + html { height: 200px; border: 1px solid black; } + body { height: 350px; } + </style> + </head> + <body> + <h1>Options page 2</h1> + </body> + </html> + `, + }, + }); + + await extension.startup(); + + const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser"); + + const onceAboutAddonsLoaded = waitAboutAddonsLoaded(); + + BrowserApp.addTab("about:addons", { + selected: true, + parentId: BrowserApp.selectedTab.id, + }); + + await onceAboutAddonsLoaded; + + is(BrowserApp.selectedTab.currentURI.spec, "about:addons", + "about:addons is the currently selected tab"); + + await SpecialPowers.spawn(BrowserApp.selectedTab.browser, [addonID], waitAboutAddonsRendered); + + await SpecialPowers.spawn(BrowserApp.selectedTab.browser, [addonID], navigateToAddonDetails); + + const optionsSizes = await SpecialPowers.spawn( + BrowserApp.selectedTab.browser, [[addonID, "Options page 1"]], waitAddonOptionsPage + ); + + ok(parseInt(optionsSizes.iframeHeight, 10) >= 500, + "The addon options iframe is at least 500px"); + + is(optionsSizes.iframeHeight, optionsSizes.documentHeight + "px", + "The addon options iframe has the expected height"); + + await SpecialPowers.spawn(BrowserApp.selectedTab.browser, ["a"], clickOnLinkInOptionsPage); + + const options2Sizes = await SpecialPowers.spawn( + BrowserApp.selectedTab.browser, [[addonID, "Options page 2"]], waitAddonOptionsPage + ); + + // The second option page has a body bigger than the document element + // and we expect the iframe to be bigger than that. + ok(parseInt(options2Sizes.iframeHeight, 10) > 200, + `The iframe is bigger then 200px (${options2Sizes.iframeHeight})`); + + // The second option page has a body smaller than the document element of the first + // page and we expect the iframe to be smaller than for the previous options page. + ok(parseInt(options2Sizes.iframeHeight, 10) < 500, + `The iframe is smaller then 500px (${options2Sizes.iframeHeight})`); + + is(options2Sizes.iframeHeight, options2Sizes.documentHeight + "px", + "The second addon options page has the expected height"); + + await SpecialPowers.spawn(BrowserApp.selectedTab.browser, [], navigateBack); + + const backToOptionsSizes = await SpecialPowers.spawn( + BrowserApp.selectedTab.browser, [[addonID, "Options page 1"]], waitAddonOptionsPage + ); + + // After going back to the first options page, + // we expect the iframe to have the same size of the previous load. + is(backToOptionsSizes.iframeHeight, optionsSizes.iframeHeight, + `When navigating back, the old iframe size is restored (${backToOptionsSizes.iframeHeight})`); + + BrowserApp.closeTab(BrowserApp.selectedTab); + + await extension.unload(); +}); + +add_task(async function test_options_ui_open_aboutaddons_details() { + const addonID = "test-options-ui-open-addon-details@mozilla.org"; + + function background() { + browser.test.onMessage.addListener(msg => { + if (msg !== "runtime.openOptionsPage") { + browser.test.fail(`Received unexpected test message: ${msg}`); + return; + } + + browser.runtime.openOptionsPage(); + }); + } + + function optionsScript() { + browser.test.sendMessage("options-page-loaded", window.location.href); + } + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + background, + manifest: { + browser_specific_settings: { + gecko: {id: addonID}, + }, + name: "Options UI open addon details Extension", + description: "Longer addon description", + options_ui: { + page: "options.html", + }, + }, + files: { + "options.js": optionsScript, + "options.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>Options page</h1> + <script src="options.js"><\/script> + </body> + </html> + `, + }, + }); + + await extension.startup(); + + const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser"); + + const onceAboutAddonsLoaded = waitAboutAddonsLoaded(); + + BrowserApp.addTab("about:addons", { + selected: true, + parentId: BrowserApp.selectedTab.id, + }); + + await onceAboutAddonsLoaded; + + is(BrowserApp.selectedTab.currentURI.spec, "about:addons", + "about:addons is the currently selected tab"); + + info("Wait runtime.openOptionsPage to open the about:addond details in the existent tab"); + extension.sendMessage("runtime.openOptionsPage"); + await extension.awaitMessage("options-page-loaded"); + + is(BrowserApp.selectedTab.currentURI.spec, "about:addons", + "about:addons is still the currently selected tab once the options has been loaded"); + + BrowserApp.closeTab(BrowserApp.selectedTab); + + await extension.unload(); +}); + +add_task(async function test_options_ui_open_in_tab() { + const addonID = "test-options-ui@mozilla.org"; + + function background() { + browser.test.onMessage.addListener(msg => { + if (msg !== "runtime.openOptionsPage") { + browser.test.fail(`Received unexpected test message: ${msg}`); + return; + } + + browser.runtime.openOptionsPage(); + }); + } + + function optionsScript() { + browser.test.sendMessage("options-page-loaded", window.location.href); + } + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + background, + manifest: { + browser_specific_settings: { + gecko: {id: addonID}, + }, + name: "Options UI open_in_tab Extension", + description: "Longer addon description", + options_ui: { + page: "options.html", + open_in_tab: true, + }, + }, + files: { + "options.js": optionsScript, + "options.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>Options page</h1> + <script src="options.js"><\/script> + </body> + </html> + `, + }, + }); + + await extension.startup(); + + const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser"); + + const onceAboutAddonsLoaded = waitAboutAddonsLoaded(); + + BrowserApp.selectOrAddTab("about:addons", { + selected: true, + parentId: BrowserApp.selectedTab.id, + }); + + await onceAboutAddonsLoaded; + + const aboutAddonsTab = BrowserApp.selectedTab; + + is(aboutAddonsTab.currentURI.spec, "about:addons", + "about:addons is the currently selected tab"); + + await SpecialPowers.spawn(aboutAddonsTab.browser, [addonID], waitAboutAddonsRendered); + await SpecialPowers.spawn(aboutAddonsTab.browser, [addonID], navigateToAddonDetails); + + const onceAddonOptionsLoaded = waitDOMContentLoaded(url => url.endsWith("options.html")); + + info("Click the Options button in the addon details"); + await SpecialPowers.spawn(aboutAddonsTab.browser, [], clickAddonOptionButton); + + info("Waiting that the addon options are loaded in a new tab"); + await onceAddonOptionsLoaded; + + const addonOptionsTab = BrowserApp.selectedTab; + + ok(aboutAddonsTab.id !== addonOptionsTab.id, + "The Addon Options page has been loaded in a new tab"); + + let optionsURL = await extension.awaitMessage("options-page-loaded"); + + is(addonOptionsTab.currentURI.spec, optionsURL, + "Got the expected extension url opened in the addon options tab"); + + const waitTabClosed = (nativeTab) => { + return new Promise(resolve => { + const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser"); + const expectedBrowser = nativeTab.browser; + + const tabCloseListener = (event) => { + const browser = event.target; + if (browser !== expectedBrowser) { + return; + } + + BrowserApp.deck.removeEventListener("TabClose", tabCloseListener); + resolve(); + }; + + BrowserApp.deck.addEventListener("TabClose", tabCloseListener); + }); + }; + + const onceOptionsTabClosed = waitTabClosed(addonOptionsTab); + const onceAboutAddonsClosed = waitTabClosed(aboutAddonsTab); + + info("Close the opened about:addons and options tab"); + BrowserApp.closeTab(addonOptionsTab); + BrowserApp.closeTab(aboutAddonsTab); + + info("Wait the tabs to be closed"); + await Promise.all([onceOptionsTabClosed, onceAboutAddonsClosed]); + + const oldSelectedTab = BrowserApp.selectedTab; + info("Call runtime.openOptionsPage"); + extension.sendMessage("runtime.openOptionsPage"); + + info("Wait runtime.openOptionsPage to open the options in a new tab"); + optionsURL = await extension.awaitMessage("options-page-loaded"); + is(BrowserApp.selectedTab.currentURI.spec, optionsURL, + "runtime.openOptionsPage has opened the expected extension page"); + ok(BrowserApp.selectedTab !== oldSelectedTab, + "runtime.openOptionsPage has opened a new tab"); + + BrowserApp.closeTab(BrowserApp.selectedTab); + + await extension.unload(); +}); + +add_task(async function test_options_ui_on_disable_and_enable() { + // Temporarily disabled for races. + /* eslint-disable no-unreachable */ + return; + + const addonID = "test-options-ui-disable-enable@mozilla.org"; + + function optionsScript() { + browser.test.sendMessage("options-page-loaded", window.location.href); + } + + const extension = ExtensionTestUtils.loadExtension({ + useAddonManager: "temporary", + manifest: { + browser_specific_settings: { + gecko: {id: addonID}, + }, + name: "Options UI open addon details Extension", + description: "Longer addon description", + options_ui: { + page: "options.html", + }, + }, + files: { + "options.js": optionsScript, + "options.html": `<!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>Options page</h1> + <script src="options.js"><\/script> + </body> + </html> + `, + }, + }); + + await extension.startup(); + + const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser"); + + const onceAboutAddonsLoaded = waitAboutAddonsLoaded(); + + BrowserApp.addTab("about:addons", { + selected: true, + parentId: BrowserApp.selectedTab.id, + }); + + await onceAboutAddonsLoaded; + + const aboutAddonsTab = BrowserApp.selectedTab; + + is(aboutAddonsTab.currentURI.spec, "about:addons", + "about:addons is the currently selected tab"); + + info("Wait the addon details to have been loaded"); + await SpecialPowers.spawn(aboutAddonsTab.browser, [addonID], waitAboutAddonsRendered); + await SpecialPowers.spawn(aboutAddonsTab.browser, [addonID], navigateToAddonDetails); + + info("Wait the addon options page to have been loaded"); + await extension.awaitMessage("options-page-loaded"); + + info("Click the addon disable button"); + await SpecialPowers.spawn(aboutAddonsTab.browser, [], clickAddonDisable); + + // NOTE: Currently after disabling the addon the extension.awaitMessage seems + // to fail be able to receive events coming from the browser.test.sendMessage API + // (nevertheless `await extension.unload()` seems to be able to remove the extension), + // falling back to wait for the options page to be loaded here. + const onceAddonOptionsLoaded = waitDOMContentLoaded(url => url.endsWith("options.html")); + + info("Click the addon enable button"); + await SpecialPowers.spawn(aboutAddonsTab.browser, [], clickAddonEnable); + + info("Wait the addon options page to have been loaded after clicking the addon enable button"); + await onceAddonOptionsLoaded; + + BrowserApp.closeTab(BrowserApp.selectedTab); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tab_runtimeConnect.html b/mobile/android/components/extensions/test/mochitest/test_ext_tab_runtimeConnect.html new file mode 100644 index 0000000000..48904c2990 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tab_runtimeConnect.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs runtimeConnect Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function() { + const win = window.open("http://mochi.test:8888/"); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background: function() { + const messages_received = []; + + let tabId; + + browser.runtime.onConnect.addListener((port) => { + browser.test.assertTrue(!!port, "tab to background port received"); + browser.test.assertEq("tab-connection-name", port.name, "port name should be defined and equal to connectInfo.name"); + browser.test.assertTrue(!!port.sender.tab, "port.sender.tab should be defined"); + browser.test.assertEq(tabId, port.sender.tab.id, "port.sender.tab.id should be equal to the expected tabId"); + + port.onMessage.addListener((msg) => { + messages_received.push(msg); + + if (messages_received.length == 1) { + browser.test.assertEq("tab to background port message", msg, "'tab to background' port message received"); + port.postMessage("background to tab port message"); + } + + if (messages_received.length == 2) { + browser.test.assertTrue(!!msg.tabReceived, "'background to tab' reply port message received"); + browser.test.assertEq("background to tab port message", msg.tabReceived, "reply port content contains the message received"); + + browser.test.notifyPass("tabRuntimeConnect.pass"); + } + }); + }); + + browser.tabs.create({url: "tab.html"}, + (tab) => { tabId = tab.id; }); + }, + + files: { + "tab.js": function() { + const port = browser.runtime.connect({name: "tab-connection-name"}); + port.postMessage("tab to background port message"); + port.onMessage.addListener((msg) => { + port.postMessage({tabReceived: msg}); + }); + }, + "tab.html": ` + <!DOCTYPE html> + <html> + <head> + <title>test tab extension page</title> + <meta charset="utf-8"> + <script src="tab.js" async><\/script> + </head> + <body> + <h1>test tab extension page</h1> + </body> + </html> + `, + }, + }); + + await extension.startup(); + await extension.awaitFinish("tabRuntimeConnect.pass"); + await extension.unload(); + + win.close(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_autoDiscardable.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_autoDiscardable.html new file mode 100644 index 0000000000..63ea8337a7 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_autoDiscardable.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>autoDiscardable test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function() { + const extension = ExtensionTestUtils.loadExtension({ + async background() { + const tab = await browser.tabs.create({}); + browser.test.assertTrue(tab.autoDiscardable, "autoDiscardable should be true on Android"); + browser.test.assertThrows(() => browser.tabs.query({ autoDiscardable: true }), + /Unexpected property "autoDiscardable"/, + `tabs.query with autoDiscardable should error out on Android`); + browser.test.assertThrows(() => browser.tabs.update(tab.id, { autoDiscardable: true }), + /Unexpected property "autoDiscardable"/, + `tabs.update with autoDiscardable should error out on Android`); + await browser.tabs.remove(tab.id); // Cleanup + browser.test.notifyPass("tabs.autoDiscardable"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("tabs.autoDiscardable"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_create.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_create.html new file mode 100644 index 0000000000..027b231fa7 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_create.html @@ -0,0 +1,153 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs create Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.userContext.enabled", true], + ["dom.security.https_first", false], + ], + }); + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs", "cookies"], + + "background": {"page": "bg/background.html"}, + }, + + files: { + "bg/blank.html": `<html><head><meta charset="utf-8"></head></html>`, + + "bg/background.html": `<html><head> + <meta charset="utf-8"> + <script src="background.js"><\/script> + </head></html>`, + + "bg/background.js": function() { + let activeTab; + + function runTests() { + const DEFAULTS = { + active: true, + url: "about:blank", + }; + + const tests = [ + { + create: {url: "http://example.com/"}, + result: {url: "http://example.com/"}, + }, + { + create: {url: "blank.html"}, + result: {url: browser.runtime.getURL("bg/blank.html")}, + }, + { + create: {}, + }, + { + create: {active: false}, + result: {active: false}, + }, + { + create: {active: true}, + result: {active: true}, + }, + { + create: {cookieStoreId: null}, + result: {cookieStoreId: "firefox-default"}, + }, + { + create: {cookieStoreId: "firefox-container-1"}, + result: {cookieStoreId: "firefox-container-1"}, + }, + ]; + + async function nextTest() { + if (!tests.length) { + browser.test.notifyPass("tabs.create"); + return; + } + + const test = tests.shift(); + const expected = Object.assign({}, DEFAULTS, test.result); + + browser.test.log(`Testing tabs.create(${JSON.stringify(test.create)}), expecting ${JSON.stringify(test.result)}`); + + const updatedPromise = new Promise(resolve => { + const onUpdated = (changedTabId, changed) => { + // Loading an extension page causes two `about:blank` messages + // because of the process switch + if (changed.url && (expected.url == "about:blank" || changed.url != "about:blank")) { + browser.tabs.onUpdated.removeListener(onUpdated); + resolve({tabId: changedTabId, url: changed.url}); + } + }; + browser.tabs.onUpdated.addListener(onUpdated); + }); + + const createdPromise = new Promise(resolve => { + const onCreated = tab => { + browser.test.assertTrue("id" in tab, `Expected tabs.onCreated callback to receive tab object`); + resolve(); + }; + browser.tabs.onCreated.addListener(onCreated); + }); + + const [tab] = await Promise.all([ + browser.tabs.create(test.create), + createdPromise, + ]); + const tabId = tab.id; + + for (const key of Object.keys(expected)) { + if (key === "url") { + // FIXME: This doesn't get updated until later in the load cycle. + continue; + } + + browser.test.assertEq(expected[key], tab[key], `Expected value for tab.${key}`); + } + + const updated = await updatedPromise; + browser.test.assertEq(tabId, updated.tabId, `Expected value for tab.id`); + browser.test.assertEq(expected.url, updated.url, `Expected value for tab.url`); + + await browser.tabs.remove(tabId); + await browser.tabs.update(activeTab, {active: true}); + + nextTest(); + } + + nextTest(); + } + + browser.tabs.query({active: true, currentWindow: true}, tabs => { + activeTab = tabs[0].id; + + runTests(); + }); + }, + }, + }); + + await extension.startup(); + await extension.awaitFinish("tabs.create"); + await extension.unload(); + await SpecialPowers.popPrefEnv(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html new file mode 100644 index 0000000000..cd708d942a --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html @@ -0,0 +1,302 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs Events Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function testTabEvents() { + async function background() { + const events = []; + let eventPromise; + const checkEvents = () => { + if (eventPromise && events.length >= eventPromise.names.length) { + eventPromise.resolve(); + } + }; + + browser.tabs.onCreated.addListener(tab => { + events.push({type: "onCreated", tab}); + checkEvents(); + }); + + browser.tabs.onAttached.addListener((tabId, info) => { + events.push(Object.assign({type: "onAttached", tabId}, info)); + checkEvents(); + }); + + browser.tabs.onDetached.addListener((tabId, info) => { + events.push(Object.assign({type: "onDetached", tabId}, info)); + checkEvents(); + }); + + browser.tabs.onRemoved.addListener((tabId, info) => { + events.push(Object.assign({type: "onRemoved", tabId}, info)); + checkEvents(); + }); + + browser.tabs.onMoved.addListener((tabId, info) => { + events.push(Object.assign({type: "onMoved", tabId}, info)); + checkEvents(); + }); + + async function expectEvents(names) { + browser.test.log(`Expecting events: ${names.join(", ")}`); + + await new Promise(resolve => { + eventPromise = {names, resolve}; + checkEvents(); + }); + + browser.test.assertEq(names.length, events.length, "Got expected number of events"); + for (const [i, name] of names.entries()) { + browser.test.assertEq(name, i in events && events[i].type, + `Got expected ${name} event`); + } + return events.splice(0); + } + + try { + browser.test.log("Create tab"); + const tab = await browser.tabs.create({url: "about:blank"}); + const oldIndex = tab.index; + + const [created] = await expectEvents(["onCreated"]); + browser.test.assertEq(tab.id, created.tab.id, "Got expected tab ID"); + browser.test.assertEq(oldIndex, created.tab.index, "Got expected tab index"); + + + browser.test.log("Remove tab"); + await browser.tabs.remove(tab.id); + const [removed] = await expectEvents(["onRemoved"]); + + browser.test.assertEq(tab.id, removed.tabId, "Expected removed tab ID"); + browser.test.assertEq(tab.windowId, removed.windowId, "Expected removed tab window ID"); + // Note: We want to test for the actual boolean value false here. + browser.test.assertEq(false, removed.isWindowClosing, "Expected isWindowClosing value"); + + browser.test.notifyPass("tabs-events"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("tabs-events"); + } + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + background, + }); + + await extension.startup(); + await extension.awaitFinish("tabs-events"); + await extension.unload(); +}); + +add_task(async function testTabRemovalEvent() { + async function background() { + function awaitLoad(tabId) { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(tabId_, changed, tab) { + if (tabId == tabId_ && changed.status == "complete") { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + } + + chrome.tabs.onRemoved.addListener((tabId, info) => { + browser.test.log("Make sure the removed tab is not available in the tabs.query callback."); + chrome.tabs.query({}, tabs => { + for (const tab of tabs) { + browser.test.assertTrue(tab.id != tabId, "Tab query should not include removed tabId"); + } + browser.test.notifyPass("tabs-events"); + }); + }); + + try { + const url = "http://example.com/mochitest/mobile/android/components/extensions/test/mochitest/context.html"; + const tab = await browser.tabs.create({url: url}); + await awaitLoad(tab.id); + + await browser.tabs.remove(tab.id); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("tabs-events"); + } + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + background, + }); + + await extension.startup(); + await extension.awaitFinish("tabs-events"); + await extension.unload(); +}); + +add_task(async function testTabActivationEvent() { + // TODO bug 1565536: tabs.onActivated is not supported in GeckoView. + if (true) { + todo(false, "skipping testTabActivationEvent"); + return; + } + async function background() { + function makeExpectable() { + let expectation = null, resolver = null; + const expectable = param => { + if (expectation === null) { + browser.test.fail("unexpected call to expectable"); + } else { + try { + resolver(expectation(param)); + } catch (e) { + resolver(Promise.reject(e)); + } finally { + expectation = null; + } + } + }; + expectable.expect = e => { + expectation = e; + return new Promise(r => { resolver = r; }); + }; + return expectable; + } + try { + const listener = makeExpectable(); + browser.tabs.onActivated.addListener(listener); + + const [tab0] = await browser.tabs.query({active: true}); + const [, tab1] = await Promise.all([ + listener.expect(info => { + browser.test.assertEq(tab0.id, info.previousTabId, "Got expected previousTabId"); + }), + browser.tabs.create({url: "about:blank"}), + ]); + const [, tab2] = await Promise.all([ + listener.expect(info => { + browser.test.assertEq(tab1.id, info.previousTabId, "Got expected previousTabId"); + }), + browser.tabs.create({url: "about:blank"}), + ]); + + await Promise.all([ + listener.expect(info => { + browser.test.assertEq(tab1.id, info.tabId, "Got expected tabId"); + browser.test.assertEq(tab2.id, info.previousTabId, "Got expected previousTabId"); + }), + browser.tabs.update(tab1.id, {active: true}), + ]); + + await Promise.all([ + listener.expect(info => { + browser.test.assertEq(tab2.id, info.tabId, "Got expected tabId"); + browser.test.assertEq(undefined, info.previousTabId, "previousTabId should not be defined when previous tab was closed"); + }), + browser.tabs.remove(tab1.id), + ]); + + browser.tabs.onActivated.removeListener(listener); + await browser.tabs.remove(tab2.id); + + browser.test.notifyPass("tabs-events"); + } catch (e) { + browser.test.fail(`${e} :: ${e.stack}`); + browser.test.notifyFail("tabs-events"); + } + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + background, + }); + + await extension.startup(); + await extension.awaitFinish("tabs-events"); + await extension.unload(); +}); + +add_task(async function test_tabs_event_page() { + function background() { + const EVENTS = [ + "onActivated", + "onRemoved", + "onUpdated", + ]; + browser.tabs.onCreated.addListener(() => { + browser.test.sendMessage("onCreated"); + }); + for (const event of EVENTS) { + browser.tabs[event].addListener(() => { + }); + } + browser.test.onMessage.addListener(async msg => { + if (msg === "createTab") { + await browser.tabs.create({url: "about:blank"}); + } + }); + browser.test.sendMessage("ready"); + } + + const apiEvents = ["onActivated", "onCreated", "onRemoved", "onUpdated"]; + const apiNs = "tabs"; + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_specific_settings: { gecko: { id: "eventpage@tabs" } }, + "permissions": ["tabs"], + background: { persistent: false }, + }, + background, + }); + + await extension.startup(); + info("Wait for event page to be started"); + await extension.awaitMessage("ready"); + // Sanity check + info("Wait for tabs.onCreated listener call"); + extension.sendMessage("createTab"); + await extension.awaitMessage("onCreated"); + + // on startup, all event listeners should not be primed + await assertPersistentListeners(extension, apiNs, apiEvents, { primed: false }); + + // when the extension is killed, all event listeners should be primed + info("Terminate event page"); + await extension.terminateBackground({ disableResetIdleForTest: true }); + await assertPersistentListeners(extension, apiNs, apiEvents, { primed: true }); + + // on start up again, all event listeners should not be primed + info("Wake up event page on a new tabs.onCreated event"); + const newWin = window.open(); + info("Wait for event page to be restarted"); + await extension.awaitMessage("ready"); + info("Wait for the primed tabs.onCreated to be received by the event page"); + await extension.awaitMessage("onCreated"); + await assertPersistentListeners(extension, apiNs, apiEvents, { primed: false }); + + await extension.unload(); + newWin.close(); +}); + +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript.html new file mode 100644 index 0000000000..09e42d73cf --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript.html @@ -0,0 +1,252 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs executeScript Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function testExecuteScript() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + const BASE = "http://mochi.test:8888/tests/mobile/android/components/extensions/test/mochitest/"; + const URL = BASE + "file_iframe_document.html"; + + const win = window.open(URL); + await new Promise(resolve => win.addEventListener("load", resolve, {once: true})); + + async function background() { + try { + const [tab] = await browser.tabs.query({active: true, currentWindow: true}); + const frames = await browser.webNavigation.getAllFrames({tabId: tab.id}); + + browser.test.log(`FRAMES: ${frames[1].frameId} ${JSON.stringify(frames)}\n`); + await Promise.all([ + browser.tabs.executeScript({ + code: "42", + }).then(result => { + browser.test.assertEq(1, result.length, "Expected one callback result"); + browser.test.assertEq(42, result[0], "Expected callback result"); + }), + + browser.tabs.executeScript({ + file: "script.js", + code: "42", + }).then(result => { + browser.test.fail("Expected not to be able to execute a script with both file and code"); + }, error => { + browser.test.assertTrue(/a 'code' or a 'file' property, but not both/.test(error.message), + "Got expected error"); + }), + + browser.tabs.executeScript({ + file: "script.js", + }).then(result => { + browser.test.assertEq(1, result.length, "Expected one callback result"); + browser.test.assertEq(undefined, result[0], "Expected callback result"); + }), + + browser.tabs.executeScript({ + file: "script2.js", + }).then(result => { + browser.test.assertEq(1, result.length, "Expected one callback result"); + browser.test.assertEq(27, result[0], "Expected callback result"); + }), + + browser.tabs.executeScript({ + code: "location.href;", + allFrames: true, + }).then(result => { + browser.test.assertTrue(Array.isArray(result), "Result is an array"); + + browser.test.assertEq(2, result.length, "Result has correct length"); + + browser.test.assertTrue(/\/file_iframe_document\.html$/.test(result[0]), "First result is correct"); + browser.test.assertEq("http://mochi.test:8888/", result[1], "Second result is correct"); + }), + + browser.tabs.executeScript({ + code: "location.href;", + allFrames: true, + matchAboutBlank: true, + }).then(result => { + browser.test.assertTrue(Array.isArray(result), "Result is an array"); + + browser.test.assertEq(3, result.length, "Result has correct length"); + + browser.test.assertTrue(/\/file_iframe_document\.html$/.test(result[0]), "First result is correct"); + browser.test.assertEq("http://mochi.test:8888/", result[1], "Second result is correct"); + browser.test.assertEq("about:blank", result[2], "Thirds result is correct"); + }), + + browser.tabs.executeScript({ + code: "location.href;", + runAt: "document_end", + }).then(result => { + browser.test.assertEq(1, result.length, "Expected callback result"); + browser.test.assertEq("string", typeof result[0], "Result is a string"); + + browser.test.assertTrue(/\/file_iframe_document\.html$/.test(result[0]), "Result is correct"); + }), + + browser.tabs.executeScript({ + code: "window", + }).then(result => { + browser.test.fail("Expected error when returning non-structured-clonable object"); + }, error => { + browser.test.assertEq("<anonymous code>", error.fileName, "Got expected fileName"); + browser.test.assertEq("Script '<anonymous code>' result is non-structured-clonable data", + error.message, "Got expected error"); + }), + + browser.tabs.executeScript({ + code: "Promise.resolve(window)", + }).then(result => { + browser.test.fail("Expected error when returning non-structured-clonable object"); + }, error => { + browser.test.assertEq("<anonymous code>", error.fileName, "Got expected fileName"); + browser.test.assertEq("Script '<anonymous code>' result is non-structured-clonable data", + error.message, "Got expected error"); + }), + + browser.tabs.executeScript({ + file: "script3.js", + }).then(result => { + browser.test.fail("Expected error when returning non-structured-clonable object"); + }, error => { + const expected = /Script '.*script3.js' result is non-structured-clonable data/; + browser.test.assertTrue(expected.test(error.message), "Got expected error"); + browser.test.assertTrue(error.fileName.endsWith("script3.js"), "Got expected fileName"); + }), + + browser.tabs.executeScript({ + frameId: Number.MAX_SAFE_INTEGER, + code: "42", + }).then(result => { + browser.test.fail("Expected error when specifying invalid frame ID"); + }, error => { + browser.test.assertEq( + `Invalid frame IDs: [${Number.MAX_SAFE_INTEGER}].`, + error.message, + "Got expected error" + ); + }), + + browser.tabs.create({url: "http://example.net/", active: false}).then(async tab => { + await browser.tabs.executeScript(tab.id, { + code: "42", + }).then(result => { + browser.test.fail("Expected error when trying to execute on invalid domain"); + }, error => { + browser.test.assertEq(`Missing host permission for the tab`, + error.message, "Got expected error"); + }); + + await browser.tabs.remove(tab.id); + }), + + browser.tabs.executeScript({ + code: "Promise.resolve(42)", + }).then(result => { + browser.test.assertEq(42, result[0], "Got expected promise resolution value as result"); + }), + + browser.tabs.executeScript({ + code: "location.href;", + runAt: "document_end", + allFrames: true, + }).then(result => { + browser.test.assertTrue(Array.isArray(result), "Result is an array"); + + browser.test.assertEq(2, result.length, "Result has correct length"); + + browser.test.assertTrue(/\/file_iframe_document\.html$/.test(result[0]), "First result is correct"); + browser.test.assertEq("http://mochi.test:8888/", result[1], "Second result is correct"); + }), + + browser.tabs.executeScript({ + code: "location.href;", + frameId: frames[0].frameId, + }).then(result => { + browser.test.assertEq(1, result.length, "Expected one result"); + browser.test.assertTrue(/\/file_iframe_document\.html$/.test(result[0]), `Result for frameId[0] is correct: ${result[0]}`); + }), + + browser.tabs.executeScript({ + code: "location.href;", + frameId: frames[1].frameId, + }).then(result => { + browser.test.assertEq(1, result.length, "Expected one result"); + browser.test.assertEq("http://mochi.test:8888/", result[0], "Result for frameId[1] is correct"); + }), + + browser.tabs.create({url: "http://example.com/"}).then(async tab => { + const result = await browser.tabs.executeScript(tab.id, {code: "location.href"}); + + browser.test.assertEq("http://example.com/", result[0], "Script executed correctly in new tab"); + + await browser.tabs.remove(tab.id); + }), + + // This currently does not work on Android. + /* + browser.tabs.create({url: "about:blank"}).then(async tab => { + const result = await browser.tabs.executeScript(tab.id, {code: "location.href", matchAboutBlank: true}); + browser.test.assertEq("about:blank", result[0], "Script executed correctly in new tab"); + await browser.tabs.remove(tab.id); + }), + */ + + new Promise(resolve => { + browser.runtime.onMessage.addListener(message => { + browser.test.assertEq("script ran", message, "Expected runtime message"); + resolve(); + }); + }), + ]); + + browser.test.notifyPass("executeScript"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("executeScript"); + } + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://mochi.test/", "http://example.com/", "webNavigation"], + }, + + background, + + files: { + "script.js": function() { + browser.runtime.sendMessage("script ran"); + }, + + "script2.js": "27", + + "script3.js": "window", + }, + }); + + await extension.startup(); + + await extension.awaitFinish("executeScript"); + + await extension.unload(); + + win.close(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_bad.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_bad.html new file mode 100644 index 0000000000..da645ef738 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_bad.html @@ -0,0 +1,151 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs executeScript Bad Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +function* testHasNoPermission(params) { + const contentSetup = params.contentSetup || (() => Promise.resolve()); + + async function background(contentSetup) { + browser.test.onMessage.addListener(async msg => { + browser.test.assertEq(msg, "execute-script"); + + await browser.test.assertRejects(browser.tabs.executeScript({ + file: "script.js", + }), /Missing host permission for the tab/); + + browser.test.notifyPass("executeScript"); + }); + + await contentSetup(); + + browser.test.sendMessage("ready"); + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: params.manifest, + + background: `(${background})(${contentSetup})`, + + files: { + "script.js": function() { + browser.runtime.sendMessage("first script ran"); + }, + }, + }); + + yield extension.startup(); + yield extension.awaitMessage("ready"); + + if (params.setup) { + yield params.setup(extension); + } + + extension.sendMessage("execute-script"); + + yield extension.awaitFinish("executeScript"); + yield extension.unload(); +} + +add_task(async function testBadPermissions() { + const win = window.open("http://mochi.test:8888/"); + + await new Promise(resolve => setTimeout(resolve, 0)); + + info("Test no special permissions"); + await testHasNoPermission({ + manifest: {"permissions": []}, + }); + + info("Test tabs permissions"); + await testHasNoPermission({ + manifest: {"permissions": ["tabs"]}, + }); + + win.close(); +}); + +add_task(async function testBadURL() { + async function background() { + const promises = [ + new Promise(resolve => { + browser.tabs.executeScript({ + file: "http://example.com/script.js", + }, result => { + browser.test.assertEq(undefined, result, "Result value"); + + browser.test.assertTrue(browser.runtime.lastError instanceof Error, + "runtime.lastError is Error"); + + browser.test.assertTrue(browser.runtime.lastError instanceof Error, + "runtime.lastError is Error"); + + browser.test.assertEq( + "Files to be injected must be within the extension", + browser.runtime.lastError && browser.runtime.lastError.message, + "runtime.lastError value"); + + browser.test.assertEq( + "Files to be injected must be within the extension", + browser.runtime.lastError && browser.runtime.lastError.message, + "runtime.lastError value"); + + resolve(); + }); + }), + + browser.tabs.executeScript({ + file: "http://example.com/script.js", + }).catch(error => { + browser.test.assertTrue(error instanceof Error, "Error is Error"); + + browser.test.assertEq(null, browser.runtime.lastError, + "runtime.lastError value"); + + browser.test.assertEq(null, browser.runtime.lastError, + "runtime.lastError value"); + + browser.test.assertEq( + "Files to be injected must be within the extension", + error && error.message, + "error value"); + }), + ]; + + await Promise.all(promises); + + browser.test.notifyPass("executeScript-lastError"); + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["<all_urls>"], + }, + + background, + }); + + await extension.startup(); + + await extension.awaitFinish("executeScript-lastError"); + + await extension.unload(); +}); + +// TODO bug 1435100: Test that |executeScript| fails if the tab has navigated +// to a new page, and no longer matches our expected state. This involves +// intentionally trying to trigger a race condition. +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_no_create.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_no_create.html new file mode 100644 index 0000000000..fa25568619 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_no_create.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs executeScript noCreate Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function testExecuteScriptAtOnUpdated() { + const BASE = "http://mochi.test:8888/tests/mobile/android/components/extensions/test/mochitest/"; + const URL = BASE + "file_iframe_document.html"; + // This is a regression test for bug 1325830. + // The bug (executeScript not completing any more) occurred when executeScript + // was called early at the onUpdated event, unless the tabs.create method is + // called. So this test does not use tabs.create to open new tabs. + // Note that if this test is run together with other tests that do call + // tabs.create, then this test case does not properly test the conditions of + // the regression any more. To verify that the regression has been resolved, + // this test must be run in isolation. + + function background() { + // Using variables to prevent listeners from running more than once, instead + // of removing the listener. This is to minimize any IPC, since the bug that + // is being tested is sensitive to timing. + let ignore = false; + let url; + browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { + if (url && changeInfo.status === "loading" && tab.url === url && !ignore) { + ignore = true; + browser.tabs.executeScript(tabId, { + code: "document.URL", + }).then(results => { + browser.test.assertEq(url, results[0], "Content script should run"); + browser.test.notifyPass("executeScript-at-onUpdated"); + }, error => { + browser.test.fail(`Unexpected error: ${error} :: ${error.stack}`); + browser.test.notifyFail("executeScript-at-onUpdated"); + }); + // (running this log call after executeScript to minimize IPC between + // onUpdated and executeScript.) + browser.test.log(`Found expected navigation to ${url}`); + } else { + // The bug occurs when executeScript is called before a tab is + // initialized. + browser.tabs.executeScript(tabId, {code: ""}); + } + }); + browser.test.onMessage.addListener(testUrl => { + url = testUrl; + browser.test.sendMessage("open-test-tab"); + }); + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://mochi.test/", "tabs"], + }, + background, + }); + + await extension.startup(); + + extension.sendMessage(URL); + await extension.awaitMessage("open-test-tab"); + + const tab = window.open(URL); + await extension.awaitFinish("executeScript-at-onUpdated"); + + await extension.unload(); + + tab.close(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_runAt.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_runAt.html new file mode 100644 index 0000000000..2e82320f8c --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_runAt.html @@ -0,0 +1,128 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs executeScript runAt Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/** + * These tests ensure that the runAt argument to tabs.executeScript delays + * script execution until the document has reached the correct state. + * + * Since tests of this nature are especially race-prone, it relies on a + * server-JS script to delay the completion of our test page's load cycle long + * enough for us to attempt to load our scripts in the earlies phase we support. + * + * And since we can't actually rely on that timing, it retries any attempts that + * fail to load as early as expected, but don't load at any illegal time. + */ + +add_task(async function testExecuteScript() { + const win = window.open("about:blank"); + + async function background(DEBUG) { + let tab; + + const BASE = "http://mochi.test:8888/tests/mobile/android/components/extensions/test/mochitest/"; + const URL = BASE + "file_slowed_document.sjs"; + + const MAX_TRIES = 30; + + const onUpdatedPromise = (tabId, url, status) => { + return new Promise(resolve => { + browser.tabs.onUpdated.addListener(function listener(_, changed, tab) { + if (tabId == tab.id && changed.status == status && tab.url == url) { + browser.tabs.onUpdated.removeListener(listener); + resolve(); + } + }); + }); + }; + + try { + [tab] = await browser.tabs.query({active: true, currentWindow: true}); + + let success = false; + for (let tries = 0; !success && tries < MAX_TRIES; tries++) { + const url = `${URL}?with-iframe&r=${Math.random()}`; + + const loadingPromise = onUpdatedPromise(tab.id, url, "loading"); + const completePromise = onUpdatedPromise(tab.id, url, "complete"); + + // TODO: Test allFrames and frameId. + + await browser.tabs.update({url}); + await loadingPromise; + + const states = await Promise.all([ + // Send the executeScript requests in the reverse order that we expect + // them to execute in, to avoid them passing only because of timing + // races. + browser.tabs.executeScript({ + code: "document.readyState", + runAt: "document_idle", + }), + browser.tabs.executeScript({ + code: "document.readyState", + runAt: "document_end", + }), + browser.tabs.executeScript({ + code: "document.readyState", + runAt: "document_start", + }), + ].reverse()); + + browser.test.log(`Got states: ${states}`); + + // Make sure that none of our scripts executed earlier than expected, + // regardless of retries. + browser.test.assertTrue(states[1] == "interactive" || states[1] == "complete", + `document_end state is valid: ${states[1]}`); + browser.test.assertTrue(states[2] == "interactive" || states[2] == "complete", + `document_idle state is valid: ${states[2]}`); + + // If we have the earliest valid states for each script, we're done. + // Otherwise, try again. + success = ((states[0] == "loading" || DEBUG) && + states[1] == "interactive" && + (states[2] == "interactive" || states[2] == "complete")); + + await completePromise; + } + + browser.test.assertTrue(success, "Got the earliest expected states at least once"); + + browser.test.notifyPass("executeScript-runAt"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("executeScript-runAt"); + } + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://mochi.test/", "tabs"], + }, + background: `(${background})(${AppConstants.DEBUG})`, + }); + + await extension.startup(); + + await extension.awaitFinish("executeScript-runAt"); + + await extension.unload(); + + win.close(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_get.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_get.html new file mode 100644 index 0000000000..109ab8f65c --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_get.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs get Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function() { + const extension = ExtensionTestUtils.loadExtension({ + async background() { + const tab1 = await browser.tabs.create({}); + const tab2 = await browser.tabs.create({}); + browser.test.assertEq(tab1.id, (await browser.tabs.get(tab1.id)).id, "tabs.get should return tab with given id"); + browser.test.assertEq(tab2.id, (await browser.tabs.get(tab2.id)).id, "tabs.get should return tab with given id"); + await browser.tabs.remove(tab1.id); + await browser.tabs.remove(tab2.id); + browser.test.notifyPass("tabs.get"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("tabs.get"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_getCurrent.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_getCurrent.html new file mode 100644 index 0000000000..c32f93f44a --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_getCurrent.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs getCurrent Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + files: { + "tab.js": function() { + const url = document.location.href; + + browser.tabs.getCurrent().then(currentTab => { + browser.test.assertEq(currentTab.url, url, "getCurrent in non-active background tab"); + + // Activate the tab. + browser.tabs.onActivated.addListener(function listener({tabId}) { + if (tabId == currentTab.id) { + browser.tabs.onActivated.removeListener(listener); + + browser.tabs.getCurrent().then(currentTab => { + browser.test.assertEq(currentTab.id, tabId, "in active background tab"); + browser.test.assertEq(currentTab.url, url, "getCurrent in non-active background tab"); + + browser.test.sendMessage("tab-finished"); + }); + } + }); + browser.tabs.update(currentTab.id, {active: true}); + }); + }, + + "tab.html": `<head><meta charset="utf-8"><script src="tab.js"><\/script></head>`, + }, + + background: function() { + browser.tabs.getCurrent().then(tab => { + browser.test.assertEq(tab, undefined, "getCurrent in background script"); + browser.test.sendMessage("background-finished"); + }); + + browser.tabs.create({url: "tab.html", active: false}); + }, + }); + + await extension.startup(); + + await extension.awaitMessage("background-finished"); + await extension.awaitMessage("tab-finished"); + + // The extension tab is automatically closed when the extension unloads. + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_goBack_goForward.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_goBack_goForward.html new file mode 100644 index 0000000000..0d143e2ac6 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_goBack_goForward.html @@ -0,0 +1,134 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Tabs goBack and goForward Test</title> + <script + type="text/javascript" + src="/tests/SimpleTest/SimpleTest.js" + ></script> + <script + type="text/javascript" + src="/tests/SimpleTest/ExtensionTestUtils.js" + ></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <script type="text/javascript"> + "use strict"; + + add_task(async function test_tabs_goBack_goForward() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + }, + + files: { + "tab1.html": `<head> + <meta charset=UTF-8"> + <title>tab1</title> + </head>`, + "tab2.html": `<head> + <meta charset=UTF-8"> + <title>tab2</title> + </head>`, + }, + + async background() { + let tabUpdatedCount = 0; + let tab = {}; + + browser.tabs.onUpdated.addListener( + async (tabId, changeInfo, tabInfo) => { + if ( + changeInfo.status !== "complete" || + tabId !== tab.id || + tabInfo.url === "about:blank" + ) { + return; + } + + tabUpdatedCount++; + switch (tabUpdatedCount) { + case 1: + browser.test.assertEq( + "tab1", + tabInfo.title, + "tab1 is found as expected" + ); + browser.tabs.update(tabId, { url: "tab2.html" }); + break; + + case 2: + browser.test.assertEq( + "tab2", + tabInfo.title, + "tab2 is found as expected" + ); + browser.tabs.update(tabId, { url: "tab1.html" }); + break; + + case 3: + browser.test.assertEq( + "tab1", + tabInfo.title, + "tab1 is found as expected" + ); + browser.tabs.goBack(); + break; + + case 4: + browser.test.assertEq( + "tab2", + tabInfo.title, + "tab2 is found after navigating backward with empty parameter" + ); + browser.tabs.goBack(tabId); + break; + + case 5: + browser.test.assertEq( + "tab1", + tabInfo.title, + "tab1 is found after navigating backward with tabId as parameter" + ); + browser.tabs.goForward(); + break; + + case 6: + browser.test.assertEq( + "tab2", + tabInfo.title, + "tab2 is found after navigating forward with empty parameter" + ); + browser.tabs.goForward(tabId); + break; + + case 7: + browser.test.assertEq( + "tab1", + tabInfo.title, + "tab1 is found after navigating forward with tabId as parameter" + ); + await browser.tabs.remove(tabId); + browser.test.notifyPass("tabs.goBack.goForward"); + break; + + default: + break; + } + } + ); + + tab = await browser.tabs.create({ url: "tab1.html", active: true }); + }, + }); + + await extension.startup(); + await extension.awaitFinish("tabs.goBack.goForward"); + await extension.unload(); + }); + </script> + </body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_insertCSS.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_insertCSS.html new file mode 100644 index 0000000000..718c2d6de4 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_insertCSS.html @@ -0,0 +1,124 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs executeScript Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function testExecuteScript() { + const win = window.open("http://mochi.test:8888/"); + + async function background() { + const tasks = [ + { + description: "CSS as moz-extension:// url", + background: "rgba(0, 0, 0, 0)", + foreground: "rgb(0, 113, 4)", + promise: () => { + return browser.tabs.insertCSS({ + file: "file2.css", + }); + }, + }, + { + description: "CSS as code snippet string", + background: "rgb(42, 42, 42)", + foreground: "rgb(0, 113, 4)", + promise: () => { + return browser.tabs.insertCSS({ + code: "* { background: rgb(42, 42, 42) }", + }); + }, + }, + { + description: "last of two author CSS wins", + background: "rgb(42, 42, 42)", + foreground: "rgb(0, 113, 4)", + promise: async () => { + await browser.tabs.insertCSS({ + code: "* { background: rgb(100, 100, 100) !important }", + cssOrigin: "author", + }); + await browser.tabs.insertCSS({ + code: "* { background: rgb(42, 42, 42) !important }", + cssOrigin: "author", + }); + }, + }, + { + description: "user CSS has higher priority", + background: "rgb(100, 100, 100)", + foreground: "rgb(0, 113, 4)", + promise: async () => { + // User has higher importance + await browser.tabs.insertCSS({ + code: "* { background: rgb(100, 100, 100) !important }", + cssOrigin: "user", + }); + await browser.tabs.insertCSS({ + code: "* { background: rgb(42, 42, 42) !important }", + cssOrigin: "author", + }); + }, + }, + ]; + + function checkCSS() { + const computedStyle = window.getComputedStyle(document.body); + return [computedStyle.backgroundColor, computedStyle.color]; + } + + try { + for (const {background, description, foreground, promise} of tasks) { + browser.test.log(`Run test case: ${description}`); + let result = await promise(); + + browser.test.assertEq(undefined, result, "Expected callback result"); + + [result] = await browser.tabs.executeScript({ + code: `(${checkCSS})()`, + }); + + browser.test.assertEq(background, result[0], "Expected background color"); + browser.test.assertEq(foreground, result[1], "Expected foreground color"); + } + + browser.test.notifyPass("insertCSS"); + } catch (e) { + browser.test.fail(`Error: ${e} :: ${e.stack}`); + browser.test.notifyFail("insertCSS"); + } + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["http://mochi.test/"], + }, + + background, + + files: { + "file2.css": "* { color: rgb(0, 113, 4) }", + }, + }); + + await extension.startup(); + + await extension.awaitFinish("insertCSS"); + + await extension.unload(); + + win.close(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html new file mode 100644 index 0000000000..5bb44ab645 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs lastAccessed Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function testLastAccessed() { + const past = Date.now(); + + window.open("https://example.com/?1"); + window.open("https://example.com/?2"); + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["tabs"], + }, + async background() { + browser.test.onMessage.addListener(async function(msg, past) { + if (msg !== "past") { + return; + } + + const [tab1] = await browser.tabs.query({url: "https://example.com/?1"}); + const [tab2] = await browser.tabs.query({url: "https://example.com/?2"}); + + browser.test.assertTrue(tab1 && tab2, "Expected tabs were found"); + + const now = Date.now(); + + browser.test.assertTrue(typeof tab1.lastAccessed == "number", + "tab1 lastAccessed should be a number"); + + browser.test.assertTrue(typeof tab2.lastAccessed == "number", + "tab2 lastAccessed should be a number"); + + browser.test.assertTrue(past <= tab1.lastAccessed && + tab1.lastAccessed <= tab2.lastAccessed && + tab2.lastAccessed <= now, + "lastAccessed timestamps are recent and in the right order"); + + await browser.tabs.remove([tab1.id, tab2.id]); + + browser.test.notifyPass("tabs.lastAccessed"); + }); + }, + }); + + await extension.startup(); + await extension.sendMessage("past", past); + await extension.awaitFinish("tabs.lastAccessed"); + await extension.unload(); +}); +</script> + +</body> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_onUpdated.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_onUpdated.html new file mode 100644 index 0000000000..8d96e79cc2 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_onUpdated.html @@ -0,0 +1,187 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs onUpdated Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_onUpdated() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://mochi.test/*/context_tabs_onUpdated_page.html"], + "js": ["content-script.js"], + "run_at": "document_start", + }], + }, + + background: function() { + const pageURL = "http://mochi.test:8888/tests/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html"; + + const expectedSequence = [ + {status: "loading"}, + {status: "loading", url: pageURL}, + {status: "complete"}, + ]; + + const collectedSequence = []; + + let tabId; + browser.tabs.onUpdated.addListener(function(tabId, updatedInfo) { + // onUpdated also fires with updatedInfo.faviconUrl, so explicitly + // check for updatedInfo.status before recording the event. + if ("status" in updatedInfo) { + collectedSequence.push(updatedInfo); + } + }); + + browser.runtime.onMessage.addListener(async msg => { + if (collectedSequence.length !== expectedSequence.length) { + browser.test.assertEq( + JSON.stringify(expectedSequence), + JSON.stringify(collectedSequence), + "got unexpected number of updateInfo data" + ); + } else { + for (let i = 0; i < expectedSequence.length; i++) { + browser.test.assertEq( + expectedSequence[i].status, + collectedSequence[i].status, + "check updatedInfo status" + ); + if (expectedSequence[i].url || collectedSequence[i].url) { + browser.test.assertEq( + expectedSequence[i].url, + collectedSequence[i].url, + "check updatedInfo url" + ); + } + } + } + + await browser.tabs.remove(tabId); + browser.test.notifyPass("tabs.onUpdated"); + }); + + browser.tabs.create({url: pageURL}).then(tab => { + tabId = tab.id; + }); + }, + files: { + "content-script.js": ` + window.addEventListener("message", function(evt) { + if (evt.data == "frame-updated") { + browser.runtime.sendMessage("load-completed"); + } + }, true); + `, + }, + }); + + await Promise.all([ + extension.startup(), + extension.awaitFinish("tabs.onUpdated"), + ]); + + await extension.unload(); +}); + +async function do_test_update(background, withPermissions = true) { + const manifest = {}; + if (withPermissions) { + manifest.permissions = ["tabs", "http://mochi.test/"]; + } + const extension = ExtensionTestUtils.loadExtension({ + manifest, + background, + }); + + await extension.startup(); + await extension.awaitFinish("finish"); + + await extension.unload(); +} + +add_task(async function test_url() { + await do_test_update(function background() { + // Create a new tab for testing update. + browser.tabs.create({}, function(tab) { + browser.tabs.onUpdated.addListener(async function onUpdated(tabId, changeInfo) { + // Check callback + browser.test.assertEq(tabId, tab.id, "Check tab id"); + browser.test.log("onUpdate: " + JSON.stringify(changeInfo)); + if ("url" in changeInfo) { + browser.test.assertEq("about:blank", changeInfo.url, + "Check changeInfo.url"); + browser.tabs.onUpdated.removeListener(onUpdated); + // Remove created tab. + await browser.tabs.remove(tabId); + browser.test.notifyPass("finish"); + } + }); + browser.tabs.update(tab.id, {url: "about:blank"}); + }); + }); +}); + +add_task(async function test_title() { + await do_test_update(async function background() { + const url = "http://mochi.test:8888/tests/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html"; + const tab = await browser.tabs.create({url}); + + browser.tabs.onUpdated.addListener(async function onUpdated(tabId, changeInfo) { + browser.test.assertEq(tabId, tab.id, "Check tab id"); + browser.test.log(`onUpdated: ${JSON.stringify(changeInfo)}`); + if ("title" in changeInfo && changeInfo.title === "New Message (1)") { + browser.test.log("changeInfo.title is correct"); + browser.tabs.onUpdated.removeListener(onUpdated); + await browser.tabs.remove(tabId); + browser.test.notifyPass("finish"); + } + }); + + browser.tabs.executeScript(tab.id, {code: "document.title = 'New Message (1)'"}); + }); +}); + +add_task(async function test_without_tabs_permission() { + await do_test_update(async function background() { + const url = "http://mochi.test:8888/tests/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html"; + const tab = await browser.tabs.create({url}); + let count = 0; + + browser.tabs.onUpdated.addListener(async function onUpdated(tabId, changeInfo) { + browser.test.assertEq(tabId, tab.id, "Check tab id"); + browser.test.log(`onUpdated: ${JSON.stringify(changeInfo)}`); + + browser.test.assertFalse("url" in changeInfo, "url should not be included without tabs permission"); + browser.test.assertFalse("favIconUrl" in changeInfo, "favIconUrl should not be included without tabs permission"); + browser.test.assertFalse("title" in changeInfo, "title should not be included without tabs permission"); + + if (changeInfo.status == "complete") { + count++; + if (count === 2) { + browser.test.log("Reload complete"); + browser.tabs.onUpdated.removeListener(onUpdated); + await browser.tabs.remove(tabId); + browser.test.notifyPass("finish"); + } + } + }); + + browser.tabs.reload(tab.id); + }, false /* withPermissions */); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_query.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_query.html new file mode 100644 index 0000000000..9a907f47de --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_query.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs create Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function test_query_index() { + const extension = ExtensionTestUtils.loadExtension({ + background: function() { + browser.tabs.onCreated.addListener(async function({index, windowId, id}) { + browser.test.assertThrows( + () => browser.tabs.query({index: -1}), + /-1 is too small \(must be at least 0\)/, + "tab indices must be non-negative"); + + let tabs = await browser.tabs.query({index, windowId}); + browser.test.assertEq(tabs.length, 1, `Got one tab at index ${index}`); + browser.test.assertEq(tabs[0].id, id, "The tab is the right one"); + + tabs = await browser.tabs.query({index: 1e5, windowId}); + browser.test.assertEq(tabs.length, 0, "There is no tab at this index"); + + browser.test.notifyPass("tabs.query"); + }); + }, + }); + + await extension.startup(); + const win = window.open("http://example.com"); + await extension.awaitFinish("tabs.query"); + win.close(); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload.html new file mode 100644 index 0000000000..30379f02a1 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs reload Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function() { + const extension = ExtensionTestUtils.loadExtension({ + files: { + "tab.js": function() { + browser.runtime.sendMessage("tab-loaded"); + }, + "tab.html": + `<head> + <meta charset="utf-8"> + <script src="tab.js"><\/script> + </head>`, + }, + + async background() { + let tabLoadedCount = 0; + // eslint-disable-next-line prefer-const + let tab; + + browser.runtime.onMessage.addListener(msg => { + if (msg == "tab-loaded") { + tabLoadedCount++; + + if (tabLoadedCount == 1) { + // Reload the tab once passing no arguments. + return browser.tabs.reload(); + } + + if (tabLoadedCount == 2) { + // Reload the tab again with explicit arguments. + return browser.tabs.reload(tab.id, { + bypassCache: false, + }); + } + + if (tabLoadedCount == 3) { + browser.test.notifyPass("tabs.reload"); + } + } + }); + tab = await browser.tabs.create({url: "tab.html", active: true}); + }, + }); + + await extension.startup(); + await extension.awaitFinish("tabs.reload"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload_bypass_cache.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload_bypass_cache.html new file mode 100644 index 0000000000..87f90ad855 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload_bypass_cache.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs executeScript bypassCache Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs", "<all_urls>"], + }, + + async background() { + const BASE = "http://mochi.test:8888/tests/mobile/android/components/extensions/test/mochitest/"; + const URL = BASE + "file_bypass_cache.sjs"; + + let tabId = null; + let loadPromise, resolveLoad; + function resetLoad() { + loadPromise = new Promise(resolve => { + resolveLoad = resolve; + }); + } + function awaitLoad() { + return loadPromise.then(() => { + resetLoad(); + }); + } + resetLoad(); + + browser.tabs.onUpdated.addListener(function listener( + tabId_, + changed, + tab + ) { + if (tabId == tabId_ && changed.status == "complete" && tab.url == URL) { + resolveLoad(); + } + }); + + try { + const tab = await browser.tabs.create({url: URL}); + tabId = tab.id; + await awaitLoad(); + + await browser.tabs.reload(tab.id, {bypassCache: false}); + await awaitLoad(); + + let [textContent] = await browser.tabs.executeScript(tab.id, {code: "document.body.textContent"}); + browser.test.assertEq("", textContent, "`textContent` should be empty when bypassCache=false"); + + await browser.tabs.reload(tab.id, {bypassCache: true}); + await awaitLoad(); + + [textContent] = await browser.tabs.executeScript(tab.id, {code: "document.body.textContent"}); + + const [pragma, cacheControl] = textContent.split(":"); + browser.test.assertEq("no-cache", pragma, "`pragma` should be set to `no-cache` when bypassCache is true"); + browser.test.assertEq("no-cache", cacheControl, "`cacheControl` should be set to `no-cache` when bypassCache is true"); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("tabs.reload_bypass_cache"); + } catch (error) { + browser.test.fail(`${error} :: ${error.stack}`); + browser.test.notifyFail("tabs.reload_bypass_cache"); + } + }, + }); + + await extension.startup(); + await extension.awaitFinish("tabs.reload_bypass_cache"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html new file mode 100644 index 0000000000..320ce4dde6 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html @@ -0,0 +1,277 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs sendMessage Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function tabsSendMessageReply() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://example.com/"], + "js": ["content-script.js"], + "run_at": "document_start", + }], + }, + + background: async function() { + // eslint-disable-next-line prefer-const + let firstTab; + const promiseResponse = new Promise(resolve => { + browser.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg == "content-script-ready") { + const tabId = sender.tab.id; + + Promise.all([ + promiseResponse, + + browser.tabs.sendMessage(tabId, "respond-now"), + browser.tabs.sendMessage(tabId, "respond-now-2"), + new Promise(resolve => browser.tabs.sendMessage(tabId, "respond-soon", resolve)), + browser.tabs.sendMessage(tabId, "respond-promise"), + browser.tabs.sendMessage(tabId, "respond-promise-false"), + browser.tabs.sendMessage(tabId, "respond-false"), + browser.tabs.sendMessage(tabId, "respond-never"), + new Promise(resolve => { + browser.runtime.sendMessage("respond-never", response => { resolve(response); }); + }), + + browser.tabs.sendMessage(tabId, "respond-error").catch(error => Promise.resolve({error})), + browser.tabs.sendMessage(tabId, "throw-error").catch(error => Promise.resolve({error})), + + browser.tabs.sendMessage(tabId, "respond-uncloneable").catch(error => Promise.resolve({ error })), + browser.tabs.sendMessage(tabId, "reject-uncloneable").catch(error => Promise.resolve({ error })), + browser.tabs.sendMessage(tabId, "reject-undefined").catch(error => Promise.resolve({ error })), + browser.tabs.sendMessage(tabId, "throw-undefined").catch(error => Promise.resolve({ error })), + + browser.tabs.sendMessage(firstTab, "no-listener").catch(error => Promise.resolve({error})), + ]).then(([response, respondNow, respondNow2, respondSoon, respondPromise, respondPromiseFalse, respondFalse, respondNever, respondNever2, respondError, throwError, respondUncloneable, rejectUncloneable, rejectUndefined, throwUndefined, noListener]) => { + browser.test.assertEq("expected-response", response, "Content script got the expected response"); + + browser.test.assertEq("respond-now", respondNow, "Got the expected immediate response"); + browser.test.assertEq("respond-now-2", respondNow2, "Got the expected immediate response from the second listener"); + browser.test.assertEq("respond-soon", respondSoon, "Got the expected delayed response"); + browser.test.assertEq("respond-promise", respondPromise, "Got the expected promise response"); + browser.test.assertEq(false, respondPromiseFalse, "Got the expected false value as a promise result"); + browser.test.assertEq(undefined, respondFalse, "Got the expected no-response when onMessage returns false"); + browser.test.assertEq(undefined, respondNever, "Got the expected no-response resolution"); + browser.test.assertEq(undefined, respondNever2, "Got the expected no-response resolution"); + + browser.test.assertEq("respond-error", respondError.error.message, "Got the expected error response"); + browser.test.assertEq("throw-error", throwError.error.message, "Got the expected thrown error response"); + + browser.test.assertEq("Could not establish connection. Receiving end does not exist.", respondUncloneable.error.message, "An uncloneable response should be ignored"); + browser.test.assertEq("An unexpected error occurred", rejectUncloneable.error.message, "Got the expected error for a rejection with an uncloneable value"); + browser.test.assertEq("An unexpected error occurred", rejectUndefined.error.message, "Got the expected error for a void rejection"); + browser.test.assertEq("An unexpected error occurred", throwUndefined.error.message, "Got the expected error for a void throw"); + + browser.test.assertEq("Could not establish connection. Receiving end does not exist.", + noListener.error.message, + "Got the expected no listener response"); + + return browser.tabs.remove(tabId); + }).then(() => { + browser.test.notifyPass("sendMessage"); + }); + + return Promise.resolve("expected-response"); + } else if (msg[0] == "got-response") { + resolve(msg[1]); + } + }); + }); + + const tabs = await browser.tabs.query({currentWindow: true, active: true}); + firstTab = tabs[0].id; + browser.tabs.create({url: "http://example.com/"}); + }, + + files: { + "content-script.js": async function() { + browser.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg == "respond-now") { + respond(msg); + } else if (msg == "respond-soon") { + setTimeout(() => { respond(msg); }, 0); + return true; + } else if (msg == "respond-promise") { + return Promise.resolve(msg); + } else if (msg == "respond-promise-false") { + return Promise.resolve(false); + } else if (msg == "respond-false") { + // return false means that respond() is not expected to be called. + setTimeout(() => respond("should be ignored")); + return false; + } else if (msg == "respond-never") { + return undefined; + } else if (msg == "respond-error") { + return Promise.reject(new Error(msg)); + } else if (msg === "respond-uncloneable") { + return Promise.resolve(window); + } else if (msg === "reject-uncloneable") { + return Promise.reject(window); + } else if (msg == "reject-undefined") { + return Promise.reject(); + } else if (msg == "throw-undefined") { + throw undefined; // eslint-disable-line no-throw-literal + } else if (msg == "throw-error") { + throw new Error(msg); + } + }); + + browser.runtime.onMessage.addListener((msg, sender, respond) => { + if (msg == "respond-now") { + respond("hello"); + } else if (msg == "respond-now-2") { + respond(msg); + } + }); + + const response = await browser.runtime.sendMessage("content-script-ready"); + browser.runtime.sendMessage(["got-response", response]); + }, + }, + }); + + await extension.startup(); + + await extension.awaitFinish("sendMessage"); + + await extension.unload(); +}); + + +add_task(async function tabsSendHidden() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + "content_scripts": [{ + "matches": ["http://example.com/content*"], + "js": ["content-script.js"], + "run_at": "document_start", + }], + }, + + background: async function() { + let resolveContent; + browser.runtime.onMessage.addListener((msg, sender) => { + if (msg[0] == "content-ready") { + resolveContent(msg[1]); + } + }); + + const awaitContent = url => { + return new Promise(resolve => { + resolveContent = resolve; + }).then(result => { + browser.test.assertEq(url, result, "Expected content script URL"); + }); + }; + + try { + const URL1 = "http://example.com/content1.html"; + const URL2 = "http://example.com/content2.html"; + + const tab = await browser.tabs.create({url: URL1}); + await awaitContent(URL1); + + let url = await browser.tabs.sendMessage(tab.id, URL1); + browser.test.assertEq(URL1, url, "Should get response from expected content window"); + + await browser.tabs.update(tab.id, {url: URL2}); + await awaitContent(URL2); + + url = await browser.tabs.sendMessage(tab.id, URL2); + browser.test.assertEq(URL2, url, "Should get response from expected content window"); + + // Repeat once just to be sure the first message was processed by all + // listeners before we exit the test. + url = await browser.tabs.sendMessage(tab.id, URL2); + browser.test.assertEq(URL2, url, "Should get response from expected content window"); + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("contentscript-bfcache-window"); + } catch (error) { + browser.test.fail(`Error: ${error} :: ${error.stack}`); + browser.test.notifyFail("contentscript-bfcache-window"); + } + }, + + files: { + "content-script.js": function() { + // Store this in a local variable to make sure we don't touch any + // properties of the possibly-hidden content window. + const href = window.location.href; + + browser.runtime.onMessage.addListener((msg, sender) => { + browser.test.assertEq(href, msg, "Should be in the expected content window"); + + return Promise.resolve(href); + }); + + browser.runtime.sendMessage(["content-ready", href]); + }, + }, + }); + + await extension.startup(); + + await extension.awaitFinish("contentscript-bfcache-window"); + + await extension.unload(); +}); + + +add_task(async function tabsSendMessageNoExceptionOnNonExistentTab() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", false]], + }); + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + async background() { + const url = "http://example.com/mochitest/tests/mobile/android/components/extensions/test/mochitest/file_dummy.html"; + const tab = await browser.tabs.create({url}); + + try { + browser.tabs.sendMessage(tab.id, "message"); + browser.tabs.sendMessage(tab.id + 100, "message"); + } catch (e) { + browser.test.fail("no exception should be raised on tabs.sendMessage to nonexistent tabs"); + } + + await browser.tabs.remove(tab.id); + + browser.test.notifyPass("tabs.sendMessage"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("tabs.sendMessage"); + + await extension.unload(); +}); + +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_update_url.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_update_url.html new file mode 100644 index 0000000000..9332efd516 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_update_url.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tabs update Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +async function testTabsUpdateURL(existentTabURL, tabsUpdateURL, isErrorExpected) { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + "permissions": ["tabs"], + }, + + files: { + "tab.html": ` + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>tab page</h1> + </body> + </html> + `.trim(), + }, + background: function() { + browser.test.sendMessage("ready", browser.runtime.getURL("tab.html")); + + browser.test.onMessage.addListener(async (msg, tabsUpdateURL, isErrorExpected) => { + const tabs = await browser.tabs.query({lastFocusedWindow: true}); + + try { + const tab = await browser.tabs.update(tabs[0].id, {url: tabsUpdateURL}); + + browser.test.assertFalse(isErrorExpected, `tabs.update with URL ${tabsUpdateURL} should be rejected`); + browser.test.assertTrue(tab, "on success the tab should be defined"); + } catch (error) { + browser.test.assertTrue(isErrorExpected, `tabs.update with URL ${tabsUpdateURL} should not be rejected`); + browser.test.assertTrue(/^Illegal URL/.test(error.message), + "tabs.update should be rejected with the expected error message"); + } + + browser.test.sendMessage("done"); + }); + }, + }); + + await extension.startup(); + + const mozExtTabURL = await extension.awaitMessage("ready"); + + if (tabsUpdateURL == "self") { + tabsUpdateURL = mozExtTabURL; + } + + info(`tab.update URL "${tabsUpdateURL}" on tab with URL "${existentTabURL}"`); + + const tab1 = window.open(existentTabURL); + + extension.sendMessage("start", tabsUpdateURL, isErrorExpected); + await extension.awaitMessage("done"); + + tab1.close(); + await extension.unload(); +} + +add_task(async function() { + info("Start testing tabs.update on javascript URLs"); + + const dataURLPage = `data:text/html, + <!DOCTYPE html> + <html> + <head> + <meta charset="utf-8"> + </head> + <body> + <h1>data url page</h1> + </body> + </html>`; + + const checkList = [ + { + tabsUpdateURL: "http://example.net", + isErrorExpected: false, + }, + { + tabsUpdateURL: "self", + isErrorExpected: false, + }, + { + tabsUpdateURL: "about:addons", + isErrorExpected: true, + }, + { + tabsUpdateURL: "javascript:console.log('tabs.update execute javascript')", + isErrorExpected: true, + }, + { + tabsUpdateURL: dataURLPage, + isErrorExpected: true, + }, + ]; + + const testCases = checkList + .map((check) => Object.assign({}, check, {existentTabURL: "about:blank"})); + + for (const {existentTabURL, tabsUpdateURL, isErrorExpected} of testCases) { + await testTabsUpdateURL(existentTabURL, tabsUpdateURL, isErrorExpected); + } + + info("done"); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_webNavigation_onCommitted.html b/mobile/android/components/extensions/test/mochitest/test_ext_webNavigation_onCommitted.html new file mode 100644 index 0000000000..33f178492d --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_webNavigation_onCommitted.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>WebNavigation onCommitted Test</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <script type="text/javascript" src="head.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +add_task(async function() { + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webNavigation", "tabs"], + }, + async background() { + const url = "http://mochi.test:8888/"; + const [tab, tabDetails] = await Promise.all([ + browser.tabs.create({url}), + new Promise(resolve => { + browser.webNavigation.onCommitted.addListener(details => { + if (details.url === "about:blank") { + // skip initial about:blank + return; + } + resolve(details); + }); + }), + ]); + + browser.test.assertEq(url, tabDetails.url, "webNavigation.onCommitted detects correct url"); + browser.test.assertEq(tab.id, tabDetails.tabId, "webNavigation.onCommitted fire for proper tabId"); + await browser.tabs.remove(tab.id); + browser.test.notifyPass("webNavigation.onCommitted"); + }, + }); + + await extension.startup(); + await extension.awaitFinish("webNavigation.onCommitted"); + await extension.unload(); +}); +</script> + +</body> +</html> diff --git a/mobile/android/components/extensions/test/xpcshell/.eslintrc.js b/mobile/android/components/extensions/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..2e6d214f4b --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + extends: + "../../../../../../toolkit/components/extensions/test/xpcshell/.eslintrc.js", +}; diff --git a/mobile/android/components/extensions/test/xpcshell/head.js b/mobile/android/components/extensions/test/xpcshell/head.js new file mode 100644 index 0000000000..e79781fba6 --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/head.js @@ -0,0 +1,24 @@ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", + ExtensionTestUtils: + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs", +}); + +// Remove this pref once bug 1535365 is fixed. +Services.prefs.setBoolPref("extensions.webextensions.remote", false); + +// https_first automatically upgrades http to https, but the tests are not +// designed to expect that. And it is not easy to change that because +// nsHttpServer does not support https (bug 1742061). So disable https_first. +Services.prefs.setBoolPref("dom.security.https_first", false); + +ExtensionTestUtils.init(this); + +Services.io.offline = true; + +var createHttpServer = (...args) => { + AddonTestUtils.maybeInit(this); + return AddonTestUtils.createHttpServer(...args); +}; diff --git a/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_geckoview.js b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_geckoview.js new file mode 100644 index 0000000000..3ba2e26139 --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_geckoview.js @@ -0,0 +1,424 @@ +"use strict"; + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerPathHandler("/", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write("<!DOCTYPE html><html></html>"); +}); + +ChromeUtils.defineESModuleGetters(this, { + GeckoViewConnection: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", +}); + +// Save reference to original implementations to restore later. +const { sendMessage, onConnect } = GeckoViewConnection.prototype; +add_setup(async () => { + // This file replaces the implementation of GeckoViewConnection; + // make sure that it is restored upon test completion. + registerCleanupFunction(() => { + GeckoViewConnection.prototype.sendMessage = sendMessage; + GeckoViewConnection.prototype.onConnect = onConnect; + }); +}); + +// Mock the embedder communication port +class EmbedderPort { + constructor(portId, messenger) { + this.id = portId; + this.messenger = messenger; + } + close() { + Assert.ok(false, "close not expected to be called"); + } + onPortDisconnect() { + Assert.ok(false, "onPortDisconnect not expected to be called"); + } + onPortMessage(holder) { + Assert.ok(false, "onPortMessage not expected to be called"); + } + triggerPortDisconnect() { + this.messenger.sendPortDisconnect(this.id); + } +} + +function stubConnectNative() { + let port; + const firstCallPromise = new Promise(resolve => { + let callCount = 0; + GeckoViewConnection.prototype.onConnect = (portId, messenger) => { + Assert.equal(++callCount, 1, "onConnect called once"); + port = new EmbedderPort(portId, messenger); + resolve(); + return port; + }; + }); + const triggerPortDisconnect = () => { + if (!port) { + Assert.ok(false, "Undefined port, connection must be established first"); + } + port.triggerPortDisconnect(); + }; + const restore = () => { + GeckoViewConnection.prototype.onConnect = onConnect; + }; + return { firstCallPromise, triggerPortDisconnect, restore }; +} + +function stubSendNativeMessage() { + let sendResponse; + const returnPromise = new Promise(resolve => { + sendResponse = resolve; + }); + const firstCallPromise = new Promise(resolve => { + let callCount = 0; + GeckoViewConnection.prototype.sendMessage = data => { + Assert.equal(++callCount, 1, "sendMessage called once"); + resolve(data); + return returnPromise; + }; + }); + const restore = () => { + GeckoViewConnection.prototype.sendMessage = sendMessage; + }; + return { firstCallPromise, sendResponse, restore }; +} + +function promiseExtensionEvent(wrapper, event) { + return new Promise(resolve => { + wrapper.extension.once(event, (...args) => resolve(args)); + }); +} + +// verify that when background sends a native message, +// the background will not be terminated to allow native messaging +add_task(async function test_sendNativeMessage_event_page() { + const extension = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + permissions: ["geckoViewAddons", "nativeMessaging"], + background: { persistent: false }, + }, + async background() { + const res = await browser.runtime.sendNativeMessage("fake", "msg"); + browser.test.assertEq("myResp", res, "expected response"); + browser.test.sendMessage("done"); + browser.runtime.onSuspend.addListener(async () => { + browser.test.assertFail("unexpected onSuspend"); + }); + }, + }); + + const stub = stubSendNativeMessage(); + await extension.startup(); + info("Wait for sendNativeMessage to be received"); + Assert.equal( + (await stub.firstCallPromise).deserialize({}), + "msg", + "expected message" + ); + + info("Trigger background script idle timeout and expect to be reset"); + const promiseResetIdle = promiseExtensionEvent( + extension, + "background-script-reset-idle" + ); + await extension.terminateBackground(); + info("Wait for 'background-script-reset-idle' event to be emitted"); + await promiseResetIdle; + + stub.sendResponse("myResp"); + + info("Wait for extension to verify sendNativeMessage response"); + await extension.awaitMessage("done"); + await extension.unload(); + + stub.restore(); +}); + +// verify that when an extension tab sends a native message, +// the background will terminate as expected +add_task(async function test_sendNativeMessage_tab() { + const extension = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + permissions: ["geckoViewAddons", "nativeMessaging"], + background: { persistent: false }, + }, + async background() { + browser.runtime.onSuspend.addListener(async () => { + browser.test.sendMessage("onSuspend_called"); + }); + }, + files: { + "tab.html": ` + <!DOCTYPE html><meta charset="utf-8"> + <script src="tab.js"></script> + `, + "tab.js": async () => { + const res = await browser.runtime.sendNativeMessage("fake", "msg"); + browser.test.assertEq("myResp", res, "expected response"); + browser.test.sendMessage("content_done"); + }, + }, + }); + + const stub = stubSendNativeMessage(); + await extension.startup(); + + const tab = await ExtensionTestUtils.loadContentPage( + `moz-extension://${extension.uuid}/tab.html?tab`, + { extension } + ); + + info("Wait for sendNativeMessage to be received"); + Assert.equal( + (await stub.firstCallPromise).deserialize({}), + "msg", + "expected message" + ); + + info("Terminate extension"); + await extension.terminateBackground(); + await extension.awaitMessage("onSuspend_called"); + + stub.sendResponse("myResp"); + + info("Wait for extension to verify sendNativeMessage response"); + await extension.awaitMessage("content_done"); + await tab.close(); + await extension.unload(); + + stub.restore(); +}); + +// verify that when a content script sends a native message, +// the background will terminate as expected +add_task(async function test_sendNativeMessage_content_script() { + const extension = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + permissions: [ + "geckoViewAddons", + "nativeMessaging", + "nativeMessagingFromContent", + ], + background: { persistent: false }, + content_scripts: [ + { + run_at: "document_end", + js: ["test.js"], + matches: ["http://example.com/"], + }, + ], + }, + files: { + "test.js": async () => { + const res = await browser.runtime.sendNativeMessage("fake", "msg"); + browser.test.assertEq("myResp", res, "expected response"); + browser.test.sendMessage("content_done"); + }, + }, + async background() { + browser.runtime.onSuspend.addListener(async () => { + browser.test.sendMessage("onSuspend_called"); + }); + }, + }); + + const stub = stubSendNativeMessage(); + await extension.startup(); + + info("Load content page"); + const page = await ExtensionTestUtils.loadContentPage("http://example.com/"); + + info("Wait for message from extension"); + Assert.equal( + (await stub.firstCallPromise).deserialize({}), + "msg", + "expected message" + ); + + info("Terminate extension"); + await extension.terminateBackground(); + await extension.awaitMessage("onSuspend_called"); + + stub.sendResponse("myResp"); + + info("Wait for extension to verify sendNativeMessage response"); + await extension.awaitMessage("content_done"); + await page.close(); + await extension.unload(); + + stub.restore(); +}); + +// verify that when native messaging ports are open, the background will not be terminated +// and once the ports disconnect, onSuspend can be called +add_task(async function test_connectNative_event_page() { + const extension = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + permissions: ["geckoViewAddons", "nativeMessaging"], + background: { persistent: false }, + }, + async background() { + const port = browser.runtime.connectNative("test"); + port.onDisconnect.addListener(() => { + browser.test.assertEq( + null, + port.error, + "port should be disconnected without errors" + ); + browser.test.sendMessage("port_disconnected"); + }); + + browser.runtime.onSuspend.addListener(async () => { + browser.test.sendMessage("onSuspend_called"); + }); + }, + }); + + const stub = stubConnectNative(); + await extension.startup(); + info("Waiting for connectNative request"); + await stub.firstCallPromise; + + info("Trigger background script idle timeout and expect to be reset"); + const promiseResetIdle = promiseExtensionEvent( + extension, + "background-script-reset-idle" + ); + + await extension.terminateBackground(); + info("Wait for 'background-script-reset-idle' event to be emitted"); + await promiseResetIdle; + + info("Trigger port disconnect, terminate background, and expect onSuspend()"); + stub.triggerPortDisconnect(); + await extension.awaitMessage("port_disconnected"); + + info("Terminate extension"); + await extension.terminateBackground(); + await extension.awaitMessage("onSuspend_called"); + + await extension.unload(); + stub.restore(); +}); + +// verify that when an extension tab opens native messaging ports, +// the background will terminate as expected +add_task(async function test_connectNative_tab() { + const extension = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + permissions: ["geckoViewAddons", "nativeMessaging"], + background: { persistent: false }, + }, + async background() { + browser.runtime.onSuspend.addListener(async () => { + browser.test.sendMessage("onSuspend_called"); + }); + }, + files: { + "tab.html": ` + <!DOCTYPE html><meta charset="utf-8"> + <script src="tab.js"></script> + `, + "tab.js": async () => { + const port = browser.runtime.connectNative("test"); + port.onDisconnect.addListener(() => { + browser.test.assertEq( + null, + port.error, + "port should be disconnected without errors" + ); + browser.test.sendMessage("port_disconnected"); + }); + browser.test.sendMessage("content_done"); + }, + }, + }); + + const stub = stubConnectNative(); + await extension.startup(); + + const tab = await ExtensionTestUtils.loadContentPage( + `moz-extension://${extension.uuid}/tab.html?tab`, + { extension } + ); + await extension.awaitMessage("content_done"); + await stub.firstCallPromise; + + info("Terminate extension"); + await extension.terminateBackground(); + await extension.awaitMessage("onSuspend_called"); + + stub.triggerPortDisconnect(); + await extension.awaitMessage("port_disconnected"); + await tab.close(); + await extension.unload(); + + stub.restore(); +}); + +// verify that when a content script opens native messaging ports, +// the background will terminate as expected +add_task(async function test_connectNative_content_script() { + const extension = ExtensionTestUtils.loadExtension({ + isPrivileged: true, + manifest: { + permissions: [ + "geckoViewAddons", + "nativeMessaging", + "nativeMessagingFromContent", + ], + background: { persistent: false }, + content_scripts: [ + { + run_at: "document_end", + js: ["test.js"], + matches: ["http://example.com/"], + }, + ], + }, + files: { + "test.js": async () => { + const port = browser.runtime.connectNative("test"); + port.onDisconnect.addListener(() => { + browser.test.assertEq( + null, + port.error, + "port should be disconnected without errors" + ); + browser.test.sendMessage("port_disconnected"); + }); + browser.test.sendMessage("content_done"); + }, + }, + async background() { + browser.runtime.onSuspend.addListener(async () => { + browser.test.sendMessage("onSuspend_called"); + }); + }, + }); + + const stub = stubConnectNative(); + await extension.startup(); + + info("Load content page"); + const page = await ExtensionTestUtils.loadContentPage("http://example.com/"); + await extension.awaitMessage("content_done"); + await stub.firstCallPromise; + + info("Terminate extension"); + await extension.terminateBackground(); + await extension.awaitMessage("onSuspend_called"); + + stub.triggerPortDisconnect(); + await extension.awaitMessage("port_disconnected"); + await page.close(); + await extension.unload(); + + stub.restore(); +}); diff --git a/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js new file mode 100644 index 0000000000..63f64b487e --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js @@ -0,0 +1,167 @@ +"use strict"; + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerPathHandler("/dum", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write("<!DOCTYPE html><html></html>"); +}); + +async function testNativeMessaging({ + isPrivileged = false, + permissions, + testBackground, + testContent, +}) { + async function runTest(testFn, completionMessage) { + try { + dump(`Running test before sending ${completionMessage}\n`); + await testFn(); + } catch (e) { + browser.test.fail(`Unexpected error: ${e}`); + } + browser.test.sendMessage(completionMessage); + } + const extension = ExtensionTestUtils.loadExtension({ + isPrivileged, + background: `(${runTest})(${testBackground}, "background_done");`, + manifest: { + content_scripts: [ + { + run_at: "document_end", + js: ["test.js"], + matches: ["http://example.com/dummy"], + }, + ], + permissions, + }, + files: { + "test.js": `(${runTest})(${testContent}, "content_done");`, + }, + }); + + // Run background script. + await extension.startup(); + await extension.awaitMessage("background_done"); + + // Run content script. + const page = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy" + ); + await extension.awaitMessage("content_done"); + await page.close(); + + await extension.unload(); +} + +// Checks that unprivileged extensions cannot use any of the nativeMessaging +// APIs on Android. +add_task(async function test_nativeMessaging_unprivileged() { + function testScript() { + browser.test.assertEq( + browser.runtime.connectNative, + undefined, + "connectNative should not be available in unprivileged extensions" + ); + browser.test.assertEq( + browser.runtime.sendNativeMessage, + undefined, + "sendNativeMessage should not be available in unprivileged extensions" + ); + } + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + await testNativeMessaging({ + isPrivileged: false, + permissions: [ + "geckoViewAddons", + "nativeMessaging", + "nativeMessagingFromContent", + ], + testBackground: testScript, + testContent: testScript, + }); + }); + AddonTestUtils.checkMessages(messages, { + expected: [ + { message: /Invalid extension permission: geckoViewAddons/ }, + { message: /Invalid extension permission: nativeMessaging/ }, + { message: /Invalid extension permission: nativeMessagingFromContent/ }, + ], + }); +}); + +// Checks that privileged extensions can still not use native messaging without +// the geckoViewAddons permission. +add_task(async function test_geckoViewAddons_missing() { + const ERROR_NATIVE_MESSAGE_FROM_BACKGROUND = + "Native manifests are not supported on android"; + const ERROR_NATIVE_MESSAGE_FROM_CONTENT = + /^Native messaging not allowed: \{.*"envType":"content_child","url":"http:\/\/example\.com\/dummy"\}$/; + + async function testBackground() { + await browser.test.assertRejects( + browser.runtime.sendNativeMessage("dummy_nativeApp", "DummyMsg"), + // Redacted error: ERROR_NATIVE_MESSAGE_FROM_BACKGROUND + "An unexpected error occurred", + "Background script cannot use nativeMessaging without geckoViewAddons" + ); + } + async function testContent() { + await browser.test.assertRejects( + browser.runtime.sendNativeMessage("dummy_nativeApp", "DummyMsg"), + // Redacted error: ERROR_NATIVE_MESSAGE_FROM_CONTENT + "An unexpected error occurred", + "Content script cannot use nativeMessaging without geckoViewAddons" + ); + } + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + await testNativeMessaging({ + isPrivileged: true, + permissions: ["nativeMessaging", "nativeMessagingFromContent"], + testBackground, + testContent, + }); + }); + AddonTestUtils.checkMessages(messages, { + expected: [ + { errorMessage: ERROR_NATIVE_MESSAGE_FROM_BACKGROUND }, + { errorMessage: ERROR_NATIVE_MESSAGE_FROM_CONTENT }, + ], + }); +}); + +// Checks that privileged extensions cannot use native messaging from content +// without the nativeMessagingFromContent permission. +add_task(async function test_nativeMessagingFromContent_missing() { + const ERROR_NATIVE_MESSAGE_FROM_CONTENT_NO_PERM = + /^Unexpected messaging sender: \{.*"envType":"content_child","url":"http:\/\/example\.com\/dummy"\}$/; + function testBackground() { + // sendNativeMessage / connectNative are expected to succeed, but we + // are not testing that here because XpcshellTestRunnerService does not + // have a WebExtension.MessageDelegate that handles the message. + // There are plenty of mochitests that rely on connectNative, so we are + // not testing that here. + } + async function testContent() { + await browser.test.assertRejects( + browser.runtime.sendNativeMessage("dummy_nativeApp", "DummyMsg"), + // Redacted error: ERROR_NATIVE_MESSAGE_FROM_CONTENT_NO_PERM + "An unexpected error occurred", + "Trying to get through to native messaging but without luck" + ); + } + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + await testNativeMessaging({ + isPrivileged: true, + permissions: ["geckoViewAddons", "nativeMessaging"], + testBackground, + testContent, + }); + }); + AddonTestUtils.checkMessages(messages, { + expected: [{ errorMessage: ERROR_NATIVE_MESSAGE_FROM_CONTENT_NO_PERM }], + }); +}); diff --git a/mobile/android/components/extensions/test/xpcshell/xpcshell.toml b/mobile/android/components/extensions/test/xpcshell/xpcshell.toml new file mode 100644 index 0000000000..c9486971a0 --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/xpcshell.toml @@ -0,0 +1,9 @@ +[DEFAULT] +head = "head.js" +firefox-appdir = "browser" +tags = "webextensions in-process-webextensions" +run-if = ["os == 'android'"] + +["test_ext_native_messaging_geckoview.js"] + +["test_ext_native_messaging_permissions.js"] |