summaryrefslogtreecommitdiffstats
path: root/mobile/android/components/extensions/test
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/components/extensions/test')
-rw-r--r--mobile/android/components/extensions/test/mochitest/.eslintrc.js6
-rw-r--r--mobile/android/components/extensions/test/mochitest/chrome.ini7
-rw-r--r--mobile/android/components/extensions/test/mochitest/context.html24
-rw-r--r--mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_iframe.html22
-rw-r--r--mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html21
-rw-r--r--mobile/android/components/extensions/test/mochitest/file_bypass_cache.sjs13
-rw-r--r--mobile/android/components/extensions/test/mochitest/file_dummy.html10
-rw-r--r--mobile/android/components/extensions/test/mochitest/file_iframe_document.html11
-rw-r--r--mobile/android/components/extensions/test/mochitest/file_slowed_document.sjs49
-rw-r--r--mobile/android/components/extensions/test/mochitest/mochitest.ini41
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html50
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_downloads_event_page.html102
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html498
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tab_runtimeConnect.html89
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_create.html153
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html302
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript.html252
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_bad.html151
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_no_create.html83
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_runAt.html128
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_get.html36
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_getCurrent.html70
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_goBack_goForward.html134
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_insertCSS.html124
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html64
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_onUpdated.html187
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_query.html46
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload.html66
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload_bypass_cache.html87
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html277
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_tabs_update_url.html125
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_webNavigation_onCommitted.html50
-rw-r--r--mobile/android/components/extensions/test/xpcshell/.eslintrc.js6
-rw-r--r--mobile/android/components/extensions/test/xpcshell/head.js24
-rw-r--r--mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js167
-rw-r--r--mobile/android/components/extensions/test/xpcshell/xpcshell.ini7
36 files changed, 3482 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.ini b/mobile/android/components/extensions/test/mochitest/chrome.ini
new file mode 100644
index 0000000000..901c516d0a
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/chrome.ini
@@ -0,0 +1,7 @@
+[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/mochitest.ini b/mobile/android/components/extensions/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..a8cadf7b5d
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/mochitest.ini
@@ -0,0 +1,41 @@
+[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_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_reload.html]
+[test_ext_tabs_reload_bypass_cache.html]
+[test_ext_tabs_onUpdated.html]
+[test_ext_tabs_query.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..c1220ff4a5
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -0,0 +1,50 @@
+<!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.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_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_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.ini b/mobile/android/components/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..f004140076
--- /dev/null
+++ b/mobile/android/components/extensions/test/xpcshell/xpcshell.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+head = head.js
+firefox-appdir = browser
+tags = webextensions in-process-webextensions
+skip-if = os != "android"
+
+[test_ext_native_messaging_permissions.js]