+
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ async function testPanel(browser) {
+ const popupPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+
+ // Wait the select element in the popup window to be ready before sending a
+ // mouse event to open the select popup.
+ await SpecialPowers.spawn(browser, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document && content.document.querySelector("#select");
+ });
+ });
+ BrowserTestUtils.synthesizeMouseAtCenter("#select", {}, browser);
+
+ const selectPopup = await popupPromise;
+
+ let elemRect = await SpecialPowers.spawn(browser, [], async function () {
+ let elem = content.document.getElementById("select");
+ let r = elem.getBoundingClientRect();
+
+ return { left: r.left, bottom: r.bottom };
+ });
+
+ let popupRect = selectPopup.getOuterScreenRect();
+ let marginTop = parseFloat(getComputedStyle(selectPopup).marginTop);
+ let marginLeft = parseFloat(getComputedStyle(selectPopup).marginLeft);
+
+ is(
+ Math.floor(browser.screenX + elemRect.left + marginLeft),
+ popupRect.left,
+ "Select popup has the correct x origin"
+ );
+
+ is(
+ Math.floor(browser.screenY + elemRect.bottom + marginTop),
+ popupRect.top,
+ "Select popup has the correct y origin"
+ );
+
+ // Close the select popup before proceeding to the next test.
+ const onPopupHidden = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popuphidden"
+ );
+ selectPopup.hidePopup();
+ await onPopupHidden;
+ }
+
+ {
+ info("Test browserAction popup");
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ await testPanel(browser);
+ await closeBrowserAction(extension);
+ }
+
+ {
+ info("Test pageAction popup");
+
+ clickPageAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ await testPanel(browser);
+ await closePageAction(extension);
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js b/browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js
new file mode 100644
index 0000000000..fa2c414047
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_select_in_oopif.js
@@ -0,0 +1,131 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// This test is based on browser_ext_popup_select.js.
+
+const iframeSrc = encodeURIComponent(`
+
+
+
+`);
+
+add_task(async function testPopupSelectPopup() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: false,
+ },
+ },
+
+ files: {
+ "popup.html": `
+
+
+
+
+
+
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ const browserForPopup = await openBrowserActionPanel(
+ extension,
+ undefined,
+ true
+ );
+
+ const iframe = await SpecialPowers.spawn(browserForPopup, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document && content.document.querySelector("iframe");
+ });
+ const iframeElement = content.document.querySelector("iframe");
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return iframeElement.browsingContext;
+ });
+ return iframeElement.browsingContext;
+ });
+
+ const selectRect = await SpecialPowers.spawn(iframe, [], async () => {
+ await ContentTaskUtils.waitForCondition(() => {
+ return content.document.querySelector("select");
+ });
+ const select = content.document.querySelector("select");
+ const focusPromise = new Promise(resolve => {
+ select.addEventListener("focus", resolve, { once: true });
+ });
+ select.focus();
+ await focusPromise;
+
+ const r = select.getBoundingClientRect();
+
+ return { left: r.left, bottom: r.bottom };
+ });
+
+ const popupPromise = BrowserTestUtils.waitForSelectPopupShown(window);
+
+ BrowserTestUtils.synthesizeMouseAtCenter("select", {}, iframe);
+
+ const selectPopup = await popupPromise;
+
+ let popupRect = selectPopup.getOuterScreenRect();
+ let popupMarginLeft = parseFloat(getComputedStyle(selectPopup).marginLeft);
+ let popupMarginTop = parseFloat(getComputedStyle(selectPopup).marginTop);
+
+ const offsetToSelectedItem =
+ selectPopup.querySelector("menuitem[selected]").getBoundingClientRect()
+ .top - selectPopup.getBoundingClientRect().top;
+ info(
+ `Browser is at ${browserForPopup.screenY}, popup is at ${popupRect.top} with ${offsetToSelectedItem} to the selected item`
+ );
+
+ is(
+ Math.floor(browserForPopup.screenX + selectRect.left),
+ popupRect.left - popupMarginLeft,
+ "Select popup has the correct x origin"
+ );
+
+ // On Mac select popup window appears aligned to the selected option.
+ let expectedY = navigator.platform.includes("Mac")
+ ? Math.floor(browserForPopup.screenY - offsetToSelectedItem)
+ : Math.floor(browserForPopup.screenY + selectRect.bottom);
+ is(
+ expectedY,
+ popupRect.top - popupMarginTop,
+ "Select popup has the correct y origin"
+ );
+
+ const onPopupHidden = BrowserTestUtils.waitForEvent(
+ selectPopup,
+ "popuphidden"
+ );
+ selectPopup.hidePopup();
+ await onPopupHidden;
+
+ await closeBrowserAction(extension);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js b/browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js
new file mode 100644
index 0000000000..632b929121
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_sendMessage.js
@@ -0,0 +1,135 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function test_popup_sendMessage_reply() {
+ let scriptPage = url =>
+ `${url}`;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+
+ page_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": scriptPage("popup.js"),
+ "popup.js": async function () {
+ browser.runtime.onMessage.addListener(async msg => {
+ if (msg == "popup-ping") {
+ return "popup-pong";
+ }
+ });
+
+ let response = await browser.runtime.sendMessage("background-ping");
+ browser.test.sendMessage("background-ping-response", response);
+ },
+ },
+
+ async background() {
+ browser.runtime.onMessage.addListener(async msg => {
+ if (msg == "background-ping") {
+ let response = await browser.runtime.sendMessage("popup-ping");
+
+ browser.test.sendMessage("popup-ping-response", response);
+
+ await new Promise(resolve => {
+ // Wait long enough that we're relatively sure the docShells have
+ // been swapped. Note that this value is fairly arbitrary. The load
+ // event that triggers the swap should happen almost immediately
+ // after the message is sent. The extra quarter of a second gives us
+ // enough leeway that we can expect to respond after the swap in the
+ // vast majority of cases.
+ setTimeout(resolve, 250);
+ });
+
+ return "background-pong";
+ }
+ });
+
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+
+ await browser.pageAction.show(tab.id);
+
+ browser.test.sendMessage("page-action-ready");
+ },
+ });
+
+ await extension.startup();
+
+ {
+ clickBrowserAction(extension);
+
+ let pong = await extension.awaitMessage("background-ping-response");
+ is(pong, "background-pong", "Got pong");
+
+ pong = await extension.awaitMessage("popup-ping-response");
+ is(pong, "popup-pong", "Got pong");
+
+ await closeBrowserAction(extension);
+ }
+
+ await extension.awaitMessage("page-action-ready");
+
+ {
+ clickPageAction(extension);
+
+ let pong = await extension.awaitMessage("background-ping-response");
+ is(pong, "background-pong", "Got pong");
+
+ pong = await extension.awaitMessage("popup-ping-response");
+ is(pong, "popup-pong", "Got pong");
+
+ await closePageAction(extension);
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_popup_close_then_sendMessage() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ },
+
+ files: {
+ "popup.html": `ghost`,
+ "popup.js"() {
+ browser.tabs.query({ active: true }).then(() => {
+ // NOTE: the message will be sent _after_ the popup is closed below.
+ browser.runtime.sendMessage("sent-after-closed");
+ });
+ window.close();
+ },
+ },
+
+ async background() {
+ browser.runtime.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "sent-after-closed", "Message from popup.");
+ browser.test.sendMessage("done");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ clickBrowserAction(extension);
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js b/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js
new file mode 100644
index 0000000000..246a83520e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_popup_shutdown.js
@@ -0,0 +1,80 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let getExtension = () => {
+ return ExtensionTestUtils.loadExtension({
+ background: async function () {
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ browser.test.sendMessage("pageAction ready");
+ },
+
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: false,
+ },
+
+ page_action: {
+ default_popup: "popup.html",
+ browser_style: false,
+ },
+ },
+
+ files: {
+ "popup.html": `
+ `,
+ },
+ });
+};
+
+add_task(async function testStandaloneBrowserAction() {
+ info("Test stand-alone browserAction popup");
+
+ let extension = getExtension();
+ await extension.startup();
+ await extension.awaitMessage("pageAction ready");
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ let panel = getPanelForNode(browser);
+
+ await extension.unload();
+
+ is(panel.parentNode, null, "Panel should be removed from the document");
+});
+
+add_task(async function testMenuPanelBrowserAction() {
+ let extension = getExtension();
+ await extension.startup();
+ await extension.awaitMessage("pageAction ready");
+
+ let widget = getBrowserActionWidget(extension);
+ CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID());
+
+ clickBrowserAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ let panel = getPanelForNode(browser);
+
+ await extension.unload();
+
+ is(panel.state, "closed", "Panel should be closed");
+});
+
+add_task(async function testPageAction() {
+ let extension = getExtension();
+ await extension.startup();
+ await extension.awaitMessage("pageAction ready");
+
+ clickPageAction(extension);
+ let browser = await awaitExtensionPanel(extension);
+ let panel = getPanelForNode(browser);
+
+ await extension.unload();
+
+ is(panel.parentNode, null, "Panel should be removed from the document");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_crash.js b/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_crash.js
new file mode 100644
index 0000000000..82ece1da3f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_crash.js
@@ -0,0 +1,113 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function connect_from_tab_to_bg_and_crash_tab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ js: ["contentscript.js"],
+ matches: ["http://example.com/?crashme"],
+ },
+ ],
+ },
+
+ background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq("tab_to_bg", port.name, "expected port");
+ browser.test.assertEq(port.sender.frameId, 0, "correct frameId");
+
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "port should be disconnected without errors"
+ );
+ browser.test.sendMessage("port_disconnected");
+ });
+ browser.test.sendMessage("bg_runtime_onConnect");
+ });
+ },
+
+ files: {
+ "contentscript.js": function () {
+ let port = browser.runtime.connect({ name: "tab_to_bg" });
+ port.onDisconnect.addListener(() => {
+ browser.test.fail("Unexpected onDisconnect event in content script");
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?crashme"
+ );
+ await extension.awaitMessage("bg_runtime_onConnect");
+ // Force the message manager to disconnect without giving the content a
+ // chance to send an "Extension:Port:Disconnect" message.
+ await BrowserTestUtils.crashFrame(tab.linkedBrowser);
+ await extension.awaitMessage("port_disconnected");
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function connect_from_bg_to_tab_and_crash_tab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ content_scripts: [
+ {
+ js: ["contentscript.js"],
+ matches: ["http://example.com/?crashme"],
+ },
+ ],
+ },
+
+ background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.assertEq("contentscript_ready", msg, "expected message");
+ let port = browser.tabs.connect(sender.tab.id, { name: "bg_to_tab" });
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "port should be disconnected without errors"
+ );
+ browser.test.sendMessage("port_disconnected");
+ });
+ });
+ },
+
+ files: {
+ "contentscript.js": function () {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq("bg_to_tab", port.name, "expected port");
+ port.onDisconnect.addListener(() => {
+ browser.test.fail(
+ "Unexpected onDisconnect event in content script"
+ );
+ });
+ browser.test.sendMessage("tab_runtime_onConnect");
+ });
+ browser.runtime.sendMessage("contentscript_ready");
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?crashme"
+ );
+ await extension.awaitMessage("tab_runtime_onConnect");
+ // Force the message manager to disconnect without giving the content a
+ // chance to send an "Extension:Port:Disconnect" message.
+ await BrowserTestUtils.crashFrame(tab.linkedBrowser);
+ await extension.awaitMessage("port_disconnected");
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_window_close.js b/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_window_close.js
new file mode 100644
index 0000000000..84bc4a3ff0
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_port_disconnect_on_window_close.js
@@ -0,0 +1,39 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Regression test for https://bugzil.la/1392067 .
+add_task(async function connect_from_window_and_close() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq("page_to_bg", port.name, "expected port");
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "port should be disconnected without errors"
+ );
+ browser.test.sendMessage("port_disconnected");
+ });
+ browser.windows.remove(port.sender.tab.windowId);
+ });
+
+ browser.windows.create({ url: "page.html" });
+ },
+
+ files: {
+ "page.html": ``,
+ "page.js": function () {
+ let port = browser.runtime.connect({ name: "page_to_bg" });
+ port.onDisconnect.addListener(() => {
+ browser.test.fail("Unexpected onDisconnect event in page");
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("port_disconnected");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_reload_manifest_cache.js b/browser/components/extensions/test/browser/browser_ext_reload_manifest_cache.js
new file mode 100644
index 0000000000..aefa8f42f5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_reload_manifest_cache.js
@@ -0,0 +1,72 @@
+"use strict";
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+add_task(async function test_reload_manifest_startupcache() {
+ const id = "id@tests.mozilla.org";
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id },
+ },
+ options_ui: {
+ open_in_tab: true,
+ page: "options.html",
+ },
+ optional_permissions: [""],
+ },
+ useAddonManager: "temporary",
+ files: {
+ "options.html": `lol`,
+ },
+ background() {
+ browser.runtime.openOptionsPage();
+ browser.permissions.onAdded.addListener(() => {
+ browser.runtime.openOptionsPage();
+ });
+ },
+ });
+
+ async function waitOptionsTab() {
+ let tab = await BrowserTestUtils.waitForNewTab(gBrowser, url =>
+ url.endsWith("options.html")
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ // Open a non-blank tab to force options to open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/"
+ );
+ let optionsTabPromise = waitOptionsTab();
+
+ await ext.startup();
+ await optionsTabPromise;
+
+ let disabledPromise = awaitEvent("shutdown", id);
+ let enabledPromise = awaitEvent("ready", id);
+ optionsTabPromise = waitOptionsTab();
+
+ let addon = await AddonManager.getAddonByID(id);
+ await addon.reload();
+
+ await Promise.all([disabledPromise, enabledPromise, optionsTabPromise]);
+
+ optionsTabPromise = waitOptionsTab();
+ ExtensionPermissions.add(id, {
+ permissions: [],
+ origins: [""],
+ });
+ await optionsTabPromise;
+
+ let policy = WebExtensionPolicy.getByID(id);
+ let optionsUrl = policy.extension.manifest.options_ui.page;
+ ok(optionsUrl.includes(policy.mozExtensionHostname), "Normalized manifest.");
+
+ await BrowserTestUtils.removeTab(tab);
+ await ext.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_request_permissions.js b/browser/components/extensions/test/browser/browser_ext_request_permissions.js
new file mode 100644
index 0000000000..3ba58bccd5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_request_permissions.js
@@ -0,0 +1,121 @@
+"use strict";
+
+// This test case verifies that `permissions.request()` resolves in the
+// expected order.
+add_task(async function test_permissions_prompt() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ optional_permissions: ["history", "bookmarks"],
+ },
+ background: async () => {
+ let hiddenTab = await browser.tabs.create({
+ url: browser.runtime.getURL("hidden.html"),
+ active: false,
+ });
+
+ await browser.tabs.create({
+ url: browser.runtime.getURL("active.html"),
+ active: true,
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "activate-hiddenTab") {
+ await browser.tabs.update(hiddenTab.id, { active: true });
+
+ browser.test.sendMessage("activate-hiddenTab-ok");
+ }
+ });
+ },
+ files: {
+ "active.html": ``,
+ "active.js": async () => {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "request-perms-activeTab") {
+ let granted = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(
+ browser.permissions.request({ permissions: ["history"] })
+ );
+ });
+ });
+ browser.test.assertTrue(granted, "permission request succeeded");
+
+ browser.test.sendMessage("request-perms-activeTab-ok");
+ }
+ });
+
+ browser.test.sendMessage("activeTab-ready");
+ },
+ "hidden.html": ``,
+ "hidden.js": async () => {
+ let resolved = false;
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "request-perms-hiddenTab") {
+ let granted = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(
+ browser.permissions.request({ permissions: ["bookmarks"] })
+ );
+ });
+ });
+ browser.test.assertTrue(granted, "permission request succeeded");
+
+ resolved = true;
+
+ browser.test.sendMessage("request-perms-hiddenTab-ok");
+ } else if (msg === "hiddenTab-read-state") {
+ browser.test.sendMessage("hiddenTab-state-value", resolved);
+ }
+ });
+
+ browser.test.sendMessage("hiddenTab-ready");
+ },
+ },
+ });
+ await extension.startup();
+
+ await extension.awaitMessage("activeTab-ready");
+ await extension.awaitMessage("hiddenTab-ready");
+
+ // Call request() on a hidden window.
+ extension.sendMessage("request-perms-hiddenTab");
+
+ let requestPromptForActiveTab = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+
+ // Call request() in the current window.
+ extension.sendMessage("request-perms-activeTab");
+ await requestPromptForActiveTab;
+ await extension.awaitMessage("request-perms-activeTab-ok");
+
+ // Check that initial request() is still pending.
+ extension.sendMessage("hiddenTab-read-state");
+ ok(
+ !(await extension.awaitMessage("hiddenTab-state-value")),
+ "initial request is pending"
+ );
+
+ let requestPromptForHiddenTab = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+
+ extension.sendMessage("activate-hiddenTab");
+ await extension.awaitMessage("activate-hiddenTab-ok");
+ await requestPromptForHiddenTab;
+ await extension.awaitMessage("request-perms-hiddenTab-ok");
+
+ extension.sendMessage("hiddenTab-read-state");
+ ok(
+ await extension.awaitMessage("hiddenTab-state-value"),
+ "initial request is resolved"
+ );
+
+ // The extension tabs are automatically closed upon unload.
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
new file mode 100644
index 0000000000..a4b01bc182
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
@@ -0,0 +1,442 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function loadExtension(options) {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: Object.assign(
+ {
+ permissions: ["tabs"],
+ },
+ options.manifest
+ ),
+
+ files: {
+ "options.html": `
+
+
+
+
+
+ `,
+
+ "options.js": function () {
+ window.iAmOption = true;
+ browser.runtime.sendMessage("options.html");
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "ping") {
+ respond("pong");
+ } else if (msg == "connect") {
+ let port = browser.runtime.connect();
+ port.postMessage("ping-from-options-html");
+ port.onMessage.addListener(msg => {
+ if (msg == "ping-from-bg") {
+ browser.test.log("Got outbound options.html pong");
+ browser.test.sendMessage("options-html-outbound-pong");
+ }
+ });
+ }
+ });
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.log("Got inbound options.html port");
+
+ port.postMessage("ping-from-options-html");
+ port.onMessage.addListener(msg => {
+ if (msg == "ping-from-bg") {
+ browser.test.log("Got inbound options.html pong");
+ browser.test.sendMessage("options-html-inbound-pong");
+ }
+ });
+ });
+ },
+ },
+
+ background: options.background,
+ });
+
+ await extension.startup();
+
+ return extension;
+}
+
+add_task(async function run_test_inline_options() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = await loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "inline_options@tests.mozilla.org" },
+ },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+
+ background: async function () {
+ let _optionsPromise;
+ let awaitOptions = () => {
+ browser.test.assertFalse(
+ _optionsPromise,
+ "Should not be awaiting options already"
+ );
+
+ return new Promise(resolve => {
+ _optionsPromise = { resolve };
+ });
+ };
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg == "options.html") {
+ if (_optionsPromise) {
+ _optionsPromise.resolve(sender.tab);
+ _optionsPromise = null;
+ } else {
+ browser.test.fail("Saw unexpected options page load");
+ }
+ }
+ });
+
+ try {
+ let [firstTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ browser.test.log("Open options page. Expect fresh load.");
+
+ let [, optionsTab] = await Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+
+ browser.test.assertEq(
+ "about:addons",
+ optionsTab.url,
+ "Tab contains AddonManager"
+ );
+ browser.test.assertTrue(optionsTab.active, "Tab is active");
+ browser.test.assertTrue(
+ optionsTab.id != firstTab.id,
+ "Tab is a new tab"
+ );
+
+ browser.test.assertEq(
+ 0,
+ browser.extension.getViews({ type: "popup" }).length,
+ "viewType is not popup"
+ );
+ browser.test.assertEq(
+ 1,
+ browser.extension.getViews({ type: "tab" }).length,
+ "viewType is tab"
+ );
+ browser.test.assertEq(
+ 1,
+ browser.extension.getViews({ windowId: optionsTab.windowId }).length,
+ "windowId matches"
+ );
+
+ let views = browser.extension.getViews();
+ browser.test.assertEq(
+ 2,
+ views.length,
+ "Expected the options page and the background page"
+ );
+ browser.test.assertTrue(
+ views.includes(window),
+ "One of the views is the background page"
+ );
+ browser.test.assertTrue(
+ views.some(w => w.iAmOption),
+ "One of the views is the options page"
+ );
+
+ browser.test.log("Switch tabs.");
+ await browser.tabs.update(firstTab.id, { active: true });
+
+ browser.test.log(
+ "Open options page again. Expect tab re-selected, no new load."
+ );
+
+ await browser.runtime.openOptionsPage();
+ let [tab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ browser.test.assertEq(
+ optionsTab.id,
+ tab.id,
+ "Tab is the same as the previous options tab"
+ );
+ browser.test.assertEq(
+ "about:addons",
+ tab.url,
+ "Tab contains AddonManager"
+ );
+
+ browser.test.log("Ping options page.");
+ let pong = await browser.runtime.sendMessage("ping");
+ browser.test.assertEq("pong", pong, "Got pong.");
+
+ let done = new Promise(resolve => {
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "ports-done") {
+ resolve();
+ }
+ });
+ });
+
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.log("Got inbound background port");
+
+ port.postMessage("ping-from-bg");
+ port.onMessage.addListener(msg => {
+ if (msg == "ping-from-options-html") {
+ browser.test.log("Got inbound background pong");
+ browser.test.sendMessage("bg-inbound-pong");
+ }
+ });
+ });
+
+ browser.runtime.sendMessage("connect");
+
+ let port = browser.runtime.connect();
+ port.postMessage("ping-from-bg");
+ port.onMessage.addListener(msg => {
+ if (msg == "ping-from-options-html") {
+ browser.test.log("Got outbound background pong");
+ browser.test.sendMessage("bg-outbound-pong");
+ }
+ });
+
+ await done;
+
+ browser.test.log("Remove options tab.");
+ await browser.tabs.remove(optionsTab.id);
+
+ browser.test.log("Open options page again. Expect fresh load.");
+ [, tab] = await Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+ browser.test.assertEq(
+ "about:addons",
+ tab.url,
+ "Tab contains AddonManager"
+ );
+ browser.test.assertTrue(tab.active, "Tab is active");
+ browser.test.assertTrue(tab.id != optionsTab.id, "Tab is a new tab");
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("options-ui");
+ } catch (error) {
+ browser.test.fail(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("options-ui");
+ }
+ },
+ });
+
+ await Promise.all([
+ extension.awaitMessage("options-html-inbound-pong"),
+ extension.awaitMessage("options-html-outbound-pong"),
+ extension.awaitMessage("bg-inbound-pong"),
+ extension.awaitMessage("bg-outbound-pong"),
+ ]);
+
+ extension.sendMessage("ports-done");
+
+ await extension.awaitFinish("options-ui");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_tab_options() {
+ info(`Test options opened in a tab`);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = await loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "tab_options@tests.mozilla.org" },
+ },
+ options_ui: {
+ page: "options.html",
+ open_in_tab: true,
+ },
+ },
+
+ background: async function () {
+ let _optionsPromise;
+ let awaitOptions = () => {
+ browser.test.assertFalse(
+ _optionsPromise,
+ "Should not be awaiting options already"
+ );
+
+ return new Promise(resolve => {
+ _optionsPromise = { resolve };
+ });
+ };
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg == "options.html") {
+ if (_optionsPromise) {
+ _optionsPromise.resolve(sender.tab);
+ _optionsPromise = null;
+ } else {
+ browser.test.fail("Saw unexpected options page load");
+ }
+ }
+ });
+
+ let optionsURL = browser.runtime.getURL("options.html");
+
+ try {
+ let [firstTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ browser.test.log("Open options page. Expect fresh load.");
+ let [, optionsTab] = await Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+ browser.test.assertEq(
+ optionsURL,
+ optionsTab.url,
+ "Tab contains options.html"
+ );
+ browser.test.assertTrue(optionsTab.active, "Tab is active");
+ browser.test.assertTrue(
+ optionsTab.id != firstTab.id,
+ "Tab is a new tab"
+ );
+
+ browser.test.assertEq(
+ 0,
+ browser.extension.getViews({ type: "popup" }).length,
+ "viewType is not popup"
+ );
+ browser.test.assertEq(
+ 1,
+ browser.extension.getViews({ type: "tab" }).length,
+ "viewType is tab"
+ );
+ browser.test.assertEq(
+ 1,
+ browser.extension.getViews({ windowId: optionsTab.windowId }).length,
+ "windowId matches"
+ );
+
+ let views = browser.extension.getViews();
+ browser.test.assertEq(
+ 2,
+ views.length,
+ "Expected the options page and the background page"
+ );
+ browser.test.assertTrue(
+ views.includes(window),
+ "One of the views is the background page"
+ );
+ browser.test.assertTrue(
+ views.some(w => w.iAmOption),
+ "One of the views is the options page"
+ );
+
+ browser.test.log("Switch tabs.");
+ await browser.tabs.update(firstTab.id, { active: true });
+
+ browser.test.log(
+ "Open options page again. Expect tab re-selected, no new load."
+ );
+
+ await browser.runtime.openOptionsPage();
+ let [tab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ browser.test.assertEq(
+ optionsTab.id,
+ tab.id,
+ "Tab is the same as the previous options tab"
+ );
+ browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html");
+
+ // Unfortunately, we can't currently do this, since onMessage doesn't
+ // currently support responses when there are multiple listeners.
+ //
+ // browser.test.log("Ping options page.");
+ // return new Promise(resolve => browser.runtime.sendMessage("ping", resolve));
+
+ browser.test.log("Remove options tab.");
+ await browser.tabs.remove(optionsTab.id);
+
+ browser.test.log("Open options page again. Expect fresh load.");
+ [, tab] = await Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+ browser.test.assertEq(optionsURL, tab.url, "Tab contains options.html");
+ browser.test.assertTrue(tab.active, "Tab is active");
+ browser.test.assertTrue(tab.id != optionsTab.id, "Tab is a new tab");
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("options-ui-tab");
+ } catch (error) {
+ browser.test.fail(`Error: ${error} :: ${error.stack}`);
+ browser.test.notifyFail("options-ui-tab");
+ }
+ },
+ });
+
+ await extension.awaitFinish("options-ui-tab");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_options_no_manifest() {
+ info(`Test with no manifest key`);
+
+ let extension = await loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "no_options@tests.mozilla.org" },
+ },
+ },
+
+ async background() {
+ browser.test.log(
+ "Try to open options page when not specified in the manifest."
+ );
+
+ await browser.test.assertRejects(
+ browser.runtime.openOptionsPage(),
+ /No `options_ui` declared/,
+ "Expected error from openOptionsPage()"
+ );
+
+ browser.test.notifyPass("options-no-manifest");
+ },
+ });
+
+ await extension.awaitFinish("options-no-manifest");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js
new file mode 100644
index 0000000000..ac9bbf1ed2
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js
@@ -0,0 +1,122 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function loadExtension(options) {
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: Object.assign(
+ {
+ permissions: ["tabs"],
+ },
+ options.manifest
+ ),
+
+ files: {
+ "options.html": `
+
+
+
+
+
+ `,
+
+ "options.js": function () {
+ browser.runtime.sendMessage("options.html");
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "ping") {
+ respond("pong");
+ }
+ });
+ },
+ },
+
+ background: options.background,
+ });
+
+ await extension.startup();
+
+ return extension;
+}
+
+add_task(async function test_inline_options_uninstall() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = await loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: { id: "inline_options_uninstall@tests.mozilla.org" },
+ },
+ options_ui: {
+ page: "options.html",
+ },
+ },
+
+ background: async function () {
+ let _optionsPromise;
+ let awaitOptions = () => {
+ browser.test.assertFalse(
+ _optionsPromise,
+ "Should not be awaiting options already"
+ );
+
+ return new Promise(resolve => {
+ _optionsPromise = { resolve };
+ });
+ };
+
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ if (msg == "options.html") {
+ if (_optionsPromise) {
+ _optionsPromise.resolve(sender.tab);
+ _optionsPromise = null;
+ } else {
+ browser.test.fail("Saw unexpected options page load");
+ }
+ }
+ });
+
+ try {
+ let [firstTab] = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+
+ browser.test.log("Open options page. Expect fresh load.");
+ let [, tab] = await Promise.all([
+ browser.runtime.openOptionsPage(),
+ awaitOptions(),
+ ]);
+
+ browser.test.assertEq(
+ "about:addons",
+ tab.url,
+ "Tab contains AddonManager"
+ );
+ browser.test.assertTrue(tab.active, "Tab is active");
+ browser.test.assertTrue(tab.id != firstTab.id, "Tab is a new tab");
+
+ browser.test.sendMessage("options-ui-open");
+ } catch (error) {
+ browser.test.fail(`Error: ${error} :: ${error.stack}`);
+ }
+ },
+ });
+
+ await extension.awaitMessage("options-ui-open");
+ await extension.unload();
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:addons",
+ "Add-on manager tab should still be open"
+ );
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js b/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js
new file mode 100644
index 0000000000..2530c28a6d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js
@@ -0,0 +1,134 @@
+"use strict";
+
+// testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js loads
+// ExtensionTestCommon, and is slated as part of the SimpleTest
+// environment in tools/lint/eslint/eslint-plugin-mozilla/lib/environments/simpletest.js
+// However, nothing but the ExtensionTestUtils global gets put
+// into the scope, and so although eslint thinks this global is
+// available, it really isn't.
+// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
+let { ExtensionTestCommon } = ChromeUtils.importESModule(
+ "resource://testing-common/ExtensionTestCommon.sys.mjs"
+);
+
+async function makeAndInstallXPI(id, backgroundScript, loadedURL) {
+ let xpi = ExtensionTestCommon.generateXPI({
+ manifest: { browser_specific_settings: { gecko: { id } } },
+ background: backgroundScript,
+ });
+ SimpleTest.registerCleanupFunction(function cleanupXPI() {
+ Services.obs.notifyObservers(xpi, "flush-cache-entry");
+ xpi.remove(false);
+ });
+
+ let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, loadedURL);
+
+ info(`installing ${xpi.path}`);
+ let addon = await AddonManager.installTemporaryAddon(xpi);
+ info("installed");
+
+ // A WebExtension is started asynchronously, we have our test extension
+ // open a new tab to signal that the background script has executed.
+ let loadTab = await loadPromise;
+ BrowserTestUtils.removeTab(loadTab);
+
+ return addon;
+}
+
+add_task(async function test_setuninstallurl_badargs() {
+ async function background() {
+ await browser.test.assertRejects(
+ browser.runtime.setUninstallURL("this is not a url"),
+ /Invalid URL/,
+ "setUninstallURL with an invalid URL should fail"
+ );
+
+ await browser.test.assertRejects(
+ browser.runtime.setUninstallURL("file:///etc/passwd"),
+ /must have the scheme http or https/,
+ "setUninstallURL with an illegal URL should fail"
+ );
+
+ browser.test.notifyPass("setUninstallURL bad params");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ });
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+// Test the documented behavior of setUninstallURL() that passing an
+// empty string is equivalent to not setting an uninstall URL
+// (i.e., no new tab is opened upon uninstall)
+add_task(async function test_setuninstall_empty_url() {
+ async function backgroundScript() {
+ await browser.runtime.setUninstallURL("");
+ browser.tabs.create({ url: "http://example.com/addon_loaded" });
+ }
+
+ let addon = await makeAndInstallXPI(
+ "test_uinstallurl2@tests.mozilla.org",
+ backgroundScript,
+ "http://example.com/addon_loaded"
+ );
+
+ addon.uninstall(true);
+ info("uninstalled");
+
+ // no need to explicitly check for the absence of a new tab,
+ // BrowserTestUtils will eventually complain if one is opened.
+});
+
+// Test the documented behavior of setUninstallURL() that passing an
+// empty string is equivalent to not setting an uninstall URL
+// (i.e., no new tab is opened upon uninstall)
+// here we pass a null value to string and test
+add_task(async function test_setuninstall_null_url() {
+ async function backgroundScript() {
+ await browser.runtime.setUninstallURL(null);
+ browser.tabs.create({ url: "http://example.com/addon_loaded" });
+ }
+
+ let addon = await makeAndInstallXPI(
+ "test_uinstallurl2@tests.mozilla.org",
+ backgroundScript,
+ "http://example.com/addon_loaded"
+ );
+
+ addon.uninstall(true);
+ info("uninstalled");
+
+ // no need to explicitly check for the absence of a new tab,
+ // BrowserTestUtils will eventually complain if one is opened.
+});
+
+add_task(async function test_setuninstallurl() {
+ async function backgroundScript() {
+ await browser.runtime.setUninstallURL(
+ "http://example.com/addon_uninstalled"
+ );
+ browser.tabs.create({ url: "http://example.com/addon_loaded" });
+ }
+
+ let addon = await makeAndInstallXPI(
+ "test_uinstallurl@tests.mozilla.org",
+ backgroundScript,
+ "http://example.com/addon_loaded"
+ );
+
+ // look for a new tab with the uninstall url.
+ let uninstallPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "http://example.com/addon_uninstalled"
+ );
+
+ addon.uninstall(true);
+ info("uninstalled");
+
+ let uninstalledTab = await uninstallPromise;
+ isnot(uninstalledTab, null, "opened tab with uninstall url");
+ BrowserTestUtils.removeTab(uninstalledTab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_search.js b/browser/components/extensions/test/browser/browser_ext_search.js
new file mode 100644
index 0000000000..c7dab1c9dc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_search.js
@@ -0,0 +1,351 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const SEARCH_TERM = "test";
+const SEARCH_URL = "https://example.org/?q={searchTerms}";
+
+AddonTestUtils.initMochitest(this);
+
+add_task(async function test_search() {
+ async function background(SEARCH_TERM) {
+ browser.test.onMessage.addListener(async (msg, tabIds) => {
+ if (msg !== "removeTabs") {
+ return;
+ }
+
+ await browser.tabs.remove(tabIds);
+ browser.test.sendMessage("onTabsRemoved");
+ });
+
+ function awaitSearchResult() {
+ return new Promise(resolve => {
+ async function listener(tabId, info, changedTab) {
+ if (changedTab.url == "about:blank") {
+ // Ignore events related to the initial tab open.
+ return;
+ }
+
+ if (info.status === "complete") {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve({ tabId, url: changedTab.url });
+ }
+ }
+
+ browser.tabs.onUpdated.addListener(listener);
+ });
+ }
+
+ let engines = await browser.search.get();
+ browser.test.sendMessage("engines", engines);
+
+ // Search with no tabId
+ browser.search.search({ query: SEARCH_TERM + "1", engine: "Search Test" });
+ let result = await awaitSearchResult();
+ browser.test.sendMessage("searchLoaded", result);
+
+ // Search with tabId
+ let tab = await browser.tabs.create({});
+ browser.search.search({
+ query: SEARCH_TERM + "2",
+ engine: "Search Test",
+ tabId: tab.id,
+ });
+ result = await awaitSearchResult();
+ browser.test.assertEq(result.tabId, tab.id, "Page loaded in right tab");
+ browser.test.sendMessage("searchLoaded", result);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search", "tabs"],
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Search Test",
+ search_url: SEARCH_URL,
+ },
+ },
+ },
+ background: `(${background})("${SEARCH_TERM}")`,
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+
+ let addonEngines = await extension.awaitMessage("engines");
+ let engines = (await Services.search.getEngines()).filter(
+ engine => !engine.hidden
+ );
+ is(addonEngines.length, engines.length, "Engine lengths are the same.");
+ let defaultEngine = addonEngines.filter(engine => engine.isDefault === true);
+ is(defaultEngine.length, 1, "One default engine");
+ is(
+ defaultEngine[0].name,
+ (await Services.search.getDefault()).name,
+ "Default engine is correct"
+ );
+
+ const result1 = await extension.awaitMessage("searchLoaded");
+ is(
+ result1.url,
+ SEARCH_URL.replace("{searchTerms}", SEARCH_TERM + "1"),
+ "Loaded page matches search"
+ );
+ await TestUtils.waitForCondition(
+ () => !gURLBar.focused,
+ "Wait for unfocusing the urlbar"
+ );
+ info("The urlbar has no focus when searching without tabId");
+
+ const result2 = await extension.awaitMessage("searchLoaded");
+ is(
+ result2.url,
+ SEARCH_URL.replace("{searchTerms}", SEARCH_TERM + "2"),
+ "Loaded page matches search"
+ );
+ await TestUtils.waitForCondition(
+ () => !gURLBar.focused,
+ "Wait for unfocusing the urlbar"
+ );
+ info("The urlbar has no focus when searching with tabId");
+
+ extension.sendMessage("removeTabs", [result1.tabId, result2.tabId]);
+ await extension.awaitMessage("onTabsRemoved");
+
+ await extension.unload();
+});
+
+add_task(async function test_search_default_engine() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search"],
+ },
+ background() {
+ browser.test.onMessage.addListener((msg, tabId) => {
+ browser.test.assertEq(msg, "search");
+ browser.search.search({ query: "searchTermForDefaultEngine", tabId });
+ });
+ browser.test.sendMessage("extension-origin", browser.runtime.getURL("/"));
+ },
+ useAddonManager: "temporary",
+ });
+
+ // Use another extension to intercept and block the search request,
+ // so that there is no outbound network activity that would kill the test.
+ // This method also allows us to verify that:
+ // 1) the search appears as a normal request in the webRequest API.
+ // 2) the request is associated with the triggering extension.
+ let extensionWithObserver = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["webRequest", "webRequestBlocking", "*://*/*"] },
+ async background() {
+ let tab = await browser.tabs.create({ url: "about:blank" });
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ browser.test.log(`Intercepted request ${JSON.stringify(details)}`);
+ browser.tabs.remove(tab.id).then(() => {
+ browser.test.sendMessage("detectedSearch", details);
+ });
+ return { cancel: true };
+ },
+ {
+ tabId: tab.id,
+ types: ["main_frame"],
+ urls: ["*://*/*"],
+ },
+ ["blocking"]
+ );
+ browser.test.sendMessage("ready", tab.id);
+ },
+ });
+ await extension.startup();
+ const EXPECTED_ORIGIN = await extension.awaitMessage("extension-origin");
+
+ await extensionWithObserver.startup();
+ let tabId = await extensionWithObserver.awaitMessage("ready");
+
+ extension.sendMessage("search", tabId);
+ let requestDetails = await extensionWithObserver.awaitMessage(
+ "detectedSearch"
+ );
+ await extension.unload();
+ await extensionWithObserver.unload();
+
+ ok(
+ requestDetails.url.includes("searchTermForDefaultEngine"),
+ `Expected search term in ${requestDetails.url}`
+ );
+ is(
+ requestDetails.originUrl,
+ EXPECTED_ORIGIN,
+ "Search request's should be associated with the originating extension."
+ );
+});
+
+add_task(async function test_search_disposition() {
+ async function background() {
+ let resolvers = {};
+
+ function tabListener(tabId, changeInfo, tab) {
+ if (tab.url == "about:blank") {
+ // Ignore events related to the initial tab open.
+ return;
+ }
+
+ if (changeInfo.status === "complete") {
+ let query = new URL(tab.url).searchParams.get("q");
+ let resolver = resolvers[query];
+ browser.test.assertTrue(resolver, `Found resolver for ${tab.url}`);
+ browser.test.assertTrue(
+ resolver.resolve,
+ `${query} was not resolved yet`
+ );
+ resolver.resolve({
+ tabId,
+ windowId: tab.windowId,
+ });
+ resolver.resolve = null; // resolve can be used only once.
+ }
+ }
+ browser.tabs.onUpdated.addListener(tabListener);
+
+ async function awaitSearchResult(args) {
+ resolvers[args.query] = {};
+ resolvers[args.query].promise = new Promise(
+ _resolve => (resolvers[args.query].resolve = _resolve)
+ );
+ await browser.search.search({ ...args, engine: "Search Test" });
+ let searchResult = await resolvers[args.query].promise;
+ return searchResult;
+ }
+
+ const firstTab = await browser.tabs.create({
+ active: true,
+ url: "about:blank",
+ });
+
+ // Search in new tab (testing default disposition)
+ let result = await awaitSearchResult({
+ query: "DefaultDisposition",
+ });
+ browser.test.assertFalse(
+ result.tabId === firstTab.id,
+ "Query ran in new tab"
+ );
+ browser.test.assertEq(
+ result.windowId,
+ firstTab.windowId,
+ "Query ran in current window"
+ );
+ await browser.tabs.remove(result.tabId); // Cleanup
+
+ // Search in new tab
+ result = await awaitSearchResult({
+ query: "NewTab",
+ disposition: "NEW_TAB",
+ });
+ browser.test.assertFalse(
+ result.tabId === firstTab.id,
+ "Query ran in new tab"
+ );
+ browser.test.assertEq(
+ result.windowId,
+ firstTab.windowId,
+ "Query ran in current window"
+ );
+ await browser.tabs.remove(result.tabId); // Cleanup
+
+ // Search in current tab
+ result = await awaitSearchResult({
+ query: "CurrentTab",
+ disposition: "CURRENT_TAB",
+ });
+ browser.test.assertDeepEq(
+ {
+ tabId: firstTab.id,
+ windowId: firstTab.windowId,
+ },
+ result,
+ "Query ran in current tab in current window"
+ );
+
+ // Search in a specific tab
+ let newTab = await browser.tabs.create({
+ active: false,
+ url: "about:blank",
+ });
+ result = await awaitSearchResult({
+ query: "SpecificTab",
+ tabId: newTab.id,
+ });
+ browser.test.assertDeepEq(
+ {
+ tabId: newTab.id,
+ windowId: firstTab.windowId,
+ },
+ result,
+ "Query ran in specific tab in current window"
+ );
+ await browser.tabs.remove(newTab.id); // Cleanup
+
+ // Search in a new window
+ result = await awaitSearchResult({
+ query: "NewWindow",
+ disposition: "NEW_WINDOW",
+ });
+ browser.test.assertFalse(
+ result.windowId === firstTab.windowId,
+ "Query ran in new window"
+ );
+ await browser.windows.remove(result.windowId); // Cleanup
+ await browser.tabs.remove(firstTab.id); // Cleanup
+
+ // Make sure tabId and disposition can't be used together
+ await browser.test.assertRejects(
+ browser.search.search({
+ query: " ",
+ tabId: 1,
+ disposition: "NEW_WINDOW",
+ }),
+ "Cannot set both 'disposition' and 'tabId'",
+ "Should not be able to set both tabId and disposition"
+ );
+
+ // Make sure we reject if an invalid tabId is used
+ await browser.test.assertRejects(
+ browser.search.search({
+ query: " ",
+ tabId: Number.MAX_SAFE_INTEGER,
+ }),
+ /Invalid tab ID/,
+ "Should not be able to set an invalid tabId"
+ );
+
+ browser.test.notifyPass("disposition");
+ }
+ let searchExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Search Test",
+ search_url: "https://example.org/?q={searchTerms}",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search", "tabs"],
+ },
+ background,
+ });
+ await searchExtension.startup();
+ await extension.startup();
+ await extension.awaitFinish("disposition");
+ await extension.unload();
+ await searchExtension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_search_favicon.js b/browser/components/extensions/test/browser/browser_ext_search_favicon.js
new file mode 100644
index 0000000000..b46796a427
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_search_favicon.js
@@ -0,0 +1,182 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+XPCShellContentUtils.initMochitest(this);
+
+// Base64-encoded "Fake icon data".
+const FAKE_ICON_DATA = "RmFrZSBpY29uIGRhdGE=";
+
+// Base64-encoded "HTTP icon data".
+const HTTP_ICON_DATA = "SFRUUCBpY29uIGRhdGE=";
+const HTTP_ICON_URL = "http://example.org/ico.png";
+const server = XPCShellContentUtils.createHttpServer({
+ hosts: ["example.org"],
+});
+server.registerPathHandler("/ico.png", (request, response) => {
+ response.write(atob(HTTP_ICON_DATA));
+});
+
+function promiseEngineIconLoaded(engineName) {
+ return TestUtils.topicObserved(
+ "browser-search-engine-modified",
+ (engine, verb) => {
+ engine.QueryInterface(Ci.nsISearchEngine);
+ return (
+ verb == "engine-changed" && engine.name == engineName && engine.iconURI
+ );
+ }
+ );
+}
+
+add_task(async function test_search_favicon() {
+ let searchExt = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Engine Only",
+ search_url: "https://example.com/",
+ favicon_url: "someFavicon.png",
+ },
+ },
+ },
+ files: {
+ "someFavicon.png": atob(FAKE_ICON_DATA),
+ },
+ useAddonManager: "temporary",
+ });
+
+ let searchExtWithBadIcon = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Bad Icon",
+ search_url: "https://example.net/",
+ favicon_url: "iDoNotExist.png",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let searchExtWithHttpIcon = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "HTTP Icon",
+ search_url: "https://example.org/",
+ favicon_url: HTTP_ICON_URL,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search"],
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "My Engine",
+ search_url: "https://example.org/",
+ favicon_url: "myFavicon.png",
+ },
+ },
+ },
+ files: {
+ "myFavicon.png": imageBuffer,
+ },
+ useAddonManager: "temporary",
+ async background() {
+ let engines = await browser.search.get();
+ browser.test.sendMessage("engines", {
+ badEngine: engines.find(engine => engine.name === "Bad Icon"),
+ httpEngine: engines.find(engine => engine.name === "HTTP Icon"),
+ myEngine: engines.find(engine => engine.name === "My Engine"),
+ otherEngine: engines.find(engine => engine.name === "Engine Only"),
+ });
+ },
+ });
+
+ await searchExt.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(searchExt);
+
+ await searchExtWithBadIcon.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(searchExtWithBadIcon);
+
+ // TODO bug 1571718: browser.search.get should behave correctly (i.e return
+ // the icon) even if the icon did not finish loading when the API was called.
+ // Currently calling it too early returns undefined, so just wait until the
+ // icon has loaded before calling browser.search.get.
+ let httpIconLoaded = promiseEngineIconLoaded("HTTP Icon");
+ await searchExtWithHttpIcon.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(searchExtWithHttpIcon);
+ await httpIconLoaded;
+
+ await extension.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+
+ let engines = await extension.awaitMessage("engines");
+
+ // An extension's own icon can surely be accessed by the extension, so its
+ // favIconUrl can be the moz-extension:-URL itself.
+ Assert.deepEqual(
+ engines.myEngine,
+ {
+ name: "My Engine",
+ isDefault: false,
+ alias: undefined,
+ favIconUrl: `moz-extension://${extension.uuid}/myFavicon.png`,
+ },
+ "browser.search.get result for own extension"
+ );
+
+ // favIconUrl of other engines need to be in base64-encoded form.
+ Assert.deepEqual(
+ engines.otherEngine,
+ {
+ name: "Engine Only",
+ isDefault: false,
+ alias: undefined,
+ favIconUrl: `data:image/png;base64,${FAKE_ICON_DATA}`,
+ },
+ "browser.search.get result for other extension"
+ );
+
+ // HTTP URLs should be provided as-is.
+ Assert.deepEqual(
+ engines.httpEngine,
+ {
+ name: "HTTP Icon",
+ isDefault: false,
+ alias: undefined,
+ favIconUrl: `data:image/png;base64,${HTTP_ICON_DATA}`,
+ },
+ "browser.search.get result for extension with HTTP icon URL"
+ );
+
+ // When the favicon does not exists, the favIconUrl must be unset.
+ Assert.deepEqual(
+ engines.badEngine,
+ {
+ name: "Bad Icon",
+ isDefault: false,
+ alias: undefined,
+ favIconUrl: undefined,
+ },
+ "browser.search.get result for other extension with non-existing icon"
+ );
+
+ await extension.unload();
+ await searchExt.unload();
+ await searchExtWithBadIcon.unload();
+ await searchExtWithHttpIcon.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_search_query.js b/browser/components/extensions/test/browser/browser_ext_search_query.js
new file mode 100644
index 0000000000..5258b12605
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_search_query.js
@@ -0,0 +1,174 @@
+"use strict";
+
+add_task(async function test_query() {
+ async function background() {
+ let resolvers = {};
+
+ function tabListener(tabId, changeInfo, tab) {
+ if (tab.url == "about:blank") {
+ // Ignore events related to the initial tab open.
+ return;
+ }
+
+ if (changeInfo.status === "complete") {
+ let query = new URL(tab.url).searchParams.get("q");
+ let resolver = resolvers[query];
+ browser.test.assertTrue(resolver, `Found resolver for ${tab.url}`);
+ browser.test.assertTrue(
+ resolver.resolve,
+ `${query} was not resolved yet`
+ );
+ resolver.resolve({
+ tabId,
+ windowId: tab.windowId,
+ });
+ resolver.resolve = null; // resolve can be used only once.
+ }
+ }
+ browser.tabs.onUpdated.addListener(tabListener);
+
+ async function awaitSearchResult(args) {
+ resolvers[args.text] = {};
+ resolvers[args.text].promise = new Promise(
+ _resolve => (resolvers[args.text].resolve = _resolve)
+ );
+ await browser.search.query(args);
+ let searchResult = await resolvers[args.text].promise;
+ return searchResult;
+ }
+
+ const firstTab = await browser.tabs.create({
+ active: true,
+ url: "about:blank",
+ });
+
+ browser.test.log("Search in current tab (testing default disposition)");
+ let result = await awaitSearchResult({
+ text: "DefaultDisposition",
+ });
+ browser.test.assertDeepEq(
+ {
+ tabId: firstTab.id,
+ windowId: firstTab.windowId,
+ },
+ result,
+ "Defaults to current tab in current window"
+ );
+
+ browser.test.log(
+ "Search in current tab (testing explicit disposition CURRENT_TAB)"
+ );
+ result = await awaitSearchResult({
+ text: "CurrentTab",
+ disposition: "CURRENT_TAB",
+ });
+ browser.test.assertDeepEq(
+ {
+ tabId: firstTab.id,
+ windowId: firstTab.windowId,
+ },
+ result,
+ "Query ran in current tab in current window"
+ );
+
+ browser.test.log("Search in new tab (testing disposition NEW_TAB)");
+ result = await awaitSearchResult({
+ text: "NewTab",
+ disposition: "NEW_TAB",
+ });
+ browser.test.assertFalse(
+ result.tabId === firstTab.id,
+ "Query ran in new tab"
+ );
+ browser.test.assertEq(
+ result.windowId,
+ firstTab.windowId,
+ "Query ran in current window"
+ );
+ await browser.tabs.remove(result.tabId); // Cleanup
+
+ browser.test.log("Search in a specific tab (testing property tabId)");
+ let newTab = await browser.tabs.create({
+ active: false,
+ url: "about:blank",
+ });
+ result = await awaitSearchResult({
+ text: "SpecificTab",
+ tabId: newTab.id,
+ });
+ browser.test.assertDeepEq(
+ {
+ tabId: newTab.id,
+ windowId: firstTab.windowId,
+ },
+ result,
+ "Query ran in specific tab in current window"
+ );
+ await browser.tabs.remove(newTab.id); // Cleanup
+
+ browser.test.log("Search in a new window (testing disposition NEW_WINDOW)");
+ result = await awaitSearchResult({
+ text: "NewWindow",
+ disposition: "NEW_WINDOW",
+ });
+ browser.test.assertFalse(
+ result.windowId === firstTab.windowId,
+ "Query ran in new window"
+ );
+ await browser.windows.remove(result.windowId); // Cleanup
+ await browser.tabs.remove(firstTab.id); // Cleanup
+
+ browser.test.log("Make sure tabId and disposition can't be used together");
+ await browser.test.assertRejects(
+ browser.search.query({
+ text: " ",
+ tabId: 1,
+ disposition: "NEW_WINDOW",
+ }),
+ "Cannot set both 'disposition' and 'tabId'",
+ "Should not be able to set both tabId and disposition"
+ );
+
+ browser.test.log("Make sure we reject if an invalid tabId is used");
+ await browser.test.assertRejects(
+ browser.search.query({
+ text: " ",
+ tabId: Number.MAX_SAFE_INTEGER,
+ }),
+ /Invalid tab ID/,
+ "Should not be able to set an invalid tabId"
+ );
+
+ browser.test.notifyPass("disposition");
+ }
+ const SEARCH_NAME = "Search Test";
+ let searchExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: SEARCH_NAME,
+ search_url: "https://example.org/?q={searchTerms}",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search", "tabs"],
+ },
+ background,
+ });
+ // We need to use a fake search engine because
+ // these tests aren't allowed to load actual
+ // webpages, like google.com for example.
+ await searchExtension.startup();
+ await Services.search.setDefault(
+ Services.search.getEngineByName(SEARCH_NAME),
+ Ci.nsISearchService.CHANGE_REASON_ADDON_INSTALL
+ );
+ await extension.startup();
+ await extension.awaitFinish("disposition");
+ await extension.unload();
+ await searchExtension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedTab.js b/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedTab.js
new file mode 100644
index 0000000000..31968a61b5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedTab.js
@@ -0,0 +1,140 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function getExtension(incognitoOverride) {
+ function background() {
+ browser.test.onMessage.addListener((msg, windowId, sessionId) => {
+ if (msg === "check-sessions") {
+ browser.sessions.getRecentlyClosed().then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ } else if (msg === "forget-tab") {
+ browser.sessions.forgetClosedTab(windowId, sessionId).then(
+ () => {
+ browser.test.sendMessage("forgot-tab");
+ },
+ error => {
+ browser.test.sendMessage("forget-reject", error.message);
+ }
+ );
+ }
+ });
+ }
+
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ incognitoOverride,
+ });
+}
+
+add_task(async function test_sessions_forget_closed_tab() {
+ let extension = getExtension();
+ await extension.startup();
+
+ let tabUrl = "http://example.com";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl);
+ BrowserTestUtils.removeTab(tab);
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl);
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let recentlyClosedLength = recentlyClosed.length;
+ let recentlyClosedTab = recentlyClosed[0].tab;
+
+ // Check that forgetting a tab works properly
+ extension.sendMessage(
+ "forget-tab",
+ recentlyClosedTab.windowId,
+ recentlyClosedTab.sessionId
+ );
+ await extension.awaitMessage("forgot-tab");
+ extension.sendMessage("check-sessions");
+ let remainingClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ remainingClosed.length,
+ recentlyClosedLength - 1,
+ "One tab was forgotten."
+ );
+ is(
+ remainingClosed[0].tab.sessionId,
+ recentlyClosed[1].tab.sessionId,
+ "The correct tab was forgotten."
+ );
+
+ // Check that re-forgetting the same tab fails properly
+ extension.sendMessage(
+ "forget-tab",
+ recentlyClosedTab.windowId,
+ recentlyClosedTab.sessionId
+ );
+ let errormsg = await extension.awaitMessage("forget-reject");
+ is(
+ errormsg,
+ `Could not find closed tab using sessionId ${recentlyClosedTab.sessionId}.`
+ );
+
+ extension.sendMessage("check-sessions");
+ remainingClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ remainingClosed.length,
+ recentlyClosedLength - 1,
+ "No extra tab was forgotten."
+ );
+ is(
+ remainingClosed[0].tab.sessionId,
+ recentlyClosed[1].tab.sessionId,
+ "The correct tab remains."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_forget_closed_tab_private() {
+ let pb_extension = getExtension("spanning");
+ await pb_extension.startup();
+ let extension = getExtension();
+ await extension.startup();
+
+ // Open a private browsing window.
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ let tabUrl = "http://example.com";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ tabUrl
+ );
+ BrowserTestUtils.removeTab(tab);
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ tabUrl
+ );
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+
+ pb_extension.sendMessage("check-sessions");
+ let recentlyClosed = await pb_extension.awaitMessage("recentlyClosed");
+ let recentlyClosedTab = recentlyClosed[0].tab;
+
+ // Check that forgetting a tab works properly
+ extension.sendMessage(
+ "forget-tab",
+ recentlyClosedTab.windowId,
+ recentlyClosedTab.sessionId
+ );
+ let errormsg = await extension.awaitMessage("forget-reject");
+ ok(/Invalid window ID/.test(errormsg), "could not access window");
+
+ await BrowserTestUtils.closeWindow(privateWin);
+ await extension.unload();
+ await pb_extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedWindow.js b/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedWindow.js
new file mode 100644
index 0000000000..471d2f4440
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_forgetClosedWindow.js
@@ -0,0 +1,121 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function getExtension(incognitoOverride) {
+ function background() {
+ browser.test.onMessage.addListener((msg, sessionId) => {
+ if (msg === "check-sessions") {
+ browser.sessions.getRecentlyClosed().then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ } else if (msg === "forget-window") {
+ browser.sessions.forgetClosedWindow(sessionId).then(
+ () => {
+ browser.test.sendMessage("forgot-window");
+ },
+ error => {
+ browser.test.sendMessage("forget-reject", error.message);
+ }
+ );
+ }
+ });
+ }
+
+ return ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ incognitoOverride,
+ });
+}
+
+async function openAndCloseWindow(url = "http://example.com", privateWin) {
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: privateWin,
+ });
+ let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ await BrowserTestUtils.closeWindow(win);
+ await sessionUpdatePromise;
+}
+
+add_task(async function test_sessions_forget_closed_window() {
+ let extension = getExtension();
+ await extension.startup();
+
+ await openAndCloseWindow("about:config");
+ await openAndCloseWindow("about:robots");
+
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let recentlyClosedWindow = recentlyClosed[0].window;
+
+ // Check that forgetting a window works properly
+ extension.sendMessage("forget-window", recentlyClosedWindow.sessionId);
+ await extension.awaitMessage("forgot-window");
+ extension.sendMessage("check-sessions");
+ let remainingClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ remainingClosed.length,
+ recentlyClosed.length - 1,
+ "One window was forgotten."
+ );
+ is(
+ remainingClosed[0].window.sessionId,
+ recentlyClosed[1].window.sessionId,
+ "The correct window was forgotten."
+ );
+
+ // Check that re-forgetting the same window fails properly
+ extension.sendMessage("forget-window", recentlyClosedWindow.sessionId);
+ let errMsg = await extension.awaitMessage("forget-reject");
+ is(
+ errMsg,
+ `Could not find closed window using sessionId ${recentlyClosedWindow.sessionId}.`
+ );
+
+ extension.sendMessage("check-sessions");
+ remainingClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ remainingClosed.length,
+ recentlyClosed.length - 1,
+ "No extra window was forgotten."
+ );
+ is(
+ remainingClosed[0].window.sessionId,
+ recentlyClosed[1].window.sessionId,
+ "The correct window remains."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_forget_closed_window_private() {
+ let pb_extension = getExtension("spanning");
+ await pb_extension.startup();
+ let extension = getExtension("not_allowed");
+ await extension.startup();
+
+ await openAndCloseWindow("about:config", true);
+ await openAndCloseWindow("about:robots", true);
+
+ pb_extension.sendMessage("check-sessions");
+ let recentlyClosed = await pb_extension.awaitMessage("recentlyClosed");
+ let recentlyClosedWindow = recentlyClosed[0].window;
+
+ extension.sendMessage("forget-window", recentlyClosedWindow.sessionId);
+ await extension.awaitMessage("forgot-window");
+ extension.sendMessage("check-sessions");
+ let remainingClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ remainingClosed.length,
+ recentlyClosed.length - 1,
+ "One window was forgotten."
+ );
+ ok(!recentlyClosedWindow.incognito, "not an incognito window");
+
+ await extension.unload();
+ await pb_extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js
new file mode 100644
index 0000000000..cd883cfb25
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed.js
@@ -0,0 +1,216 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+loadTestSubscript("head_sessions.js");
+
+add_task(async function test_sessions_get_recently_closed() {
+ async function openAndCloseWindow(url = "http://example.com", tabUrls) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ if (tabUrls) {
+ for (let url of tabUrls) {
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ }
+ }
+ await BrowserTestUtils.closeWindow(win);
+ }
+
+ function background() {
+ Promise.all([
+ browser.sessions.getRecentlyClosed(),
+ browser.tabs.query({ active: true, currentWindow: true }),
+ ]).then(([recentlyClosed, tabs]) => {
+ browser.test.sendMessage("initialData", {
+ recentlyClosed,
+ currentWindowId: tabs[0].windowId,
+ });
+ });
+
+ browser.test.onMessage.addListener((msg, filter) => {
+ if (msg == "check-sessions") {
+ browser.sessions.getRecentlyClosed(filter).then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ // Open and close a window that will be ignored, to prove that we are removing previous entries
+ await openAndCloseWindow();
+
+ await extension.startup();
+
+ let { recentlyClosed, currentWindowId } = await extension.awaitMessage(
+ "initialData"
+ );
+ recordInitialTimestamps(recentlyClosed.map(item => item.lastModified));
+
+ await openAndCloseWindow();
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ checkRecentlyClosed(
+ recentlyClosed.filter(onlyNewItemsFilter),
+ 1,
+ currentWindowId
+ );
+
+ await openAndCloseWindow("about:config", ["about:robots", "about:mozilla"]);
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ // Check for multiple tabs in most recently closed window
+ is(
+ recentlyClosed[0].window.tabs.length,
+ 3,
+ "most recently closed window has the expected number of tabs"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ await openAndCloseWindow();
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let finalResult = recentlyClosed.filter(onlyNewItemsFilter);
+ checkRecentlyClosed(finalResult, 5, currentWindowId);
+
+ isnot(finalResult[0].window, undefined, "first item is a window");
+ is(finalResult[0].tab, undefined, "first item is not a tab");
+ isnot(finalResult[1].tab, undefined, "second item is a tab");
+ is(finalResult[1].window, undefined, "second item is not a window");
+ isnot(finalResult[2].tab, undefined, "third item is a tab");
+ is(finalResult[2].window, undefined, "third item is not a window");
+ isnot(finalResult[3].window, undefined, "fourth item is a window");
+ is(finalResult[3].tab, undefined, "fourth item is not a tab");
+ isnot(finalResult[4].window, undefined, "fifth item is a window");
+ is(finalResult[4].tab, undefined, "fifth item is not a tab");
+
+ // test with filter
+ extension.sendMessage("check-sessions", { maxResults: 2 });
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ checkRecentlyClosed(
+ recentlyClosed.filter(onlyNewItemsFilter),
+ 2,
+ currentWindowId
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_get_recently_closed_navigated() {
+ function background() {
+ browser.sessions
+ .getRecentlyClosed({ maxResults: 1 })
+ .then(recentlyClosed => {
+ let tab = recentlyClosed[0].window.tabs[0];
+ browser.test.assertEq(
+ "http://example.com/",
+ tab.url,
+ "Tab in closed window has the expected url."
+ );
+ browser.test.assertTrue(
+ tab.title.includes("mochitest index"),
+ "Tab in closed window has the expected title."
+ );
+ browser.test.notifyPass("getRecentlyClosed with navigation");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ // Test with a window with navigation history.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ for (let url of ["about:robots", "about:mozilla", "http://example.com/"]) {
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ }
+
+ await BrowserTestUtils.closeWindow(win);
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+});
+
+add_task(
+ async function test_sessions_get_recently_closed_empty_history_in_closed_window() {
+ function background() {
+ browser.sessions
+ .getRecentlyClosed({ maxResults: 1 })
+ .then(recentlyClosed => {
+ let win = recentlyClosed[0].window;
+ browser.test.assertEq(
+ 3,
+ win.tabs.length,
+ "The closed window has 3 tabs."
+ );
+ browser.test.assertEq(
+ "about:blank",
+ win.tabs[0].url,
+ "The first tab is about:blank."
+ );
+ browser.test.assertFalse(
+ "url" in win.tabs[1],
+ "The second tab with empty.xpi has no url field due to empty history."
+ );
+ browser.test.assertEq(
+ "http://example.com/",
+ win.tabs[2].url,
+ "The third tab is example.com."
+ );
+ browser.test.notifyPass("getRecentlyClosed with empty history");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ // Test with a window with empty history.
+ let xpi =
+ "http://example.com/browser/browser/components/extensions/test/browser/empty.xpi";
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: newWin.gBrowser,
+ url: xpi,
+ // A tab with broken xpi file doesn't finish loading.
+ waitForLoad: false,
+ });
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: newWin.gBrowser,
+ url: "http://example.com/",
+ });
+ await BrowserTestUtils.closeWindow(newWin);
+
+ await extension.startup();
+ await extension.awaitFinish();
+ await extension.unload();
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js
new file mode 100644
index 0000000000..45b1b34be1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_private.js
@@ -0,0 +1,93 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+SimpleTest.requestCompleteLog();
+
+loadTestSubscript("head_sessions.js");
+
+async function run_test_extension(incognitoOverride) {
+ function background() {
+ browser.test.onMessage.addListener((msg, filter) => {
+ if (msg == "check-sessions") {
+ browser.sessions.getRecentlyClosed(filter).then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ incognitoOverride,
+ });
+
+ // Open a private browsing window.
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await extension.startup();
+
+ const {
+ Management: {
+ global: { windowTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+ let privateWinId = windowTracker.getId(privateWin);
+
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ recordInitialTimestamps(recentlyClosed.map(item => item.lastModified));
+
+ // Open and close two tabs in the private window
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "http://example.com"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ tab = await BrowserTestUtils.openNewForegroundTab(
+ privateWin.gBrowser,
+ "http://example.com"
+ );
+ let sessionPromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionPromise;
+
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let expectedCount =
+ !incognitoOverride || incognitoOverride == "not_allowed" ? 0 : 2;
+ checkRecentlyClosed(
+ recentlyClosed.filter(onlyNewItemsFilter),
+ expectedCount,
+ privateWinId,
+ true
+ );
+
+ // Close the private window.
+ await BrowserTestUtils.closeWindow(privateWin);
+
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ is(
+ recentlyClosed.filter(onlyNewItemsFilter).length,
+ 0,
+ "the closed private window info was not found in recently closed data"
+ );
+
+ await extension.unload();
+}
+
+add_task(async function test_sessions_get_recently_closed_default() {
+ await run_test_extension();
+});
+
+add_task(async function test_sessions_get_recently_closed_private_incognito() {
+ await run_test_extension("spanning");
+ await run_test_extension("not_allowed");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
new file mode 100644
index 0000000000..6c386aaf97
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_getRecentlyClosed_tabs.js
@@ -0,0 +1,287 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function expectedTabInfo(tab, window) {
+ let browser = tab.linkedBrowser;
+ return {
+ url: browser.currentURI.spec,
+ title: browser.contentTitle,
+ favIconUrl: window.gBrowser.getIcon(tab) || undefined,
+ // 'selected' is marked as unsupported in schema, so we've removed it.
+ // For more details, see bug 1337509
+ selected: undefined,
+ };
+}
+
+function checkTabInfo(expected, actual) {
+ for (let prop in expected) {
+ is(
+ actual[prop],
+ expected[prop],
+ `Expected value found for ${prop} of tab object.`
+ );
+ }
+}
+
+add_task(async function test_sessions_get_recently_closed_tabs() {
+ // Below, the test makes assumptions about the last accessed time of tabs that are
+ // not true is we execute fast and reduce the timer precision enough
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.reduceTimerPrecision", false],
+ ["browser.navigation.requireUserInteraction", false],
+ ],
+ });
+
+ async function background() {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "check-sessions") {
+ let recentlyClosed = await browser.sessions.getRecentlyClosed();
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let tabBrowser = win.gBrowser.selectedBrowser;
+ for (let url of ["about:robots", "about:mozilla", "about:config"]) {
+ BrowserTestUtils.loadURIString(tabBrowser, url);
+ await BrowserTestUtils.browserLoaded(tabBrowser, false, url);
+ }
+
+ // Ensure that getRecentlyClosed returns correct results after the back
+ // button has been used.
+ let goBackPromise = BrowserTestUtils.waitForLocationChange(
+ win.gBrowser,
+ "about:mozilla"
+ );
+ tabBrowser.goBack();
+ await goBackPromise;
+
+ let expectedTabs = [];
+ let tab = win.gBrowser.selectedTab;
+ // Because there is debounce logic in ContentLinkHandler.jsm to reduce the
+ // favicon loads, we have to wait some time before checking that icon was
+ // stored properly. If that page doesn't have favicon links, let it timeout.
+ try {
+ await BrowserTestUtils.waitForCondition(
+ () => {
+ return gBrowser.getIcon(tab) != null;
+ },
+ "wait for favicon load to finish",
+ 100,
+ 5
+ );
+ } catch (e) {
+ // This page doesn't have any favicon link, just continue.
+ }
+ expectedTabs.push(expectedTabInfo(tab, win));
+ let lastAccessedTimes = new Map();
+ lastAccessedTimes.set("about:mozilla", tab.lastAccessed);
+
+ for (let url of ["about:robots", "about:buildconfig"]) {
+ tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ try {
+ await BrowserTestUtils.waitForCondition(
+ () => {
+ return gBrowser.getIcon(tab) != null;
+ },
+ "wait for favicon load to finish",
+ 100,
+ 5
+ );
+ } catch (e) {
+ // This page doesn't have any favicon link, just continue.
+ }
+ expectedTabs.push(expectedTabInfo(tab, win));
+ lastAccessedTimes.set(url, tab.lastAccessed);
+ }
+
+ await extension.startup();
+
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ // Test with a closed tab.
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let tabInfo = recentlyClosed[0].tab;
+ let expectedTab = expectedTabs.pop();
+ checkTabInfo(expectedTab, tabInfo);
+ ok(
+ tabInfo.lastAccessed > lastAccessedTimes.get(tabInfo.url),
+ "lastAccessed has been updated"
+ );
+
+ // Test with a closed window containing tabs.
+ await BrowserTestUtils.closeWindow(win);
+
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ let tabInfos = recentlyClosed[0].window.tabs;
+ is(tabInfos.length, 2, "Expected number of tabs in closed window.");
+ for (let x = 0; x < tabInfos.length; x++) {
+ checkTabInfo(expectedTabs[x], tabInfos[x]);
+ ok(
+ tabInfos[x].lastAccessed > lastAccessedTimes.get(tabInfos[x].url),
+ "lastAccessed has been updated"
+ );
+ }
+
+ await extension.unload();
+
+ // Test without tabs and host permissions.
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ tabInfos = recentlyClosed[0].window.tabs;
+ is(tabInfos.length, 2, "Expected number of tabs in closed window.");
+ for (let tabInfo of tabInfos) {
+ for (let prop in expectedTabs[0]) {
+ is(
+ undefined,
+ tabInfo[prop],
+ `${prop} of tab object is undefined without tabs permission.`
+ );
+ }
+ }
+
+ await extension.unload();
+
+ // Test with host permission.
+ win = await BrowserTestUtils.openNewBrowserWindow();
+ tabBrowser = win.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURIString(tabBrowser, "http://example.com/testpage");
+ await BrowserTestUtils.browserLoaded(
+ tabBrowser,
+ false,
+ "http://example.com/testpage"
+ );
+ tab = win.gBrowser.getTabForBrowser(tabBrowser);
+ try {
+ await BrowserTestUtils.waitForCondition(
+ () => {
+ return gBrowser.getIcon(tab) != null;
+ },
+ "wait for favicon load to finish",
+ 100,
+ 5
+ );
+ } catch (e) {
+ // This page doesn't have any favicon link, just continue.
+ }
+ expectedTab = expectedTabInfo(tab, win);
+ await BrowserTestUtils.closeWindow(win);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "http://example.com/*"],
+ },
+ background,
+ });
+ await extension.startup();
+
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ tabInfo = recentlyClosed[0].window.tabs[0];
+ checkTabInfo(expectedTab, tabInfo);
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_sessions_get_recently_closed_for_loading_non_web_controlled_blank_page() {
+ info("Prepare extension that calls browser.sessions.getRecentlyClosed()");
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background: async () => {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg == "check-sessions") {
+ let recentlyClosed = await browser.sessions.getRecentlyClosed();
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ }
+ });
+ },
+ });
+
+ info(
+ "Open a page having a link for non web controlled page in _blank target"
+ );
+ const testRoot = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+ );
+ let url = `${testRoot}file_has_non_web_controlled_blank_page_link.html`;
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser,
+ false,
+ url
+ );
+
+ info("Open the non web controlled page in _blank target");
+ let onNewTabOpened = new Promise(resolve =>
+ win.gBrowser.addTabsProgressListener({
+ onStateChange(browser, webProgress, request, stateFlags, status) {
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ win.gBrowser.removeTabsProgressListener(this);
+ resolve(win.gBrowser.getTabForBrowser(browser));
+ }
+ },
+ })
+ );
+ let targetUrl = await SpecialPowers.spawn(
+ win.gBrowser.selectedBrowser,
+ [],
+ () => {
+ const target = content.document.querySelector("a");
+ EventUtils.synthesizeMouseAtCenter(target, {}, content);
+ return target.href;
+ }
+ );
+ let tab = await onNewTabOpened;
+
+ info("Remove tab while loading to get getRecentlyClosed()");
+ await extension.startup();
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+
+ info("Check the result of getRecentlyClosed()");
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ checkTabInfo(
+ {
+ index: 1,
+ url: targetUrl,
+ title: targetUrl,
+ favIconUrl: undefined,
+ selected: undefined,
+ },
+ recentlyClosed[0].tab
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_incognito.js b/browser/components/extensions/test/browser/browser_ext_sessions_incognito.js
new file mode 100644
index 0000000000..aecad9e8ec
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_incognito.js
@@ -0,0 +1,113 @@
+"use strict";
+
+add_task(async function test_sessions_tab_value_private() {
+ Services.obs.notifyObservers(null, "browser:purge-session-history");
+ is(
+ SessionStore.getClosedWindowCount(),
+ 0,
+ "No closed window sessions at start of test"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "exampleextension@mozilla.org",
+ },
+ },
+ permissions: ["sessions"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async (msg, pbw) => {
+ if (msg == "value") {
+ await browser.test.assertRejects(
+ browser.sessions.setWindowValue(pbw.windowId, "foo", "bar"),
+ /Invalid window ID/,
+ "should not be able to set incognito window session data"
+ );
+ await browser.test.assertRejects(
+ browser.sessions.getWindowValue(pbw.windowId, "foo"),
+ /Invalid window ID/,
+ "should not be able to get incognito window session data"
+ );
+ await browser.test.assertRejects(
+ browser.sessions.removeWindowValue(pbw.windowId, "foo"),
+ /Invalid window ID/,
+ "should not be able to remove incognito window session data"
+ );
+ await browser.test.assertRejects(
+ browser.sessions.setTabValue(pbw.tabId, "foo", "bar"),
+ /Invalid tab ID/,
+ "should not be able to set incognito tab session data"
+ );
+ await browser.test.assertRejects(
+ browser.sessions.getTabValue(pbw.tabId, "foo"),
+ /Invalid tab ID/,
+ "should not be able to get incognito tab session data"
+ );
+ await browser.test.assertRejects(
+ browser.sessions.removeTabValue(pbw.tabId, "foo"),
+ /Invalid tab ID/,
+ "should not be able to remove incognito tab session data"
+ );
+ }
+ if (msg == "restore") {
+ await browser.test.assertRejects(
+ browser.sessions.restore(),
+ /Could not restore object/,
+ "should not be able to restore incognito last window session data"
+ );
+ if (pbw) {
+ await browser.test.assertRejects(
+ browser.sessions.restore(pbw.sessionId),
+ /Could not restore object/,
+ `should not be able to restore incognito session ID ${pbw.sessionId} session data`
+ );
+ }
+ }
+ browser.test.sendMessage("done");
+ });
+ },
+ });
+
+ let winData = await getIncognitoWindow("http://mochi.test:8888/");
+ await extension.startup();
+
+ // Test value set/get APIs on a private window and tab.
+ extension.sendMessage("value", winData.details);
+ await extension.awaitMessage("done");
+
+ // Test restoring a private tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ winData.win.gBrowser,
+ "http://example.com"
+ );
+ let sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(tab);
+ BrowserTestUtils.removeTab(tab);
+ await sessionUpdatePromise;
+ let closedTabData = SessionStore.getClosedTabDataForWindow(winData.win);
+
+ extension.sendMessage("restore", {
+ sessionId: String(closedTabData[0].closedId),
+ });
+ await extension.awaitMessage("done");
+
+ // Test restoring a private window.
+ sessionUpdatePromise = BrowserTestUtils.waitForSessionStoreUpdate(
+ winData.win.gBrowser.selectedTab
+ );
+ await BrowserTestUtils.closeWindow(winData.win);
+ await sessionUpdatePromise;
+
+ is(
+ SessionStore.getClosedWindowCount(),
+ 0,
+ "The closed window was added to Recently Closed Windows"
+ );
+
+ // If the window gets restored, test will fail with an unclosed window.
+ extension.sendMessage("restore");
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_restore.js b/browser/components/extensions/test/browser/browser_ext_sessions_restore.js
new file mode 100644
index 0000000000..0f10f66517
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_restore.js
@@ -0,0 +1,228 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+SimpleTest.requestCompleteLog();
+
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+});
+
+add_task(async function test_sessions_restore() {
+ function background() {
+ let notificationCount = 0;
+ browser.sessions.onChanged.addListener(() => {
+ notificationCount++;
+ browser.test.sendMessage("notificationCount", notificationCount);
+ });
+ browser.test.onMessage.addListener((msg, data) => {
+ if (msg == "check-sessions") {
+ browser.sessions.getRecentlyClosed().then(recentlyClosed => {
+ browser.test.sendMessage("recentlyClosed", recentlyClosed);
+ });
+ } else if (msg == "restore") {
+ browser.sessions.restore(data).then(sessions => {
+ browser.test.sendMessage("restored", sessions);
+ });
+ } else if (msg == "restore-reject") {
+ browser.sessions.restore("not-a-valid-session-id").then(
+ sessions => {
+ browser.test.fail("restore rejected with an invalid sessionId");
+ },
+ error => {
+ browser.test.assertTrue(
+ error.message.includes(
+ "Could not restore object using sessionId not-a-valid-session-id."
+ )
+ );
+ browser.test.sendMessage("restore-rejected");
+ }
+ );
+ }
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ async function assertNotificationCount(expected) {
+ let notificationCount = await extension.awaitMessage("notificationCount");
+ is(
+ notificationCount,
+ expected,
+ "the expected number of notifications was fired"
+ );
+ }
+
+ await extension.startup();
+
+ const {
+ Management: {
+ global: { windowTracker, tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ function checkLocalTab(tab, expectedUrl) {
+ let realTab = tabTracker.getTab(tab.id);
+ let tabState = JSON.parse(SessionStore.getTabState(realTab));
+ is(
+ tabState.entries[0].url,
+ expectedUrl,
+ "restored tab has the expected url"
+ );
+ }
+
+ await extension.awaitMessage("ready");
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, "about:config");
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ for (let url of ["about:robots", "about:mozilla"]) {
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ }
+ await BrowserTestUtils.closeWindow(win);
+ await assertNotificationCount(1);
+
+ extension.sendMessage("check-sessions");
+ let recentlyClosed = await extension.awaitMessage("recentlyClosed");
+
+ // Check that our expected window is the most recently closed.
+ is(
+ recentlyClosed[0].window.tabs.length,
+ 3,
+ "most recently closed window has the expected number of tabs"
+ );
+
+ // Restore the window.
+ extension.sendMessage("restore");
+ await assertNotificationCount(2);
+ let restored = await extension.awaitMessage("restored");
+
+ is(
+ restored.window.tabs.length,
+ 3,
+ "restore returned a window with the expected number of tabs"
+ );
+ checkLocalTab(restored.window.tabs[0], "about:config");
+ checkLocalTab(restored.window.tabs[1], "about:robots");
+ checkLocalTab(restored.window.tabs[2], "about:mozilla");
+
+ // Close the window again.
+ let window = windowTracker.getWindow(restored.window.id);
+ await BrowserTestUtils.closeWindow(window);
+ await assertNotificationCount(3);
+
+ // Restore the window using the sessionId.
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ extension.sendMessage("restore", recentlyClosed[0].window.sessionId);
+ await assertNotificationCount(4);
+ restored = await extension.awaitMessage("restored");
+
+ is(
+ restored.window.tabs.length,
+ 3,
+ "restore returned a window with the expected number of tabs"
+ );
+ checkLocalTab(restored.window.tabs[0], "about:config");
+ checkLocalTab(restored.window.tabs[1], "about:robots");
+ checkLocalTab(restored.window.tabs[2], "about:mozilla");
+
+ // Close the window again.
+ window = windowTracker.getWindow(restored.window.id);
+ await BrowserTestUtils.closeWindow(window);
+ // notificationCount = yield extension.awaitMessage("notificationCount");
+ await assertNotificationCount(5);
+
+ // Open and close a tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots"
+ );
+ await TabStateFlusher.flush(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+ await assertNotificationCount(6);
+
+ // Restore the most recently closed item.
+ extension.sendMessage("restore");
+ await assertNotificationCount(7);
+ restored = await extension.awaitMessage("restored");
+
+ tab = restored.tab;
+ ok(tab, "restore returned a tab");
+ checkLocalTab(tab, "about:robots");
+
+ // Close the tab again.
+ let realTab = tabTracker.getTab(tab.id);
+ BrowserTestUtils.removeTab(realTab);
+ await assertNotificationCount(8);
+
+ // Restore the tab using the sessionId.
+ extension.sendMessage("check-sessions");
+ recentlyClosed = await extension.awaitMessage("recentlyClosed");
+ extension.sendMessage("restore", recentlyClosed[0].tab.sessionId);
+ await assertNotificationCount(9);
+ restored = await extension.awaitMessage("restored");
+
+ tab = restored.tab;
+ ok(tab, "restore returned a tab");
+ checkLocalTab(tab, "about:robots");
+
+ // Close the tab again.
+ realTab = tabTracker.getTab(tab.id);
+ BrowserTestUtils.removeTab(realTab);
+ await assertNotificationCount(10);
+
+ // Try to restore something with an invalid sessionId.
+ extension.sendMessage("restore-reject");
+ restored = await extension.awaitMessage("restore-rejected");
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_event_page() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@sessions" } },
+ permissions: ["sessions", "tabs"],
+ background: { persistent: false },
+ },
+ background() {
+ browser.sessions.onChanged.addListener(() => {
+ browser.test.sendMessage("changed");
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ // test events waken background
+ await extension.terminateBackground();
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ BrowserTestUtils.loadURIString(win.gBrowser.selectedBrowser, "about:config");
+ await BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser);
+ for (let url of ["about:robots", "about:mozilla"]) {
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, url);
+ }
+ await BrowserTestUtils.closeWindow(win);
+
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("changed");
+ ok(true, "persistent event woke background");
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js b/browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js
new file mode 100644
index 0000000000..679e1fbd6c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_restoreTab.js
@@ -0,0 +1,137 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+});
+
+/**
+ This test checks that after closing an extension made tab it restores correctly.
+ The tab is given an expanded triggering principal and we didn't use to serialize
+ these correctly into session history.
+ */
+
+// Check that we can restore a tab modified by an extension.
+add_task(async function test_restoringModifiedTab() {
+ function background() {
+ browser.tabs.create({ url: "http://example.com/" });
+ browser.test.onMessage.addListener((msg, filter) => {
+ if (msg == "change-tab") {
+ browser.tabs.executeScript({ code: 'location.href += "?changedTab";' });
+ }
+ });
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", ""],
+ },
+ browser_action: {
+ default_title: "Navigate current tab via content script",
+ },
+ background,
+ });
+
+ const contentScriptTabURL = "http://example.com/?changedTab";
+
+ let win = await BrowserTestUtils.openNewBrowserWindow({});
+
+ // Open and close a tabs.
+ let tabPromise = BrowserTestUtils.waitForNewTab(
+ win.gBrowser,
+ "http://example.com/",
+ true
+ );
+ await extension.startup();
+ let firstTab = await tabPromise;
+ let locationChange = BrowserTestUtils.waitForLocationChange(
+ win.gBrowser,
+ contentScriptTabURL
+ );
+ extension.sendMessage("change-tab");
+ await locationChange;
+ is(
+ firstTab.linkedBrowser.currentURI.spec,
+ contentScriptTabURL,
+ "Got expected URL"
+ );
+
+ let sessionPromise = BrowserTestUtils.waitForSessionStoreUpdate(firstTab);
+ BrowserTestUtils.removeTab(firstTab);
+ await sessionPromise;
+
+ tabPromise = BrowserTestUtils.waitForNewTab(
+ win.gBrowser,
+ contentScriptTabURL,
+ true
+ );
+ SessionStore.undoCloseTab(win, 0);
+ let restoredTab = await tabPromise;
+ ok(restoredTab, "We returned a tab here");
+ is(
+ restoredTab.linkedBrowser.currentURI.spec,
+ contentScriptTabURL,
+ "Got expected URL"
+ );
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(restoredTab);
+
+ // Close the window.
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_restoringClosedTabWithTooLargeIndex() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, filter) => {
+ if (msg != "restoreTab") {
+ return;
+ }
+ const recentlyClosed = await browser.sessions.getRecentlyClosed({
+ maxResults: 2,
+ });
+ let tabWithTooLargeIndex;
+ for (const info of recentlyClosed) {
+ if (info.tab && info.tab.index > 1) {
+ tabWithTooLargeIndex = info.tab;
+ break;
+ }
+ }
+ const onRestored = tab => {
+ browser.tabs.onCreated.removeListener(onRestored);
+ browser.test.sendMessage("restoredTab", tab);
+ };
+ browser.tabs.onCreated.addListener(onRestored);
+ browser.sessions.restore(tabWithTooLargeIndex.sessionId);
+ });
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "sessions"],
+ },
+ background,
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({});
+ const tabs = await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank?0"),
+ BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank?1"),
+ ]);
+ const promsiedSessionStored = Promise.all([
+ BrowserTestUtils.waitForSessionStoreUpdate(tabs[0]),
+ BrowserTestUtils.waitForSessionStoreUpdate(tabs[1]),
+ ]);
+ // Close the rightmost tab at first
+ BrowserTestUtils.removeTab(tabs[1]);
+ BrowserTestUtils.removeTab(tabs[0]);
+ await promsiedSessionStored;
+
+ await extension.startup();
+ const promisedRestoredTab = extension.awaitMessage("restoredTab");
+ extension.sendMessage("restoreTab");
+ const restoredTab = await promisedRestoredTab;
+ is(restoredTab.index, 1, "Got valid index");
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sessions_window_tab_value.js b/browser/components/extensions/test/browser/browser_ext_sessions_window_tab_value.js
new file mode 100644
index 0000000000..b21b59fe8c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sessions_window_tab_value.js
@@ -0,0 +1,398 @@
+"use strict";
+
+add_task(async function test_sessions_tab_value() {
+ info("Testing set/get/deleteTabValue.");
+
+ async function background() {
+ let tests = [
+ { key: "tabkey1", value: "Tab Value" },
+ { key: "tabkey2", value: 25 },
+ { key: "tabkey3", value: { val: "Tab Value" } },
+ {
+ key: "tabkey4",
+ value: function () {
+ return null;
+ },
+ },
+ ];
+
+ async function test(params) {
+ let { key, value } = params;
+ let tabs = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ let currentTabId = tabs[0].id;
+
+ browser.sessions.setTabValue(currentTabId, key, value);
+
+ let testValue1 = await browser.sessions.getTabValue(currentTabId, key);
+ let valueType = typeof value;
+
+ browser.test.log(
+ `Test that setting, getting and deleting tab value behaves properly when value is type "${valueType}"`
+ );
+
+ if (valueType == "string") {
+ browser.test.assertEq(
+ value,
+ testValue1,
+ `Value for key '${key}' should be '${value}'.`
+ );
+ browser.test.assertEq(
+ "string",
+ typeof testValue1,
+ "typeof value should be '${valueType}'."
+ );
+ } else if (valueType == "number") {
+ browser.test.assertEq(
+ value,
+ testValue1,
+ `Value for key '${key}' should be '${value}'.`
+ );
+ browser.test.assertEq(
+ "number",
+ typeof testValue1,
+ "typeof value should be '${valueType}'."
+ );
+ } else if (valueType == "object") {
+ let innerVal1 = value.val;
+ let innerVal2 = testValue1.val;
+ browser.test.assertEq(
+ innerVal1,
+ innerVal2,
+ `Value for key '${key}' should be '${innerVal1}'.`
+ );
+ } else if (valueType == "function") {
+ browser.test.assertEq(
+ null,
+ testValue1,
+ `Value for key '${key}' is non-JSON-able and should be 'null'.`
+ );
+ }
+
+ // Remove the tab key/value.
+ browser.sessions.removeTabValue(currentTabId, key);
+
+ // This should now return undefined.
+ testValue1 = await browser.sessions.getTabValue(currentTabId, key);
+ browser.test.assertEq(
+ undefined,
+ testValue1,
+ `Key has been deleted and value for key "${key}" should be 'undefined'.`
+ );
+ }
+
+ for (let params of tests) {
+ await test(params);
+ }
+
+ // Attempt to remove a non-existent key, should not throw error.
+ let tabs = await browser.tabs.query({ currentWindow: true, active: true });
+ await browser.sessions.removeTabValue(tabs[0].id, "non-existent-key");
+ browser.test.succeed(
+ "Attempting to remove a non-existent key should not fail."
+ );
+
+ browser.test.sendMessage("testComplete");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "exampleextension@mozilla.org",
+ },
+ },
+ permissions: ["sessions", "tabs"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("testComplete");
+ ok(true, "Testing completed for set/get/deleteTabValue.");
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_tab_value_persistence() {
+ info("Testing for persistence of set tab values.");
+
+ async function background() {
+ let key = "tabkey1";
+ let value1 = "Tab Value 1a";
+ let value2 = "Tab Value 1b";
+
+ browser.test.log(
+ "Test that two different tabs hold different values for a given key."
+ );
+
+ await browser.tabs.create({ url: "http://example.com" });
+
+ // Wait until the newly created tab has completed loading or it will still have
+ // about:blank url when it gets removed and will not appear in the removed tabs history.
+ browser.webNavigation.onCompleted.addListener(
+ async function newTabListener(details) {
+ browser.webNavigation.onCompleted.removeListener(newTabListener);
+
+ let tabs = await browser.tabs.query({ currentWindow: true });
+
+ let tabId_1 = tabs[0].id;
+ let tabId_2 = tabs[1].id;
+
+ browser.sessions.setTabValue(tabId_1, key, value1);
+ browser.sessions.setTabValue(tabId_2, key, value2);
+
+ let testValue1 = await browser.sessions.getTabValue(tabId_1, key);
+ let testValue2 = await browser.sessions.getTabValue(tabId_2, key);
+
+ browser.test.assertEq(
+ value1,
+ testValue1,
+ `Value for key '${key}' should be '${value1}'.`
+ );
+ browser.test.assertEq(
+ value2,
+ testValue2,
+ `Value for key '${key}' should be '${value2}'.`
+ );
+
+ browser.test.log(
+ "Test that value is copied to duplicated tab for a given key."
+ );
+
+ let duptab = await browser.tabs.duplicate(tabId_2);
+ let tabId_3 = duptab.id;
+
+ let testValue3 = await browser.sessions.getTabValue(tabId_3, key);
+
+ browser.test.assertEq(
+ value2,
+ testValue3,
+ `Value for key '${key}' should be '${value2}'.`
+ );
+
+ browser.test.log(
+ "Test that restored tab still holds the value for a given key."
+ );
+
+ await browser.tabs.remove([tabId_3]);
+
+ let sessions = await browser.sessions.getRecentlyClosed({
+ maxResults: 1,
+ });
+
+ let sessionData = await browser.sessions.restore(
+ sessions[0].tab.sessionId
+ );
+ let restoredId = sessionData.tab.id;
+
+ let testValue = await browser.sessions.getTabValue(restoredId, key);
+
+ browser.test.assertEq(
+ value2,
+ testValue,
+ `Value for key '${key}' should be '${value2}'.`
+ );
+
+ await browser.tabs.remove(tabId_2);
+ await browser.tabs.remove(restoredId);
+
+ browser.test.sendMessage("testComplete");
+ },
+ { url: [{ hostContains: "example.com" }] }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "exampleextension@mozilla.org",
+ },
+ },
+ permissions: ["sessions", "tabs", "webNavigation"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("testComplete");
+ ok(true, "Testing completed for persistance of set tab values.");
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_window_value() {
+ info("Testing set/get/deleteWindowValue.");
+
+ async function background() {
+ let tests = [
+ { key: "winkey1", value: "Window Value" },
+ { key: "winkey2", value: 25 },
+ { key: "winkey3", value: { val: "Window Value" } },
+ {
+ key: "winkey4",
+ value: function () {
+ return null;
+ },
+ },
+ ];
+
+ async function test(params) {
+ let { key, value } = params;
+ let win = await browser.windows.getCurrent();
+ let currentWinId = win.id;
+
+ browser.sessions.setWindowValue(currentWinId, key, value);
+
+ let testValue1 = await browser.sessions.getWindowValue(currentWinId, key);
+ let valueType = typeof value;
+
+ browser.test.log(
+ `Test that setting, getting and deleting window value behaves properly when value is type "${valueType}"`
+ );
+
+ if (valueType == "string") {
+ browser.test.assertEq(
+ value,
+ testValue1,
+ `Value for key '${key}' should be '${value}'.`
+ );
+ browser.test.assertEq(
+ "string",
+ typeof testValue1,
+ "typeof value should be '${valueType}'."
+ );
+ } else if (valueType == "number") {
+ browser.test.assertEq(
+ value,
+ testValue1,
+ `Value for key '${key}' should be '${value}'.`
+ );
+ browser.test.assertEq(
+ "number",
+ typeof testValue1,
+ "typeof value should be '${valueType}'."
+ );
+ } else if (valueType == "object") {
+ let innerVal1 = value.val;
+ let innerVal2 = testValue1.val;
+ browser.test.assertEq(
+ innerVal1,
+ innerVal2,
+ `Value for key '${key}' should be '${innerVal1}'.`
+ );
+ } else if (valueType == "function") {
+ browser.test.assertEq(
+ null,
+ testValue1,
+ `Value for key '${key}' is non-JSON-able and should be 'null'.`
+ );
+ }
+
+ // Remove the window key/value.
+ browser.sessions.removeWindowValue(currentWinId, key);
+
+ // This should return undefined as the key no longer exists.
+ testValue1 = await browser.sessions.getWindowValue(currentWinId, key);
+ browser.test.assertEq(
+ undefined,
+ testValue1,
+ `Key has been deleted and value for key '${key}' should be 'undefined'.`
+ );
+ }
+
+ for (let params of tests) {
+ await test(params);
+ }
+
+ // Attempt to remove a non-existent key, should not throw error.
+ let win = await browser.windows.getCurrent();
+ await browser.sessions.removeWindowValue(win.id, "non-existent-key");
+ browser.test.succeed(
+ "Attempting to remove a non-existent key should not fail."
+ );
+
+ browser.test.sendMessage("testComplete");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "exampleextension@mozilla.org",
+ },
+ },
+ permissions: ["sessions"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("testComplete");
+ ok(true, "Testing completed for set/get/deleteWindowValue.");
+
+ await extension.unload();
+});
+
+add_task(async function test_sessions_window_value_persistence() {
+ info(
+ "Testing that different values for the same key in different windows are persisted properly."
+ );
+
+ async function background() {
+ let key = "winkey1";
+ let value1 = "Window Value 1a";
+ let value2 = "Window Value 1b";
+
+ let window1 = await browser.windows.getCurrent();
+ let window2 = await browser.windows.create({});
+
+ let window1Id = window1.id;
+ let window2Id = window2.id;
+
+ browser.sessions.setWindowValue(window1Id, key, value1);
+ browser.sessions.setWindowValue(window2Id, key, value2);
+
+ let testValue1 = await browser.sessions.getWindowValue(window1Id, key);
+ let testValue2 = await browser.sessions.getWindowValue(window2Id, key);
+
+ browser.test.assertEq(
+ value1,
+ testValue1,
+ `Value for key '${key}' should be '${value1}'.`
+ );
+ browser.test.assertEq(
+ value2,
+ testValue2,
+ `Value for key '${key}' should be '${value2}'.`
+ );
+
+ await browser.windows.remove(window2Id);
+ browser.test.sendMessage("testComplete");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "exampleextension@mozilla.org",
+ },
+ },
+ permissions: ["sessions"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("testComplete");
+ ok(true, "Testing completed for persistance of set window values.");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js b/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js
new file mode 100644
index 0000000000..74eaa6e634
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_settings_overrides_default_search.js
@@ -0,0 +1,881 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+const EXTENSION1_ID = "extension1@mozilla.com";
+const EXTENSION2_ID = "extension2@mozilla.com";
+const DEFAULT_SEARCH_STORE_TYPE = "default_search";
+const DEFAULT_SEARCH_SETTING_NAME = "defaultSearch";
+
+AddonTestUtils.initMochitest(this);
+SearchTestUtils.init(this);
+
+const DEFAULT_ENGINE = {
+ id: "basic",
+ name: "basic",
+ loadPath: "[addon]basic@search.mozilla.org",
+ submissionUrl:
+ "https://mochi.test:8888/browser/browser/components/search/test/browser/?search=&foo=1",
+};
+const ALTERNATE_ENGINE = {
+ id: "simple",
+ name: "Simple Engine",
+ loadPath: "[addon]simple@search.mozilla.org",
+ submissionUrl: "https://example.com/?sourceId=Mozilla-search&search=",
+};
+const ALTERNATE2_ENGINE = {
+ id: "simple",
+ name: "another",
+ loadPath: "",
+ submissionUrl: "",
+};
+
+async function restoreDefaultEngine() {
+ let engine = Services.search.getEngineByName(DEFAULT_ENGINE.name);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+}
+
+function clearTelemetry() {
+ Services.telemetry.clearEvents();
+ Services.fog.testResetFOG();
+}
+
+async function checkTelemetry(source, prevEngine, newEngine) {
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "change_default",
+ value: source,
+ extra: {
+ prev_id: prevEngine.id,
+ new_id: newEngine.id,
+ new_name: newEngine.name,
+ new_load_path: newEngine.loadPath,
+ // Telemetry has a limit of 80 characters.
+ new_sub_url: newEngine.submissionUrl.slice(0, 80),
+ },
+ },
+ ],
+ { category: "search", method: "engine" }
+ );
+
+ let snapshot = await Glean.searchEngineDefault.changed.testGetValue();
+ delete snapshot[0].timestamp;
+ Assert.deepEqual(
+ snapshot[0],
+ {
+ category: "search.engine.default",
+ name: "changed",
+ extra: {
+ change_source: source,
+ previous_engine_id: prevEngine.id,
+ new_engine_id: newEngine.id,
+ new_display_name: newEngine.name,
+ new_load_path: newEngine.loadPath,
+ new_submission_url: newEngine.submissionUrl,
+ },
+ },
+ "Should have received the correct event details"
+ );
+}
+
+add_setup(async function () {
+ let searchExtensions = getChromeDir(getResolvedURI(gTestPath));
+ searchExtensions.append("search-engines");
+
+ await SearchTestUtils.useMochitestEngines(searchExtensions);
+
+ SearchTestUtils.useMockIdleService();
+ let response = await fetch(`resource://search-extensions/engines.json`);
+ let json = await response.json();
+ await SearchTestUtils.updateRemoteSettingsConfig(json.data);
+
+ registerCleanupFunction(async () => {
+ let settingsWritten = SearchTestUtils.promiseSearchNotification(
+ "write-settings-to-disk-complete"
+ );
+ await SearchTestUtils.updateRemoteSettingsConfig();
+ await settingsWritten;
+ });
+});
+
+/* This tests setting a default engine. */
+add_task(async function test_extension_setting_default_engine() {
+ clearTelemetry();
+
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await checkTelemetry("addon-install", DEFAULT_ENGINE, ALTERNATE_ENGINE);
+
+ clearTelemetry();
+
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+
+ await checkTelemetry("addon-uninstall", ALTERNATE_ENGINE, DEFAULT_ENGINE);
+});
+
+/* This tests what happens when the engine you're setting it to is hidden. */
+add_task(async function test_extension_setting_default_engine_hidden() {
+ let engine = Services.search.getEngineByName(ALTERNATE_ENGINE.name);
+ engine.hidden = true;
+
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ "Default engine should have remained as the default"
+ );
+ is(
+ ExtensionSettingsStore.getSetting("default_search", "defaultSearch"),
+ null,
+ "The extension should not have been recorded as having set the default search"
+ );
+
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+ engine.hidden = false;
+});
+
+// Test the popup displayed when trying to add a non-built-in default
+// search engine.
+add_task(async function test_extension_setting_default_engine_external() {
+ const NAME = "Example Engine";
+
+ // Load an extension that tries to set the default engine,
+ // and wait for the ensuing prompt.
+ async function startExtension(win = window) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ icons: {
+ 48: "icon.png",
+ 96: "icon@2x.png",
+ },
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION1_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: NAME,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ files: {
+ "icon.png": "",
+ "icon@2x.png": "",
+ },
+ useAddonManager: "temporary",
+ });
+
+ let [panel] = await Promise.all([
+ promisePopupNotificationShown("addon-webext-defaultsearch", win),
+ extension.startup(),
+ ]);
+
+ isnot(
+ panel,
+ null,
+ "Doorhanger was displayed for non-built-in default engine"
+ );
+
+ return { panel, extension };
+ }
+
+ // First time around, don't accept the default engine.
+ let { panel, extension } = await startExtension();
+ ok(
+ panel.getAttribute("icon").endsWith("/icon.png"),
+ "expected custom icon set on the notification"
+ );
+
+ panel.secondaryButton.click();
+
+ await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ "Default engine was not changed after rejecting prompt"
+ );
+
+ await extension.unload();
+
+ clearTelemetry();
+
+ // Do it again, this time accept the prompt.
+ ({ panel, extension } = await startExtension());
+
+ panel.button.click();
+ await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");
+
+ is(
+ (await Services.search.getDefault()).name,
+ NAME,
+ "Default engine was changed after accepting prompt"
+ );
+
+ await checkTelemetry("addon-install", DEFAULT_ENGINE, {
+ id: "other-Example Engine",
+ name: "Example Engine",
+ loadPath: "[addon]extension1@mozilla.com",
+ submissionUrl: "https://example.com/?q=",
+ });
+ clearTelemetry();
+
+ // Do this twice to make sure we're definitely handling disable/enable
+ // correctly. Disabling and enabling the addon here like this also
+ // replicates the behavior when an addon is added then removed in the
+ // blocklist.
+ let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
+ let addon = await AddonManager.getAddonByID(EXTENSION1_ID);
+ await addon.disable();
+ await disabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name} after disabling`
+ );
+
+ await checkTelemetry(
+ "addon-uninstall",
+ {
+ id: "other-Example Engine",
+ name: "Example Engine",
+ loadPath: "[addon]extension1@mozilla.com",
+ submissionUrl: "https://example.com/?q=",
+ },
+ DEFAULT_ENGINE
+ );
+ clearTelemetry();
+
+ let opened = promisePopupNotificationShown(
+ "addon-webext-defaultsearch",
+ window
+ );
+ await addon.enable();
+ panel = await opened;
+ panel.button.click();
+ await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");
+
+ is(
+ (await Services.search.getDefault()).name,
+ NAME,
+ `Default engine is ${NAME} after enabling`
+ );
+
+ await checkTelemetry("addon-install", DEFAULT_ENGINE, {
+ id: "other-Example Engine",
+ name: "Example Engine",
+ loadPath: "[addon]extension1@mozilla.com",
+ submissionUrl: "https://example.com/?q=",
+ });
+
+ await extension.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ "Default engine is reverted after uninstalling extension."
+ );
+
+ // One more time, this time close the window where the prompt
+ // appears instead of explicitly accepting or denying it.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank");
+
+ ({ extension } = await startExtension(win));
+
+ await BrowserTestUtils.closeWindow(win);
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ "Default engine is unchanged when prompt is dismissed"
+ );
+
+ await extension.unload();
+});
+
+/* This tests that uninstalling add-ons maintains the proper
+ * search default. */
+add_task(async function test_extension_setting_multiple_default_engine() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE2_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await ext2.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext2);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ await ext2.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+});
+
+/* This tests that uninstalling add-ons in reverse order maintains the proper
+ * search default. */
+add_task(
+ async function test_extension_setting_multiple_default_engine_reversed() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE2_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await ext2.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext2);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ await ext2.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+ }
+);
+
+/* This tests that when the user changes the search engine and the add-on
+ * is unistalled, search stays with the user's choice. */
+add_task(async function test_user_changing_default_engine() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ let engine = Services.search.getEngineByName(ALTERNATE2_ENGINE.name);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ // This simulates the preferences UI when the setting is changed.
+ ExtensionSettingsStore.select(
+ ExtensionSettingsStore.SETTING_USER_SET,
+ DEFAULT_SEARCH_STORE_TYPE,
+ DEFAULT_SEARCH_SETTING_NAME
+ );
+
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+ restoreDefaultEngine();
+});
+
+/* This tests that when the user changes the search engine while it is
+ * disabled, user choice is maintained when the add-on is reenabled. */
+add_task(async function test_user_change_with_disabling() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION1_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ let engine = Services.search.getEngineByName(ALTERNATE2_ENGINE.name);
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ // This simulates the preferences UI when the setting is changed.
+ ExtensionSettingsStore.select(
+ ExtensionSettingsStore.SETTING_USER_SET,
+ DEFAULT_SEARCH_STORE_TYPE,
+ DEFAULT_SEARCH_SETTING_NAME
+ );
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
+ let addon = await AddonManager.getAddonByID(EXTENSION1_ID);
+ await addon.disable();
+ await disabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let processedPromise = awaitEvent("searchEngineProcessed", EXTENSION1_ID);
+ await addon.enable();
+ await processedPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+ await ext1.unload();
+ await restoreDefaultEngine();
+});
+
+/* This tests that when two add-ons are installed that change default
+ * search and the first one is disabled, before the second one is installed,
+ * when the first one is reenabled, the second add-on keeps the search. */
+add_task(async function test_two_addons_with_first_disabled_before_second() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION1_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION2_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE2_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
+ let addon1 = await AddonManager.getAddonByID(EXTENSION1_ID);
+ await addon1.disable();
+ await disabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+
+ await ext2.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext2);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let enabledPromise = awaitEvent("ready", EXTENSION1_ID);
+ await addon1.enable();
+ await enabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+ await ext2.unload();
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+});
+
+/* This tests that when two add-ons are installed that change default
+ * search and the first one is disabled, the second one maintains
+ * the search. */
+add_task(async function test_two_addons_with_first_disabled() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION1_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION2_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE2_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await ext2.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext2);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let disabledPromise = awaitEvent("shutdown", EXTENSION1_ID);
+ let addon1 = await AddonManager.getAddonByID(EXTENSION1_ID);
+ await addon1.disable();
+ await disabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let enabledPromise = awaitEvent("ready", EXTENSION1_ID);
+ await addon1.enable();
+ await enabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+ await ext2.unload();
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+});
+
+/* This tests that when two add-ons are installed that change default
+ * search and the second one is disabled, the first one properly
+ * gets the search. */
+add_task(async function test_two_addons_with_second_disabled() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION1_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION2_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: ALTERNATE2_ENGINE.name,
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ await ext2.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext2);
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+
+ let disabledPromise = awaitEvent("shutdown", EXTENSION2_ID);
+ let addon2 = await AddonManager.getAddonByID(EXTENSION2_ID);
+ await addon2.disable();
+ await disabledPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+
+ let defaultPromise = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ // No prompt, because this is switching to an app-provided engine.
+ await addon2.enable();
+ await defaultPromise;
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE2_ENGINE.name,
+ `Default engine is ${ALTERNATE2_ENGINE.name}`
+ );
+ await ext2.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ ALTERNATE_ENGINE.name,
+ `Default engine is ${ALTERNATE_ENGINE.name}`
+ );
+ await ext1.unload();
+
+ is(
+ (await Services.search.getDefault()).name,
+ DEFAULT_ENGINE.name,
+ `Default engine is ${DEFAULT_ENGINE.name}`
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
new file mode 100644
index 0000000000..e943b708cb
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction.js
@@ -0,0 +1,268 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+let extData = {
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+
+ background: function () {
+ browser.test.onMessage.addListener(async ({ msg, data }) => {
+ if (msg === "set-panel") {
+ await browser.sidebarAction.setPanel({ panel: null });
+ browser.test.assertEq(
+ await browser.sidebarAction.getPanel({}),
+ browser.runtime.getURL("sidebar.html"),
+ "Global panel can be reverted to the default."
+ );
+ } else if (msg === "isOpen") {
+ let { arg = {}, result } = data;
+ let isOpen = await browser.sidebarAction.isOpen(arg);
+ browser.test.assertEq(result, isOpen, "expected value from isOpen");
+ }
+ browser.test.sendMessage("done");
+ });
+ },
+};
+
+function getExtData(manifestUpdates = {}) {
+ return {
+ ...extData,
+ manifest: {
+ ...extData.manifest,
+ ...manifestUpdates,
+ },
+ };
+}
+
+async function sendMessage(ext, msg, data = undefined) {
+ ext.sendMessage({ msg, data });
+ await ext.awaitMessage("done");
+}
+
+add_task(async function sidebar_initial_install() {
+ ok(
+ document.getElementById("sidebar-box").hidden,
+ "sidebar box is not visible"
+ );
+ let extension = ExtensionTestUtils.loadExtension(getExtData());
+ await extension.startup();
+ await extension.awaitMessage("sidebar");
+
+ // Test sidebar is opened on install
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible");
+
+ await extension.unload();
+ // Test that the sidebar was closed on unload.
+ ok(
+ document.getElementById("sidebar-box").hidden,
+ "sidebar box is not visible"
+ );
+});
+
+add_task(async function sidebar__install_closed() {
+ ok(
+ document.getElementById("sidebar-box").hidden,
+ "sidebar box is not visible"
+ );
+ let tempExtData = getExtData();
+ tempExtData.manifest.sidebar_action.open_at_install = false;
+ let extension = ExtensionTestUtils.loadExtension(tempExtData);
+ await extension.startup();
+
+ // Test sidebar is closed on install
+ ok(document.getElementById("sidebar-box").hidden, "sidebar box is hidden");
+
+ await extension.unload();
+ // This is the default value
+ tempExtData.manifest.sidebar_action.open_at_install = true;
+});
+
+add_task(async function sidebar_two_sidebar_addons() {
+ let extension2 = ExtensionTestUtils.loadExtension(getExtData());
+ await extension2.startup();
+ // Test sidebar is opened on install
+ await extension2.awaitMessage("sidebar");
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible");
+
+ // Test second sidebar install opens new sidebar
+ let extension3 = ExtensionTestUtils.loadExtension(getExtData());
+ await extension3.startup();
+ // Test sidebar is opened on install
+ await extension3.awaitMessage("sidebar");
+ ok(!document.getElementById("sidebar-box").hidden, "sidebar box is visible");
+ await extension3.unload();
+
+ // We just close the sidebar on uninstall of the current sidebar.
+ ok(
+ document.getElementById("sidebar-box").hidden,
+ "sidebar box is not visible"
+ );
+
+ await extension2.unload();
+});
+
+add_task(async function sidebar_empty_panel() {
+ let extension = ExtensionTestUtils.loadExtension(getExtData());
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+ ok(
+ !document.getElementById("sidebar-box").hidden,
+ "sidebar box is visible in first window"
+ );
+ await sendMessage(extension, "set-panel");
+ await extension.unload();
+});
+
+add_task(async function sidebar_isOpen() {
+ info("Load extension1");
+ let extension1 = ExtensionTestUtils.loadExtension(getExtData());
+ await extension1.startup();
+
+ info("Test extension1's sidebar is opened on install");
+ await extension1.awaitMessage("sidebar");
+ await sendMessage(extension1, "isOpen", { result: true });
+ let sidebar1ID = SidebarUI.currentID;
+
+ info("Load extension2");
+ let extension2 = ExtensionTestUtils.loadExtension(getExtData());
+ await extension2.startup();
+
+ info("Test extension2's sidebar is opened on install");
+ await extension2.awaitMessage("sidebar");
+ await sendMessage(extension1, "isOpen", { result: false });
+ await sendMessage(extension2, "isOpen", { result: true });
+
+ info("Switch back to extension1's sidebar");
+ SidebarUI.show(sidebar1ID);
+ await extension1.awaitMessage("sidebar");
+ await sendMessage(extension1, "isOpen", { result: true });
+ await sendMessage(extension2, "isOpen", { result: false });
+
+ info("Test passing a windowId parameter");
+ let windowId = window.docShell.outerWindowID;
+ let WINDOW_ID_CURRENT = -2;
+ await sendMessage(extension1, "isOpen", { arg: { windowId }, result: true });
+ await sendMessage(extension2, "isOpen", { arg: { windowId }, result: false });
+ await sendMessage(extension1, "isOpen", {
+ arg: { windowId: WINDOW_ID_CURRENT },
+ result: true,
+ });
+ await sendMessage(extension2, "isOpen", {
+ arg: { windowId: WINDOW_ID_CURRENT },
+ result: false,
+ });
+
+ info("Open a new window");
+ open("", "", "noopener");
+ let newWin = Services.wm.getMostRecentWindow("navigator:browser");
+
+ info("The new window has no sidebar");
+ await sendMessage(extension1, "isOpen", { result: false });
+ await sendMessage(extension2, "isOpen", { result: false });
+
+ info("But the original window still does");
+ await sendMessage(extension1, "isOpen", { arg: { windowId }, result: true });
+ await sendMessage(extension2, "isOpen", { arg: { windowId }, result: false });
+
+ info("Close the new window");
+ newWin.close();
+
+ info("Close the sidebar in the original window");
+ SidebarUI.hide();
+ await sendMessage(extension1, "isOpen", { result: false });
+ await sendMessage(extension2, "isOpen", { result: false });
+
+ await extension1.unload();
+ await extension2.unload();
+});
+
+add_task(async function testShortcuts() {
+ function verifyShortcut(id, commandKey) {
+ // We're just testing the command key since the modifiers have different
+ // icons on different platforms.
+ let button = document.getElementById(
+ `button_${makeWidgetId(id)}-sidebar-action`
+ );
+ ok(button.hasAttribute("key"), "The menu item has a key specified");
+ let key = document.getElementById(button.getAttribute("key"));
+ ok(key, "The key attribute finds the related key element");
+ ok(
+ button.getAttribute("shortcut").endsWith(commandKey),
+ "The shortcut has the right key"
+ );
+ }
+
+ let extension1 = ExtensionTestUtils.loadExtension(
+ getExtData({
+ commands: {
+ _execute_sidebar_action: {
+ suggested_key: {
+ default: "Ctrl+Shift+I",
+ },
+ },
+ },
+ })
+ );
+ let extension2 = ExtensionTestUtils.loadExtension(
+ getExtData({
+ commands: {
+ _execute_sidebar_action: {
+ suggested_key: {
+ default: "Ctrl+Shift+E",
+ },
+ },
+ },
+ })
+ );
+
+ await extension1.startup();
+ await extension1.awaitMessage("sidebar");
+
+ // Open and close the switcher panel to trigger shortcut content rendering.
+ let switcherPanelShown = promisePopupShown(SidebarUI._switcherPanel);
+ SidebarUI.showSwitcherPanel();
+ await switcherPanelShown;
+ let switcherPanelHidden = promisePopupHidden(SidebarUI._switcherPanel);
+ SidebarUI.hideSwitcherPanel();
+ await switcherPanelHidden;
+
+ // Test that the key is set for the extension after the shortcuts are rendered.
+ verifyShortcut(extension1.id, "I");
+
+ await extension2.startup();
+ await extension2.awaitMessage("sidebar");
+
+ // Once the switcher panel has been opened new shortcuts should be added
+ // automatically.
+ verifyShortcut(extension2.id, "E");
+
+ await extension1.unload();
+ await extension2.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_browser_style.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_browser_style.js
new file mode 100644
index 0000000000..866d7a3b3d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_browser_style.js
@@ -0,0 +1,90 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testSidebarBrowserStyle(sidebarAction, assertMessage) {
+ function sidebarScript() {
+ browser.test.onMessage.addListener((msgName, info, assertMessage) => {
+ if (msgName !== "check-style") {
+ browser.test.notifyFail("sidebar-browser-style");
+ }
+
+ let style = window.getComputedStyle(document.getElementById("button"));
+ let buttonBackgroundColor = style.backgroundColor;
+ let browserStyleBackgroundColor = "rgb(9, 150, 248)";
+ if (!("browser_style" in info) || info.browser_style) {
+ browser.test.assertEq(
+ browserStyleBackgroundColor,
+ buttonBackgroundColor,
+ assertMessage
+ );
+ } else {
+ browser.test.assertTrue(
+ browserStyleBackgroundColor !== buttonBackgroundColor,
+ assertMessage
+ );
+ }
+
+ browser.test.notifyPass("sidebar-browser-style");
+ });
+ browser.test.sendMessage("sidebar-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ sidebar_action: sidebarAction,
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "panel.html": `
+
+
+
+
+ `,
+ "panel.js": sidebarScript,
+ },
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await extension.startup();
+ await extension.awaitMessage("sidebar-ready");
+
+ extension.sendMessage("check-style", sidebarAction, assertMessage);
+ await extension.awaitFinish("sidebar-browser-style");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_sidebar_without_setting_browser_style() {
+ await testSidebarBrowserStyle(
+ {
+ default_panel: "panel.html",
+ },
+ "Expected correct style when browser_style is excluded"
+ );
+});
+
+add_task(async function test_sidebar_with_browser_style_set_to_true() {
+ await testSidebarBrowserStyle(
+ {
+ default_panel: "panel.html",
+ browser_style: true,
+ },
+ "Expected correct style when browser_style is set to `true`"
+ );
+});
+
+add_task(async function test_sidebar_with_browser_style_set_to_false() {
+ await testSidebarBrowserStyle(
+ {
+ default_panel: "panel.html",
+ browser_style: false,
+ },
+ "Expected no style when browser_style is set to `false`"
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js
new file mode 100644
index 0000000000..621d2d1180
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_click.js
@@ -0,0 +1,74 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_sidebar_click_isAppTab_behavior() {
+ function sidebarScript() {
+ browser.tabs.onUpdated.addListener(function onUpdated(
+ tabId,
+ changeInfo,
+ tab
+ ) {
+ if (
+ changeInfo.status == "complete" &&
+ tab.url == "http://mochi.test:8888/"
+ ) {
+ browser.tabs.remove(tab.id);
+ browser.test.notifyPass("sidebar-click");
+ }
+ });
+ window.addEventListener(
+ "load",
+ () => {
+ browser.test.sendMessage("sidebar-ready");
+ },
+ { once: true }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ sidebar_action: {
+ default_panel: "panel.html",
+ browser_style: false,
+ },
+ permissions: ["tabs"],
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "panel.html": `
+
+
+
+
+
+
+ Bugzilla
+ `,
+ "panel.js": sidebarScript,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("sidebar-ready");
+
+ // This test fails if docShell.isAppTab has not been set to true.
+ let content = SidebarUI.browser.contentWindow;
+
+ // Wait for the layout to be flushed, otherwise this test may
+ // fail intermittently if synthesizeMouseAtCenter is being called
+ // while the sidebar is still opening and the browser window layout
+ // being recomputed.
+ await content.promiseDocumentFlushed(() => {});
+
+ info("Clicking link in extension sidebar");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#testlink",
+ {},
+ content.gBrowser.selectedBrowser
+ );
+ await extension.awaitFinish("sidebar-click");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
new file mode 100644
index 0000000000..6e5b4d6cd0
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_context.js
@@ -0,0 +1,683 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function runTests(options) {
+ async function background(getTests) {
+ async function checkDetails(expecting, details) {
+ let title = await browser.sidebarAction.getTitle(details);
+ browser.test.assertEq(
+ expecting.title,
+ title,
+ "expected value from getTitle in " + JSON.stringify(details)
+ );
+
+ let panel = await browser.sidebarAction.getPanel(details);
+ browser.test.assertEq(
+ expecting.panel,
+ panel,
+ "expected value from getPanel in " + JSON.stringify(details)
+ );
+ }
+
+ let tabs = [];
+ let windows = [];
+ let tests = getTests(tabs, windows);
+
+ {
+ let tabId = 0xdeadbeef;
+ let calls = [
+ () => browser.sidebarAction.setTitle({ tabId, title: "foo" }),
+ () => browser.sidebarAction.setIcon({ tabId, path: "foo.png" }),
+ () => browser.sidebarAction.setPanel({ tabId, panel: "foo.html" }),
+ ];
+
+ for (let call of calls) {
+ await browser.test.assertRejects(
+ new Promise(resolve => resolve(call())),
+ RegExp(`Invalid tab ID: ${tabId}`),
+ "Expected invalid tab ID error"
+ );
+ }
+ }
+
+ // Runs the next test in the `tests` array, checks the results,
+ // and passes control back to the outer test scope.
+ function nextTest() {
+ let test = tests.shift();
+
+ test(async (expectTab, expectWindow, expectGlobal, expectDefault) => {
+ expectGlobal = { ...expectDefault, ...expectGlobal };
+ expectWindow = { ...expectGlobal, ...expectWindow };
+ expectTab = { ...expectWindow, ...expectTab };
+
+ // Check that the API returns the expected values, and then
+ // run the next test.
+ let [{ windowId, id: tabId }] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await checkDetails(expectTab, { tabId });
+ await checkDetails(expectWindow, { windowId });
+ await checkDetails(expectGlobal, {});
+
+ // Check that the actual icon has the expected values, then
+ // run the next test.
+ browser.test.sendMessage("nextTest", expectTab, windowId, tests.length);
+ });
+ }
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg != "runNextTest") {
+ browser.test.fail("Expecting 'runNextTest' message");
+ }
+
+ nextTest();
+ });
+
+ let [{ id, windowId }] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ tabs.push(id);
+ windows.push(windowId);
+
+ browser.test.sendMessage("background-page-ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: options.manifest,
+ useAddonManager: "temporary",
+
+ files: options.files || {},
+
+ background: `(${background})(${options.getTests})`,
+ });
+
+ let sidebarActionId;
+ function checkDetails(details, windowId) {
+ let { document } = Services.wm.getOuterWindowWithId(windowId);
+ if (!sidebarActionId) {
+ sidebarActionId = `${makeWidgetId(extension.id)}-sidebar-action`;
+ }
+
+ let menuId = `menu_${sidebarActionId}`;
+ let menu = document.getElementById(menuId);
+ ok(menu, "menu exists");
+
+ let title = details.title || options.manifest.name;
+
+ is(getListStyleImage(menu), details.icon, "icon URL is correct");
+ is(menu.getAttribute("label"), title, "image label is correct");
+ }
+
+ let awaitFinish = new Promise(resolve => {
+ extension.onMessage("nextTest", (expecting, windowId, testsRemaining) => {
+ checkDetails(expecting, windowId);
+
+ if (testsRemaining) {
+ extension.sendMessage("runNextTest");
+ } else {
+ resolve();
+ }
+ });
+ });
+
+ // Wait for initial sidebar load.
+ SidebarUI.browser.addEventListener(
+ "load",
+ async () => {
+ // Wait for the background page listeners to be ready and
+ // then start the tests.
+ await extension.awaitMessage("background-page-ready");
+ extension.sendMessage("runNextTest");
+ },
+ { capture: true, once: true }
+ );
+
+ await extension.startup();
+
+ await awaitFinish;
+ await extension.unload();
+}
+
+let sidebar = `
+
+
+
+
+ A Test Sidebar
+
+`;
+
+add_task(async function testTabSwitchContext() {
+ await runTests({
+ manifest: {
+ sidebar_action: {
+ default_icon: "default.png",
+ default_panel: "__MSG_panel__",
+ default_title: "Default __MSG_title__",
+ },
+
+ default_locale: "en",
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "default.html": sidebar,
+ "global.html": sidebar,
+ "2.html": sidebar,
+
+ "_locales/en/messages.json": {
+ panel: {
+ message: "default.html",
+ description: "Panel",
+ },
+
+ title: {
+ message: "Title",
+ description: "Title",
+ },
+ },
+
+ "default.png": imageBuffer,
+ "global.png": imageBuffer,
+ "1.png": imageBuffer,
+ "2.png": imageBuffer,
+ },
+
+ getTests: function (tabs) {
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ panel: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ },
+ { icon: browser.runtime.getURL("1.png") },
+ {
+ icon: browser.runtime.getURL("2.png"),
+ panel: browser.runtime.getURL("2.html"),
+ title: "Title 2",
+ },
+ {
+ icon: browser.runtime.getURL("global.png"),
+ panel: browser.runtime.getURL("global.html"),
+ title: "Global Title",
+ },
+ {
+ icon: browser.runtime.getURL("1.png"),
+ panel: browser.runtime.getURL("2.html"),
+ },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Change the icon in the current tab. Expect default properties excluding the icon."
+ );
+ await browser.sidebarAction.setIcon({
+ tabId: tabs[0],
+ path: "1.png",
+ });
+
+ expect(details[1], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab. Expect default properties.");
+ let tab = await browser.tabs.create({
+ active: true,
+ url: "about:blank?0",
+ });
+ tabs.push(tab.id);
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Change properties. Expect new properties.");
+ let tabId = tabs[1];
+ await Promise.all([
+ browser.sidebarAction.setIcon({ tabId, path: "2.png" }),
+ browser.sidebarAction.setPanel({ tabId, panel: "2.html" }),
+ browser.sidebarAction.setTitle({ tabId, title: "Title 2" }),
+ ]);
+ expect(details[2], null, null, details[0]);
+ },
+ expect => {
+ browser.test.log("Navigate to a new page. Expect no changes.");
+
+ // TODO: This listener should not be necessary, but the |tabs.update|
+ // callback currently fires too early in e10s windows.
+ browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
+ if (tabId == tabs[1] && changed.url) {
+ browser.tabs.onUpdated.removeListener(listener);
+ expect(details[2], null, null, details[0]);
+ }
+ });
+
+ browser.tabs.update(tabs[1], { url: "about:blank?1" });
+ },
+ async expect => {
+ browser.test.log(
+ "Switch back to the first tab. Expect previously set properties."
+ );
+ await browser.tabs.update(tabs[0], { active: true });
+ expect(details[1], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Change global values, expect those changes reflected."
+ );
+ await Promise.all([
+ browser.sidebarAction.setIcon({ path: "global.png" }),
+ browser.sidebarAction.setPanel({ panel: "global.html" }),
+ browser.sidebarAction.setTitle({ title: "Global Title" }),
+ ]);
+
+ expect(details[1], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Switch back to tab 2. Expect former tab values, and new global values from previous step."
+ );
+ await browser.tabs.update(tabs[1], { active: true });
+
+ expect(details[2], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Delete tab, switch back to tab 1. Expect previous results again."
+ );
+ await browser.tabs.remove(tabs[1]);
+ expect(details[1], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab. Expect new global properties.");
+ let tab = await browser.tabs.create({
+ active: true,
+ url: "about:blank?2",
+ });
+ tabs.push(tab.id);
+ expect(null, null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log("Delete tab.");
+ await browser.tabs.remove(tabs[2]);
+ expect(details[1], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log("Change tab panel.");
+ let tabId = tabs[0];
+ await browser.sidebarAction.setPanel({ tabId, panel: "2.html" });
+ expect(details[4], null, details[3], details[0]);
+ },
+ async expect => {
+ browser.test.log("Revert tab panel.");
+ let tabId = tabs[0];
+ await browser.sidebarAction.setPanel({ tabId, panel: null });
+ expect(details[1], null, details[3], details[0]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testDefaultTitle() {
+ await runTests({
+ manifest: {
+ name: "Foo Extension",
+
+ sidebar_action: {
+ default_icon: "icon.png",
+ default_panel: "sidebar.html",
+ },
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "sidebar.html": sidebar,
+ "icon.png": imageBuffer,
+ },
+
+ getTests: function (tabs) {
+ let details = [
+ {
+ title: "Foo Extension",
+ panel: browser.runtime.getURL("sidebar.html"),
+ icon: browser.runtime.getURL("icon.png"),
+ },
+ { title: "Foo Title" },
+ { title: "Bar Title" },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state. Expect default extension title.");
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Change the tab title. Expect new title.");
+ browser.sidebarAction.setTitle({
+ tabId: tabs[0],
+ title: "Foo Title",
+ });
+
+ expect(details[1], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Change the global title. Expect same properties.");
+ browser.sidebarAction.setTitle({ title: "Bar Title" });
+
+ expect(details[1], null, details[2], details[0]);
+ },
+ async expect => {
+ browser.test.log("Clear the tab title. Expect new global title.");
+ browser.sidebarAction.setTitle({ tabId: tabs[0], title: null });
+
+ expect(null, null, details[2], details[0]);
+ },
+ async expect => {
+ browser.test.log("Clear the global title. Expect default title.");
+ browser.sidebarAction.setTitle({ title: null });
+
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.assertRejects(
+ browser.sidebarAction.setPanel({ panel: "about:addons" }),
+ /Access denied for URL about:addons/,
+ "unable to set panel to about:addons"
+ );
+
+ expect(null, null, null, details[0]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testPropertyRemoval() {
+ await runTests({
+ manifest: {
+ name: "Foo Extension",
+
+ sidebar_action: {
+ default_icon: "default.png",
+ default_panel: "default.html",
+ default_title: "Default Title",
+ },
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "default.html": sidebar,
+ "global.html": sidebar,
+ "global2.html": sidebar,
+ "window.html": sidebar,
+ "tab.html": sidebar,
+ "default.png": imageBuffer,
+ "global.png": imageBuffer,
+ "global2.png": imageBuffer,
+ "window.png": imageBuffer,
+ "tab.png": imageBuffer,
+ },
+
+ getTests: function (tabs, windows) {
+ let defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ panel: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ },
+ {
+ icon: browser.runtime.getURL("global.png"),
+ panel: browser.runtime.getURL("global.html"),
+ title: "global",
+ },
+ {
+ icon: browser.runtime.getURL("window.png"),
+ panel: browser.runtime.getURL("window.html"),
+ title: "window",
+ },
+ {
+ icon: browser.runtime.getURL("tab.png"),
+ panel: browser.runtime.getURL("tab.html"),
+ title: "tab",
+ },
+ { icon: defaultIcon, title: "" },
+ {
+ icon: browser.runtime.getURL("global2.png"),
+ panel: browser.runtime.getURL("global2.html"),
+ title: "global2",
+ },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set global values, expect the new values.");
+ browser.sidebarAction.setIcon({ path: "global.png" });
+ browser.sidebarAction.setPanel({ panel: "global.html" });
+ browser.sidebarAction.setTitle({ title: "global" });
+ expect(null, null, details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set window values, expect the new values.");
+ let windowId = windows[0];
+ browser.sidebarAction.setIcon({ windowId, path: "window.png" });
+ browser.sidebarAction.setPanel({ windowId, panel: "window.html" });
+ browser.sidebarAction.setTitle({ windowId, title: "window" });
+ expect(null, details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set tab values, expect the new values.");
+ let tabId = tabs[0];
+ browser.sidebarAction.setIcon({ tabId, path: "tab.png" });
+ browser.sidebarAction.setPanel({ tabId, panel: "tab.html" });
+ browser.sidebarAction.setTitle({ tabId, title: "tab" });
+ expect(details[3], details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Set empty tab values.");
+ let tabId = tabs[0];
+ browser.sidebarAction.setIcon({ tabId, path: "" });
+ browser.sidebarAction.setPanel({ tabId, panel: "" });
+ browser.sidebarAction.setTitle({ tabId, title: "" });
+ expect(details[4], details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Remove tab values, expect window values.");
+ let tabId = tabs[0];
+ browser.sidebarAction.setIcon({ tabId, path: null });
+ browser.sidebarAction.setPanel({ tabId, panel: null });
+ browser.sidebarAction.setTitle({ tabId, title: null });
+ expect(null, details[2], details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Remove window values, expect global values.");
+ let windowId = windows[0];
+ browser.sidebarAction.setIcon({ windowId, path: null });
+ browser.sidebarAction.setPanel({ windowId, panel: null });
+ browser.sidebarAction.setTitle({ windowId, title: null });
+ expect(null, null, details[1], details[0]);
+ },
+ async expect => {
+ browser.test.log("Change global values, expect the new values.");
+ browser.sidebarAction.setIcon({ path: "global2.png" });
+ browser.sidebarAction.setPanel({ panel: "global2.html" });
+ browser.sidebarAction.setTitle({ title: "global2" });
+ expect(null, null, details[5], details[0]);
+ },
+ async expect => {
+ browser.test.log("Remove global values, expect defaults.");
+ browser.sidebarAction.setIcon({ path: null });
+ browser.sidebarAction.setPanel({ panel: null });
+ browser.sidebarAction.setTitle({ title: null });
+ expect(null, null, null, details[0]);
+ },
+ ];
+ },
+ });
+});
+
+add_task(async function testMultipleWindows() {
+ await runTests({
+ manifest: {
+ name: "Foo Extension",
+
+ sidebar_action: {
+ default_icon: "default.png",
+ default_panel: "default.html",
+ default_title: "Default Title",
+ },
+
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "default.html": sidebar,
+ "window1.html": sidebar,
+ "window2.html": sidebar,
+ "default.png": imageBuffer,
+ "window1.png": imageBuffer,
+ "window2.png": imageBuffer,
+ },
+
+ getTests: function (tabs, windows) {
+ let details = [
+ {
+ icon: browser.runtime.getURL("default.png"),
+ panel: browser.runtime.getURL("default.html"),
+ title: "Default Title",
+ },
+ {
+ icon: browser.runtime.getURL("window1.png"),
+ panel: browser.runtime.getURL("window1.html"),
+ title: "window1",
+ },
+ {
+ icon: browser.runtime.getURL("window2.png"),
+ panel: browser.runtime.getURL("window2.html"),
+ title: "window2",
+ },
+ { title: "tab" },
+ ];
+
+ return [
+ async expect => {
+ browser.test.log("Initial state, expect default properties.");
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set window values, expect the new values.");
+ let windowId = windows[0];
+ browser.sidebarAction.setIcon({ windowId, path: "window1.png" });
+ browser.sidebarAction.setPanel({ windowId, panel: "window1.html" });
+ browser.sidebarAction.setTitle({ windowId, title: "window1" });
+ expect(null, details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Create a new tab, expect window values.");
+ let tab = await browser.tabs.create({ active: true });
+ tabs.push(tab.id);
+ expect(null, details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set a tab title, expect it.");
+ await browser.sidebarAction.setTitle({
+ tabId: tabs[1],
+ title: "tab",
+ });
+ expect(details[3], details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Open a new window, expect default values.");
+ let { id } = await browser.windows.create();
+ windows.push(id);
+ expect(null, null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Set window values, expect the new values.");
+ let windowId = windows[1];
+ browser.sidebarAction.setIcon({ windowId, path: "window2.png" });
+ browser.sidebarAction.setPanel({ windowId, panel: "window2.html" });
+ browser.sidebarAction.setTitle({ windowId, title: "window2" });
+ expect(null, details[2], null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Move tab from old window to the new one. Tab-specific data" +
+ " is preserved but inheritance is from the new window"
+ );
+ await browser.tabs.move(tabs[1], { windowId: windows[1], index: -1 });
+ await browser.tabs.update(tabs[1], { active: true });
+ expect(details[3], details[2], null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Close the initial tab of the new window.");
+ let [{ id }] = await browser.tabs.query({
+ windowId: windows[1],
+ index: 0,
+ });
+ await browser.tabs.remove(id);
+ expect(details[3], details[2], null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Move the previous tab to a 3rd window, the 2nd one will close."
+ );
+ await browser.windows.create({ tabId: tabs[1] });
+ expect(details[3], null, null, details[0]);
+ },
+ async expect => {
+ browser.test.log("Close the tab, go back to the 1st window.");
+ await browser.tabs.remove(tabs[1]);
+ expect(null, details[1], null, details[0]);
+ },
+ async expect => {
+ browser.test.log(
+ "Assert failures for bad parameters. Expect no change"
+ );
+
+ let calls = {
+ setIcon: { path: "default.png" },
+ setPanel: { panel: "default.html" },
+ setTitle: { title: "Default Title" },
+ getPanel: {},
+ getTitle: {},
+ };
+ for (let [method, arg] of Object.entries(calls)) {
+ browser.test.assertThrows(
+ () => browser.sidebarAction[method]({ ...arg, windowId: -3 }),
+ /-3 is too small \(must be at least -2\)/,
+ method + " with invalid windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction[method]({
+ ...arg,
+ tabId: tabs[0],
+ windowId: windows[0],
+ }),
+ /Only one of tabId and windowId can be specified/,
+ method + " with both tabId and windowId"
+ );
+ }
+
+ expect(null, details[1], null, details[0]);
+ },
+ ];
+ },
+ });
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_contextMenu.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_contextMenu.js
new file mode 100644
index 0000000000..3317e6b7e0
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_contextMenu.js
@@ -0,0 +1,133 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let extData = {
+ manifest: {
+ permissions: ["contextMenus"],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+
+ background: function () {
+ browser.contextMenus.create({
+ id: "clickme-page",
+ title: "Click me!",
+ contexts: ["all"],
+ onclick(info, tab) {
+ browser.test.sendMessage("menu-click", tab);
+ },
+ });
+ },
+};
+
+let contextMenuItems = {
+ "context-sep-navigation": "hidden",
+ "context-viewsource": "",
+ "inspect-separator": "hidden",
+ "context-inspect": "hidden",
+ "context-inspect-a11y": "hidden",
+ "context-bookmarkpage": "hidden",
+};
+if (AppConstants.platform == "macosx") {
+ contextMenuItems["context-back"] = "hidden";
+ contextMenuItems["context-forward"] = "hidden";
+ contextMenuItems["context-reload"] = "hidden";
+ contextMenuItems["context-stop"] = "hidden";
+} else {
+ contextMenuItems["context-navigation"] = "hidden";
+}
+
+add_task(async function sidebar_contextmenu() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+
+ let contentAreaContextMenu = await openContextMenuInSidebar();
+ let item = contentAreaContextMenu.getElementsByAttribute(
+ "label",
+ "Click me!"
+ );
+ is(item.length, 1, "contextMenu item for page was found");
+
+ item[0].click();
+ await closeContextMenu(contentAreaContextMenu);
+ let tab = await extension.awaitMessage("menu-click");
+ is(
+ tab,
+ null,
+ "tab argument is optional, and missing in clicks from sidebars"
+ );
+
+ await extension.unload();
+});
+
+add_task(async function sidebar_contextmenu_hidden_items() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+
+ let contentAreaContextMenu = await openContextMenuInSidebar("#text");
+
+ let item, state;
+ for (const itemID in contextMenuItems) {
+ item = contentAreaContextMenu.querySelector(`#${itemID}`);
+ state = contextMenuItems[itemID];
+
+ if (state !== "") {
+ ok(item[state], `${itemID} is ${state}`);
+
+ if (state !== "hidden") {
+ ok(!item.hidden, `Disabled ${itemID} is not hidden`);
+ }
+ } else {
+ ok(!item.hidden, `${itemID} is not hidden`);
+ ok(!item.disabled, `${itemID} is not disabled`);
+ }
+ }
+
+ await closeContextMenu(contentAreaContextMenu);
+
+ await extension.unload();
+});
+
+add_task(async function sidebar_image_contextmenu() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+
+ let contentAreaContextMenu = await openContextMenuInSidebar("#testimg");
+
+ let item = contentAreaContextMenu.querySelector("#context-copyimage");
+ ok(!item.hidden);
+ ok(!item.disabled);
+
+ await closeContextMenu(contentAreaContextMenu);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js
new file mode 100644
index 0000000000..d50d96b822
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_httpAuth.js
@@ -0,0 +1,72 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+add_task(async function sidebar_httpAuthPrompt() {
+ let data = {
+ manifest: {
+ permissions: ["https://example.com/*"],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+ "sidebar.js": function () {
+ fetch(
+ "https://example.com/browser/browser/components/extensions/test/browser/authenticate.sjs?user=user&pass=pass",
+ { credentials: "include" }
+ ).then(response => {
+ browser.test.sendMessage("fetchResult", response.ok);
+ });
+ },
+ },
+ };
+
+ // Wait for the http auth prompt and close it with accept button.
+ let promptPromise = PromptTestUtils.handleNextPrompt(
+ SidebarUI.browser.contentWindow,
+ {
+ modalType: Services.prompt.MODAL_TYPE_WINDOW,
+ promptType: "promptUserAndPass",
+ },
+ { buttonNumClick: 0, loginInput: "user", passwordInput: "pass" }
+ );
+
+ let extension = ExtensionTestUtils.loadExtension(data);
+ await extension.startup();
+ let fetchResultPromise = extension.awaitMessage("fetchResult");
+
+ await promptPromise;
+ ok(true, "Extension fetch should trigger auth prompt.");
+
+ let responseOk = await fetchResultPromise;
+ ok(responseOk, "Login should succeed.");
+
+ await extension.unload();
+
+ // Cleanup
+ await new Promise(resolve =>
+ Services.clearData.deleteData(
+ Ci.nsIClearDataService.CLEAR_AUTH_CACHE,
+ resolve
+ )
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js
new file mode 100644
index 0000000000..221447cf2e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_incognito.js
@@ -0,0 +1,139 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_sidebarAction_not_allowed() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener(async pbw => {
+ await browser.test.assertRejects(
+ browser.sidebarAction.setTitle({
+ windowId: pbw.windowId,
+ title: "test",
+ }),
+ /Invalid window ID/,
+ "should not be able to set title with windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.setTitle({
+ tabId: pbw.tabId,
+ title: "test",
+ }),
+ /Invalid tab ID/,
+ "should not be able to set title"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.getTitle({
+ windowId: pbw.windowId,
+ }),
+ /Invalid window ID/,
+ "should not be able to get title with windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.getTitle({
+ tabId: pbw.tabId,
+ }),
+ /Invalid tab ID/,
+ "should not be able to get title with tabId"
+ );
+
+ await browser.test.assertRejects(
+ browser.sidebarAction.setIcon({
+ windowId: pbw.windowId,
+ path: "test",
+ }),
+ /Invalid window ID/,
+ "should not be able to set icon with windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.setIcon({
+ tabId: pbw.tabId,
+ path: "test",
+ }),
+ /Invalid tab ID/,
+ "should not be able to set icon with tabId"
+ );
+
+ await browser.test.assertRejects(
+ browser.sidebarAction.setPanel({
+ windowId: pbw.windowId,
+ panel: "test",
+ }),
+ /Invalid window ID/,
+ "should not be able to set panel with windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.setPanel({
+ tabId: pbw.tabId,
+ panel: "test",
+ }),
+ /Invalid tab ID/,
+ "should not be able to set panel with tabId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.getPanel({
+ windowId: pbw.windowId,
+ }),
+ /Invalid window ID/,
+ "should not be able to get panel with windowId"
+ );
+ await browser.test.assertRejects(
+ browser.sidebarAction.getPanel({
+ tabId: pbw.tabId,
+ }),
+ /Invalid tab ID/,
+ "should not be able to get panel with tabId"
+ );
+
+ await browser.test.assertRejects(
+ browser.sidebarAction.isOpen({
+ windowId: pbw.windowId,
+ }),
+ /Invalid window ID/,
+ "should not be able to determine openness with windowId"
+ );
+
+ browser.test.notifyPass("pass");
+ });
+ },
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+ });
+
+ await extension.startup();
+ let sidebarID = `${makeWidgetId(extension.id)}-sidebar-action`;
+ ok(SidebarUI.sidebars.has(sidebarID), "sidebar exists in non-private window");
+
+ let winData = await getIncognitoWindow();
+
+ let hasSidebar = winData.win.SidebarUI.sidebars.has(sidebarID);
+ ok(!hasSidebar, "sidebar does not exist in private window");
+ // Test API access to private window data.
+ extension.sendMessage(winData.details);
+ await extension.awaitFinish("pass");
+
+ await BrowserTestUtils.closeWindow(winData.win);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js
new file mode 100644
index 0000000000..55c83ee0b1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_runtime.js
@@ -0,0 +1,76 @@
+"use strict";
+
+function background() {
+ browser.runtime.onConnect.addListener(port => {
+ browser.test.assertEq(port.name, "ernie", "port name correct");
+ port.onDisconnect.addListener(() => {
+ browser.test.assertEq(
+ null,
+ port.error,
+ "The port is implicitly closed without errors when the other context unloads"
+ );
+ port.disconnect();
+ browser.test.sendMessage("disconnected");
+ });
+ browser.test.sendMessage("connected");
+ });
+}
+
+let extensionData = {
+ background,
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.runtime.connect({ name: "ernie" });
+ };
+ },
+ },
+};
+
+add_task(async function test_sidebar_disconnect() {
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ let connected = extension.awaitMessage("connected");
+ await extension.startup();
+ await connected;
+
+ // Bug 1445080 fixes currentURI, test to avoid future breakage.
+ let currentURI = window.SidebarUI.browser.contentDocument.getElementById(
+ "webext-panels-browser"
+ ).currentURI;
+ is(currentURI.scheme, "moz-extension", "currentURI is set correctly");
+
+ // switching sidebar to another extension
+ let extension2 = ExtensionTestUtils.loadExtension(extensionData);
+ let switched = Promise.all([
+ extension.awaitMessage("disconnected"),
+ extension2.awaitMessage("connected"),
+ ]);
+ await extension2.startup();
+ await switched;
+
+ // switching sidebar to built-in sidebar
+ let disconnected = extension2.awaitMessage("disconnected");
+ window.SidebarUI.show("viewBookmarksSidebar");
+ await disconnected;
+
+ await extension.unload();
+ await extension2.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_tabs.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_tabs.js
new file mode 100644
index 0000000000..7af75cdc19
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_tabs.js
@@ -0,0 +1,48 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function sidebar_tab_query_bug_1340739() {
+ let data = {
+ manifest: {
+ permissions: ["tabs"],
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+ "sidebar.js": function () {
+ Promise.all([
+ browser.tabs.query({}).then(tabs => {
+ browser.test.assertEq(
+ 1,
+ tabs.length,
+ "got tab without currentWindow"
+ );
+ }),
+ browser.tabs.query({ currentWindow: true }).then(tabs => {
+ browser.test.assertEq(1, tabs.length, "got tab with currentWindow");
+ }),
+ ]).then(() => {
+ browser.test.sendMessage("sidebar");
+ });
+ },
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(data);
+ await extension.startup();
+ await extension.awaitMessage("sidebar");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js b/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js
new file mode 100644
index 0000000000..58f2b07797
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebarAction_windows.js
@@ -0,0 +1,69 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+let extData = {
+ manifest: {
+ sidebar_action: {
+ default_panel: "sidebar.html",
+ },
+ },
+ useAddonManager: "temporary",
+
+ files: {
+ "sidebar.html": `
+
+
+
+
+
+
+ A Test Sidebar
+
+ `,
+
+ "sidebar.js": function () {
+ window.onload = () => {
+ browser.test.sendMessage("sidebar");
+ };
+ },
+ },
+};
+
+add_task(async function sidebar_windows() {
+ let extension = ExtensionTestUtils.loadExtension(extData);
+ await extension.startup();
+ // Test sidebar is opened on install
+ await extension.awaitMessage("sidebar");
+ ok(
+ !document.getElementById("sidebar-box").hidden,
+ "sidebar box is visible in first window"
+ );
+ // Check that the menuitem has our image styling.
+ let elements = document.getElementsByClassName("webextension-menuitem");
+ // ui is in flux, at time of writing we potentially have 3 menuitems, later
+ // it may be two or one, just make sure one is there.
+ ok(!!elements.length, "have a menuitem");
+ let style = elements[0].getAttribute("style");
+ ok(style.includes("webextension-menuitem-image"), "this menu has style");
+
+ let secondSidebar = extension.awaitMessage("sidebar");
+
+ // SidebarUI relies on window.opener being set, which is normal behavior when
+ // using menu or key commands to open a new browser window.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await secondSidebar;
+ ok(
+ !win.document.getElementById("sidebar-box").hidden,
+ "sidebar box is visible in second window"
+ );
+ // Check that the menuitem has our image styling.
+ elements = win.document.getElementsByClassName("webextension-menuitem");
+ ok(!!elements.length, "have a menuitem");
+ style = elements[0].getAttribute("style");
+ ok(style.includes("webextension-menuitem-image"), "this menu has style");
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_sidebar_requestPermission.js b/browser/components/extensions/test/browser/browser_ext_sidebar_requestPermission.js
new file mode 100644
index 0000000000..393efcf99e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_sidebar_requestPermission.js
@@ -0,0 +1,43 @@
+"use strict";
+
+add_task(async function test_sidebar_requestPermission_resolve() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ sidebar_action: {
+ default_panel: "panel.html",
+ browser_style: false,
+ },
+ optional_permissions: ["tabs"],
+ },
+ useAddonManager: "temporary",
+ files: {
+ "panel.html": ``,
+ "panel.js": async () => {
+ const success = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(
+ browser.permissions.request({
+ permissions: ["tabs"],
+ })
+ );
+ });
+ });
+ browser.test.assertTrue(
+ success,
+ "browser.permissions.request promise resolves"
+ );
+ browser.test.sendMessage("done");
+ },
+ },
+ });
+
+ const requestPrompt = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+ await extension.startup();
+ await requestPrompt;
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_simple.js b/browser/components/extensions/test/browser/browser_ext_simple.js
new file mode 100644
index 0000000000..4d9d7c73fa
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_simple.js
@@ -0,0 +1,60 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_simple() {
+ let extensionData = {
+ manifest: {
+ name: "Simple extension test",
+ version: "1.0",
+ manifest_version: 2,
+ description: "",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ info("load complete");
+ await extension.startup();
+ info("startup complete");
+ await extension.unload();
+ info("extension unloaded successfully");
+});
+
+add_task(async function test_background() {
+ function backgroundScript() {
+ browser.test.log("running background script");
+
+ browser.test.onMessage.addListener((x, y) => {
+ browser.test.assertEq(x, 10, "x is 10");
+ browser.test.assertEq(y, 20, "y is 20");
+
+ browser.test.notifyPass("background test passed");
+ });
+
+ browser.test.sendMessage("running", 1);
+ }
+
+ let extensionData = {
+ background: "(" + backgroundScript.toString() + ")()",
+ manifest: {
+ name: "Simple extension test",
+ version: "1.0",
+ manifest_version: 2,
+ description: "",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ info("load complete");
+ let [, x] = await Promise.all([
+ extension.startup(),
+ extension.awaitMessage("running"),
+ ]);
+ is(x, 1, "got correct value from extension");
+ info("startup complete");
+ extension.sendMessage(10, 20);
+ await extension.awaitFinish();
+ info("test complete");
+ await extension.unload();
+ info("extension unloaded successfully");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_slow_script.js b/browser/components/extensions/test/browser/browser_ext_slow_script.js
new file mode 100644
index 0000000000..bd9369a904
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_slow_script.js
@@ -0,0 +1,72 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const DEFAULT_PROCESS_COUNT = Services.prefs
+ .getDefaultBranch(null)
+ .getIntPref("dom.ipc.processCount");
+
+add_task(async function test_slow_content_script() {
+ // Make sure we get a new process for our tab, or our reportProcessHangs
+ // preferences value won't apply to it.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", 1],
+ ["dom.ipc.keepProcessesAlive.web", 0],
+ ],
+ });
+ await SpecialPowers.popPrefEnv();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.ipc.processCount", DEFAULT_PROCESS_COUNT * 2],
+ ["dom.ipc.processPrelaunch.enabled", false],
+ ["dom.ipc.reportProcessHangs", true],
+ ["dom.max_script_run_time.require_critical_input", false],
+ ],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ name: "Slow Script Extension",
+
+ content_scripts: [
+ {
+ matches: ["http://example.com/"],
+ js: ["content.js"],
+ },
+ ],
+ },
+
+ files: {
+ "content.js": function () {
+ while (true) {
+ // Busy wait.
+ }
+ },
+ },
+ });
+
+ await extension.startup();
+
+ let alert = BrowserTestUtils.waitForGlobalNotificationBar(
+ window,
+ "process-hang"
+ );
+
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+
+ let notification = await alert;
+ let text = notification.messageText.textContent;
+
+ ok(text.includes("\u201cSlow Script Extension\u201d"), "Label is correct");
+
+ let stopButton = notification.buttonContainer.querySelector("[label='Stop']");
+ stopButton.click();
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js b/browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js
new file mode 100644
index 0000000000..622916edda
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tab_runtimeConnect.js
@@ -0,0 +1,100 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ let 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 () {
+ let 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": `
+
+
+
+ test tab extension page
+
+
+
+
+
test tab extension page
+
+
+ `,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabRuntimeConnect.pass");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_attention.js b/browser/components/extensions/test/browser/browser_ext_tabs_attention.js
new file mode 100644
index 0000000000..0f267460f3
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_attention.js
@@ -0,0 +1,64 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function tabsAttention() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?2",
+ true
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?1",
+ true
+ );
+ gBrowser.selectedTab = tab2;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "http://example.com/*"],
+ },
+
+ background: async function () {
+ function onActive(tabId, changeInfo, tab) {
+ browser.test.assertFalse(
+ changeInfo.attention,
+ "changeInfo.attention should be false"
+ );
+ browser.test.assertFalse(
+ tab.attention,
+ "tab.attention should be false"
+ );
+ browser.test.assertTrue(tab.active, "tab.active should be true");
+ browser.test.notifyPass("tabsAttention");
+ }
+
+ function onUpdated(tabId, changeInfo, tab) {
+ browser.test.assertTrue(
+ changeInfo.attention,
+ "changeInfo.attention should be true"
+ );
+ browser.test.assertTrue(tab.attention, "tab.attention should be true");
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ browser.tabs.onUpdated.addListener(onActive);
+ browser.tabs.update(tabId, { active: true });
+ }
+
+ browser.tabs.onUpdated.addListener(onUpdated, {
+ properties: ["attention"],
+ });
+ const tabs = await browser.tabs.query({ index: 1 });
+ browser.tabs.executeScript(tabs[0].id, {
+ code: `alert("tab attention")`,
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabsAttention");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_audio.js b/browser/components/extensions/test/browser/browser_ext_tabs_audio.js
new file mode 100644
index 0000000000..978c3697c8
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_audio.js
@@ -0,0 +1,261 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank?1"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank?2"
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ async function background() {
+ function promiseUpdated(tabId, attr) {
+ return new Promise(resolve => {
+ let onUpdated = (tabId_, changeInfo, tab) => {
+ if (tabId == tabId_ && attr in changeInfo) {
+ browser.tabs.onUpdated.removeListener(onUpdated);
+
+ resolve({ changeInfo, tab });
+ }
+ };
+ browser.tabs.onUpdated.addListener(onUpdated);
+ });
+ }
+
+ let deferred = {};
+ browser.test.onMessage.addListener((message, tabId, result) => {
+ if (message == "change-tab-done" && deferred[tabId]) {
+ deferred[tabId].resolve(result);
+ }
+ });
+
+ function changeTab(tabId, attr, on) {
+ return new Promise((resolve, reject) => {
+ deferred[tabId] = { resolve, reject };
+ browser.test.sendMessage("change-tab", tabId, attr, on);
+ });
+ }
+
+ try {
+ let tabs = await browser.tabs.query({ lastFocusedWindow: true });
+ browser.test.assertEq(tabs.length, 3, "We have three tabs");
+
+ for (let tab of tabs) {
+ // Note: We want to check that these are actual boolean values, not
+ // just that they evaluate as false.
+ browser.test.assertEq(false, tab.mutedInfo.muted, "Tab is not muted");
+ browser.test.assertEq(
+ undefined,
+ tab.mutedInfo.reason,
+ "Tab has no muted info reason"
+ );
+ browser.test.assertEq(false, tab.audible, "Tab is not audible");
+ }
+
+ let windowId = tabs[0].windowId;
+ let tabIds = [tabs[1].id, tabs[2].id];
+
+ browser.test.log(
+ "Test initial queries for muted and audible return no tabs"
+ );
+ let silent = await browser.tabs.query({ windowId, audible: false });
+ let audible = await browser.tabs.query({ windowId, audible: true });
+ let muted = await browser.tabs.query({ windowId, muted: true });
+ let nonMuted = await browser.tabs.query({ windowId, muted: false });
+
+ browser.test.assertEq(3, silent.length, "Three silent tabs");
+ browser.test.assertEq(0, audible.length, "No audible tabs");
+
+ browser.test.assertEq(0, muted.length, "No muted tabs");
+ browser.test.assertEq(3, nonMuted.length, "Three non-muted tabs");
+
+ browser.test.log(
+ "Toggle muted and audible externally on one tab each, and check results"
+ );
+ [muted, audible] = await Promise.all([
+ promiseUpdated(tabIds[0], "mutedInfo"),
+ promiseUpdated(tabIds[1], "audible"),
+ changeTab(tabIds[0], "muted", true),
+ changeTab(tabIds[1], "audible", true),
+ ]);
+
+ for (let obj of [muted.changeInfo, muted.tab]) {
+ browser.test.assertEq(true, obj.mutedInfo.muted, "Tab is muted");
+ browser.test.assertEq(
+ "user",
+ obj.mutedInfo.reason,
+ "Tab was muted by the user"
+ );
+ }
+
+ browser.test.assertEq(
+ true,
+ audible.changeInfo.audible,
+ "Tab audible state changed"
+ );
+ browser.test.assertEq(true, audible.tab.audible, "Tab is audible");
+
+ browser.test.log(
+ "Re-check queries. Expect one audible and one muted tab"
+ );
+ silent = await browser.tabs.query({ windowId, audible: false });
+ audible = await browser.tabs.query({ windowId, audible: true });
+ muted = await browser.tabs.query({ windowId, muted: true });
+ nonMuted = await browser.tabs.query({ windowId, muted: false });
+
+ browser.test.assertEq(2, silent.length, "Two silent tabs");
+ browser.test.assertEq(1, audible.length, "One audible tab");
+
+ browser.test.assertEq(1, muted.length, "One muted tab");
+ browser.test.assertEq(2, nonMuted.length, "Two non-muted tabs");
+
+ browser.test.assertEq(true, muted[0].mutedInfo.muted, "Tab is muted");
+ browser.test.assertEq(
+ "user",
+ muted[0].mutedInfo.reason,
+ "Tab was muted by the user"
+ );
+
+ browser.test.assertEq(true, audible[0].audible, "Tab is audible");
+
+ browser.test.log(
+ "Toggle muted internally on two tabs, and check results"
+ );
+ [nonMuted, muted] = await Promise.all([
+ promiseUpdated(tabIds[0], "mutedInfo"),
+ promiseUpdated(tabIds[1], "mutedInfo"),
+ browser.tabs.update(tabIds[0], { muted: false }),
+ browser.tabs.update(tabIds[1], { muted: true }),
+ ]);
+
+ for (let obj of [nonMuted.changeInfo, nonMuted.tab]) {
+ browser.test.assertEq(false, obj.mutedInfo.muted, "Tab is not muted");
+ }
+ for (let obj of [muted.changeInfo, muted.tab]) {
+ browser.test.assertEq(true, obj.mutedInfo.muted, "Tab is muted");
+ }
+
+ for (let obj of [
+ nonMuted.changeInfo,
+ nonMuted.tab,
+ muted.changeInfo,
+ muted.tab,
+ ]) {
+ browser.test.assertEq(
+ "extension",
+ obj.mutedInfo.reason,
+ "Mute state changed by extension"
+ );
+
+ browser.test.assertEq(
+ browser.runtime.id,
+ obj.mutedInfo.extensionId,
+ "Mute state changed by extension"
+ );
+ }
+
+ browser.test.log("Test that mutedInfo is preserved by sessionstore");
+ let tab = await changeTab(tabIds[1], "duplicate").then(browser.tabs.get);
+
+ browser.test.assertEq(true, tab.mutedInfo.muted, "Tab is muted");
+
+ browser.test.assertEq(
+ "extension",
+ tab.mutedInfo.reason,
+ "Mute state changed by extension"
+ );
+
+ browser.test.assertEq(
+ browser.runtime.id,
+ tab.mutedInfo.extensionId,
+ "Mute state changed by extension"
+ );
+
+ browser.test.log("Unmute externally, and check results");
+ [nonMuted] = await Promise.all([
+ promiseUpdated(tabIds[1], "mutedInfo"),
+ changeTab(tabIds[1], "muted", false),
+ browser.tabs.remove(tab.id),
+ ]);
+
+ for (let obj of [nonMuted.changeInfo, nonMuted.tab]) {
+ browser.test.assertEq(false, obj.mutedInfo.muted, "Tab is not muted");
+ browser.test.assertEq(
+ "user",
+ obj.mutedInfo.reason,
+ "Mute state changed by user"
+ );
+ }
+
+ browser.test.notifyPass("tab-audio");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tab-audio");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ extension.onMessage("change-tab", (tabId, attr, on) => {
+ const {
+ Management: {
+ global: { tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let tab = tabTracker.getTab(tabId);
+
+ if (attr == "muted") {
+ // Ideally we'd simulate a click on the tab audio icon for this, but the
+ // handler relies on CSS :hover states, which are complicated and fragile
+ // to simulate.
+ if (tab.muted != on) {
+ tab.toggleMuteAudio();
+ }
+ } else if (attr == "audible") {
+ let browser = tab.linkedBrowser;
+ if (on) {
+ browser.audioPlaybackStarted();
+ } else {
+ browser.audioPlaybackStopped();
+ }
+ } else if (attr == "duplicate") {
+ // This is a bit of a hack. It won't be necessary once we have
+ // `tabs.duplicate`.
+ let newTab = gBrowser.duplicateTab(tab);
+ BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(
+ () => {
+ extension.sendMessage(
+ "change-tab-done",
+ tabId,
+ tabTracker.getId(newTab)
+ );
+ }
+ );
+ return;
+ }
+
+ extension.sendMessage("change-tab-done", tabId);
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("tab-audio");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_containerIsolation.js b/browser/components/extensions/test/browser/browser_ext_tabs_containerIsolation.js
new file mode 100644
index 0000000000..1960366bb5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_containerIsolation.js
@@ -0,0 +1,360 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+const { XPCShellContentUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/XPCShellContentUtils.sys.mjs"
+);
+XPCShellContentUtils.initMochitest(this);
+const server = XPCShellContentUtils.createHttpServer({
+ hosts: ["www.example.com"],
+});
+server.registerPathHandler("/", (request, response) => {
+ response.setHeader("Content-Type", "text/html; charset=UTF-8", false);
+ response.write(`
+
+
+
+
+
+ This is example.com page content
+
+
+ `);
+});
+
+add_task(async function containerIsolation_restricted() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.userContextIsolation.enabled", true],
+ ["privacy.userContext.enabled", true],
+ ],
+ });
+
+ let helperExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+
+ async background() {
+ browser.test.onMessage.addListener(async message => {
+ let tab;
+ if (message?.subject !== "createTab") {
+ browser.test.fail(
+ `Unexpected test message received: ${JSON.stringify(message)}`
+ );
+ return;
+ }
+
+ tab = await browser.tabs.create({
+ url: message.data.url,
+ cookieStoreId: message.data.cookieStoreId,
+ });
+ browser.test.sendMessage("tabCreated", tab.id);
+ browser.test.assertEq(
+ message.data.cookieStoreId,
+ tab.cookieStoreId,
+ "Created tab is associated with the expected cookieStoreId"
+ );
+ });
+ },
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies", "", "tabHide"],
+ },
+ async background() {
+ const [restrictedTab, unrestrictedTab] = await new Promise(resolve => {
+ browser.test.onMessage.addListener(message => resolve(message));
+ });
+
+ // Check that print preview method fails
+ await browser.test.assertRejects(
+ browser.tabs.printPreview(),
+ /Cannot access activeTab/,
+ "should refuse to print a preview of the tab for the container which doesn't have permission"
+ );
+
+ // Check that save As PDF method fails
+ await browser.test.assertRejects(
+ browser.tabs.saveAsPDF({}),
+ /Cannot access activeTab/,
+ "should refuse to save as PDF of the tab for the container which doesn't have permission"
+ );
+
+ // Check that create method fails
+ await browser.test.assertRejects(
+ browser.tabs.create({ cookieStoreId: "firefox-container-1" }),
+ /Cannot access firefox-container-1/,
+ "should refuse to create container tab for the container which doesn't have permission"
+ );
+
+ // Check that detect language method fails
+ await browser.test.assertRejects(
+ browser.tabs.detectLanguage(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to detect language of a tab for the container which doesn't have permission"
+ );
+
+ // Check that move tabs method fails
+ await browser.test.assertRejects(
+ browser.tabs.move(restrictedTab, { index: 1 }),
+ /Invalid tab ID/,
+ "should refuse to move tab for the container which doesn't have permission"
+ );
+
+ // Check that duplicate method fails.
+ await browser.test.assertRejects(
+ browser.tabs.duplicate(restrictedTab),
+ /Invalid tab ID:/,
+ "should refuse to duplicate tab for the container which doesn't have permission"
+ );
+
+ // Check that captureTab method fails.
+ await browser.test.assertRejects(
+ browser.tabs.captureTab(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to capture the tab for the container which doesn't have permission"
+ );
+
+ // Check that discard method fails.
+ await browser.test.assertRejects(
+ browser.tabs.discard([restrictedTab]),
+ /Invalid tab ID/,
+ "should refuse to discard the tab for the container which doesn't have permission "
+ );
+
+ // Check that get method fails.
+ await browser.test.assertRejects(
+ browser.tabs.get(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to get the tab for the container which doesn't have permissiond"
+ );
+
+ // Check that highlight method fails.
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ populate: true, tabs: [restrictedTab] }),
+ `No tab at index: ${restrictedTab}`,
+ "should refuse to highlight the tab for the container which doesn't have permission"
+ );
+
+ // Test for moveInSuccession method of tab
+
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([restrictedTab]),
+ /Invalid tab ID/,
+ "should refuse to moveInSuccession for the container which doesn't have permission"
+ );
+
+ // Check that executeScript method fails.
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(restrictedTab, { frameId: 0 }),
+ /Invalid tab ID/,
+ "should refuse to execute a script of the tab for the container which doesn't have permission"
+ );
+
+ // Check that getZoom method fails.
+
+ await browser.test.assertRejects(
+ browser.tabs.getZoom(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to zoom the tab for the container which doesn't have permission"
+ );
+
+ // Check that getZoomSetting method fails.
+ await browser.test.assertRejects(
+ browser.tabs.getZoomSettings(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse the setting of zoom of the tab for the container which doesn't have permission"
+ );
+
+ //Test for hide method of tab
+ await browser.test.assertRejects(
+ browser.tabs.hide(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to hide a tab for the container which doesn't have permission"
+ );
+
+ // Check that insertCSS method fails.
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(restrictedTab, { frameId: 0 }),
+ /Invalid tab ID/,
+ "should refuse to insert a stylesheet to the tab for the container which doesn't have permission"
+ );
+
+ // Check that removeCSS method fails.
+ await browser.test.assertRejects(
+ browser.tabs.removeCSS(restrictedTab, { frameId: 0 }),
+ /Invalid tab ID/,
+ "should refuse to remove a stylesheet to the tab for the container which doesn't have permission"
+ );
+
+ //Test for show method of tab
+ await browser.test.assertRejects(
+ browser.tabs.show([restrictedTab]),
+ /Invalid tab ID/,
+ "should refuse to show the tab for the container which doesn't have permission"
+ );
+
+ // Check that toggleReaderMode method fails.
+
+ await browser.test.assertRejects(
+ browser.tabs.toggleReaderMode(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to toggle reader mode in the tab for the container which doesn't have permission"
+ );
+
+ // Check that setZoom method fails.
+ await browser.test.assertRejects(
+ browser.tabs.setZoom(restrictedTab, 0),
+ /Invalid tab ID/,
+ "should refuse to set zoom of the tab for the container which doesn't have permission"
+ );
+
+ // Check that setZoomSettings method fails.
+ await browser.test.assertRejects(
+ browser.tabs.setZoomSettings(restrictedTab, { defaultZoomFactor: 1 }),
+ /Invalid tab ID/,
+ "should refuse to set zoom setting of the tab for the container which doesn't have permission"
+ );
+
+ // Check that goBack method fails.
+
+ await browser.test.assertRejects(
+ browser.tabs.goBack(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to go back to the tab for the container which doesn't have permission"
+ );
+
+ // Check that goForward method fails.
+
+ await browser.test.assertRejects(
+ browser.tabs.goForward(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to go forward to the tab for the container which doesn't have permission"
+ );
+
+ // Check that update method fails.
+ await browser.test.assertRejects(
+ browser.tabs.update(restrictedTab, { highlighted: true }),
+ /Invalid tab ID/,
+ "should refuse to update the tab for the container which doesn't have permission"
+ );
+
+ // Check that reload method fails.
+ await browser.test.assertRejects(
+ browser.tabs.reload(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to reload tab for the container which doesn't have permission"
+ );
+
+ //Test for warmup method of tab
+ await browser.test.assertRejects(
+ browser.tabs.warmup(restrictedTab),
+ /Invalid tab ID/,
+ "should refuse to warmup a tab for the container which doesn't have permission"
+ );
+
+ let gettab = await browser.tabs.get(unrestrictedTab);
+ browser.test.assertEq(
+ gettab.cookieStoreId,
+ "firefox-container-2",
+ "get tab should open"
+ );
+
+ let lang = await browser.tabs.detectLanguage(unrestrictedTab);
+ await browser.test.assertEq(
+ "en",
+ lang,
+ "English document should be detected"
+ );
+
+ let duptab = await browser.tabs.duplicate(unrestrictedTab);
+
+ browser.test.assertEq(
+ duptab.cookieStoreId,
+ "firefox-container-2",
+ "duplicated tab should open"
+ );
+ await browser.tabs.remove(duptab.id);
+
+ let moved = await browser.tabs.move(unrestrictedTab, {
+ index: 0,
+ });
+ browser.test.assertEq(moved.length, 1, "move() returned no moved tab");
+
+ //Test for query method of tab
+ let tabs = await browser.tabs.query({
+ cookieStoreId: "firefox-container-1",
+ });
+ await browser.test.assertEq(
+ 0,
+ tabs.length,
+ "should not return restricted container's tab"
+ );
+
+ tabs = await browser.tabs.query({});
+ await browser.test.assertEq(
+ tabs
+ .map(tab => tab.cookieStoreId)
+ .sort()
+ .join(","),
+ "firefox-container-2,firefox-default",
+ "should return two tabs - firefox-default and firefox-container-2"
+ );
+
+ // Check that remove method fails.
+
+ await browser.test.assertRejects(
+ browser.tabs.remove([restrictedTab]),
+ /Invalid tab ID/,
+ "should refuse to remove tab for the container which doesn't have permission"
+ );
+
+ let removedPromise = new Promise(resolve => {
+ browser.tabs.onRemoved.addListener(tabId => {
+ browser.test.assertEq(unrestrictedTab, tabId, "expected remove tab");
+ resolve();
+ });
+ });
+ await browser.tabs.remove(unrestrictedTab);
+ await removedPromise;
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await helperExtension.startup();
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: {
+ url: "http://www.example.com/",
+ cookieStoreId: "firefox-container-2",
+ },
+ });
+ const unrestrictedTab = await helperExtension.awaitMessage("tabCreated");
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: {
+ url: "http://www.example.com/",
+ cookieStoreId: "firefox-container-1",
+ },
+ });
+ const restrictedTab = await helperExtension.awaitMessage("tabCreated");
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.userContextIsolation.defaults.restricted", "[1]"]],
+ });
+
+ await extension.startup();
+ extension.sendMessage([restrictedTab, unrestrictedTab]);
+
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await helperExtension.unload();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js
new file mode 100644
index 0000000000..27ea5d92bf
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js
@@ -0,0 +1,328 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_setup(async function () {
+ // make sure userContext is enabled.
+ return SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+});
+
+add_task(async function () {
+ info("Start testing tabs.create with cookieStoreId");
+
+ let testCases = [
+ // No private window
+ {
+ privateTab: false,
+ cookieStoreId: null,
+ success: true,
+ expectedCookieStoreId: "firefox-default",
+ },
+ {
+ privateTab: false,
+ cookieStoreId: "firefox-default",
+ success: true,
+ expectedCookieStoreId: "firefox-default",
+ },
+ {
+ privateTab: false,
+ cookieStoreId: "firefox-container-1",
+ success: true,
+ expectedCookieStoreId: "firefox-container-1",
+ },
+ {
+ privateTab: false,
+ cookieStoreId: "firefox-container-2",
+ success: true,
+ expectedCookieStoreId: "firefox-container-2",
+ },
+ {
+ privateTab: false,
+ cookieStoreId: "firefox-container-42",
+ failure: "exist",
+ },
+ {
+ privateTab: false,
+ cookieStoreId: "firefox-private",
+ failure: "defaultToPrivate",
+ },
+ { privateTab: false, cookieStoreId: "wow", failure: "illegal" },
+
+ // Private window
+ {
+ privateTab: true,
+ cookieStoreId: null,
+ success: true,
+ expectedCookieStoreId: "firefox-private",
+ },
+ {
+ privateTab: true,
+ cookieStoreId: "firefox-private",
+ success: true,
+ expectedCookieStoreId: "firefox-private",
+ },
+ {
+ privateTab: true,
+ cookieStoreId: "firefox-default",
+ failure: "privateToDefault",
+ },
+ {
+ privateTab: true,
+ cookieStoreId: "firefox-container-1",
+ failure: "privateToDefault",
+ },
+ { privateTab: true, cookieStoreId: "wow", failure: "illegal" },
+ ];
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+
+ background: function () {
+ function testTab(data, tab) {
+ browser.test.assertTrue(data.success, "we want a success");
+ browser.test.assertTrue(!!tab, "we have a tab");
+ browser.test.assertEq(
+ data.expectedCookieStoreId,
+ tab.cookieStoreId,
+ "tab should have the correct cookieStoreId"
+ );
+ }
+
+ async function runTest(data) {
+ try {
+ // Tab Creation
+ let tab;
+ try {
+ tab = await browser.tabs.create({
+ windowId: data.privateTab
+ ? this.privateWindowId
+ : this.defaultWindowId,
+ cookieStoreId: data.cookieStoreId,
+ });
+
+ browser.test.assertTrue(!data.failure, "we want a success");
+ } catch (error) {
+ browser.test.assertTrue(!!data.failure, "we want a failure");
+
+ if (data.failure == "illegal") {
+ browser.test.assertEq(
+ `Illegal cookieStoreId: ${data.cookieStoreId}`,
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else if (data.failure == "defaultToPrivate") {
+ browser.test.assertEq(
+ "Illegal to set private cookieStoreId in a non-private window",
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else if (data.failure == "privateToDefault") {
+ browser.test.assertEq(
+ "Illegal to set non-private cookieStoreId in a private window",
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else if (data.failure == "exist") {
+ browser.test.assertEq(
+ `No cookie store exists with ID ${data.cookieStoreId}`,
+ error.message,
+ "runtime.lastError should report the expected error message"
+ );
+ } else {
+ browser.test.fail("The test is broken");
+ }
+
+ browser.test.sendMessage("test-done");
+ return;
+ }
+
+ // Tests for tab creation
+ testTab(data, tab);
+
+ {
+ // Tests for tab querying
+ let [tab] = await browser.tabs.query({
+ windowId: data.privateTab
+ ? this.privateWindowId
+ : this.defaultWindowId,
+ cookieStoreId: data.cookieStoreId,
+ });
+
+ browser.test.assertTrue(tab != undefined, "Tab found!");
+ testTab(data, tab);
+ }
+
+ let stores = await browser.cookies.getAllCookieStores();
+
+ let store = stores.find(store => store.id === tab.cookieStoreId);
+ browser.test.assertTrue(!!store, "We have a store for this tab.");
+ browser.test.assertTrue(
+ store.tabIds.includes(tab.id),
+ "tabIds includes this tab."
+ );
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.sendMessage("test-done");
+ } catch (e) {
+ browser.test.fail("An exception has been thrown");
+ }
+ }
+
+ async function initialize() {
+ let win = await browser.windows.create({ incognito: true });
+ this.privateWindowId = win.id;
+
+ win = await browser.windows.create({ incognito: false });
+ this.defaultWindowId = win.id;
+
+ browser.test.sendMessage("ready");
+ }
+
+ async function shutdown() {
+ await browser.windows.remove(this.privateWindowId);
+ await browser.windows.remove(this.defaultWindowId);
+ browser.test.sendMessage("gone");
+ }
+
+ // Waiting for messages
+ browser.test.onMessage.addListener((msg, data) => {
+ if (msg == "be-ready") {
+ initialize();
+ } else if (msg == "test") {
+ runTest(data);
+ } else {
+ browser.test.assertTrue("finish", msg, "Shutting down");
+ shutdown();
+ }
+ });
+ },
+ });
+
+ await extension.startup();
+
+ info("Tests must be ready...");
+ extension.sendMessage("be-ready");
+ await extension.awaitMessage("ready");
+ info("Tests are ready to run!");
+
+ for (let test of testCases) {
+ info(`test tab.create with cookieStoreId: "${test.cookieStoreId}"`);
+ extension.sendMessage("test", test);
+ await extension.awaitMessage("test-done");
+ }
+
+ info("Waiting for shutting down...");
+ extension.sendMessage("finish");
+ await extension.awaitMessage("gone");
+
+ await extension.unload();
+});
+
+add_task(async function userContext_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", false]],
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.tabs.create({ cookieStoreId: "firefox-container-1" }),
+ /Contextual identities are currently disabled/,
+ "should refuse to open container tab when contextual identities are disabled"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function tabs_query_cookiestoreid_nocookiepermission() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let tab = await browser.tabs.create({});
+ browser.test.assertEq(
+ "firefox-default",
+ tab.cookieStoreId,
+ "Expecting cookieStoreId for new tab"
+ );
+ let query = await browser.tabs.query({
+ index: tab.index,
+ cookieStoreId: tab.cookieStoreId,
+ });
+ browser.test.assertEq(
+ "firefox-default",
+ query[0].cookieStoreId,
+ "Expecting cookieStoreId for new tab through browser.tabs.query"
+ );
+ await browser.tabs.remove(tab.id);
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function tabs_query_multiple_cookiestoreId() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["cookies"],
+ },
+
+ async background() {
+ let tab1 = await browser.tabs.create({
+ cookieStoreId: "firefox-container-1",
+ });
+ browser.test.log(`Tab created for cookieStoreId:${tab1.cookieStoreId}`);
+
+ let tab2 = await browser.tabs.create({
+ cookieStoreId: "firefox-container-2",
+ });
+ browser.test.log(`Tab created for cookieStoreId:${tab2.cookieStoreId}`);
+
+ let tab3 = await browser.tabs.create({
+ cookieStoreId: "firefox-container-3",
+ });
+ browser.test.log(`Tab created for cookieStoreId:${tab3.cookieStoreId}`);
+
+ let tabs = await browser.tabs.query({
+ cookieStoreId: ["firefox-container-1", "firefox-container-2"],
+ });
+
+ browser.test.assertEq(
+ 2,
+ tabs.length,
+ "Expecting tabs for firefox-container-1 and firefox-container-2"
+ );
+
+ browser.test.assertEq(
+ "firefox-container-1",
+ tabs[0].cookieStoreId,
+ "Expecting tab for firefox-container-1 cookieStoreId"
+ );
+
+ browser.test.assertEq(
+ "firefox-container-2",
+ tabs[1].cookieStoreId,
+ "Expecting tab forfirefox-container-2 cookieStoreId"
+ );
+
+ await browser.tabs.remove([tab1.id, tab2.id, tab3.id]);
+ browser.test.sendMessage("test-done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("test-done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId_private.js b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId_private.js
new file mode 100644
index 0000000000..556aa78288
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_cookieStoreId_private.js
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function perma_private_browsing_mode() {
+ // make sure userContext is enabled.
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ Assert.equal(
+ Services.prefs.getBoolPref("browser.privatebrowsing.autostart"),
+ true,
+ "Permanent private browsing is enabled"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ let win = await browser.windows.create({});
+ browser.test.assertTrue(
+ win.incognito,
+ "New window should be private when perma-PBM is enabled."
+ );
+ await browser.test.assertRejects(
+ browser.tabs.create({
+ cookieStoreId: "firefox-container-1",
+ windowId: win.id,
+ }),
+ /Illegal to set non-private cookieStoreId in a private window/,
+ "should refuse to open container tab in private browsing window"
+ );
+ await browser.windows.remove(win.id);
+
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_create.js b/browser/components/extensions/test/browser/browser_ext_tabs_create.js
new file mode 100644
index 0000000000..a3b6e78331
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_create.js
@@ -0,0 +1,299 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_create_options() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots"
+ );
+ gBrowser.selectedTab = tab;
+
+ // TODO: Multiple windows.
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Using pre-loaded new tab pages interferes with onUpdated events.
+ // It probably shouldn't.
+ ["browser.newtab.preload", false],
+ // Some test cases below load http and check the behavior of https-first.
+ ["dom.security.https_first", true],
+ ],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+
+ background: { page: "bg/background.html" },
+ },
+
+ files: {
+ "bg/blank.html": ``,
+
+ "bg/background.html": `
+
+
+ `,
+
+ "bg/background.js": function () {
+ let activeTab;
+ let activeWindow;
+
+ function runTests() {
+ const DEFAULTS = {
+ index: 2,
+ windowId: activeWindow,
+ active: true,
+ pinned: false,
+ url: "about:newtab",
+ // 'selected' is marked as unsupported in schema, so we've removed it.
+ // For more details, see bug 1337509
+ selected: undefined,
+ mutedInfo: {
+ muted: false,
+ extensionId: undefined,
+ reason: undefined,
+ },
+ };
+
+ let tests = [
+ {
+ create: { url: "https://example.com/" },
+ result: { url: "https://example.com/" },
+ },
+ {
+ create: { url: "view-source:https://example.com/" },
+ result: { url: "view-source:https://example.com/" },
+ },
+ {
+ create: { url: "blank.html" },
+ result: { url: browser.runtime.getURL("bg/blank.html") },
+ },
+ {
+ create: { url: "https://example.com/", openInReaderMode: true },
+ result: {
+ url: `about:reader?url=${encodeURIComponent(
+ "https://example.com/"
+ )}`,
+ },
+ },
+ {
+ create: {},
+ result: { url: "about:newtab" },
+ },
+ {
+ create: { active: false },
+ result: { active: false },
+ },
+ {
+ create: { active: true },
+ result: { active: true },
+ },
+ {
+ create: { pinned: true },
+ result: { pinned: true, index: 0 },
+ },
+ {
+ create: { pinned: true, active: true },
+ result: { pinned: true, active: true, index: 0 },
+ },
+ {
+ create: { pinned: true, active: false },
+ result: { pinned: true, active: false, index: 0 },
+ },
+ {
+ create: { index: 1 },
+ result: { index: 1 },
+ },
+ {
+ create: { index: 1, active: false },
+ result: { index: 1, active: false },
+ },
+ {
+ create: { windowId: activeWindow },
+ result: { windowId: activeWindow },
+ },
+ {
+ create: { index: 9999 },
+ result: { index: 2 },
+ },
+ {
+ // https-first redirects http to https.
+ create: { url: "http://example.com/" },
+ result: { url: "https://example.com/" },
+ },
+ {
+ // https-first redirects http to https.
+ create: { url: "view-source:http://example.com/" },
+ result: { url: "view-source:https://example.com/" },
+ },
+ {
+ // Despite https-first, the about:reader URL does not change.
+ create: { url: "http://example.com/", openInReaderMode: true },
+ result: {
+ url: `about:reader?url=${encodeURIComponent(
+ "http://example.com/"
+ )}`,
+ },
+ },
+ {
+ create: { muted: true },
+ result: {
+ mutedInfo: {
+ muted: true,
+ extensionId: browser.runtime.id,
+ reason: "extension",
+ },
+ },
+ },
+ {
+ create: { muted: false },
+ result: {
+ mutedInfo: {
+ muted: false,
+ extensionId: undefined,
+ reason: undefined,
+ },
+ },
+ },
+ ];
+
+ async function nextTest() {
+ if (!tests.length) {
+ browser.test.notifyPass("tabs.create");
+ return;
+ }
+
+ let test = tests.shift();
+ let expected = Object.assign({}, DEFAULTS, test.result);
+
+ browser.test.log(
+ `Testing tabs.create(${JSON.stringify(
+ test.create
+ )}), expecting ${JSON.stringify(test.result)}`
+ );
+
+ let updatedPromise = new Promise(resolve => {
+ let onUpdated = (changedTabId, changed) => {
+ if (changed.url) {
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ resolve({ tabId: changedTabId, url: changed.url });
+ }
+ };
+ browser.tabs.onUpdated.addListener(onUpdated);
+ });
+
+ let createdPromise = new Promise(resolve => {
+ let onCreated = tab => {
+ browser.test.assertTrue(
+ "id" in tab,
+ `Expected tabs.onCreated callback to receive tab object`
+ );
+ resolve();
+ };
+ browser.tabs.onCreated.addListener(onCreated);
+ });
+
+ let [tab] = await Promise.all([
+ browser.tabs.create(test.create),
+ createdPromise,
+ ]);
+ let tabId = tab.id;
+
+ for (let key of Object.keys(expected)) {
+ if (key === "url") {
+ // FIXME: This doesn't get updated until later in the load cycle.
+ continue;
+ }
+
+ if (key === "mutedInfo") {
+ for (let key of Object.keys(expected.mutedInfo)) {
+ browser.test.assertEq(
+ expected.mutedInfo[key],
+ tab.mutedInfo[key],
+ `Expected value for tab.mutedInfo.${key}`
+ );
+ }
+ } else {
+ browser.test.assertEq(
+ expected[key],
+ tab[key],
+ `Expected value for tab.${key}`
+ );
+ }
+ }
+
+ let 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;
+ activeWindow = tabs[0].windowId;
+
+ runTests();
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.create");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_create_with_popup() {
+ const extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let normalWin = await browser.windows.create();
+ let lastFocusedNormalWin = await browser.windows.getLastFocused({});
+ browser.test.assertEq(
+ lastFocusedNormalWin.id,
+ normalWin.id,
+ "The normal window is the last focused window."
+ );
+ let popupWin = await browser.windows.create({ type: "popup" });
+ let lastFocusedPopupWin = await browser.windows.getLastFocused({});
+ browser.test.assertEq(
+ lastFocusedPopupWin.id,
+ popupWin.id,
+ "The popup window is the last focused window."
+ );
+ let newtab = await browser.tabs.create({});
+ browser.test.assertEq(
+ normalWin.id,
+ newtab.windowId,
+ "New tab was created in last focused normal window."
+ );
+ await Promise.all([
+ browser.windows.remove(normalWin.id),
+ browser.windows.remove(popupWin.id),
+ ]);
+ browser.test.sendMessage("complete");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("complete");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js b/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js
new file mode 100644
index 0000000000..55bb33f26e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_create_invalid_url.js
@@ -0,0 +1,79 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const FILE_URL = Services.io.newFileURI(
+ new FileUtils.File(getTestFilePath("file_dummy.html"))
+).spec;
+
+async function testTabsCreateInvalidURL(tabsCreateURL) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.test.sendMessage("ready");
+ browser.test.onMessage.addListener((msg, tabsCreateURL) => {
+ browser.tabs.create({ url: tabsCreateURL }, tab => {
+ browser.test.assertEq(
+ undefined,
+ tab,
+ "on error tab should be undefined"
+ );
+ browser.test.assertTrue(
+ /Illegal URL/.test(browser.runtime.lastError.message),
+ "runtime.lastError should report the expected error message"
+ );
+
+ // Remove the opened tab is any.
+ if (tab) {
+ browser.tabs.remove(tab.id);
+ }
+ browser.test.sendMessage("done");
+ });
+ });
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("ready");
+
+ info(`test tab.create on invalid URL "${tabsCreateURL}"`);
+
+ extension.sendMessage("start", tabsCreateURL);
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+}
+
+add_task(async function () {
+ info("Start testing tabs.create on invalid URLs");
+
+ let dataURLPage = `data:text/html,
+
+
+
+
+
+
+
data url page
+
+ `;
+
+ let testCases = [
+ { tabsCreateURL: "about:addons" },
+ {
+ tabsCreateURL: "javascript:console.log('tabs.update execute javascript')",
+ },
+ { tabsCreateURL: dataURLPage },
+ { tabsCreateURL: FILE_URL },
+ ];
+
+ for (let { tabsCreateURL } of testCases) {
+ await testTabsCreateInvalidURL(tabsCreateURL);
+ }
+
+ info("done");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_create_url.js b/browser/components/extensions/test/browser/browser_ext_tabs_create_url.js
new file mode 100644
index 0000000000..91cafa6e7e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_create_url.js
@@ -0,0 +1,230 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function runWithDisabledPrivateBrowsing(callback) {
+ const { EnterprisePolicyTesting, PoliciesPrefTracker } =
+ ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+ );
+
+ PoliciesPrefTracker.start();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: { DisablePrivateBrowsing: true },
+ });
+
+ try {
+ await callback();
+ } finally {
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+ EnterprisePolicyTesting.resetRunOnceState();
+ PoliciesPrefTracker.stop();
+ }
+}
+
+add_task(async function test_urlbar_focus() {
+ // Disable preloaded new tab because the urlbar is automatically focused when
+ // a preloaded new tab is opened, while this test is supposed to test that the
+ // implementation of tabs.create automatically focuses the urlbar of new tabs.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtab.preload", false]],
+ });
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.tabs.onUpdated.addListener(function onUpdated(_, info, tab) {
+ if (info.status === "complete" && tab.url !== "about:blank") {
+ browser.test.sendMessage("complete");
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ }
+ });
+ browser.test.onMessage.addListener(async (cmd, ...args) => {
+ const result = await browser.tabs[cmd](...args);
+ browser.test.sendMessage("result", result);
+ });
+ },
+ });
+
+ await extension.startup();
+
+ // Test content is focused after opening a regular url
+ extension.sendMessage("create", { url: "https://example.com" });
+ const [tab1] = await Promise.all([
+ extension.awaitMessage("result"),
+ extension.awaitMessage("complete"),
+ ]);
+
+ is(
+ document.activeElement.tagName,
+ "browser",
+ "Content focused after opening a web page"
+ );
+
+ extension.sendMessage("remove", tab1.id);
+ await extension.awaitMessage("result");
+
+ // Test urlbar is focused after opening an empty tab
+ extension.sendMessage("create", {});
+ const tab2 = await extension.awaitMessage("result");
+
+ const active = document.activeElement;
+ info(
+ `Active element: ${active.tagName}, id: ${active.id}, class: ${active.className}`
+ );
+
+ const parent = active.parentNode;
+ info(
+ `Parent element: ${parent.tagName}, id: ${parent.id}, class: ${parent.className}`
+ );
+
+ info(`After opening an empty tab, gURLBar.focused: ${gURLBar.focused}`);
+
+ is(active.tagName, "html:input", "Input element focused");
+ is(active.id, "urlbar-input", "Urlbar focused");
+
+ extension.sendMessage("remove", tab2.id);
+ await extension.awaitMessage("result");
+
+ await extension.unload();
+});
+
+add_task(async function default_url() {
+ const extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ function promiseNonBlankTab() {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId,
+ changeInfo,
+ tab
+ ) {
+ if (changeInfo.status === "complete" && tab.url !== "about:blank") {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve(tab);
+ }
+ });
+ });
+ }
+
+ browser.test.onMessage.addListener(
+ async (msg, { incognito, expectedNewWindowUrl, expectedNewTabUrl }) => {
+ browser.test.assertEq(
+ "start",
+ msg,
+ `Start test, incognito=${incognito}`
+ );
+
+ let tabPromise = promiseNonBlankTab();
+ let win;
+ try {
+ win = await browser.windows.create({ incognito });
+ browser.test.assertEq(
+ 1,
+ win.tabs.length,
+ "Expected one tab in the new window."
+ );
+ } catch (e) {
+ browser.test.assertEq(
+ expectedNewWindowUrl,
+ e.message,
+ "Expected error"
+ );
+ browser.test.sendMessage("done");
+ return;
+ }
+ let tab = await tabPromise;
+ browser.test.assertEq(
+ expectedNewWindowUrl,
+ tab.url,
+ "Expected default URL of new window"
+ );
+
+ tabPromise = promiseNonBlankTab();
+ await browser.tabs.create({ windowId: win.id });
+ tab = await tabPromise;
+ browser.test.assertEq(
+ expectedNewTabUrl,
+ tab.url,
+ "Expected default URL of new tab"
+ );
+
+ await browser.windows.remove(win.id);
+ browser.test.sendMessage("done");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+
+ extension.sendMessage("start", {
+ incognito: false,
+ expectedNewWindowUrl: "about:home",
+ expectedNewTabUrl: "about:newtab",
+ });
+ await extension.awaitMessage("done");
+ extension.sendMessage("start", {
+ incognito: true,
+ expectedNewWindowUrl: "about:privatebrowsing",
+ expectedNewTabUrl: "about:privatebrowsing",
+ });
+ await extension.awaitMessage("done");
+
+ info("Testing with multiple homepages.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.startup.homepage", "about:robots|about:blank|about:home"]],
+ });
+ extension.sendMessage("start", {
+ incognito: false,
+ expectedNewWindowUrl: "about:robots",
+ expectedNewTabUrl: "about:newtab",
+ });
+ await extension.awaitMessage("done");
+ extension.sendMessage("start", {
+ incognito: true,
+ expectedNewWindowUrl: "about:privatebrowsing",
+ expectedNewTabUrl: "about:privatebrowsing",
+ });
+ await extension.awaitMessage("done");
+ await SpecialPowers.popPrefEnv();
+
+ info("Testing with perma-private browsing mode.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.privatebrowsing.autostart", true]],
+ });
+ extension.sendMessage("start", {
+ incognito: false,
+ expectedNewWindowUrl: "about:home",
+ expectedNewTabUrl: "about:newtab",
+ });
+ await extension.awaitMessage("done");
+ extension.sendMessage("start", {
+ incognito: true,
+ expectedNewWindowUrl: "about:home",
+ expectedNewTabUrl: "about:newtab",
+ });
+ await extension.awaitMessage("done");
+ await SpecialPowers.popPrefEnv();
+
+ info("Testing with disabled private browsing mode.");
+ await runWithDisabledPrivateBrowsing(async () => {
+ extension.sendMessage("start", {
+ incognito: false,
+ expectedNewWindowUrl: "about:home",
+ expectedNewTabUrl: "about:newtab",
+ });
+ await extension.awaitMessage("done");
+ extension.sendMessage("start", {
+ incognito: true,
+ expectedNewWindowUrl:
+ "`incognito` cannot be used if incognito mode is disabled",
+ });
+ await extension.awaitMessage("done");
+ });
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_detectLanguage.js b/browser/components/extensions/test/browser/browser_ext_tabs_detectLanguage.js
new file mode 100644
index 0000000000..2fbfad9d43
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_detectLanguage.js
@@ -0,0 +1,65 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testDetectLanguage() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ const BASE_PATH = "browser/browser/components/extensions/test/browser";
+
+ function loadTab(url) {
+ return browser.tabs.create({ url });
+ }
+
+ try {
+ let tab = await loadTab(
+ `http://example.co.jp/${BASE_PATH}/file_language_ja.html`
+ );
+ let lang = await browser.tabs.detectLanguage(tab.id);
+ browser.test.assertEq(
+ "ja",
+ lang,
+ "Japanese document should be detected as Japanese"
+ );
+ await browser.tabs.remove(tab.id);
+
+ tab = await loadTab(
+ `http://example.co.jp/${BASE_PATH}/file_language_fr_en.html`
+ );
+ lang = await browser.tabs.detectLanguage(tab.id);
+ browser.test.assertEq(
+ "fr",
+ lang,
+ "French/English document should be detected as primarily French"
+ );
+ await browser.tabs.remove(tab.id);
+
+ tab = await loadTab(
+ `http://example.co.jp/${BASE_PATH}/file_language_tlh.html`
+ );
+ lang = await browser.tabs.detectLanguage(tab.id);
+ browser.test.assertEq(
+ "und",
+ lang,
+ "Klingon document should not be detected, should return 'und'"
+ );
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("detectLanguage");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("detectLanguage");
+ }
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("detectLanguage");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_discard.js b/browser/components/extensions/test/browser/browser_ext_tabs_discard.js
new file mode 100644
index 0000000000..818c29c6c3
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discard.js
@@ -0,0 +1,95 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* global gBrowser */
+"use strict";
+
+add_task(async function test_discarded() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs = await browser.tabs.query({ currentWindow: true });
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ async function finishTest() {
+ try {
+ await browser.tabs.discard(tabs[0].id);
+ await browser.tabs.discard(tabs[2].id);
+ browser.test.succeed(
+ "attempting to discard an already discarded tab or the active tab should not throw error"
+ );
+ } catch (e) {
+ browser.test.fail(
+ "attempting to discard an already discarded tab or the active tab should not throw error"
+ );
+ }
+ let discardedTab = await browser.tabs.get(tabs[2].id);
+ browser.test.assertEq(
+ false,
+ discardedTab.discarded,
+ "attempting to discard the active tab should not have succeeded"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.discard(999999999),
+ /Invalid tab ID/,
+ "attempt to discard invalid tabId should throw"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.discard([999999999, tabs[1].id]),
+ /Invalid tab ID/,
+ "attempt to discard a valid and invalid tabId should throw"
+ );
+ discardedTab = await browser.tabs.get(tabs[1].id);
+ browser.test.assertEq(
+ false,
+ discardedTab.discarded,
+ "tab is still not discarded"
+ );
+
+ browser.test.notifyPass("test-finished");
+ }
+
+ browser.tabs.onUpdated.addListener(async function (tabId, updatedInfo) {
+ if ("discarded" in updatedInfo) {
+ browser.test.assertEq(
+ tabId,
+ tabs[0].id,
+ "discarding tab triggered onUpdated"
+ );
+ let discardedTab = await browser.tabs.get(tabs[0].id);
+ browser.test.assertEq(
+ true,
+ discardedTab.discarded,
+ "discarded tab discard property"
+ );
+
+ await finishTest();
+ }
+ });
+
+ browser.tabs.discard(tabs[0].id);
+ },
+ });
+
+ BrowserTestUtils.loadURIString(gBrowser.browsers[0], "http://example.com");
+ await BrowserTestUtils.browserLoaded(gBrowser.browsers[0]);
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+
+ await extension.startup();
+
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_discard_reversed.js b/browser/components/extensions/test/browser/browser_ext_tabs_discard_reversed.js
new file mode 100644
index 0000000000..5fad30a6fb
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discard_reversed.js
@@ -0,0 +1,129 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function tabs_discarded_load_and_discard() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+ async background() {
+ browser.test.sendMessage("ready");
+ const SHIP = await new Promise(resolve =>
+ browser.test.onMessage.addListener((msg, data) => {
+ resolve(data);
+ })
+ );
+
+ const PAGE_URL_BEFORE = "http://example.com/initiallyDiscarded";
+ const PAGE_URL =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+ // Tabs without titles default to URLs without scheme, according to the
+ // logic of tabbrowser.js's setTabTitle/_setTabLabel.
+ // TODO bug 1695512: discarded tabs should also follow this logic instead
+ // of using the unmodified original URL.
+ const PAGE_TITLE_BEFORE = PAGE_URL_BEFORE;
+ const PAGE_TITLE_INITIAL = PAGE_URL.replace("http://", "");
+ const PAGE_TITLE = "Dummy test page";
+
+ function assertDeepEqual(expected, actual, message) {
+ browser.test.assertDeepEq(expected, actual, message);
+ }
+
+ let tab = await browser.tabs.create({
+ url: PAGE_URL_BEFORE,
+ discarded: true,
+ });
+ const TAB_ID = tab.id;
+ browser.test.assertTrue(tab.discarded, "Tab initially discarded");
+ browser.test.assertEq(PAGE_URL_BEFORE, tab.url, "Initial URL");
+ browser.test.assertEq(PAGE_TITLE_BEFORE, tab.title, "Initial title");
+
+ const observedChanges = {
+ discarded: [],
+ title: [],
+ url: [],
+ };
+ function tabsOnUpdatedAfterLoad(tabId, changeInfo, tab) {
+ browser.test.assertEq(TAB_ID, tabId, "tabId for tabs.onUpdated");
+ for (let [prop, value] of Object.entries(changeInfo)) {
+ observedChanges[prop].push(value);
+ }
+ }
+ browser.tabs.onUpdated.addListener(tabsOnUpdatedAfterLoad, {
+ properties: ["discarded", "url", "title"],
+ });
+
+ // Load new URL to transition from discarded:true to discarded:false.
+ await new Promise(resolve => {
+ browser.webNavigation.onCompleted.addListener(details => {
+ browser.test.assertEq(TAB_ID, details.tabId, "onCompleted for tab");
+ browser.test.assertEq(PAGE_URL, details.url, "URL ater load");
+ resolve();
+ });
+ browser.tabs.update(TAB_ID, { url: PAGE_URL });
+ });
+ assertDeepEqual(
+ [false],
+ observedChanges.discarded,
+ "changes to tab.discarded after update"
+ );
+ // TODO bug 1669356: the tabs.onUpdated events should only see the
+ // requested URL and its title. However, the current implementation
+ // reports several events (including url/title "changes") as part of
+ // "restoring" the lazy browser prior to loading the requested URL.
+
+ let expectedUrlChanges = [PAGE_URL_BEFORE, PAGE_URL];
+ if (SHIP && observedChanges.url.length === 1) {
+ // Except when SHIP is enabled, which turns this into a race,
+ // so sometimes only the final URL is seen (see bug 1696815#c22).
+ expectedUrlChanges = [PAGE_URL];
+ }
+
+ assertDeepEqual(
+ expectedUrlChanges,
+ observedChanges.url,
+ "changes to tab.url after update"
+ );
+ assertDeepEqual(
+ [PAGE_TITLE_INITIAL, PAGE_TITLE],
+ observedChanges.title,
+ "changes to tab.title after update"
+ );
+
+ tab = await browser.tabs.get(TAB_ID);
+ browser.test.assertFalse(tab.discarded, "tab.discarded after load");
+ browser.test.assertEq(PAGE_URL, tab.url, "tab.url after load");
+ browser.test.assertEq(PAGE_TITLE, tab.title, "tab.title after load");
+
+ // Reset counters.
+ observedChanges.discarded.length = 0;
+ observedChanges.title.length = 0;
+ observedChanges.url.length = 0;
+
+ // Transition from discarded:false to discarded:true
+ await browser.tabs.discard(TAB_ID);
+ assertDeepEqual(
+ [true],
+ observedChanges.discarded,
+ "changes to tab.discarded after discard"
+ );
+ assertDeepEqual([], observedChanges.url, "tab.url not changed");
+ assertDeepEqual([], observedChanges.title, "tab.title not changed");
+
+ tab = await browser.tabs.get(TAB_ID);
+ browser.test.assertTrue(tab.discarded, "tab.discarded after discard");
+ browser.test.assertEq(PAGE_URL, tab.url, "tab.url after discard");
+ browser.test.assertEq(PAGE_TITLE, tab.title, "tab.title after discard");
+
+ await browser.tabs.remove(TAB_ID);
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ extension.sendMessage("SHIP", Services.appinfo.sessionHistoryInParent);
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js b/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
new file mode 100644
index 0000000000..48c57b5a05
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_discarded.js
@@ -0,0 +1,386 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* global gBrowser SessionStore */
+"use strict";
+
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+let lazyTabState = {
+ entries: [
+ {
+ url: "http://example.com/",
+ triggeringPrincipal_base64,
+ title: "Example Domain",
+ },
+ ],
+};
+
+add_task(async function test_discarded() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+
+ background() {
+ browser.webNavigation.onCompleted.addListener(
+ async details => {
+ browser.test.log(`webNav onCompleted received for ${details.tabId}`);
+ let updatedTab = await browser.tabs.get(details.tabId);
+ browser.test.assertEq(
+ false,
+ updatedTab.discarded,
+ "lazy to non-lazy update discard property"
+ );
+ browser.test.notifyPass("test-finished");
+ },
+ { url: [{ hostContains: "example.com" }] }
+ );
+
+ browser.tabs.onCreated.addListener(function (tab) {
+ browser.test.assertEq(
+ true,
+ tab.discarded,
+ "non-lazy tab onCreated discard property"
+ );
+ browser.tabs.update(tab.id, { active: true });
+ });
+ },
+ });
+
+ await extension.startup();
+
+ let testTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ createLazyBrowser: true,
+ });
+ SessionStore.setTabState(testTab, lazyTabState);
+
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(testTab);
+});
+
+// Regression test for Bug 1819794.
+add_task(async function test_create_discarded_with_cookieStoreId() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextualIdentities", "cookies"],
+ },
+ async background() {
+ const [{ cookieStoreId }] = await browser.contextualIdentities.query({});
+ browser.test.assertEq(
+ "firefox-container-1",
+ cookieStoreId,
+ "Got expected cookieStoreId"
+ );
+ await browser.tabs.create({
+ url: `http://example.com/#${cookieStoreId}`,
+ cookieStoreId,
+ discarded: true,
+ });
+ await browser.tabs.create({
+ url: `http://example.com/#no-container`,
+ discarded: true,
+ });
+ },
+ // Needed by ExtensionSettingsStore (as a side-effect of contextualIdentities permission).
+ useAddonManager: "temporary",
+ });
+
+ const tabContainerPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "TabOpen",
+ false,
+ evt => {
+ return evt.target.getAttribute("usercontextid", "1");
+ }
+ ).then(evt => evt.target);
+ const tabDefaultPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "TabOpen",
+ false,
+ evt => {
+ return !evt.target.hasAttribute("usercontextid");
+ }
+ ).then(evt => evt.target);
+
+ await extension.startup();
+
+ const tabContainer = await tabContainerPromise;
+ ok(
+ tabContainer.hasAttribute("pending"),
+ "new container tab should be discarded"
+ );
+ const tabContainerState = SessionStore.getTabState(tabContainer);
+ is(
+ JSON.parse(tabContainerState).userContextId,
+ 1,
+ `Expect a userContextId associated to the new discarded container tab: ${tabContainerState}`
+ );
+
+ const tabDefault = await tabDefaultPromise;
+ ok(
+ tabDefault.hasAttribute("pending"),
+ "new non-container tab should be discarded"
+ );
+ const tabDefaultState = SessionStore.getTabState(tabDefault);
+ is(
+ JSON.parse(tabDefaultState).userContextId,
+ 0,
+ `Expect userContextId 0 associated to the new discarded non-container tab: ${tabDefaultState}`
+ );
+
+ BrowserTestUtils.removeTab(tabContainer);
+ BrowserTestUtils.removeTab(tabDefault);
+ await extension.unload();
+});
+
+// If discard is called immediately after creating a new tab, the new tab may not have loaded,
+// and the sessionstore for that tab is not ready for discarding. The result was a corrupted
+// sessionstore for the tab, which when the tab was activated, resulted in a tab with partial
+// state.
+add_task(async function test_create_then_discard() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+
+ background: async function () {
+ let createdTab;
+
+ browser.tabs.onUpdated.addListener((tabId, updatedInfo) => {
+ if (!updatedInfo.discarded) {
+ return;
+ }
+
+ browser.webNavigation.onCompleted.addListener(
+ async details => {
+ browser.test.assertEq(
+ createdTab.id,
+ details.tabId,
+ "created tab navigation is completed"
+ );
+ let activeTab = await browser.tabs.get(details.tabId);
+ browser.test.assertEq(
+ "http://example.com/",
+ details.url,
+ "created tab url is correct"
+ );
+ browser.test.assertEq(
+ "http://example.com/",
+ activeTab.url,
+ "created tab url is correct"
+ );
+ browser.tabs.remove(details.tabId);
+ browser.test.notifyPass("test-finished");
+ },
+ { url: [{ hostContains: "example.com" }] }
+ );
+
+ browser.tabs.update(tabId, { active: true });
+ });
+
+ createdTab = await browser.tabs.create({
+ url: "http://example.com/",
+ active: false,
+ });
+ browser.tabs.discard(createdTab.id);
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
+
+add_task(async function test_create_discarded() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+
+ background() {
+ let tabOpts = {
+ url: "http://example.com/",
+ active: false,
+ discarded: true,
+ title: "discarded tab",
+ };
+
+ browser.webNavigation.onCompleted.addListener(
+ async details => {
+ let activeTab = await browser.tabs.get(details.tabId);
+ browser.test.assertEq(
+ tabOpts.url,
+ activeTab.url,
+ "restored tab url matches active tab url"
+ );
+ browser.test.assertEq(
+ "mochitest index /",
+ activeTab.title,
+ "restored tab title is correct"
+ );
+ browser.tabs.remove(details.tabId);
+ browser.test.notifyPass("test-finished");
+ },
+ { url: [{ hostContains: "example.com" }] }
+ );
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.assertEq(
+ tabOpts.active,
+ tab.active,
+ "lazy tab is not active"
+ );
+ browser.test.assertEq(
+ tabOpts.discarded,
+ tab.discarded,
+ "lazy tab is discarded"
+ );
+ browser.test.assertEq(tabOpts.url, tab.url, "lazy tab url is correct");
+ browser.test.assertEq(
+ tabOpts.title,
+ tab.title,
+ "lazy tab title is correct"
+ );
+ browser.tabs.update(tab.id, { active: true });
+ });
+
+ browser.tabs.create(tabOpts);
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
+
+add_task(async function test_discarded_private_tab_restored() {
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+
+ background() {
+ let isDiscarding = false;
+ browser.tabs.onUpdated.addListener(
+ async function listener(tabId, changeInfo, tab) {
+ const { active, discarded, incognito } = tab;
+ if (!incognito || active || discarded || isDiscarding) {
+ return;
+ }
+ // Remove the onUpdated listener to prevent intermittent failure
+ // to be hit if the listener gets called again for unrelated
+ // tabs.onUpdated events that may get fired after the test case got
+ // the tab-discarded test message that was expecting.
+ isDiscarding = true;
+ browser.tabs.onUpdated.removeListener(listener);
+ browser.test.log(
+ `Test extension discarding ${tabId}: ${JSON.stringify(changeInfo)}`
+ );
+ await browser.tabs.discard(tabId);
+ browser.test.sendMessage("tab-discarded");
+ },
+ { properties: ["status"] }
+ );
+ },
+ });
+
+ // Open a private browsing window.
+ const privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await extension.startup();
+
+ const newTab = await BrowserTestUtils.addTab(
+ privateWin.gBrowser,
+ "https://example.com/"
+ );
+ await extension.awaitMessage("tab-discarded");
+ is(newTab.getAttribute("pending"), "true", "private tab should be discarded");
+
+ const promiseTabLoaded = BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+
+ info("Switching to the discarded background tab");
+ await BrowserTestUtils.switchTab(privateWin.gBrowser, newTab);
+
+ info("Wait for the restored tab to complete loading");
+ await promiseTabLoaded;
+ is(
+ newTab.hasAttribute("pending"),
+ false,
+ "discarded private tab should have been restored"
+ );
+
+ is(
+ newTab.linkedBrowser.currentURI.spec,
+ "https://example.com/",
+ "Got the expected url on the restored tab"
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function test_update_discarded() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", ""],
+ },
+
+ background() {
+ browser.test.onMessage.addListener(async msg => {
+ let [tab] = await browser.tabs.query({ url: "http://example.com/" });
+ if (msg == "update") {
+ await browser.tabs.update(tab.id, { url: "https://example.com/" });
+ } else {
+ browser.test.fail(`Unexpected message received: ${msg}`);
+ }
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ let lazyTab = BrowserTestUtils.addTab(gBrowser, "http://example.com/", {
+ createLazyBrowser: true,
+ lazyTabTitle: "Example Domain",
+ });
+
+ let tabBrowserInsertedPromise = BrowserTestUtils.waitForEvent(
+ lazyTab,
+ "TabBrowserInserted"
+ );
+
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [
+ {
+ message:
+ /Lazy browser prematurely inserted via 'loadURI' property access:/,
+ forbid: true,
+ },
+ ]);
+ });
+
+ extension.sendMessage("update");
+ await tabBrowserInsertedPromise;
+
+ await BrowserTestUtils.waitForBrowserStateChange(
+ lazyTab.linkedBrowser,
+ "https://example.com/",
+ stateFlags => {
+ return (
+ stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
+ stateFlags & Ci.nsIWebProgressListener.STATE_STOP
+ );
+ }
+ );
+
+ await TestUtils.waitForTick();
+ BrowserTestUtils.removeTab(lazyTab);
+
+ await extension.unload();
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js b/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js
new file mode 100644
index 0000000000..50c56ea796
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_duplicate.js
@@ -0,0 +1,316 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testDuplicateTab() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let [source] = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+
+ let tab = await browser.tabs.duplicate(source.id);
+
+ browser.test.assertEq(
+ "http://example.net/",
+ tab.url,
+ "duplicated tab should have the same URL as the source tab"
+ );
+ browser.test.assertEq(
+ source.index + 1,
+ tab.index,
+ "duplicated tab should open next to the source tab"
+ );
+ browser.test.assertTrue(
+ tab.active,
+ "duplicated tab should be active by default"
+ );
+
+ await browser.tabs.remove([source.id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate");
+ await extension.unload();
+});
+
+add_task(async function testDuplicateTabLazily() {
+ async function background() {
+ let tabLoadComplete = new Promise(resolve => {
+ browser.test.onMessage.addListener((message, tabId, result) => {
+ if (message == "duplicate-tab-done") {
+ resolve(tabId);
+ }
+ });
+ });
+
+ 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();
+ }
+ });
+ });
+ }
+
+ try {
+ let url =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+ let tab = await browser.tabs.create({ url });
+ let startTabId = tab.id;
+
+ await awaitLoad(startTabId);
+ browser.test.sendMessage("duplicate-tab", startTabId);
+
+ let unloadedTabId = await tabLoadComplete;
+ let loadedtab = await browser.tabs.get(startTabId);
+ browser.test.assertEq(
+ "Dummy test page",
+ loadedtab.title,
+ "Title should be returned for loaded pages"
+ );
+ browser.test.assertEq(
+ "complete",
+ loadedtab.status,
+ "Tab status should be complete for loaded pages"
+ );
+
+ let unloadedtab = await browser.tabs.get(unloadedTabId);
+ browser.test.assertEq(
+ "Dummy test page",
+ unloadedtab.title,
+ "Title should be returned after page has been unloaded"
+ );
+
+ await browser.tabs.remove([tab.id, unloadedTabId]);
+ browser.test.notifyPass("tabs.hasCorrectTabTitle");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tabs.hasCorrectTabTitle");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ extension.onMessage("duplicate-tab", tabId => {
+ const {
+ Management: {
+ global: { tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let tab = tabTracker.getTab(tabId);
+ // This is a bit of a hack to load a tab in the background.
+ let newTab = gBrowser.duplicateTab(tab, true, { skipLoad: true });
+
+ BrowserTestUtils.waitForEvent(newTab, "SSTabRestored", () => true).then(
+ () => {
+ extension.sendMessage("duplicate-tab-done", tabTracker.getId(newTab));
+ }
+ );
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.hasCorrectTabTitle");
+ await extension.unload();
+});
+
+add_task(async function testDuplicatePinnedTab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+ gBrowser.pinTab(tab);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let [source] = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+ let tab = await browser.tabs.duplicate(source.id);
+
+ browser.test.assertEq(
+ source.index + 1,
+ tab.index,
+ "duplicated tab should open next to the source tab"
+ );
+ browser.test.assertFalse(
+ tab.pinned,
+ "duplicated tab should not be pinned by default, even if source tab is"
+ );
+
+ await browser.tabs.remove([source.id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate.pinned");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate.pinned");
+ await extension.unload();
+});
+
+add_task(async function testDuplicateTabInBackground() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+ let tab = await browser.tabs.duplicate(tabs[0].id, { active: false });
+ // Should not be the active tab
+ browser.test.assertFalse(tab.active);
+
+ await browser.tabs.remove([tabs[0].id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate.background");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate.background");
+ await extension.unload();
+});
+
+add_task(async function testDuplicateTabAtIndex() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+ let tab = await browser.tabs.duplicate(tabs[0].id, { index: 0 });
+ browser.test.assertEq(0, tab.index);
+
+ await browser.tabs.remove([tabs[0].id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate.index");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate.index");
+ await extension.unload();
+});
+
+add_task(async function testDuplicatePinnedTabAtIncorrectIndex() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+ gBrowser.pinTab(tab);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+ let tab = await browser.tabs.duplicate(tabs[0].id, { index: 0 });
+ browser.test.assertEq(1, tab.index);
+ browser.test.assertFalse(
+ tab.pinned,
+ "Duplicated tab should not be pinned"
+ );
+
+ await browser.tabs.remove([tabs[0].id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate.incorrect_index");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate.incorrect_index");
+ await extension.unload();
+});
+
+add_task(async function testDuplicateResolvePromiseRightAway() {
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/file_slowed_document.sjs"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ // The host permission matches the above URL. No :8888 due to bug 1468162.
+ permissions: ["tabs", "http://mochi.test/"],
+ },
+
+ background: async function () {
+ let [source] = await browser.tabs.query({
+ lastFocusedWindow: true,
+ active: true,
+ });
+
+ let resolvedRightAway = true;
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo, tab) => {
+ resolvedRightAway = false;
+ },
+ { urls: [source.url] }
+ );
+
+ let tab = await browser.tabs.duplicate(source.id);
+ // if the promise is resolved before any onUpdated event has been fired,
+ // then the promise has been resolved before waiting for the tab to load
+ browser.test.assertTrue(
+ resolvedRightAway,
+ "tabs.duplicate() should resolve as soon as possible"
+ );
+
+ // Regression test for bug 1559216
+ let code = "document.URL";
+ let [result] = await browser.tabs.executeScript(tab.id, { code });
+ browser.test.assertEq(
+ source.url,
+ result,
+ "APIs such as tabs.executeScript should be queued until tabs.duplicate has restored the tab"
+ );
+
+ await browser.tabs.remove([source.id, tab.id]);
+ browser.test.notifyPass("tabs.duplicate.resolvePromiseRightAway");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.duplicate.resolvePromiseRightAway");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_events.js b/browser/components/extensions/test/browser/browser_ext_tabs_events.js
new file mode 100644
index 0000000000..fe9317b4a6
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_events.js
@@ -0,0 +1,794 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// A single monitor for the tests. If it receives any
+// incognito data in event listeners it will fail.
+let monitor;
+add_task(async function startup() {
+ monitor = await startIncognitoMonitorExtension();
+});
+registerCleanupFunction(async function finish() {
+ await monitor.unload();
+});
+
+// Test tab events from private windows, the monitor above will fail
+// if it receives any.
+add_task(async function test_tab_events_incognito_monitored() {
+ async function background() {
+ let incognito = true;
+ let events = [];
+ let eventPromise;
+ let 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 (let [i, name] of names.entries()) {
+ browser.test.assertEq(
+ name,
+ i in events && events[i].type,
+ `Got expected ${name} event`
+ );
+ }
+ return events.splice(0);
+ }
+
+ try {
+ let firstWindow = await browser.windows.create({
+ url: "about:blank",
+ incognito,
+ });
+ let otherWindow = await browser.windows.create({
+ url: "about:blank",
+ incognito,
+ });
+
+ let windowId = firstWindow.id;
+ let otherWindowId = otherWindow.id;
+
+ // Wait for a tab in each window
+ await expectEvents(["onCreated", "onCreated"]);
+ let initialTab = (
+ await browser.tabs.query({
+ active: true,
+ windowId: otherWindowId,
+ })
+ )[0];
+
+ browser.test.log("Create tab in window 1");
+ let tab = await browser.tabs.create({
+ windowId,
+ index: 0,
+ url: "about:blank",
+ });
+ let oldIndex = tab.index;
+ browser.test.assertEq(0, oldIndex, "Tab has the expected index");
+ browser.test.assertEq(tab.incognito, incognito, "Tab is incognito");
+
+ let [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("Move tab to window 2");
+ await browser.tabs.move([tab.id], { windowId: otherWindowId, index: 0 });
+
+ let [detached, attached] = await expectEvents([
+ "onDetached",
+ "onAttached",
+ ]);
+ browser.test.assertEq(
+ tab.id,
+ detached.tabId,
+ "Expected onDetached tab ID"
+ );
+ browser.test.assertEq(
+ oldIndex,
+ detached.oldPosition,
+ "Expected old index"
+ );
+ browser.test.assertEq(
+ windowId,
+ detached.oldWindowId,
+ "Expected old window ID"
+ );
+
+ browser.test.assertEq(
+ tab.id,
+ attached.tabId,
+ "Expected onAttached tab ID"
+ );
+ browser.test.assertEq(0, attached.newPosition, "Expected new index");
+ browser.test.assertEq(
+ otherWindowId,
+ attached.newWindowId,
+ "Expected new window ID"
+ );
+
+ browser.test.log("Move tab within the same window");
+ let [moved] = await browser.tabs.move([tab.id], { index: 1 });
+ browser.test.assertEq(1, moved.index, "Expected new index");
+
+ [moved] = await expectEvents(["onMoved"]);
+ browser.test.assertEq(tab.id, moved.tabId, "Expected tab ID");
+ browser.test.assertEq(0, moved.fromIndex, "Expected old index");
+ browser.test.assertEq(1, moved.toIndex, "Expected new index");
+ browser.test.assertEq(
+ otherWindowId,
+ moved.windowId,
+ "Expected window ID"
+ );
+
+ browser.test.log("Remove tab");
+ await browser.tabs.remove(tab.id);
+ let [removed] = await expectEvents(["onRemoved"]);
+
+ browser.test.assertEq(
+ tab.id,
+ removed.tabId,
+ "Expected removed tab ID for tabs.remove"
+ );
+ browser.test.assertEq(
+ otherWindowId,
+ 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.log("Close second window");
+ await browser.windows.remove(otherWindowId);
+ [removed] = await expectEvents(["onRemoved"]);
+ browser.test.assertEq(
+ initialTab.id,
+ removed.tabId,
+ "Expected removed tab ID for windows.remove"
+ );
+ browser.test.assertEq(
+ otherWindowId,
+ removed.windowId,
+ "Expected removed tab window ID"
+ );
+ browser.test.assertEq(
+ true,
+ removed.isWindowClosing,
+ "Expected isWindowClosing value"
+ );
+
+ browser.test.log("Create additional tab in window 1");
+ tab = await browser.tabs.create({ windowId, url: "about:blank" });
+ await expectEvents(["onCreated"]);
+ browser.test.assertEq(tab.incognito, incognito, "Tab is incognito");
+
+ browser.test.log("Create a new window, adopting the new tab");
+ // We have to explicitly wait for the event here, since its timing is
+ // not predictable.
+ let promiseAttached = new Promise(resolve => {
+ browser.tabs.onAttached.addListener(function listener(tabId) {
+ browser.tabs.onAttached.removeListener(listener);
+ resolve();
+ });
+ });
+
+ let [window] = await Promise.all([
+ browser.windows.create({ tabId: tab.id, incognito }),
+ promiseAttached,
+ ]);
+
+ [detached, attached] = await expectEvents(["onDetached", "onAttached"]);
+
+ browser.test.assertEq(
+ tab.id,
+ detached.tabId,
+ "Expected onDetached tab ID"
+ );
+ browser.test.assertEq(
+ 1,
+ detached.oldPosition,
+ "Expected onDetached old index"
+ );
+ browser.test.assertEq(
+ windowId,
+ detached.oldWindowId,
+ "Expected onDetached old window ID"
+ );
+
+ browser.test.assertEq(
+ tab.id,
+ attached.tabId,
+ "Expected onAttached tab ID"
+ );
+ browser.test.assertEq(
+ 0,
+ attached.newPosition,
+ "Expected onAttached new index"
+ );
+ browser.test.assertEq(
+ window.id,
+ attached.newWindowId,
+ "Expected onAttached new window id"
+ );
+
+ browser.test.log(
+ "Close the new window by moving the tab into former window"
+ );
+ await browser.tabs.move(tab.id, { index: 1, windowId });
+ [detached, attached] = await expectEvents(["onDetached", "onAttached"]);
+
+ browser.test.assertEq(
+ tab.id,
+ detached.tabId,
+ "Expected onDetached tab ID"
+ );
+ browser.test.assertEq(
+ 0,
+ detached.oldPosition,
+ "Expected onDetached old index"
+ );
+ browser.test.assertEq(
+ window.id,
+ detached.oldWindowId,
+ "Expected onDetached old window ID"
+ );
+
+ browser.test.assertEq(
+ tab.id,
+ attached.tabId,
+ "Expected onAttached tab ID"
+ );
+ browser.test.assertEq(
+ 1,
+ attached.newPosition,
+ "Expected onAttached new index"
+ );
+ browser.test.assertEq(
+ windowId,
+ attached.newWindowId,
+ "Expected onAttached new window id"
+ );
+ browser.test.assertEq(tab.incognito, incognito, "Tab is incognito");
+
+ browser.test.log("Remove the tab");
+ await browser.tabs.remove(tab.id);
+ browser.windows.remove(windowId);
+
+ browser.test.notifyPass("tabs-events");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tabs-events");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background,
+ incognitoOverride: "spanning",
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs-events");
+ await extension.unload();
+});
+
+add_task(async function testTabEventsSize() {
+ function background() {
+ function sendSizeMessages(tab, type) {
+ browser.test.sendMessage(`${type}-dims`, {
+ width: tab.width,
+ height: tab.height,
+ });
+ }
+
+ browser.tabs.onCreated.addListener(tab => {
+ sendSizeMessages(tab, "on-created");
+ });
+
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (changeInfo.status == "complete") {
+ sendSizeMessages(tab, "on-updated");
+ }
+ });
+
+ browser.test.onMessage.addListener(async (msg, arg) => {
+ if (msg === "create-tab") {
+ let tab = await browser.tabs.create({ url: "https://example.com/" });
+ sendSizeMessages(tab, "create");
+ browser.test.sendMessage("created-tab-id", tab.id);
+ } else if (msg === "update-tab") {
+ let tab = await browser.tabs.update(arg, {
+ url: "https://example.org/",
+ });
+ sendSizeMessages(tab, "update");
+ } else if (msg === "remove-tab") {
+ browser.tabs.remove(arg);
+ browser.test.sendMessage("tab-removed");
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background,
+ });
+
+ const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
+ registerCleanupFunction(() => {
+ SpecialPowers.clearUserPref(RESOLUTION_PREF);
+ });
+
+ function checkDimensions(dims, type) {
+ is(
+ dims.width,
+ gBrowser.selectedBrowser.clientWidth,
+ `tab from ${type} reports expected width`
+ );
+ is(
+ dims.height,
+ gBrowser.selectedBrowser.clientHeight,
+ `tab from ${type} reports expected height`
+ );
+ }
+
+ await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+ for (let resolution of [2, 1]) {
+ SpecialPowers.setCharPref(RESOLUTION_PREF, String(resolution));
+ is(
+ window.devicePixelRatio,
+ resolution,
+ "window has the required resolution"
+ );
+
+ extension.sendMessage("create-tab");
+ let tabId = await extension.awaitMessage("created-tab-id");
+
+ checkDimensions(await extension.awaitMessage("create-dims"), "create");
+ checkDimensions(
+ await extension.awaitMessage("on-created-dims"),
+ "onCreated"
+ );
+ checkDimensions(
+ await extension.awaitMessage("on-updated-dims"),
+ "onUpdated"
+ );
+
+ extension.sendMessage("update-tab", tabId);
+
+ checkDimensions(await extension.awaitMessage("update-dims"), "update");
+ checkDimensions(
+ await extension.awaitMessage("on-updated-dims"),
+ "onUpdated"
+ );
+
+ extension.sendMessage("remove-tab", tabId);
+ await extension.awaitMessage("tab-removed");
+ }
+
+ await extension.unload();
+ SpecialPowers.clearUserPref(RESOLUTION_PREF);
+}).skip(); // Bug 1614075 perma-fail comparing devicePixelRatio
+
+add_task(async function testTabRemovalEvent() {
+ async function background() {
+ let events = [];
+
+ 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.assertEq(
+ 0,
+ events.length,
+ "No events recorded before onRemoved."
+ );
+ events.push("onRemoved");
+ browser.test.log(
+ "Make sure the removed tab is not available in the tabs.query callback."
+ );
+ chrome.tabs.query({}, tabs => {
+ for (let tab of tabs) {
+ browser.test.assertTrue(
+ tab.id != tabId,
+ "Tab query should not include removed tabId"
+ );
+ }
+ });
+ });
+
+ try {
+ let url =
+ "https://example.com/browser/browser/components/extensions/test/browser/context.html";
+ let tab = await browser.tabs.create({ url: url });
+ await awaitLoad(tab.id);
+
+ chrome.tabs.onActivated.addListener(info => {
+ browser.test.assertEq(
+ 1,
+ events.length,
+ "One event recorded before onActivated."
+ );
+ events.push("onActivated");
+ browser.test.assertEq(
+ "onRemoved",
+ events[0],
+ "onRemoved fired before onActivated."
+ );
+ browser.test.notifyPass("tabs-events");
+ });
+
+ await browser.tabs.remove(tab.id);
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tabs-events");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs-events");
+ await extension.unload();
+});
+
+add_task(async function testTabCreateRelated() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.opentabfor.middleclick", true],
+ ["browser.tabs.insertRelatedAfterCurrent", true],
+ ],
+ });
+
+ async function background() {
+ let created;
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.log(`tabs.onCreated, index=${tab.index}`);
+ browser.test.assertEq(1, tab.index, "expecting tab index of 1");
+ created = tab.id;
+ });
+ browser.tabs.onMoved.addListener((id, info) => {
+ browser.test.log(
+ `tabs.onMoved, from ${info.fromIndex} to ${info.toIndex}`
+ );
+ browser.test.fail("tabMoved was received");
+ });
+ browser.tabs.onRemoved.addListener((tabId, info) => {
+ browser.test.assertEq(created, tabId, "removed id same as created");
+ browser.test.sendMessage("tabRemoved");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ // Create a *opener* tab page which has a link to "example.com".
+ let pageURL =
+ "https://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+ let openerTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ pageURL
+ );
+ gBrowser.moveTabTo(openerTab, 0);
+
+ await extension.startup();
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "https://example.com/#linkclick",
+ true
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#link_to_example_com",
+ { button: 1 },
+ gBrowser.selectedBrowser
+ );
+ let openTab = await newTabPromise;
+ is(
+ openTab.linkedBrowser.currentURI.spec,
+ "https://example.com/#linkclick",
+ "Middle click should open site to correct url."
+ );
+ BrowserTestUtils.removeTab(openTab);
+
+ await extension.awaitMessage("tabRemoved");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(openerTab);
+});
+
+add_task(async function testLastTabRemoval() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.closeWindowWithLastTab", false]],
+ });
+
+ async function background() {
+ let windowId;
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.assertEq(
+ windowId,
+ tab.windowId,
+ "expecting onCreated after onRemoved on the same window"
+ );
+ browser.test.sendMessage("tabCreated", `${tab.width}x${tab.height}`);
+ });
+ browser.tabs.onRemoved.addListener((tabId, info) => {
+ windowId = info.windowId;
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background,
+ });
+
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+ await extension.startup();
+
+ const oldBrowser = newWin.gBrowser.selectedBrowser;
+ const expectedDims = `${oldBrowser.clientWidth}x${oldBrowser.clientHeight}`;
+ BrowserTestUtils.removeTab(newWin.gBrowser.selectedTab);
+
+ const actualDims = await extension.awaitMessage("tabCreated");
+ is(
+ actualDims,
+ expectedDims,
+ "created tab reports a size same to the removed last tab"
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(newWin);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function testTabActivationEvent() {
+ 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 [
+ ,
+ {
+ tabs: [tab1],
+ },
+ ] = await Promise.all([
+ listener.expect(info => {
+ browser.test.assertEq(
+ undefined,
+ info.previousTabId,
+ "previousTabId should not be defined when window is first opened"
+ );
+ }),
+ browser.windows.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),
+ ]);
+
+ await browser.tabs.remove(tab2.id);
+
+ browser.test.notifyPass("tabs-events");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tabs-events");
+ }
+ }
+
+ let 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() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@tabs" } },
+ permissions: ["tabs"],
+ background: { persistent: false },
+ },
+ background() {
+ const EVENTS = [
+ "onActivated",
+ "onAttached",
+ "onDetached",
+ "onRemoved",
+ "onMoved",
+ "onHighlighted",
+ "onUpdated",
+ ];
+ browser.tabs.onCreated.addListener(() => {
+ browser.test.sendMessage("onCreated");
+ });
+ for (let event of EVENTS) {
+ browser.tabs[event].addListener(() => {});
+ }
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const EVENTS = [
+ "onActivated",
+ "onAttached",
+ "onCreated",
+ "onDetached",
+ "onRemoved",
+ "onMoved",
+ "onHighlighted",
+ "onUpdated",
+ ];
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "tabs", event, {
+ primed: false,
+ });
+ }
+
+ // test events waken background
+ await extension.terminateBackground();
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "tabs", event, {
+ primed: true,
+ });
+ }
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onCreated");
+ ok(true, "persistent event woke background");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "tabs", event, {
+ primed: false,
+ });
+ }
+ await BrowserTestUtils.closeWindow(win);
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_events_order.js b/browser/components/extensions/test/browser/browser_ext_tabs_events_order.js
new file mode 100644
index 0000000000..ab998109de
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_events_order.js
@@ -0,0 +1,206 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function testTabEvents() {
+ async function background() {
+ /** The list of active tab ID's */
+ let tabIds = [];
+
+ /**
+ * Stores the events that fire for each tab.
+ *
+ * events {
+ * tabId1: [event1, event2, ...],
+ * tabId2: [event1, event2, ...],
+ * }
+ */
+ let events = {};
+
+ browser.tabs.onActivated.addListener(info => {
+ if (info.tabId in events) {
+ events[info.tabId].push("onActivated");
+ } else {
+ events[info.tabId] = ["onActivated"];
+ }
+ });
+
+ browser.tabs.onCreated.addListener(info => {
+ if (info.id in events) {
+ events[info.id].push("onCreated");
+ } else {
+ events[info.id] = ["onCreated"];
+ }
+ });
+
+ browser.tabs.onHighlighted.addListener(info => {
+ if (info.tabIds[0] in events) {
+ events[info.tabIds[0]].push("onHighlighted");
+ } else {
+ events[info.tabIds[0]] = ["onHighlighted"];
+ }
+ });
+
+ /**
+ * Asserts that the expected events are fired for the tab with id = tabId.
+ * The events associated to the specified tab are removed after this check is made.
+ *
+ * @param {number} tabId
+ * @param {Array} expectedEvents
+ */
+ async function expectEvents(tabId, expectedEvents) {
+ browser.test.log(`Expecting events: ${expectedEvents.join(", ")}`);
+
+ // Wait up to 5000 ms for the expected number of events.
+ for (
+ let i = 0;
+ i < 50 &&
+ (!events[tabId] || events[tabId].length < expectedEvents.length);
+ i++
+ ) {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ browser.test.assertEq(
+ expectedEvents.length,
+ events[tabId].length,
+ `Got expected number of events for ${tabId}`
+ );
+
+ for (let name of expectedEvents) {
+ browser.test.assertTrue(
+ events[tabId].includes(name),
+ `Got expected ${name} event`
+ );
+ }
+
+ if (expectedEvents.includes("onCreated")) {
+ browser.test.assertEq(
+ events[tabId].indexOf("onCreated"),
+ 0,
+ "onCreated happened first"
+ );
+ }
+
+ delete events[tabId];
+ }
+
+ /**
+ * Opens a new tab and asserts that the correct events are fired.
+ *
+ * @param {number} windowId
+ */
+ async function openTab(windowId) {
+ browser.test.assertEq(
+ 0,
+ Object.keys(events).length,
+ "No events remaining before testing openTab."
+ );
+
+ let tab = await browser.tabs.create({ windowId });
+
+ tabIds.push(tab.id);
+ browser.test.log(`Opened tab ${tab.id}`);
+
+ await expectEvents(tab.id, ["onCreated", "onActivated", "onHighlighted"]);
+ }
+
+ /**
+ * Opens a new window and asserts that the correct events are fired.
+ *
+ * @param {Array} urls A list of urls for which to open tabs in the new window.
+ */
+ async function openWindow(urls) {
+ browser.test.assertEq(
+ 0,
+ Object.keys(events).length,
+ "No events remaining before testing openWindow."
+ );
+
+ let window = await browser.windows.create({ url: urls });
+ browser.test.log(`Opened new window ${window.id}`);
+
+ for (let [i] of urls.entries()) {
+ let tab = window.tabs[i];
+ tabIds.push(tab.id);
+
+ let expectedEvents = ["onCreated", "onActivated", "onHighlighted"];
+ if (i !== 0) {
+ expectedEvents.splice(1);
+ }
+ await expectEvents(window.tabs[i].id, expectedEvents);
+ }
+ }
+
+ /**
+ * Highlights an existing tab and asserts that the correct events are fired.
+ *
+ * @param {number} tabId
+ */
+ async function highlightTab(tabId) {
+ browser.test.assertEq(
+ 0,
+ Object.keys(events).length,
+ "No events remaining before testing highlightTab."
+ );
+
+ browser.test.log(`Highlighting tab ${tabId}`);
+ let tab = await browser.tabs.update(tabId, { active: true });
+
+ browser.test.assertEq(tab.id, tabId, `Tab ${tab.id} highlighted`);
+
+ await expectEvents(tab.id, ["onActivated", "onHighlighted"]);
+ }
+
+ /**
+ * The main entry point to the tests.
+ */
+ let tabs = await browser.tabs.query({ active: true, currentWindow: true });
+
+ let activeWindow = tabs[0].windowId;
+ await Promise.all([
+ openTab(activeWindow),
+ openTab(activeWindow),
+ openTab(activeWindow),
+ ]);
+
+ await Promise.all([
+ highlightTab(tabIds[0]),
+ highlightTab(tabIds[1]),
+ highlightTab(tabIds[2]),
+ ]);
+
+ await Promise.all([
+ openWindow(["http://example.com"]),
+ openWindow(["http://example.com", "http://example.org"]),
+ openWindow([
+ "http://example.com",
+ "http://example.org",
+ "http://example.net",
+ ]),
+ ]);
+
+ browser.test.assertEq(
+ 0,
+ Object.keys(events).length,
+ "No events remaining after tests."
+ );
+
+ await Promise.all(tabIds.map(id => browser.tabs.remove(id)));
+
+ browser.test.notifyPass("tabs.highlight");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.highlight");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
new file mode 100644
index 0000000000..c02aef3da9
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
@@ -0,0 +1,453 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScript() {
+ let { MessageChannel } = ChromeUtils.importESModule(
+ "resource://testing-common/MessageChannel.sys.mjs"
+ );
+
+ function countMM(messageManagerMap) {
+ let count = 0;
+ // List of permanent message managers in the main process. We should not
+ // count them in the test because MessageChannel unsubscribes when the
+ // message manager closes, which never happens to these, of course.
+ let globalMMs = [Services.mm, Services.ppmm, Services.ppmm.getChildAt(0)];
+ for (let mm of messageManagerMap.keys()) {
+ if (!globalMMs.includes(mm)) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ let messageManagersSize = countMM(MessageChannel.messageManagers);
+
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ const URL = BASE + "file_iframe_document.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL, true);
+
+ async function background() {
+ try {
+ // This promise is meant to be resolved when browser.tabs.executeScript({file: "script.js"})
+ // is called and the content script does message back, registering the runtime.onMessage
+ // listener here is meant to prevent intermittent failures due to a race on executing the
+ // array of promises passed to the `await Promise.all(...)` below.
+ const promiseRuntimeOnMessage = new Promise(resolve => {
+ browser.runtime.onMessage.addListener(message => {
+ browser.test.assertEq(
+ "script ran",
+ message,
+ "Expected runtime message"
+ );
+ resolve();
+ });
+ });
+
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ let frames = await browser.webNavigation.getAllFrames({ tabId: tab.id });
+ browser.test.assertEq(3, frames.length, "Expect exactly three frames");
+ browser.test.assertEq(0, frames[0].frameId, "Main frame has frameId:0");
+ browser.test.assertTrue(frames[1].frameId > 0, "Subframe has a valid 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(
+ "",
+ error.fileName,
+ "Got expected fileName"
+ );
+ browser.test.assertEq(
+ "Script '' 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(
+ "",
+ error.fileName,
+ "Got expected fileName"
+ );
+ browser.test.assertEq(
+ "Script '' 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 main frame (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 => {
+ let 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);
+ }),
+
+ promiseRuntimeOnMessage,
+ ]);
+
+ browser.test.notifyPass("executeScript");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript");
+ }
+ }
+
+ let 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();
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Make sure that we're not holding on to references to closed message
+ // managers.
+ is(
+ countMM(MessageChannel.messageManagers),
+ messageManagersSize,
+ "Message manager count"
+ );
+ is(MessageChannel.pendingResponses.size, 0, "Pending response count");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_about_blank.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_about_blank.js
new file mode 100644
index 0000000000..685f7ef907
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_about_blank.js
@@ -0,0 +1,30 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScript_at_about_blank() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ try {
+ const tab = await browser.tabs.create({ url: "about:blank" });
+ 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);
+ browser.test.notifyPass("executeScript");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript");
+ }
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("executeScript");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
new file mode 100644
index 0000000000..6b460243b0
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
@@ -0,0 +1,361 @@
+"use strict";
+
+async function testHasNoPermission(params) {
+ let 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");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: params.manifest,
+
+ background: `(${background})(${contentSetup})`,
+
+ files: {
+ "script.js": function () {
+ browser.runtime.sendMessage("first script ran");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ if (params.setup) {
+ await params.setup(extension);
+ }
+
+ extension.sendMessage("execute-script");
+
+ await extension.awaitFinish("executeScript");
+ await extension.unload();
+}
+
+add_task(async function testBadPermissions() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+
+ info("Test no special permissions");
+ await testHasNoPermission({
+ manifest: { permissions: [] },
+ });
+
+ info("Test tabs permissions");
+ await testHasNoPermission({
+ manifest: { permissions: ["tabs"] },
+ });
+
+ info("Test no special permissions, commands, key press");
+ await testHasNoPermission({
+ manifest: {
+ permissions: [],
+ commands: {
+ "test-tabs-executeScript": {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: function () {
+ browser.commands.onCommand.addListener(function (command) {
+ if (command == "test-tabs-executeScript") {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ }
+ });
+ return Promise.resolve();
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test no special permissions, _execute_browser_action command");
+ await testHasNoPermission({
+ manifest: {
+ permissions: [],
+ browser_action: {},
+ commands: {
+ _execute_browser_action: {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: function () {
+ browser.browserAction.onClicked.addListener(() => {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ });
+ return Promise.resolve();
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test no special permissions, _execute_page_action command");
+ await testHasNoPermission({
+ manifest: {
+ permissions: [],
+ page_action: {},
+ commands: {
+ _execute_page_action: {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: async function () {
+ browser.pageAction.onClicked.addListener(() => {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ });
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test active tab, commands, no key press");
+ await testHasNoPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ commands: {
+ "test-tabs-executeScript": {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ });
+
+ info("Test active tab, browser action, no click");
+ await testHasNoPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ });
+
+ info("Test active tab, page action, no click");
+ await testHasNoPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ page_action: {},
+ },
+ contentSetup: async function () {
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testMatchDataURI() {
+ // allow top level data: URI navigations, otherwise
+ // window.location.href = data: would be blocked
+ await SpecialPowers.pushPrefEnv({
+ set: [["security.data_uri.block_toplevel_data_uri_navigations", false]],
+ });
+
+ const target = ExtensionTestUtils.loadExtension({
+ files: {
+ "page.html": `
+
+
+
+ `,
+ "page.js": function () {
+ browser.test.onMessage.addListener((msg, url) => {
+ if (msg !== "navigate") {
+ return;
+ }
+ window.location.href = url;
+ });
+ },
+ },
+ background() {
+ browser.tabs.create({
+ active: true,
+ url: browser.runtime.getURL("page.html"),
+ });
+ },
+ });
+
+ const scripts = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["", "webNavigation"],
+ },
+ background() {
+ browser.webNavigation.onCompleted.addListener(({ url, frameId }) => {
+ browser.test.log(`Document loading complete: ${url}`);
+ if (frameId === 0) {
+ browser.test.sendMessage("tab-ready", url);
+ }
+ });
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "execute") {
+ return;
+ }
+ await browser.test.assertRejects(
+ browser.tabs.executeScript({
+ code: "location.href;",
+ allFrames: true,
+ }),
+ /Missing host permission/,
+ "Should not execute in `data:` frame"
+ );
+
+ browser.test.sendMessage("done");
+ });
+ },
+ });
+
+ await scripts.startup();
+ await target.startup();
+
+ // Test extension page with a data: iframe.
+ const page = await scripts.awaitMessage("tab-ready");
+ ok(page.endsWith("page.html"), "Extension page loaded into a tab");
+
+ scripts.sendMessage("execute");
+ await scripts.awaitMessage("done");
+
+ // Test extension tab navigated to a data: URI.
+ const data = "data:text/html;charset=utf-8,also-inherits";
+ target.sendMessage("navigate", data);
+
+ const url = await scripts.awaitMessage("tab-ready");
+ is(url, data, "Extension tab navigated to a data: URI");
+
+ scripts.sendMessage("execute");
+ await scripts.awaitMessage("done");
+
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await scripts.unload();
+ await target.unload();
+});
+
+add_task(async function testBadURL() {
+ async function background() {
+ let 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");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [""],
+ },
+
+ 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.
+
+add_task(forceGC);
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_file.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_file.js
new file mode 100644
index 0000000000..a8ba389602
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_file.js
@@ -0,0 +1,93 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const FILE_URL = Services.io.newFileURI(
+ new FileUtils.File(getTestFilePath("file_dummy.html"))
+).spec;
+
+add_task(async function testExecuteScript_at_file_url() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "file:///*"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async () => {
+ try {
+ const [tab] = await browser.tabs.query({ url: "file://*/*/*dummy*" });
+ const result = await browser.tabs.executeScript(tab.id, {
+ code: "location.protocol",
+ });
+ browser.test.assertEq(
+ "file:",
+ result[0],
+ "Script executed correctly in new tab"
+ );
+ browser.test.notifyPass("executeScript");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript");
+ }
+ });
+ },
+ });
+ await extension.startup();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, FILE_URL);
+
+ extension.sendMessage();
+ await extension.awaitFinish("executeScript");
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
+
+add_task(async function testExecuteScript_at_file_url_with_activeTab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ background() {
+ browser.browserAction.onClicked.addListener(async tab => {
+ try {
+ const result = await browser.tabs.executeScript(tab.id, {
+ code: "location.protocol",
+ });
+ browser.test.assertEq(
+ "file:",
+ result[0],
+ "Script executed correctly in active tab"
+ );
+ browser.test.notifyPass("executeScript");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript");
+ }
+ });
+
+ browser.test.onMessage.addListener(async () => {
+ await browser.test.assertRejects(
+ browser.tabs.executeScript({ code: "location.protocol" }),
+ /Missing host permission for the tab/,
+ "activeTab not active yet, executeScript should be rejected"
+ );
+ browser.test.sendMessage("next-step");
+ });
+ },
+ });
+ await extension.startup();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, FILE_URL);
+
+ extension.sendMessage();
+ await extension.awaitMessage("next-step");
+
+ await clickBrowserAction(extension);
+ await extension.awaitFinish("executeScript");
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js
new file mode 100644
index 0000000000..e9d008bf92
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js
@@ -0,0 +1,190 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+async function testHasPermission(params) {
+ let contentSetup = params.contentSetup || (() => Promise.resolve());
+
+ async function background(contentSetup) {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.assertEq(msg, "script ran", "script ran");
+ browser.test.notifyPass("executeScript");
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "execute-script");
+
+ browser.tabs.executeScript({
+ file: "script.js",
+ });
+ });
+
+ await contentSetup();
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: params.manifest,
+
+ background: `(${background})(${contentSetup})`,
+
+ files: {
+ "panel.html": `
+
+
+
+
+ `,
+ "script.js": function () {
+ browser.runtime.sendMessage("script ran");
+ },
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ if (params.setup) {
+ await params.setup(extension);
+ }
+
+ extension.sendMessage("execute-script");
+
+ await extension.awaitFinish("executeScript");
+
+ if (params.tearDown) {
+ await params.tearDown(extension);
+ }
+
+ await extension.unload();
+}
+
+add_task(async function testGoodPermissions() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/",
+ true
+ );
+
+ info("Test activeTab permission with a command key press");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ commands: {
+ "test-tabs-executeScript": {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: function () {
+ browser.commands.onCommand.addListener(function (command) {
+ if (command == "test-tabs-executeScript") {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ }
+ });
+ return Promise.resolve();
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test activeTab permission with _execute_browser_action command");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ commands: {
+ _execute_browser_action: {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: function () {
+ browser.browserAction.onClicked.addListener(() => {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ });
+ return Promise.resolve();
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test activeTab permission with _execute_page_action command");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab"],
+ page_action: {},
+ commands: {
+ _execute_page_action: {
+ suggested_key: {
+ default: "Alt+Shift+K",
+ },
+ },
+ },
+ },
+ contentSetup: async function () {
+ browser.pageAction.onClicked.addListener(() => {
+ browser.test.sendMessage("tabs-command-key-pressed");
+ });
+ let [tab] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tab.id);
+ },
+ setup: async function (extension) {
+ await EventUtils.synthesizeKey("k", { altKey: true, shiftKey: true });
+ await extension.awaitMessage("tabs-command-key-pressed");
+ },
+ });
+
+ info("Test activeTab permission with a context menu click");
+ await testHasPermission({
+ manifest: {
+ permissions: ["activeTab", "contextMenus"],
+ },
+ contentSetup: function () {
+ browser.contextMenus.create({ title: "activeTab", contexts: ["all"] });
+ return Promise.resolve();
+ },
+ setup: async function (extension) {
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "a[href]",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await awaitPopupShown;
+
+ let item = contextMenu.querySelector("[label=activeTab]");
+
+ contextMenu.activateItem(item);
+
+ await awaitPopupHidden;
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(forceGC);
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_multiple.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_multiple.js
new file mode 100644
index 0000000000..4e9cc907da
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_multiple.js
@@ -0,0 +1,61 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScript() {
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ const URL = BASE + "file_dummy.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL, true);
+
+ async function background() {
+ try {
+ await browser.tabs.executeScript({ code: "this.foo = 'bar'" });
+ await browser.tabs.executeScript({ file: "script.js" });
+
+ let [result1] = await browser.tabs.executeScript({
+ code: "[this.foo, this.bar]",
+ });
+ let [result2] = await browser.tabs.executeScript({ file: "script2.js" });
+
+ browser.test.assertEq(
+ "bar,baz",
+ String(result1),
+ "executeScript({code}) result"
+ );
+ browser.test.assertEq(
+ "bar,baz",
+ String(result2),
+ "executeScript({file}) result"
+ );
+
+ browser.test.notifyPass("executeScript-multiple");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("executeScript-multiple");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ },
+
+ background,
+
+ files: {
+ "script.js": function () {
+ this.bar = "baz";
+ },
+
+ "script2.js": "[this.foo, this.bar]",
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("executeScript-multiple");
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js
new file mode 100644
index 0000000000..e8e1f1255f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_no_create.js
@@ -0,0 +1,80 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScriptAtOnUpdated() {
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ 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 (ignore) {
+ return;
+ }
+ if (url && changeInfo.status === "loading" && tab.url === url) {
+ 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");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/", "tabs"],
+ },
+ background,
+ });
+
+ await extension.startup();
+ extension.sendMessage(URL);
+ await extension.awaitMessage("open-test-tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, URL, true);
+
+ await extension.awaitFinish("executeScript-at-onUpdated");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js
new file mode 100644
index 0000000000..bab0182a3f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_runAt.js
@@ -0,0 +1,134 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"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() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank",
+ true
+ );
+
+ async function background() {
+ let tab;
+
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ const URL = BASE + "file_slowed_document.sjs";
+
+ const MAX_TRIES = 10;
+
+ let 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++) {
+ let url = `${URL}?with-iframe&r=${Math.random()}`;
+
+ let loadingPromise = onUpdatedPromise(tab.id, url, "loading");
+ let completePromise = onUpdatedPromise(tab.id, url, "complete");
+
+ // TODO: Test allFrames and frameId.
+
+ await browser.tabs.update({ url });
+ await loadingPromise;
+
+ let 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",
+ // Testing default `runAt`.
+ }),
+ 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" &&
+ states[1] == "interactive" &&
+ states[2] == "interactive" &&
+ states[3] == "interactive";
+
+ 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");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/", "tabs"],
+ },
+
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("executeScript-runAt");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js b/browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js
new file mode 100644
index 0000000000..9304d3a5b4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_getCurrent.js
@@ -0,0 +1,86 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+
+ browser_action: { default_popup: "popup.html" },
+ },
+
+ files: {
+ "tab.js": function () {
+ let url = document.location.href;
+
+ browser.tabs.getCurrent(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(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 });
+ });
+ },
+
+ "popup.js": function () {
+ browser.tabs.getCurrent(tab => {
+ browser.test.assertEq(tab, undefined, "getCurrent in popup script");
+ browser.test.sendMessage("popup-finished");
+ });
+ },
+
+ "tab.html": ``,
+ "popup.html": ``,
+ },
+
+ background: function () {
+ browser.tabs.getCurrent(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");
+
+ clickBrowserAction(extension);
+ await awaitExtensionPanel(extension);
+ await extension.awaitMessage("popup-finished");
+ await closeBrowserAction(extension);
+
+ // The extension tab is automatically closed when the extension unloads.
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_goBack_goForward.js b/browser/components/extensions/test/browser/browser_ext_tabs_goBack_goForward.js
new file mode 100644
index 0000000000..2ab960699d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_goBack_goForward.js
@@ -0,0 +1,113 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_tabs_goBack_goForward() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.navigation.requireUserInteraction", false]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "tab1.html": `
+
+ tab1
+ `,
+ "tab2.html": `
+
+ tab2
+ `,
+ },
+
+ async background() {
+ let tabUpdatedCount = 0;
+ let tab = {};
+
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tabInfo) => {
+ if (changeInfo.status !== "complete" || tabId !== tab.id) {
+ 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();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_hide.js b/browser/components/extensions/test/browser/browser_ext_tabs_hide.js
new file mode 100644
index 0000000000..89c50db692
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_hide.js
@@ -0,0 +1,375 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionControlledPopup:
+ "resource:///modules/ExtensionControlledPopup.sys.mjs",
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+});
+
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+async function doorhangerTest(testFn) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "tabHide"],
+ icons: {
+ 48: "addon-icon.png",
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener(async (msg, data) => {
+ let tabs = await browser.tabs.query(data);
+ await browser.tabs[msg](tabs.map(t => t.id));
+ browser.test.sendMessage("done");
+ });
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ // Open some tabs so we can hide them.
+ let firstTab = gBrowser.selectedTab;
+ let tabs = [
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?one",
+ true,
+ true
+ ),
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/?two",
+ true,
+ true
+ ),
+ ];
+ gBrowser.selectedTab = firstTab;
+
+ await testFn(extension);
+
+ BrowserTestUtils.removeTab(tabs[0]);
+ BrowserTestUtils.removeTab(tabs[1]);
+
+ await extension.unload();
+}
+
+add_task(function test_doorhanger_keep() {
+ return doorhangerTest(async function (extension) {
+ is(gBrowser.visibleTabs.length, 3, "There are 3 visible tabs");
+
+ // Hide the first tab, expect the doorhanger.
+ let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(document);
+ let popupShown = promisePopupShown(panel);
+ extension.sendMessage("hide", { url: "*://*/?one" });
+ await extension.awaitMessage("done");
+ await popupShown;
+
+ is(gBrowser.visibleTabs.length, 2, "There are 2 visible tabs now");
+ is(
+ panel.anchorNode.closest("toolbarbutton").id,
+ "alltabs-button",
+ "The doorhanger is anchored to the all tabs button"
+ );
+
+ // Click the Keep Tabs Hidden button.
+ let popupnotification = document.getElementById(
+ "extension-tab-hide-notification"
+ );
+ let popupHidden = promisePopupHidden(panel);
+ popupnotification.button.click();
+ await popupHidden;
+
+ // Hide another tab and ensure the popup didn't open.
+ extension.sendMessage("hide", { url: "*://*/?two" });
+ await extension.awaitMessage("done");
+ is(panel.state, "closed", "The popup is still closed");
+ is(gBrowser.visibleTabs.length, 1, "There's one visible tab now");
+
+ extension.sendMessage("show", {});
+ await extension.awaitMessage("done");
+ });
+});
+
+add_task(function test_doorhanger_disable() {
+ return doorhangerTest(async function (extension) {
+ is(gBrowser.visibleTabs.length, 3, "There are 3 visible tabs");
+
+ // Hide the first tab, expect the doorhanger.
+ let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(document);
+ let popupShown = promisePopupShown(panel);
+ extension.sendMessage("hide", { url: "*://*/?one" });
+ await extension.awaitMessage("done");
+ await popupShown;
+
+ is(gBrowser.visibleTabs.length, 2, "There are 2 visible tabs now");
+ is(
+ panel.anchorNode.closest("toolbarbutton").id,
+ "alltabs-button",
+ "The doorhanger is anchored to the all tabs button"
+ );
+
+ // verify the contents of the description.
+ let popupnotification = document.getElementById(
+ "extension-tab-hide-notification"
+ );
+ let description = popupnotification.querySelector(
+ "#extension-tab-hide-notification-description"
+ );
+ let addon = await AddonManager.getAddonByID(extension.id);
+ ok(
+ description.textContent.includes(addon.name),
+ "The extension name is in the description"
+ );
+ let images = Array.from(description.querySelectorAll("image"));
+ is(images.length, 2, "There are two images");
+ ok(
+ images.some(img => img.src.includes("addon-icon.png")),
+ "There's an icon for the extension"
+ );
+ ok(
+ images.some(img =>
+ getComputedStyle(img).backgroundImage.includes("arrow-down.svg")
+ ),
+ "There's an icon for the all tabs menu"
+ );
+
+ // Click the Disable Extension button.
+ let popupHidden = promisePopupHidden(panel);
+ popupnotification.secondaryButton.click();
+ await popupHidden;
+ await new Promise(executeSoon);
+
+ is(gBrowser.visibleTabs.length, 3, "There are 3 visible tabs again");
+ is(addon.userDisabled, true, "The extension is now disabled");
+ });
+});
+
+add_task(async function test_tabs_showhide() {
+ async function background() {
+ browser.test.onMessage.addListener(async (msg, data) => {
+ switch (msg) {
+ case "hideall": {
+ let tabs = await browser.tabs.query({ hidden: false });
+ browser.test.assertEq(tabs.length, 5, "got 5 tabs");
+ let ids = tabs.map(tab => tab.id);
+ browser.test.log(`working with ids ${JSON.stringify(ids)}`);
+
+ let hidden = await browser.tabs.hide(ids);
+ browser.test.assertEq(hidden.length, 3, "hid 3 tabs");
+ tabs = await browser.tabs.query({ hidden: true });
+ ids = tabs.map(tab => tab.id);
+ browser.test.assertEq(
+ JSON.stringify(hidden.sort()),
+ JSON.stringify(ids.sort()),
+ "hidden tabIds match"
+ );
+
+ browser.test.sendMessage("hidden", { hidden });
+ break;
+ }
+ case "showall": {
+ let tabs = await browser.tabs.query({ hidden: true });
+ for (let tab of tabs) {
+ browser.test.assertTrue(tab.hidden, "tab is hidden");
+ }
+ let ids = tabs.map(tab => tab.id);
+ browser.tabs.show(ids);
+ browser.test.sendMessage("shown");
+ break;
+ }
+ }
+ });
+ }
+
+ let extdata = {
+ manifest: { permissions: ["tabs", "tabHide"] },
+ background,
+ useAddonManager: "temporary", // So the doorhanger can find the addon.
+ };
+ let extension = ExtensionTestUtils.loadExtension(extdata);
+ await extension.startup();
+
+ let sessData = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ {
+ entries: [
+ { url: "https://example.com/", triggeringPrincipal_base64 },
+ ],
+ },
+ {
+ entries: [
+ { url: "https://mochi.test:8888/", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ {
+ entries: [
+ { url: "http://test1.example.com/", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+ };
+
+ // Set up a test session with 2 windows and 5 tabs.
+ let oldState = SessionStore.getBrowserState();
+ let restored = TestUtils.topicObserved("sessionstore-browser-state-restored");
+ SessionStore.setBrowserState(JSON.stringify(sessData));
+ await restored;
+
+ if (!Services.prefs.getBoolPref("browser.tabs.tabmanager.enabled")) {
+ for (let win of BrowserWindowIterator()) {
+ let allTabsButton = win.document.getElementById("alltabs-button");
+ is(
+ getComputedStyle(allTabsButton).display,
+ "none",
+ "The all tabs button is hidden"
+ );
+ }
+ }
+
+ // Attempt to hide all the tabs, however the active tab in each window cannot
+ // be hidden, so the result will be 3 hidden tabs.
+ extension.sendMessage("hideall");
+ await extension.awaitMessage("hidden");
+
+ // We have 2 windows in this session. Otherwin is the non-current window.
+ // In each window, the first tab will be the selected tab and should not be
+ // hidden. The rest of the tabs should be hidden at this point. Hidden
+ // status was already validated inside the extension, this double checks
+ // from chrome code.
+ let otherwin;
+ for (let win of BrowserWindowIterator()) {
+ if (win != window) {
+ otherwin = win;
+ }
+ let tabs = Array.from(win.gBrowser.tabs);
+ ok(!tabs[0].hidden, "first tab not hidden");
+ for (let i = 1; i < tabs.length; i++) {
+ ok(tabs[i].hidden, "tab hidden value is correct");
+ let id = SessionStore.getCustomTabValue(tabs[i], "hiddenBy");
+ is(id, extension.id, "tab hiddenBy value is correct");
+ await TabStateFlusher.flush(tabs[i].linkedBrowser);
+ }
+
+ let allTabsButton = win.document.getElementById("alltabs-button");
+ isnot(
+ getComputedStyle(allTabsButton).display,
+ "none",
+ "The all tabs button is visible"
+ );
+ }
+
+ // Close the other window then restore it to test that the tabs are
+ // restored with proper hidden state, and the correct extension id.
+ await BrowserTestUtils.closeWindow(otherwin);
+
+ otherwin = SessionStore.undoCloseWindow(0);
+ await BrowserTestUtils.waitForEvent(otherwin, "load");
+ let tabs = Array.from(otherwin.gBrowser.tabs);
+ ok(!tabs[0].hidden, "first tab not hidden");
+ for (let i = 1; i < tabs.length; i++) {
+ ok(tabs[i].hidden, "tab hidden value is correct");
+ let id = SessionStore.getCustomTabValue(tabs[i], "hiddenBy");
+ is(id, extension.id, "tab hiddenBy value is correct");
+ }
+
+ // Test closing the last visible tab, the next tab which is hidden should become
+ // the selectedTab and will be visible.
+ ok(!otherwin.gBrowser.selectedTab.hidden, "selected tab is not hidden");
+ BrowserTestUtils.removeTab(otherwin.gBrowser.selectedTab);
+ ok(!otherwin.gBrowser.selectedTab.hidden, "tab was unhidden");
+
+ // Showall will unhide any remaining hidden tabs.
+ extension.sendMessage("showall");
+ await extension.awaitMessage("shown");
+
+ // Check from chrome code that all tabs are visible again.
+ for (let win of BrowserWindowIterator()) {
+ let tabs = Array.from(win.gBrowser.tabs);
+ for (let i = 0; i < tabs.length; i++) {
+ ok(!tabs[i].hidden, "tab hidden value is correct");
+ }
+ }
+
+ // Close second window.
+ await BrowserTestUtils.closeWindow(otherwin);
+
+ await extension.unload();
+
+ // Restore pre-test state.
+ restored = TestUtils.topicObserved("sessionstore-browser-state-restored");
+ SessionStore.setBrowserState(oldState);
+ await restored;
+});
+
+// Test our shutdown handling. Currently this means any hidden tabs will be
+// shown when a tabHide extension is shutdown. We additionally test the
+// tabs.onUpdated listener gets called with hidden state changes.
+add_task(async function test_tabs_shutdown() {
+ let tabs = [
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/",
+ true,
+ true
+ ),
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/",
+ true,
+ true
+ ),
+ ];
+
+ async function background() {
+ let tabs = await browser.tabs.query({ url: "http://example.com/" });
+ let testTab = tabs[0];
+
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if ("hidden" in changeInfo) {
+ browser.test.assertEq(tabId, testTab.id, "correct tab was hidden");
+ browser.test.assertTrue(changeInfo.hidden, "tab is hidden");
+ browser.test.assertEq(tab.url, testTab.url, "tab has correct URL");
+ browser.test.sendMessage("changeInfo");
+ }
+ });
+
+ let hidden = await browser.tabs.hide(testTab.id);
+ browser.test.assertEq(hidden[0], testTab.id, "tab was hidden");
+ tabs = await browser.tabs.query({ hidden: true });
+ browser.test.assertEq(tabs[0].id, testTab.id, "tab was hidden");
+ browser.test.sendMessage("ready");
+ }
+
+ let extdata = {
+ manifest: { permissions: ["tabs", "tabHide"] },
+ useAddonManager: "temporary", // For testing onShutdown.
+ background,
+ };
+ let extension = ExtensionTestUtils.loadExtension(extdata);
+ await extension.startup();
+
+ // test onUpdated
+ await Promise.all([
+ extension.awaitMessage("ready"),
+ extension.awaitMessage("changeInfo"),
+ ]);
+ Assert.ok(tabs[0].hidden, "Tab is hidden by extension");
+
+ await extension.unload();
+
+ Assert.ok(!tabs[0].hidden, "Tab is not hidden after unloading extension");
+ BrowserTestUtils.removeTab(tabs[0]);
+ BrowserTestUtils.removeTab(tabs[1]);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_hide_update.js b/browser/components/extensions/test/browser/browser_ext_tabs_hide_update.js
new file mode 100644
index 0000000000..7fbf185704
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_hide_update.js
@@ -0,0 +1,146 @@
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const ID = "@test-tabs-addon";
+
+async function updateExtension(ID, options) {
+ let xpi = AddonTestUtils.createTempWebExtensionFile(options);
+ await Promise.all([
+ AddonTestUtils.promiseWebExtensionStartup(ID),
+ AddonManager.installTemporaryAddon(xpi),
+ ]);
+}
+
+async function disableExtension(ID) {
+ let disabledPromise = awaitEvent("shutdown", ID);
+ let addon = await AddonManager.getAddonByID(ID);
+ await addon.disable();
+ await disabledPromise;
+}
+
+function getExtension() {
+ async function background() {
+ let tabs = await browser.tabs.query({ url: "http://example.com/" });
+ let testTab = tabs[0];
+
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if ("hidden" in changeInfo) {
+ browser.test.assertEq(tabId, testTab.id, "correct tab was hidden");
+ browser.test.assertTrue(changeInfo.hidden, "tab is hidden");
+ browser.test.sendMessage("changeInfo");
+ }
+ });
+
+ let hidden = await browser.tabs.hide(testTab.id);
+ browser.test.assertEq(hidden[0], testTab.id, "tabs.hide hide the tab");
+ tabs = await browser.tabs.query({ hidden: true });
+ browser.test.assertEq(
+ tabs[0].id,
+ testTab.id,
+ "tabs.query result was hidden"
+ );
+ browser.test.sendMessage("ready");
+ }
+
+ let extdata = {
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: ID,
+ },
+ },
+ permissions: ["tabs", "tabHide"],
+ },
+ background,
+ useAddonManager: "temporary",
+ };
+ return ExtensionTestUtils.loadExtension(extdata);
+}
+
+// Test our update handling. Currently this means any hidden tabs will be
+// shown when a tabHide extension is shutdown. We additionally test the
+// tabs.onUpdated listener gets called with hidden state changes.
+add_task(async function test_tabs_update() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+
+ const extension = getExtension();
+ await extension.startup();
+
+ // test onUpdated
+ await Promise.all([
+ extension.awaitMessage("ready"),
+ extension.awaitMessage("changeInfo"),
+ ]);
+ Assert.ok(tab.hidden, "Tab is hidden by extension");
+
+ // Test that update doesn't hide tabs when tabHide permission is present.
+ let extdata = {
+ manifest: {
+ version: "2.0",
+ browser_specific_settings: {
+ gecko: {
+ id: ID,
+ },
+ },
+ permissions: ["tabs", "tabHide"],
+ },
+ };
+ await updateExtension(ID, extdata);
+ Assert.ok(tab.hidden, "Tab is hidden hidden after update");
+
+ // Test that update does hide tabs when tabHide permission is removed.
+ extdata.manifest = {
+ version: "3.0",
+ browser_specific_settings: {
+ gecko: {
+ id: ID,
+ },
+ },
+ permissions: ["tabs"],
+ };
+ await updateExtension(ID, extdata);
+ Assert.ok(!tab.hidden, "Tab is not hidden hidden after update");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Test our update handling. Currently this means any hidden tabs will be
+// shown when a tabHide extension is shutdown. We additionally test the
+// tabs.onUpdated listener gets called with hidden state changes.
+add_task(async function test_tabs_disable() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
+
+ const extension = getExtension();
+ await extension.startup();
+
+ // test onUpdated
+ await Promise.all([
+ extension.awaitMessage("ready"),
+ extension.awaitMessage("changeInfo"),
+ ]);
+ Assert.ok(tab.hidden, "Tab is hidden by extension");
+
+ // Test that disable does hide tabs.
+ await disableExtension(ID);
+ Assert.ok(!tab.hidden, "Tab is not hidden hidden after disable");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js b/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
new file mode 100644
index 0000000000..d622b79e7a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
@@ -0,0 +1,118 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* global gBrowser */
+"use strict";
+
+add_task(async function test_highlighted() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ async function testHighlighted(activeIndex, highlightedIndices) {
+ let tabs = await browser.tabs.query({ currentWindow: true });
+ for (let { index, active, highlighted } of tabs) {
+ browser.test.assertEq(
+ index == activeIndex,
+ active,
+ "Check Tab.active: " + index
+ );
+ let expected =
+ highlightedIndices.includes(index) || index == activeIndex;
+ browser.test.assertEq(
+ expected,
+ highlighted,
+ "Check Tab.highlighted: " + index
+ );
+ }
+ let highlightedTabs = await browser.tabs.query({
+ currentWindow: true,
+ highlighted: true,
+ });
+ browser.test.assertEq(
+ highlightedIndices
+ .concat(activeIndex)
+ .sort((a, b) => a - b)
+ .join(),
+ highlightedTabs.map(tab => tab.index).join(),
+ "Check tabs.query with highlighted:true provides the expected tabs"
+ );
+ }
+
+ browser.test.log(
+ "Check that last tab is active, and no other is highlighted"
+ );
+ await testHighlighted(2, []);
+
+ browser.test.log("Highlight first and second tabs");
+ await browser.tabs.highlight({ tabs: [0, 1] });
+ await testHighlighted(0, [1]);
+
+ browser.test.log("Highlight second and first tabs");
+ await browser.tabs.highlight({ tabs: [1, 0] });
+ await testHighlighted(1, [0]);
+
+ browser.test.log("Test that highlight fails for invalid data");
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ tabs: [] }),
+ /No highlighted tab/,
+ "Attempt to highlight no tab should throw"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ windowId: 999999999, tabs: 0 }),
+ /Invalid window ID: 999999999/,
+ "Attempt to highlight tabs in invalid window should throw"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ tabs: 999999999 }),
+ /No tab at index: 999999999/,
+ "Attempt to highlight invalid tab index should throw"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ tabs: [2, 999999999] }),
+ /No tab at index: 999999999/,
+ "Attempt to highlight invalid tab index should throw"
+ );
+
+ browser.test.log(
+ "Highlighted tabs shouldn't be affected by failures above"
+ );
+ await testHighlighted(1, [0]);
+
+ browser.test.log("Highlight last tab");
+ let window = await browser.tabs.highlight({ tabs: 2 });
+ await testHighlighted(2, []);
+
+ browser.test.assertEq(
+ 3,
+ window.tabs.length,
+ "Returned window should be populated"
+ );
+
+ window = await browser.tabs.highlight({ tabs: 2, populate: false });
+ browser.test.assertFalse(
+ "tabs" in window,
+ "Returned window shouldn't be populated"
+ );
+
+ browser.test.notifyPass("test-finished");
+ },
+ });
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_incognito_not_allowed.js b/browser/components/extensions/test/browser/browser_ext_tabs_incognito_not_allowed.js
new file mode 100644
index 0000000000..e998f64afc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_incognito_not_allowed.js
@@ -0,0 +1,155 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScriptIncognitoNotAllowed() {
+ const url =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/file_iframe_document.html";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ // captureTab requires all_urls permission
+ permissions: ["", "tabs", "tabHide"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async pbw => {
+ // expect one tab from the non-pb window
+ let tabs = await browser.tabs.query({ windowId: pbw.windowId });
+ browser.test.assertEq(
+ 0,
+ tabs.length,
+ "unable to query tabs in private window"
+ );
+ tabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(
+ 1,
+ tabs.length,
+ "unable to query active tab in private window"
+ );
+ browser.test.assertTrue(
+ tabs[0].windowId != pbw.windowId,
+ "unable to query active tab in private window"
+ );
+
+ // apis that take a tabId
+ let tabIdAPIs = [
+ "captureTab",
+ "detectLanguage",
+ "duplicate",
+ "get",
+ "hide",
+ "reload",
+ "getZoomSettings",
+ "getZoom",
+ "toggleReaderMode",
+ ];
+ for (let name of tabIdAPIs) {
+ await browser.test.assertRejects(
+ browser.tabs[name](pbw.tabId),
+ /Invalid tab ID/,
+ `should not be able to ${name}`
+ );
+ }
+ await browser.test.assertRejects(
+ browser.tabs.captureVisibleTab(pbw.windowId),
+ /Invalid window ID/,
+ "should not be able to duplicate"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.create({
+ windowId: pbw.windowId,
+ url: "http://mochi.test/",
+ }),
+ /Invalid window ID/,
+ "unable to create tab in private window"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.executeScript(pbw.tabId, { code: "document.URL" }),
+ /Invalid tab ID/,
+ "should not be able to executeScript"
+ );
+ let currentTab = await browser.tabs.getCurrent();
+ browser.test.assertTrue(
+ !currentTab,
+ "unable to get current tab in private window"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.highlight({ windowId: pbw.windowId, tabs: [pbw.tabId] }),
+ /Invalid window ID/,
+ "should not be able to highlight"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.insertCSS(pbw.tabId, {
+ code: "* { background: rgb(42, 42, 42) }",
+ }),
+ /Invalid tab ID/,
+ "should not be able to insertCSS"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.move(pbw.tabId, {
+ index: 0,
+ windowId: tabs[0].windowId,
+ }),
+ /Invalid tab ID/,
+ "unable to move tab to private window"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.move(tabs[0].id, { index: 0, windowId: pbw.windowId }),
+ /Invalid window ID/,
+ "unable to move tab to private window"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.printPreview(),
+ /Cannot access activeTab/,
+ "unable to printpreview tab"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.removeCSS(pbw.tabId, {}),
+ /Invalid tab ID/,
+ "unable to remove tab css"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.sendMessage(pbw.tabId, "test"),
+ /Could not establish connection/,
+ "unable to sendmessage"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.setZoomSettings(pbw.tabId, {}),
+ /Invalid tab ID/,
+ "should not be able to set zoom settings"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.setZoom(pbw.tabId, 3),
+ /Invalid tab ID/,
+ "should not be able to set zoom"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.update(pbw.tabId, {}),
+ /Invalid tab ID/,
+ "should not be able to update tab"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([pbw.tabId], tabs[0].id),
+ /Invalid tab ID/,
+ "should not be able to moveInSuccession"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([tabs[0].id], pbw.tabId),
+ /Invalid tab ID/,
+ "should not be able to moveInSuccession"
+ );
+
+ browser.test.notifyPass("pass");
+ });
+ },
+ });
+
+ let winData = await getIncognitoWindow(url);
+ await extension.startup();
+
+ extension.sendMessage(winData.details);
+
+ await extension.awaitFinish("pass");
+ await BrowserTestUtils.closeWindow(winData.win);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js b/browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js
new file mode 100644
index 0000000000..1a4bbd0c74
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_insertCSS.js
@@ -0,0 +1,312 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+add_task(async function testExecuteScript() {
+ let { MessageChannel } = ChromeUtils.importESModule(
+ "resource://testing-common/MessageChannel.sys.mjs"
+ );
+
+ // When the first extension is started, ProxyMessenger.init adds MessageChannel
+ // listeners for Services.mm and Services.ppmm, and they are never unsubscribed.
+ // We have to exclude them after the extension has been unloaded to get an accurate
+ // test.
+ function getMessageManagersSize(messageManagers) {
+ return Array.from(messageManagers).filter(([mm]) => {
+ return ![Services.mm, Services.ppmm].includes(mm);
+ }).length;
+ }
+
+ let messageManagersSize = getMessageManagersSize(
+ MessageChannel.messageManagers
+ );
+ let responseManagersSize = MessageChannel.responseManagers.size;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/",
+ true
+ );
+
+ async function background() {
+ let tasks = [
+ {
+ background: "rgba(0, 0, 0, 0)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs.insertCSS({
+ file: "file2.css",
+ });
+ },
+ },
+ {
+ background: "rgb(42, 42, 42)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs.insertCSS({
+ code: "* { background: rgb(42, 42, 42) }",
+ });
+ },
+ },
+ {
+ background: "rgb(43, 43, 43)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs
+ .insertCSS({
+ code: "* { background: rgb(100, 100, 100) !important }",
+ cssOrigin: "author",
+ })
+ .then(r =>
+ browser.tabs.insertCSS({
+ code: "* { background: rgb(43, 43, 43) !important }",
+ cssOrigin: "author",
+ })
+ );
+ },
+ },
+ {
+ background: "rgb(100, 100, 100)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ // User has higher importance
+ return browser.tabs
+ .insertCSS({
+ code: "* { background: rgb(100, 100, 100) !important }",
+ cssOrigin: "user",
+ })
+ .then(r =>
+ browser.tabs.insertCSS({
+ code: "* { background: rgb(44, 44, 44) !important }",
+ cssOrigin: "author",
+ })
+ );
+ },
+ },
+ ];
+
+ function checkCSS() {
+ let computedStyle = window.getComputedStyle(document.body);
+ return [computedStyle.backgroundColor, computedStyle.color];
+ }
+
+ try {
+ for (let { promise, background, foreground } of tasks) {
+ 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");
+ }
+ }
+
+ let 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();
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Make sure that we're not holding on to references to closed message
+ // managers.
+ is(
+ getMessageManagersSize(MessageChannel.messageManagers),
+ messageManagersSize,
+ "Message manager count"
+ );
+ is(
+ MessageChannel.responseManagers.size,
+ responseManagersSize,
+ "Response manager count"
+ );
+ is(MessageChannel.pendingResponses.size, 0, "Pending response count");
+});
+
+add_task(async function testInsertCSS_cleanup() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/",
+ true
+ );
+
+ async function background() {
+ await browser.tabs.insertCSS({ code: "* { background: rgb(42, 42, 42) }" });
+ await browser.tabs.insertCSS({ file: "customize_fg_color.css" });
+
+ browser.test.notifyPass("insertCSS");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ },
+ background,
+ files: {
+ "customize_fg_color.css": `* { color: rgb(255, 0, 0) }`,
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("insertCSS");
+
+ const getTabContentComputedStyle = async () => {
+ let computedStyle = content.getComputedStyle(content.document.body);
+ return [computedStyle.backgroundColor, computedStyle.color];
+ };
+
+ const appliedStyles = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ getTabContentComputedStyle
+ );
+
+ is(
+ appliedStyles[0],
+ "rgb(42, 42, 42)",
+ "The injected CSS code has been applied as expected"
+ );
+ is(
+ appliedStyles[1],
+ "rgb(255, 0, 0)",
+ "The injected CSS file has been applied as expected"
+ );
+
+ await extension.unload();
+
+ const unloadedStyles = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ getTabContentComputedStyle
+ );
+
+ is(
+ unloadedStyles[0],
+ "rgba(0, 0, 0, 0)",
+ "The injected CSS code has been removed as expected"
+ );
+ is(
+ unloadedStyles[1],
+ "rgb(0, 0, 0)",
+ "The injected CSS file has been removed as expected"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Verify that no removeSheet/removeSheetUsingURIString errors are logged while
+// cleaning up css injected using a manifest content script or tabs.insertCSS.
+add_task(async function test_csscode_cleanup_on_closed_windows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://example.com/*"],
+ content_scripts: [
+ {
+ matches: ["http://example.com/*"],
+ css: ["content.css"],
+ run_at: "document_start",
+ },
+ ],
+ },
+
+ files: {
+ "content.css": "body { min-width: 15px; }",
+ },
+
+ async background() {
+ browser.runtime.onConnect.addListener(port => {
+ port.onDisconnect.addListener(() => {
+ browser.test.sendMessage("port-disconnected");
+ });
+ browser.test.sendMessage("port-connected");
+ });
+
+ await browser.tabs.create({
+ url: "http://example.com/",
+ active: true,
+ });
+
+ await browser.tabs.insertCSS({
+ code: "body { max-width: 50px; }",
+ });
+
+ // Create a port, as a way to detect when the content script has been
+ // destroyed and any removeSheet error already collected (if it has been
+ // raised during the content scripts cleanup).
+ await browser.tabs.executeScript({
+ code: `(${function () {
+ const { maxWidth, minWidth } = window.getComputedStyle(document.body);
+ browser.test.sendMessage("body-styles", { maxWidth, minWidth });
+ browser.runtime.connect();
+ }})();`,
+ });
+ },
+ });
+
+ await extension.startup();
+
+ let { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ info("Waiting for content scripts to be injected");
+
+ const { maxWidth, minWidth } = await extension.awaitMessage("body-styles");
+ is(maxWidth, "50px", "tabs.insertCSS applied");
+ is(minWidth, "15px", "manifest.content_scripts CSS applied");
+
+ await extension.awaitMessage("port-connected");
+ const tab = gBrowser.selectedTab;
+
+ info("Close tab and wait for content script port to be disconnected");
+ BrowserTestUtils.removeTab(tab);
+ await extension.awaitMessage("port-disconnected");
+ });
+
+ // Look for nsIDOMWindowUtils.removeSheet and
+ // nsIDOMWindowUtils.removeSheetUsingURIString errors.
+ AddonTestUtils.checkMessages(
+ messages,
+ {
+ forbidden: [{ errorMessage: /nsIDOMWindowUtils.removeSheet/ }],
+ },
+ "Expect no remoteSheet errors"
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_lastAccessed.js b/browser/components/extensions/test/browser/browser_ext_tabs_lastAccessed.js
new file mode 100644
index 0000000000..c4738d7f2e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_lastAccessed.js
@@ -0,0 +1,52 @@
+"use strict";
+
+add_task(async function testLastAccessed() {
+ let past = Date.now();
+
+ for (let url of ["https://example.com/?1", "https://example.com/?2"]) {
+ let tab = BrowserTestUtils.addTab(gBrowser, url, { skipAnimation: true });
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ browser.test.onMessage.addListener(async function (msg, past) {
+ let [tab1] = await browser.tabs.query({
+ url: "https://example.com/?1",
+ });
+ let [tab2] = await browser.tabs.query({
+ url: "https://example.com/?2",
+ });
+
+ browser.test.assertTrue(tab1 && tab2, "Expected tabs were found");
+
+ let now = Date.now();
+
+ browser.test.assertTrue(
+ past <= tab1.lastAccessed,
+ "lastAccessed of tab 1 is later than the test start time."
+ );
+ browser.test.assertTrue(
+ tab1.lastAccessed < tab2.lastAccessed,
+ "lastAccessed of tab 2 is later than lastAccessed of tab 1."
+ );
+ browser.test.assertTrue(
+ tab2.lastAccessed <= now,
+ "lastAccessed of tab 2 is earlier than now."
+ );
+
+ 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();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_lazy.js b/browser/components/extensions/test/browser/browser_ext_tabs_lazy.js
new file mode 100644
index 0000000000..68205089d5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_lazy.js
@@ -0,0 +1,49 @@
+"use strict";
+
+const triggeringPrincipal_base64 = E10SUtils.SERIALIZED_SYSTEMPRINCIPAL;
+
+const SESSION = {
+ windows: [
+ {
+ tabs: [
+ { entries: [{ url: "about:blank", triggeringPrincipal_base64 }] },
+ {
+ entries: [
+ { url: "https://example.com/", triggeringPrincipal_base64 },
+ ],
+ },
+ ],
+ },
+ ],
+};
+
+add_task(async function () {
+ SessionStore.setBrowserState(JSON.stringify(SESSION));
+ await promiseWindowRestored(window);
+ const tab = gBrowser.tabs[1];
+
+ is(tab.getAttribute("pending"), "true", "The tab is pending restore");
+ is(tab.linkedBrowser.isConnected, false, "The tab is lazy");
+
+ async function background() {
+ const [tab] = await browser.tabs.query({ url: "https://example.com/" });
+ browser.test.assertRejects(
+ browser.tabs.sendMessage(tab.id, "void"),
+ /Could not establish connection. Receiving end does not exist/,
+ "No recievers in a tab pending restore."
+ );
+ browser.test.notifyPass("lazy");
+ }
+
+ const manifest = { permissions: ["tabs"] };
+ const extension = ExtensionTestUtils.loadExtension({ manifest, background });
+
+ await extension.startup();
+ await extension.awaitFinish("lazy");
+ await extension.unload();
+
+ is(tab.getAttribute("pending"), "true", "The tab is still pending restore");
+ is(tab.linkedBrowser.isConnected, false, "The tab is still lazy");
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_array.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_array.js
new file mode 100644
index 0000000000..539c60c232
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_array.js
@@ -0,0 +1,95 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function moveMultiple() {
+ let tabs = [];
+ for (let k of [1, 2, 3, 4]) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ `http://example.com/?${k}`
+ );
+ tabs.push(tab);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["tabs"] },
+ background: async function () {
+ function num(url) {
+ return parseInt(url.slice(-1), 10);
+ }
+
+ async function check(expected) {
+ let tabs = await browser.tabs.query({ url: "http://example.com/*" });
+ let endings = tabs.map(tab => num(tab.url));
+ browser.test.assertTrue(
+ expected.every((v, i) => v === endings[i]),
+ `Tab order should be ${expected}, got ${endings}.`
+ );
+ }
+
+ async function reset() {
+ let tabs = await browser.tabs.query({ url: "http://example.com/*" });
+ await browser.tabs.move(
+ tabs.sort((a, b) => num(a.url) - num(b.url)).map(tab => tab.id),
+ { index: 0 }
+ );
+ }
+
+ async function move(moveIndexes, moveTo) {
+ let tabs = await browser.tabs.query({ url: "http://example.com/*" });
+ await browser.tabs.move(
+ moveIndexes.map(e => tabs[e - 1].id),
+ {
+ index: moveTo,
+ }
+ );
+ }
+
+ let tests = [
+ { move: [2], index: 0, result: [2, 1, 3, 4] },
+ { move: [2], index: -1, result: [1, 3, 4, 2] },
+ // Start -> After first tab -> After second tab
+ { move: [4, 3], index: 0, result: [4, 3, 1, 2] },
+ // [1, 2, 3, 4] -> [1, 4, 2, 3] -> [1, 4, 3, 2]
+ { move: [4, 3], index: 1, result: [1, 4, 3, 2] },
+ // [1, 2, 3, 4] -> [2, 3, 1, 4] -> [3, 1, 2, 4]
+ { move: [1, 2], index: 2, result: [3, 1, 2, 4] },
+ // [1, 2, 3, 4] -> [1, 2, 4, 3] -> [2, 4, 1, 3]
+ { move: [4, 1], index: 2, result: [2, 4, 1, 3] },
+ // [1, 2, 3, 4] -> [2, 3, 1, 4] -> [2, 3, 1, 4]
+ { move: [1, 4], index: 2, result: [2, 3, 1, 4] },
+ ];
+
+ for (let test of tests) {
+ await reset();
+ await move(test.move, test.index);
+ await check(test.result);
+ }
+
+ let firstId = (
+ await browser.tabs.query({
+ url: "http://example.com/*",
+ })
+ )[0].id;
+ // Assuming that tab.id of 12345 does not exist.
+ await browser.test.assertRejects(
+ browser.tabs.move([firstId, 12345], { index: -1 }),
+ /Invalid tab/,
+ "Should receive invalid tab error"
+ );
+ // The first argument got moved, the second on failed.
+ await check([2, 3, 1, 4]);
+
+ browser.test.notifyPass("tabs.move");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move");
+ await extension.unload();
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_array_multiple_windows.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_array_multiple_windows.js
new file mode 100644
index 0000000000..484197cbc5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_array_multiple_windows.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function moveMultipleWindows() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["tabs"] },
+ background: async function () {
+ let numToId = new Map();
+ let idToNum = new Map();
+ let windowToInitialTabs = new Map();
+
+ async function createWindow(nums) {
+ let window = await browser.windows.create({
+ url: nums.map(k => `https://example.com/?${k}`),
+ });
+ let tabIds = window.tabs.map(tab => tab.id);
+ windowToInitialTabs.set(window.id, tabIds);
+ for (let i = 0; i < nums.length; ++i) {
+ numToId.set(nums[i], tabIds[i]);
+ idToNum.set(tabIds[i], nums[i]);
+ }
+ return window.id;
+ }
+
+ let win1 = await createWindow([0, 1, 2, 3, 4]);
+ let win2 = await createWindow([5, 6, 7, 8, 9]);
+
+ async function getNums(windowId) {
+ let tabs = await browser.tabs.query({ windowId });
+ return tabs.map(tab => idToNum.get(tab.id));
+ }
+
+ async function check(msg, expected) {
+ let nums1 = getNums(win1);
+ let nums2 = getNums(win2);
+ browser.test.assertEq(
+ JSON.stringify(expected),
+ JSON.stringify({ win1: await nums1, win2: await nums2 }),
+ `Check ${msg}`
+ );
+ }
+
+ async function reset() {
+ for (let [windowId, tabIds] of windowToInitialTabs) {
+ await browser.tabs.move(tabIds, { index: 0, windowId });
+ }
+ }
+
+ async function move(nums, params) {
+ await browser.tabs.move(
+ nums.map(k => numToId.get(k)),
+ params
+ );
+ }
+
+ let tests = [
+ {
+ move: [1, 6],
+ params: { index: 0 },
+ result: { win1: [1, 0, 2, 3, 4], win2: [6, 5, 7, 8, 9] },
+ },
+ {
+ move: [6, 1],
+ params: { index: 0 },
+ result: { win1: [1, 0, 2, 3, 4], win2: [6, 5, 7, 8, 9] },
+ },
+ {
+ move: [1, 6],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [0, 2, 3, 4], win2: [1, 6, 5, 7, 8, 9] },
+ },
+ {
+ move: [6, 1],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [0, 2, 3, 4], win2: [6, 1, 5, 7, 8, 9] },
+ },
+ {
+ move: [1, 6],
+ params: { index: -1 },
+ result: { win1: [0, 2, 3, 4, 1], win2: [5, 7, 8, 9, 6] },
+ },
+ {
+ move: [6, 1],
+ params: { index: -1 },
+ result: { win1: [0, 2, 3, 4, 1], win2: [5, 7, 8, 9, 6] },
+ },
+ {
+ move: [1, 6],
+ params: { index: -1, windowId: win2 },
+ result: { win1: [0, 2, 3, 4], win2: [5, 7, 8, 9, 1, 6] },
+ },
+ {
+ move: [6, 1],
+ params: { index: -1, windowId: win2 },
+ result: { win1: [0, 2, 3, 4], win2: [5, 7, 8, 9, 6, 1] },
+ },
+ {
+ move: [2, 1, 7, 6],
+ params: { index: 3 },
+ result: { win1: [0, 3, 2, 1, 4], win2: [5, 8, 7, 6, 9] },
+ },
+ {
+ move: [1, 2, 3, 4],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [0], win2: [1, 2, 3, 4, 5, 6, 7, 8, 9] },
+ },
+ {
+ move: [0, 1, 2, 3],
+ params: { index: 5, windowId: win2 },
+ result: { win1: [4], win2: [5, 6, 7, 8, 9, 0, 1, 2, 3] },
+ },
+ {
+ move: [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [0], win2: [1, 2, 3, 4, 5, 6, 7, 8, 9] },
+ },
+ {
+ move: [5, 6, 7, 8, 9, 0, 1, 2, 3],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [4], win2: [5, 6, 7, 8, 9, 0, 1, 2, 3] },
+ },
+ {
+ move: [5, 1, 6, 2, 7, 3, 8, 4, 9],
+ params: { index: 0, windowId: win2 },
+ result: { win1: [0], win2: [5, 1, 6, 2, 7, 3, 8, 4, 9] },
+ },
+ {
+ move: [5, 1, 6, 2, 7, 3, 8, 4, 9],
+ params: { index: 1, windowId: win2 },
+ result: { win1: [0], win2: [5, 1, 6, 2, 7, 3, 8, 4, 9] },
+ },
+ {
+ move: [5, 1, 6, 2, 7, 3, 8, 4, 9],
+ params: { index: 999, windowId: win2 },
+ result: { win1: [0], win2: [5, 1, 6, 2, 7, 3, 8, 4, 9] },
+ },
+ ];
+
+ const initial = { win1: [0, 1, 2, 3, 4], win2: [5, 6, 7, 8, 9] };
+ await check("initial", initial);
+ for (let test of tests) {
+ browser.test.log(JSON.stringify(test));
+ await move(test.move, test.params);
+ await check("move", test.result);
+ await reset();
+ await check("reset", initial);
+ }
+
+ await browser.windows.remove(win1);
+ await browser.windows.remove(win2);
+ browser.test.notifyPass("tabs.move");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_discarded.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_discarded.js
new file mode 100644
index 0000000000..74099f0f24
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_discarded.js
@@ -0,0 +1,94 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function move_discarded_to_window() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["tabs"] },
+ background: async function () {
+ // Create a discarded tab
+ let url = "http://example.com/";
+ let tab = await browser.tabs.create({ url, discarded: true });
+ browser.test.assertEq(true, tab.discarded, "Tab should be discarded");
+ browser.test.assertEq(url, tab.url, "Tab URL should be correct");
+
+ // Create a new window
+ let { id: windowId } = await browser.windows.create();
+
+ // Move the tab into that window
+ [tab] = await browser.tabs.move(tab.id, { windowId, index: -1 });
+ browser.test.assertTrue(tab.discarded, "Tab should still be discarded");
+ browser.test.assertEq(url, tab.url, "Tab URL should still be correct");
+
+ await browser.windows.remove(windowId);
+ browser.test.notifyPass("tabs.move");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move");
+ await extension.unload();
+});
+
+add_task(async function move_hidden_discarded_to_window() {
+ let extensionWithoutTabsPermission = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://example.com/"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (changeInfo.hidden) {
+ browser.test.assertEq(
+ tab.url,
+ "http://example.com/?hideme",
+ "tab.url is correctly observed without tabs permission"
+ );
+ browser.test.sendMessage("onUpdated_checked");
+ }
+ });
+ // Listener with "urls" filter, regression test for
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1695346
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo, tab) => {
+ browser.test.assertTrue(changeInfo.hidden, "tab was hidden");
+ browser.test.sendMessage("onUpdated_urls_filter");
+ },
+ {
+ properties: ["hidden"],
+ urls: ["http://example.com/?hideme"],
+ }
+ );
+ },
+ });
+ await extensionWithoutTabsPermission.startup();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: { permissions: ["tabs", "tabHide"] },
+ // ExtensionControlledPopup's populateDescription method requires an addon:
+ useAddonManager: "temporary",
+ async background() {
+ let url = "http://example.com/?hideme";
+ let tab = await browser.tabs.create({ url, discarded: true });
+ await browser.tabs.hide(tab.id);
+
+ let { id: windowId } = await browser.windows.create();
+
+ // Move the tab into that window
+ [tab] = await browser.tabs.move(tab.id, { windowId, index: -1 });
+ browser.test.assertTrue(tab.discarded, "Tab should still be discarded");
+ browser.test.assertTrue(tab.hidden, "Tab should still be hidden");
+ browser.test.assertEq(url, tab.url, "Tab URL should still be correct");
+
+ await browser.windows.remove(windowId);
+ browser.test.notifyPass("move_hidden_discarded_to_window");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("move_hidden_discarded_to_window");
+ await extension.unload();
+
+ await extensionWithoutTabsPermission.awaitMessage("onUpdated_checked");
+ await extensionWithoutTabsPermission.awaitMessage("onUpdated_urls_filter");
+ await extensionWithoutTabsPermission.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js
new file mode 100644
index 0000000000..bb0b174876
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window.js
@@ -0,0 +1,178 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ incognitoOverride: "spanning",
+ async background() {
+ const URL = "http://example.com/";
+ let mainWindow = await browser.windows.getCurrent();
+ let newWindow = await browser.windows.create({
+ url: [URL, URL],
+ });
+ let privateWindow = await browser.windows.create({
+ incognito: true,
+ url: [URL, URL],
+ });
+
+ browser.tabs.onUpdated.addListener(() => {
+ // Bug 1398272: Adding onUpdated listener broke tab IDs across windows.
+ });
+
+ let tab = newWindow.tabs[0].id;
+ let privateTab = privateWindow.tabs[0].id;
+
+ // Assuming that this windowId does not exist.
+ await browser.test.assertRejects(
+ browser.tabs.move(tab, { windowId: 123144576, index: 0 }),
+ /Invalid window/,
+ "Should receive invalid window error"
+ );
+
+ // Test that a tab cannot be moved to a private window.
+ let moved = await browser.tabs.move(tab, {
+ windowId: privateWindow.id,
+ index: 0,
+ });
+ browser.test.assertEq(
+ moved.length,
+ 0,
+ "tab was not moved to private window"
+ );
+ // Test that a private tab cannot be moved to a non-private window.
+ moved = await browser.tabs.move(privateTab, {
+ windowId: newWindow.id,
+ index: 0,
+ });
+ browser.test.assertEq(
+ moved.length,
+ 0,
+ "tab was not moved from private window"
+ );
+
+ // Verify tabs did not move between windows via another query.
+ let windows = await browser.windows.getAll({ populate: true });
+ let newWin2 = windows.find(w => w.id === newWindow.id);
+ browser.test.assertTrue(newWin2, "Found window");
+ browser.test.assertEq(
+ newWin2.tabs.length,
+ 2,
+ "Window still has two tabs"
+ );
+ for (let origTab of newWindow.tabs) {
+ browser.test.assertTrue(
+ newWin2.tabs.find(t => t.id === origTab.id),
+ `Window still has tab ${origTab.id}`
+ );
+ }
+
+ let privateWin2 = windows.find(w => w.id === privateWindow.id);
+ browser.test.assertTrue(privateWin2 !== null, "Found private window");
+ browser.test.assertEq(
+ privateWin2.incognito,
+ true,
+ "Private window is still private"
+ );
+ browser.test.assertEq(
+ privateWin2.tabs.length,
+ 2,
+ "Private window still has two tabs"
+ );
+ for (let origTab of privateWindow.tabs) {
+ browser.test.assertTrue(
+ privateWin2.tabs.find(t => t.id === origTab.id),
+ `Private window still has tab ${origTab.id}`
+ );
+ }
+
+ // Move a tab from one non-private window to another
+ await browser.tabs.move(tab, { windowId: mainWindow.id, index: 0 });
+
+ mainWindow = await browser.windows.get(mainWindow.id, { populate: true });
+ browser.test.assertTrue(
+ mainWindow.tabs.find(t => t.id === tab),
+ "Moved tab is in main window"
+ );
+
+ newWindow = await browser.windows.get(newWindow.id, { populate: true });
+ browser.test.assertEq(
+ newWindow.tabs.length,
+ 1,
+ "New window has 1 tab left"
+ );
+ browser.test.assertTrue(
+ newWindow.tabs[0].id != tab,
+ "Moved tab is no longer in original window"
+ );
+
+ await browser.windows.remove(newWindow.id);
+ await browser.windows.remove(privateWindow.id);
+ await browser.tabs.remove(tab);
+
+ browser.test.notifyPass("tabs.move.window");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move.window");
+ await extension.unload();
+});
+
+add_task(async function test_currentWindowAfterTabMoved() {
+ const files = {
+ "current.html": "",
+ "current.js": function () {
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "current") {
+ browser.windows.getCurrent(win => {
+ browser.test.sendMessage("id", win.id);
+ });
+ }
+ });
+ browser.test.sendMessage("ready");
+ },
+ };
+
+ async function background() {
+ let tabId;
+
+ const url = browser.runtime.getURL("current.html");
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "move") {
+ await browser.windows.create({ tabId });
+ browser.test.sendMessage("moved");
+ } else if (msg === "close") {
+ await browser.tabs.remove(tabId);
+ browser.test.sendMessage("done");
+ }
+ });
+
+ let tab = await browser.tabs.create({ url });
+ tabId = tab.id;
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({ files, background });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ extension.sendMessage("current");
+ const first = await extension.awaitMessage("id");
+
+ extension.sendMessage("move");
+ await extension.awaitMessage("moved");
+
+ extension.sendMessage("current");
+ const second = await extension.awaitMessage("id");
+
+ isnot(first, second, "current window id is different after moving the tab");
+
+ extension.sendMessage("close");
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js
new file mode 100644
index 0000000000..62fe12aeb4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_multiple.js
@@ -0,0 +1,64 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ const URL = "http://example.com/";
+ let mainWin = await browser.windows.getCurrent();
+ let tab1 = await browser.tabs.create({ url: URL });
+ let tab2 = await browser.tabs.create({ url: URL });
+
+ let newWin = await browser.windows.create({ url: [URL, URL] });
+ browser.test.assertEq(newWin.tabs.length, 2, "New window has 2 tabs");
+ let [tab3, tab4] = newWin.tabs;
+
+ // move tabs in both windows to index 0 in a single call
+ await browser.tabs.move([tab2.id, tab4.id], { index: 0 });
+
+ tab1 = await browser.tabs.get(tab1.id);
+ browser.test.assertEq(
+ tab1.windowId,
+ mainWin.id,
+ "tab 1 is still in main window"
+ );
+
+ tab2 = await browser.tabs.get(tab2.id);
+ browser.test.assertEq(
+ tab2.windowId,
+ mainWin.id,
+ "tab 2 is still in main window"
+ );
+ browser.test.assertEq(tab2.index, 0, "tab 2 moved to index 0");
+
+ tab3 = await browser.tabs.get(tab3.id);
+ browser.test.assertEq(
+ tab3.windowId,
+ newWin.id,
+ "tab 3 is still in new window"
+ );
+
+ tab4 = await browser.tabs.get(tab4.id);
+ browser.test.assertEq(
+ tab4.windowId,
+ newWin.id,
+ "tab 4 is still in new window"
+ );
+ browser.test.assertEq(tab4.index, 0, "tab 4 moved to index 0");
+
+ await browser.tabs.remove([tab1.id, tab2.id]);
+ await browser.windows.remove(newWin.id);
+
+ browser.test.notifyPass("tabs.move.multiple");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move.multiple");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js
new file mode 100644
index 0000000000..b2953dea48
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_move_window_pinned.js
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ const URL = "http://example.com/";
+
+ let mainWin = await browser.windows.getCurrent();
+ let tab = await browser.tabs.create({ url: URL });
+
+ let newWin = await browser.windows.create({ url: URL });
+ let tab2 = newWin.tabs[0];
+ await browser.tabs.update(tab2.id, { pinned: true });
+
+ // Try to move a tab before the pinned tab. The move should be ignored.
+ let moved = await browser.tabs.move(tab.id, {
+ windowId: newWin.id,
+ index: 0,
+ });
+ browser.test.assertEq(moved.length, 0, "move() returned no moved tab");
+
+ tab = await browser.tabs.get(tab.id);
+ browser.test.assertEq(
+ tab.windowId,
+ mainWin.id,
+ "Tab stayed in its original window"
+ );
+
+ await browser.tabs.remove(tab.id);
+ await browser.windows.remove(newWin.id);
+ browser.test.notifyPass("tabs.move.pin");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.move.pin");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_newtab_private.js b/browser/components/extensions/test/browser/browser_ext_tabs_newtab_private.js
new file mode 100644
index 0000000000..19146fbe42
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_newtab_private.js
@@ -0,0 +1,96 @@
+"use strict";
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+const NEWTAB_PRIVATE_ALLOWED = "browser.newtab.privateAllowed";
+const NEWTAB_EXTENSION_CONTROLLED = "browser.newtab.extensionControlled";
+const NEWTAB_URI = "webext-newtab-1.html";
+
+function promisePrefChange(pref) {
+ return new Promise((resolve, reject) => {
+ Services.prefs.addObserver(pref, function observer() {
+ Services.prefs.removeObserver(pref, observer);
+ resolve(arguments);
+ });
+ });
+}
+
+function verifyPrefSettings(controlled, allowed) {
+ is(
+ Services.prefs.getBoolPref(NEWTAB_EXTENSION_CONTROLLED, false),
+ controlled,
+ "newtab extension controlled"
+ );
+ is(
+ Services.prefs.getBoolPref(NEWTAB_PRIVATE_ALLOWED, false),
+ allowed,
+ "newtab private permission after permission change"
+ );
+
+ if (controlled) {
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI),
+ "Newtab url is overridden by the extension."
+ );
+ }
+ if (controlled && allowed) {
+ ok(
+ BROWSER_NEW_TAB_URL.endsWith(NEWTAB_URI),
+ "active newtab url is overridden by the extension."
+ );
+ } else {
+ let expectednewTab = controlled ? "about:privatebrowsing" : "about:newtab";
+ is(BROWSER_NEW_TAB_URL, expectednewTab, "active newtab url is default.");
+ }
+}
+
+async function promiseUpdatePrivatePermission(allowed, extension) {
+ info(`update private allowed permission`);
+ let ext = WebExtensionPolicy.getByID(extension.id).extension;
+ await Promise.all([
+ promisePrefChange(NEWTAB_PRIVATE_ALLOWED),
+ ExtensionPermissions[allowed ? "add" : "remove"](
+ extension.id,
+ { permissions: ["internal:privateBrowsingAllowed"], origins: [] },
+ ext
+ ),
+ ]);
+
+ verifyPrefSettings(true, allowed);
+}
+
+add_task(async function test_new_tab_private() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "@private-newtab",
+ },
+ },
+ chrome_url_overrides: {
+ newtab: NEWTAB_URI,
+ },
+ },
+ files: {
+ NEWTAB_URI: `
+
+
+
+
+
+
+
+ `,
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+
+ verifyPrefSettings(true, false);
+
+ await promiseUpdatePrivatePermission(true, extension);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js b/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js
new file mode 100644
index 0000000000..b48047abde
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onCreated.js
@@ -0,0 +1,35 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function test_onCreated_active() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ browser.tabs.onCreated.addListener(tab => {
+ browser.tabs.remove(tab.id);
+ browser.test.sendMessage("onCreated", tab);
+ });
+ browser.tabs.onUpdated.addListener((tabId, changes, tab) => {
+ browser.test.assertEq(
+ '["status"]',
+ JSON.stringify(Object.keys(changes)),
+ "Should get no update other than 'status' during tab creation."
+ );
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ BrowserOpenTab();
+
+ let tab = await extension.awaitMessage("onCreated");
+ is(true, tab.active, "Tab should be active");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js b/browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js
new file mode 100644
index 0000000000..a614dc6144
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onHighlighted.js
@@ -0,0 +1,130 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function test_onHighlighted() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ async function expectHighlighted(fn, action) {
+ let resolve;
+ let promise = new Promise(r => {
+ resolve = r;
+ });
+ let expected;
+ let events = [];
+ let listener = highlightInfo => {
+ events.push(highlightInfo);
+ if (expected && expected.length >= events.length) {
+ resolve();
+ }
+ };
+ browser.tabs.onHighlighted.addListener(listener);
+ expected = (await fn()) || [];
+ if (events.length < expected.length) {
+ await promise;
+ }
+ let unexpected = events.splice(expected.length);
+ browser.test.assertEq(
+ JSON.stringify(expected),
+ JSON.stringify(events),
+ `Should get ${expected.length} expected onHighlighted events when ${action}`
+ );
+ if (unexpected.length) {
+ browser.test.fail(
+ `${unexpected.length} unexpected onHighlighted events when ${action}: ` +
+ JSON.stringify(unexpected)
+ );
+ }
+ browser.tabs.onHighlighted.removeListener(listener);
+ }
+
+ let [{ id, windowId }] = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ let windows = [windowId];
+ let tabs = [id];
+
+ await expectHighlighted(async () => {
+ let tab = await browser.tabs.create({
+ active: true,
+ url: "about:blank?1",
+ });
+ tabs.push(tab.id);
+ return [{ tabIds: [tabs[1]], windowId: windows[0] }];
+ }, "creating a new active tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.update(tabs[0], { active: true });
+ return [{ tabIds: [tabs[0]], windowId: windows[0] }];
+ }, "selecting former tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [0, 1] });
+ return [{ tabIds: [tabs[0], tabs[1]], windowId: windows[0] }];
+ }, "highlighting both tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [1, 0] });
+ return [{ tabIds: [tabs[0], tabs[1]], windowId: windows[0] }];
+ }, "highlighting same tabs but changing selected one");
+
+ await expectHighlighted(async () => {
+ let tab = await browser.tabs.create({
+ active: false,
+ url: "about:blank?2",
+ });
+ tabs.push(tab.id);
+ }, "create a new inactive tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [2, 0, 1] });
+ return [{ tabIds: [tabs[0], tabs[1], tabs[2]], windowId: windows[0] }];
+ }, "highlighting all tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.move(tabs[1], { index: 0 });
+ }, "reordering tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [0] });
+ return [{ tabIds: [tabs[1]], windowId: windows[0] }];
+ }, "highlighting moved tab");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [0] });
+ }, "highlighting again");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [2, 1, 0] });
+ return [{ tabIds: [tabs[1], tabs[0], tabs[2]], windowId: windows[0] }];
+ }, "highlighting all tabs");
+
+ await expectHighlighted(async () => {
+ await browser.tabs.highlight({ tabs: [2, 0, 1] });
+ }, "highlighting same tabs with different order");
+
+ await expectHighlighted(async () => {
+ let window = await browser.windows.create({ tabId: tabs[2] });
+ windows.push(window.id);
+ // Bug 1481185: on Chrome it's [tabs[1], tabs[0]] instead of [tabs[0]]
+ return [
+ { tabIds: [tabs[0]], windowId: windows[0] },
+ { tabIds: [tabs[2]], windowId: windows[1] },
+ ];
+ }, "moving selected tab into a new window");
+
+ await browser.tabs.remove(tabs.slice(1));
+ browser.test.notifyPass("test-finished");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js
new file mode 100644
index 0000000000..a59fa21f8a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated.js
@@ -0,0 +1,339 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+add_task(async function () {
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await focusWindow(win1);
+
+ let 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 () {
+ let pageURL =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html";
+
+ let expectedSequence = [
+ { status: "loading" },
+ { status: "loading", url: pageURL },
+ { status: "complete" },
+ ];
+ let collectedSequence = [];
+
+ 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(function () {
+ 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"
+ );
+ }
+ }
+ }
+
+ browser.test.notifyPass("tabs.onUpdated");
+ });
+
+ browser.tabs.create({ url: pageURL });
+ },
+ 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();
+
+ await BrowserTestUtils.closeWindow(win1);
+});
+
+async function do_test_update(background, withPermissions = true) {
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await focusWindow(win1);
+
+ let manifest = {};
+ if (withPermissions) {
+ manifest.permissions = ["tabs", "http://mochi.test/"];
+ }
+ let extension = ExtensionTestUtils.loadExtension({ manifest, background });
+
+ await extension.startup();
+ await extension.awaitFinish("finish");
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(win1);
+}
+
+add_task(async function test_pinned() {
+ await do_test_update(function background() {
+ // Create a new tab for testing update.
+ browser.tabs.create({}, function (tab) {
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ // Check callback
+ browser.test.assertEq(tabId, tab.id, "Check tab id");
+ browser.test.log("onUpdate: " + JSON.stringify(changeInfo));
+ if ("pinned" in changeInfo) {
+ browser.test.assertTrue(changeInfo.pinned, "Check changeInfo.pinned");
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ // Remove created tab.
+ browser.tabs.remove(tabId);
+ browser.test.notifyPass("finish");
+ }
+ });
+ browser.tabs.update(tab.id, { pinned: true });
+ });
+ });
+});
+
+add_task(async function test_unpinned() {
+ await do_test_update(function background() {
+ // Create a new tab for testing update.
+ browser.tabs.create({ pinned: true }, function (tab) {
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ // Check callback
+ browser.test.assertEq(tabId, tab.id, "Check tab id");
+ browser.test.log("onUpdate: " + JSON.stringify(changeInfo));
+ if ("pinned" in changeInfo) {
+ browser.test.assertFalse(
+ changeInfo.pinned,
+ "Check changeInfo.pinned"
+ );
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ // Remove created tab.
+ browser.tabs.remove(tabId);
+ browser.test.notifyPass("finish");
+ }
+ });
+ browser.tabs.update(tab.id, { pinned: false });
+ });
+ });
+});
+
+add_task(async function test_url() {
+ await do_test_update(function background() {
+ // Create a new tab for testing update.
+ browser.tabs.create({ url: "about:blank?initial_url=1" }, function (tab) {
+ const expectedUpdatedURL = "about:blank?updated_url=1";
+
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ // Wait for the tabs.onUpdated events related to the updated url (because
+ // there is a good chance that we may still be receiving events related to
+ // the browser.tabs.create API call above before we are able to start
+ // loading the new url from the browser.tabs.update API call below).
+ if ("url" in changeInfo && changeInfo.url === expectedUpdatedURL) {
+ browser.test.assertEq(
+ expectedUpdatedURL,
+ changeInfo.url,
+ "Got tabs.onUpdated event for the expected url"
+ );
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ // Remove created tab.
+ browser.tabs.remove(tabId);
+ browser.test.notifyPass("finish");
+ }
+ // Check callback
+ browser.test.assertEq(tabId, tab.id, "Check tab id");
+ browser.test.log("onUpdate: " + JSON.stringify(changeInfo));
+ });
+
+ browser.tabs.update(tab.id, { url: expectedUpdatedURL });
+ });
+ });
+});
+
+add_task(async function test_title() {
+ await do_test_update(async function background() {
+ const url =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html";
+ const tab = await browser.tabs.create({ url });
+
+ browser.tabs.onUpdated.addListener(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);
+ 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/browser/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html";
+ let tab = null;
+ let count = 0;
+
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ // An attention change can happen during tabs.create, so
+ // we can't compare against tab yet.
+ if (!("attention" in 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 === 1) {
+ browser.tabs.reload(tabId);
+ } else {
+ browser.test.log("Reload complete");
+ browser.tabs.onUpdated.removeListener(onUpdated);
+ browser.tabs.remove(tabId);
+ browser.test.notifyPass("finish");
+ }
+ }
+ });
+
+ tab = await browser.tabs.create({ url });
+ }, false /* withPermissions */);
+});
+
+add_task(async function test_onUpdated_after_onRemoved() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ const url =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context_tabs_onUpdated_page.html";
+ let removed = false;
+ let tab;
+
+ // If remove happens fast and we never receive onUpdated, that is ok, but
+ // we never want to receive onUpdated after onRemoved.
+ browser.tabs.onUpdated.addListener(function onUpdated(tabId, changeInfo) {
+ if (!tab || tab.id !== tabId) {
+ return;
+ }
+ browser.test.assertFalse(
+ removed,
+ "tab has not been removed before onUpdated"
+ );
+ });
+
+ browser.tabs.onRemoved.addListener((tabId, removedInfo) => {
+ if (!tab || tab.id !== tabId) {
+ return;
+ }
+ removed = true;
+ browser.test.notifyPass("onRemoved");
+ });
+
+ tab = await browser.tabs.create({ url });
+ browser.tabs.remove(tab.id);
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("onRemoved");
+ await extension.unload();
+});
+
+// Regression test for Bug 1852391.
+add_task(async function test_pin_discarded_tab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ const url = "http://mochi.test:8888";
+ const newTab = await browser.tabs.create({
+ url,
+ active: false,
+ discarded: true,
+ });
+ browser.tabs.onUpdated.addListener(
+ async (tabId, changeInfo) => {
+ browser.test.assertEq(
+ tabId,
+ newTab.id,
+ "Expect onUpdated to be fired for the expected tab"
+ );
+ browser.test.assertEq(
+ changeInfo.pinned,
+ true,
+ "Expect pinned to be set to true"
+ );
+ await browser.tabs.remove(newTab.id);
+ browser.test.notifyPass("onPinned");
+ },
+ { properties: ["pinned"] }
+ );
+ await browser.tabs.update(newTab.id, { pinned: true }).catch(err => {
+ browser.test.fail(`Got unexpected rejection from tabs.update: ${err}`);
+ browser.test.notifyFail("onPinned");
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("onPinned");
+ await extension.unload();
+});
+
+add_task(forceGC);
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated_filter.js b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated_filter.js
new file mode 100644
index 0000000000..83d305e491
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_onUpdated_filter.js
@@ -0,0 +1,354 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_filter_url() {
+ let ext_fail = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.fail(
+ `received unexpected onUpdated event ${JSON.stringify(changeInfo)}`
+ );
+ },
+ { urls: ["*://*.mozilla.org/*"] }
+ );
+ },
+ });
+ await ext_fail.startup();
+
+ let ext_perm = ExtensionTestUtils.loadExtension({
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.fail(
+ `received unexpected onUpdated event without tabs permission`
+ );
+ },
+ { urls: ["*://mochi.test/*"] }
+ );
+ },
+ });
+ await ext_perm.startup();
+
+ let ext_ok = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.log(`got onUpdated ${JSON.stringify(changeInfo)}`);
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { urls: ["*://mochi.test/*"] }
+ );
+ },
+ });
+ await ext_ok.startup();
+ let ok1 = ext_ok.awaitFinish("onUpdated");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ await ok1;
+
+ await ext_ok.unload();
+ await ext_fail.unload();
+ await ext_perm.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_url_activeTab() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["activeTab"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.fail(
+ "should only have notification for activeTab, selectedTab is not activeTab"
+ );
+ },
+ { urls: ["*://mochi.test/*"] }
+ );
+ },
+ });
+ await ext.startup();
+
+ let ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { urls: ["*://mochi.test/*"] }
+ );
+ },
+ });
+ await ext2.startup();
+ let ok = ext2.awaitFinish("onUpdated");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/#foreground"
+ );
+ await Promise.all([ok]);
+
+ await ext.unload();
+ await ext2.unload();
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_tabId() {
+ let ext_fail = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.fail(
+ `received unexpected onUpdated event ${JSON.stringify(changeInfo)}`
+ );
+ },
+ { tabId: 12345 }
+ );
+ },
+ });
+ await ext_fail.startup();
+
+ let ext_ok = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ });
+ },
+ });
+ await ext_ok.startup();
+ let ok = ext_ok.awaitFinish("onUpdated");
+
+ let ext_ok2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onCreated.addListener(tab => {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { tabId: tab.id }
+ );
+ browser.test.log(`Tab specific tab listener on tab ${tab.id}`);
+ });
+ },
+ });
+ await ext_ok2.startup();
+ let ok2 = ext_ok2.awaitFinish("onUpdated");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ await Promise.all([ok, ok2]);
+
+ await ext_ok.unload();
+ await ext_ok2.unload();
+ await ext_fail.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_windowId() {
+ let ext_fail = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.fail(
+ `received unexpected onUpdated event ${JSON.stringify(changeInfo)}`
+ );
+ },
+ { windowId: 12345 }
+ );
+ },
+ });
+ await ext_fail.startup();
+
+ let ext_ok = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { windowId: browser.windows.WINDOW_ID_CURRENT }
+ );
+ },
+ });
+ await ext_ok.startup();
+ let ok = ext_ok.awaitFinish("onUpdated");
+
+ let ext_ok2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ let window = await browser.windows.getCurrent();
+ browser.test.log(`Window specific tab listener on window ${window.id}`);
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { windowId: window.id }
+ );
+ browser.test.sendMessage("ready");
+ },
+ });
+ await ext_ok2.startup();
+ await ext_ok2.awaitMessage("ready");
+ let ok2 = ext_ok2.awaitFinish("onUpdated");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ await Promise.all([ok, ok2]);
+
+ await ext_ok.unload();
+ await ext_ok2.unload();
+ await ext_fail.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_isArticle() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background() {
+ // We expect only status updates, anything else is a failure.
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ browser.test.log(`got onUpdated ${JSON.stringify(changeInfo)}`);
+ if ("isArticle" in changeInfo) {
+ browser.test.notifyPass("isArticle");
+ }
+ },
+ { properties: ["isArticle"] }
+ );
+ },
+ });
+ await extension.startup();
+ let ok = extension.awaitFinish("isArticle");
+
+ const baseUrl = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://mochi.test:8888/"
+ );
+ const url = `${baseUrl}/readerModeArticle.html`;
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await ok;
+
+ await extension.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_filter_property() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ // We expect only status updates, anything else is a failure.
+ let properties = new Set([
+ "audible",
+ "discarded",
+ "favIconUrl",
+ "hidden",
+ "isArticle",
+ "mutedInfo",
+ "pinned",
+ "sharingState",
+ "title",
+ "url",
+ ]);
+
+ // Test that updated only happens after created.
+ let created = false;
+ let tabIds = (await browser.tabs.query({})).map(t => t.id);
+ browser.tabs.onCreated.addListener(tab => {
+ created = tab.id;
+ });
+
+ browser.tabs.onUpdated.addListener(
+ (tabId, changeInfo) => {
+ // ignore tabs created prior to extension startup
+ if (tabIds.includes(tabId)) {
+ return;
+ }
+ browser.test.assertEq(created, tabId, "tab created before updated");
+
+ browser.test.log(`got onUpdated ${JSON.stringify(changeInfo)}`);
+ browser.test.assertTrue(!!changeInfo.status, "changeInfo has status");
+ if (Object.keys(changeInfo).some(p => properties.has(p))) {
+ browser.test.fail(
+ `received unexpected onUpdated event ${JSON.stringify(
+ changeInfo
+ )}`
+ );
+ }
+ if (changeInfo.status === "complete") {
+ browser.test.notifyPass("onUpdated");
+ }
+ },
+ { properties: ["status"] }
+ );
+ browser.test.sendMessage("ready");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ let ok = extension.awaitFinish("onUpdated");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/"
+ );
+ await ok;
+
+ await extension.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_opener.js b/browser/components/extensions/test/browser/browser_ext_tabs_opener.js
new file mode 100644
index 0000000000..f5ea6c7a27
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_opener.js
@@ -0,0 +1,130 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank?1"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank?2"
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background() {
+ let activeTab;
+ let tabId;
+ let tabIds;
+ browser.tabs
+ .query({ lastFocusedWindow: true })
+ .then(tabs => {
+ browser.test.assertEq(3, tabs.length, "We have three tabs");
+
+ browser.test.assertTrue(tabs[1].active, "Tab 1 is active");
+ activeTab = tabs[1];
+
+ tabIds = tabs.map(tab => tab.id);
+
+ return browser.tabs.create({
+ openerTabId: activeTab.id,
+ active: false,
+ });
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ activeTab.id,
+ tab.openerTabId,
+ "Tab opener ID is correct"
+ );
+ browser.test.assertEq(
+ activeTab.index + 1,
+ tab.index,
+ "Tab was inserted after the related current tab"
+ );
+
+ tabId = tab.id;
+ return browser.tabs.get(tabId);
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ activeTab.id,
+ tab.openerTabId,
+ "Tab opener ID is still correct"
+ );
+
+ return browser.tabs.update(tabId, { openerTabId: tabIds[0] });
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ tabIds[0],
+ tab.openerTabId,
+ "Updated tab opener ID is correct"
+ );
+
+ return browser.tabs.get(tabId);
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ tabIds[0],
+ tab.openerTabId,
+ "Updated tab opener ID is still correct"
+ );
+
+ return browser.tabs.create({ openerTabId: tabId, active: false });
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ tabId,
+ tab.openerTabId,
+ "New tab opener ID is correct"
+ );
+ browser.test.assertEq(
+ tabIds.length,
+ tab.index,
+ "New tab was not inserted after the unrelated current tab"
+ );
+
+ let promise = browser.tabs.remove(tabId);
+
+ tabId = tab.id;
+ return promise;
+ })
+ .then(() => {
+ return browser.tabs.get(tabId);
+ })
+ .then(tab => {
+ browser.test.assertEq(
+ undefined,
+ tab.openerTabId,
+ "Tab opener ID was cleared after opener tab closed"
+ );
+
+ return browser.tabs.remove(tabId);
+ })
+ .then(() => {
+ browser.test.notifyPass("tab-opener");
+ })
+ .catch(e => {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("tab-opener");
+ });
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("tab-opener");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_printPreview.js b/browser/components/extensions/test/browser/browser_ext_tabs_printPreview.js
new file mode 100644
index 0000000000..5db423878e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_printPreview.js
@@ -0,0 +1,44 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testPrintPreview() {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.net/");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ await browser.tabs.printPreview();
+ browser.test.assertTrue(true, "print preview entered");
+ browser.test.notifyPass("tabs.printPreview");
+ },
+ });
+
+ is(
+ document.querySelector(".printPreviewBrowser"),
+ null,
+ "There shouldn't be any print preview browser"
+ );
+
+ await extension.startup();
+
+ // Ensure we're showing the preview...
+ await BrowserTestUtils.waitForCondition(() => {
+ let preview = document.querySelector(".printPreviewBrowser");
+ return preview && BrowserTestUtils.is_visible(preview);
+ });
+
+ gBrowser.getTabDialogBox(gBrowser.selectedBrowser).abortAllDialogs();
+ // Wait for the preview to go away
+ await BrowserTestUtils.waitForCondition(
+ () => !document.querySelector(".printPreviewBrowser")
+ );
+
+ await extension.awaitFinish("tabs.printPreview");
+
+ await extension.unload();
+ BrowserTestUtils.removeTab(gBrowser.tabs[1]);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_query.js b/browser/components/extensions/test/browser/browser_ext_tabs_query.js
new file mode 100644
index 0000000000..099588c701
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_query.js
@@ -0,0 +1,468 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+requestLongerTimeout(2);
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:config"
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ let tabs = await browser.tabs.query({ lastFocusedWindow: true });
+ browser.test.assertEq(tabs.length, 3, "should have three tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(tabs[0].url, "about:blank", "first tab blank");
+ tabs.shift();
+
+ browser.test.assertTrue(tabs[0].active, "tab 0 active");
+ browser.test.assertFalse(tabs[1].active, "tab 1 inactive");
+
+ browser.test.assertFalse(tabs[0].pinned, "tab 0 unpinned");
+ browser.test.assertFalse(tabs[1].pinned, "tab 1 unpinned");
+
+ browser.test.assertEq(tabs[0].url, "about:robots", "tab 0 url correct");
+ browser.test.assertEq(tabs[1].url, "about:config", "tab 1 url correct");
+
+ browser.test.assertEq(tabs[0].status, "complete", "tab 0 status correct");
+ browser.test.assertEq(tabs[1].status, "complete", "tab 1 status correct");
+
+ browser.test.assertEq(
+ tabs[0].title,
+ "Gort! Klaatu barada nikto!",
+ "tab 0 title correct"
+ );
+
+ tabs = await browser.tabs.query({ url: "about:blank" });
+ browser.test.assertEq(tabs.length, 1, "about:blank query finds one tab");
+ browser.test.assertEq(tabs[0].url, "about:blank", "with the correct url");
+
+ browser.test.notifyPass("tabs.query");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+
+ tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+ let tab3 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://test1.example.org/MochiKit/"
+ );
+
+ // test simple queries
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.tabs.query(
+ {
+ url: "",
+ },
+ function (tabs) {
+ browser.test.assertEq(tabs.length, 3, "should have three tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(
+ tabs[0].url,
+ "http://example.com/",
+ "tab 0 url correct"
+ );
+ browser.test.assertEq(
+ tabs[1].url,
+ "http://example.net/",
+ "tab 1 url correct"
+ );
+ browser.test.assertEq(
+ tabs[2].url,
+ "http://test1.example.org/MochiKit/",
+ "tab 2 url correct"
+ );
+
+ browser.test.notifyPass("tabs.query");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ // match pattern
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.tabs.query(
+ {
+ url: "http://*/MochiKit*",
+ },
+ function (tabs) {
+ browser.test.assertEq(tabs.length, 1, "should have one tab");
+
+ browser.test.assertEq(
+ tabs[0].url,
+ "http://test1.example.org/MochiKit/",
+ "tab 0 url correct"
+ );
+
+ browser.test.notifyPass("tabs.query");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ // match array of patterns
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.tabs.query(
+ {
+ url: ["http://*/MochiKit*", "http://*.com/*"],
+ },
+ function (tabs) {
+ browser.test.assertEq(tabs.length, 2, "should have two tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(
+ tabs[0].url,
+ "http://example.com/",
+ "tab 0 url correct"
+ );
+ browser.test.assertEq(
+ tabs[1].url,
+ "http://test1.example.org/MochiKit/",
+ "tab 1 url correct"
+ );
+
+ browser.test.notifyPass("tabs.query");
+ }
+ );
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ // match title pattern
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ let tabs = await browser.tabs.query({
+ title: "mochitest index /",
+ });
+
+ browser.test.assertEq(tabs.length, 2, "should have two tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(
+ tabs[0].title,
+ "mochitest index /",
+ "tab 0 title correct"
+ );
+ browser.test.assertEq(
+ tabs[1].title,
+ "mochitest index /",
+ "tab 1 title correct"
+ );
+
+ tabs = await browser.tabs.query({
+ title: "?ochitest index /*",
+ });
+
+ browser.test.assertEq(tabs.length, 3, "should have three tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(
+ tabs[0].title,
+ "mochitest index /",
+ "tab 0 title correct"
+ );
+ browser.test.assertEq(
+ tabs[1].title,
+ "mochitest index /",
+ "tab 1 title correct"
+ );
+ browser.test.assertEq(
+ tabs[2].title,
+ "mochitest index /MochiKit/",
+ "tab 2 title correct"
+ );
+
+ browser.test.notifyPass("tabs.query");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ // match highlighted
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs1 = await browser.tabs.query({ highlighted: false });
+ browser.test.assertEq(
+ 3,
+ tabs1.length,
+ "should have three non-highlighted tabs"
+ );
+
+ let tabs2 = await browser.tabs.query({ highlighted: true });
+ browser.test.assertEq(1, tabs2.length, "should have one highlighted tab");
+
+ for (let tab of [...tabs1, ...tabs2]) {
+ browser.test.assertEq(
+ tab.active,
+ tab.highlighted,
+ "highlighted and active are equal in tab " + tab.index
+ );
+ }
+
+ browser.test.notifyPass("tabs.query");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+
+ // test width and height
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.test.onMessage.addListener(async msg => {
+ let tabs = await browser.tabs.query({ active: true });
+
+ browser.test.assertEq(tabs.length, 1, "should have one tab");
+ browser.test.sendMessage("dims", {
+ width: tabs[0].width,
+ height: tabs[0].height,
+ });
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(RESOLUTION_PREF);
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+ for (let resolution of [2, 1]) {
+ Services.prefs.setCharPref(RESOLUTION_PREF, String(resolution));
+ is(
+ window.devicePixelRatio,
+ resolution,
+ "window has the required resolution"
+ );
+
+ let { clientHeight, clientWidth } = gBrowser.selectedBrowser;
+
+ extension.sendMessage("check-size");
+ let dims = await extension.awaitMessage("dims");
+ is(dims.width, clientWidth, "tab reports expected width");
+ is(dims.height, clientHeight, "tab reports expected height");
+ }
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+ BrowserTestUtils.removeTab(tab3);
+ Services.prefs.clearUserPref(RESOLUTION_PREF);
+});
+
+add_task(async function testQueryPermissions() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [],
+ },
+
+ async background() {
+ try {
+ let tabs = await browser.tabs.query({
+ currentWindow: true,
+ active: true,
+ });
+ browser.test.assertEq(tabs.length, 1, "Expect query to return tabs");
+ browser.test.notifyPass("queryPermissions");
+ } catch (e) {
+ browser.test.notifyFail("queryPermissions");
+ }
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("queryPermissions");
+
+ await extension.unload();
+});
+
+add_task(async function testInvalidUrl() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.tabs.query({ url: "http://test1.net" }),
+ "Invalid url pattern: http://test1.net",
+ "Expected url to match pattern"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.query({ url: ["test2"] }),
+ "Invalid url pattern: test2",
+ "Expected an array with an invalid match pattern"
+ );
+ await browser.test.assertRejects(
+ browser.tabs.query({ url: ["http://www.bbc.com/", "test3"] }),
+ "Invalid url pattern: test3",
+ "Expected an array with an invalid match pattern"
+ );
+ browser.test.notifyPass("testInvalidUrl");
+ },
+ });
+ await extension.startup();
+ await extension.awaitFinish("testInvalidUrl");
+ await extension.unload();
+});
+
+add_task(async function test_query_index() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ 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();
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/"
+ );
+ await extension.awaitFinish("tabs.query");
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_query_window() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let badWindowId = 0;
+ for (let { id } of await browser.windows.getAll()) {
+ badWindowId = Math.max(badWindowId, id + 1);
+ }
+
+ let tabs = await browser.tabs.query({ windowId: badWindowId });
+ browser.test.assertEq(
+ tabs.length,
+ 0,
+ "No tabs because there is no such window ID"
+ );
+
+ let { id: currentWindowId } = await browser.windows.getCurrent();
+ tabs = await browser.tabs.query({ currentWindow: true });
+ browser.test.assertEq(
+ tabs[0].windowId,
+ currentWindowId,
+ "Got tabs from the current window"
+ );
+
+ let { id: lastFocusedWindowId } = await browser.windows.getLastFocused();
+ tabs = await browser.tabs.query({ lastFocusedWindow: true });
+ browser.test.assertEq(
+ tabs[0].windowId,
+ lastFocusedWindowId,
+ "Got tabs from the last focused window"
+ );
+
+ browser.test.notifyPass("tabs.query");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.query");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_readerMode.js b/browser/components/extensions/test/browser/browser_ext_tabs_readerMode.js
new file mode 100644
index 0000000000..1b86094611
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_readerMode.js
@@ -0,0 +1,138 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_reader_mode() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ let tab;
+ let tabId;
+ let expected = { isInReaderMode: false };
+ let testState = {};
+ browser.test.onMessage.addListener(async (msg, ...args) => {
+ switch (msg) {
+ case "updateUrl":
+ expected.isArticle = args[0];
+ expected.url = args[1];
+ tab = await browser.tabs.update({ url: expected.url });
+ tabId = tab.id;
+ break;
+ case "enterReaderMode":
+ expected.isArticle = !args[0];
+ expected.isInReaderMode = true;
+ tab = await browser.tabs.get(tabId);
+ browser.test.assertEq(
+ false,
+ tab.isInReaderMode,
+ "The tab is not in reader mode."
+ );
+ if (args[0]) {
+ browser.tabs.toggleReaderMode(tabId);
+ } else {
+ await browser.test.assertRejects(
+ browser.tabs.toggleReaderMode(tabId),
+ /The specified tab cannot be placed into reader mode/,
+ "Toggle fails with an unreaderable document."
+ );
+ browser.test.assertEq(
+ false,
+ tab.isInReaderMode,
+ "The tab is still not in reader mode."
+ );
+ browser.test.sendMessage("enterFailed");
+ }
+ break;
+ case "leaveReaderMode":
+ expected.isInReaderMode = false;
+ tab = await browser.tabs.get(tabId);
+ browser.test.assertTrue(
+ tab.isInReaderMode,
+ "The tab is in reader mode."
+ );
+ browser.tabs.toggleReaderMode(tabId);
+ break;
+ }
+ });
+
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
+ if (changeInfo.status === "complete") {
+ testState.url = tab.url;
+ let urlOk = expected.isInReaderMode
+ ? testState.url.startsWith("about:reader")
+ : expected.url == testState.url;
+ if (urlOk && expected.isArticle == testState.isArticle) {
+ browser.test.sendMessage("tabUpdated", tab);
+ }
+ return;
+ }
+ if (
+ changeInfo.isArticle == expected.isArticle &&
+ changeInfo.isArticle != testState.isArticle
+ ) {
+ testState.isArticle = changeInfo.isArticle;
+ let urlOk = expected.isInReaderMode
+ ? testState.url.startsWith("about:reader")
+ : expected.url == testState.url;
+ if (urlOk && expected.isArticle == testState.isArticle) {
+ browser.test.sendMessage("isArticle", tab);
+ }
+ }
+ });
+ },
+ });
+
+ const TEST_PATH = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+ );
+ const READER_MODE_PREFIX = "about:reader";
+
+ await extension.startup();
+ extension.sendMessage(
+ "updateUrl",
+ true,
+ `${TEST_PATH}readerModeArticle.html`
+ );
+ let tab = await extension.awaitMessage("isArticle");
+
+ ok(
+ !tab.url.startsWith(READER_MODE_PREFIX),
+ "Tab url does not indicate reader mode."
+ );
+ ok(tab.isArticle, "Tab is readerable.");
+
+ extension.sendMessage("enterReaderMode", true);
+ tab = await extension.awaitMessage("tabUpdated");
+ ok(tab.url.startsWith(READER_MODE_PREFIX), "Tab url indicates reader mode.");
+ ok(tab.isInReaderMode, "tab.isInReaderMode indicates reader mode.");
+
+ extension.sendMessage("leaveReaderMode");
+ tab = await extension.awaitMessage("tabUpdated");
+ ok(
+ !tab.url.startsWith(READER_MODE_PREFIX),
+ "Tab url does not indicate reader mode."
+ );
+ ok(!tab.isInReaderMode, "tab.isInReaderMode does not indicate reader mode.");
+
+ extension.sendMessage(
+ "updateUrl",
+ false,
+ `${TEST_PATH}readerModeNonArticle.html`
+ );
+ tab = await extension.awaitMessage("tabUpdated");
+ ok(
+ !tab.url.startsWith(READER_MODE_PREFIX),
+ "Tab url does not indicate reader mode."
+ );
+ ok(!tab.isArticle, "Tab is not readerable.");
+ ok(!tab.isInReaderMode, "tab.isInReaderMode does not indicate reader mode.");
+
+ extension.sendMessage("enterReaderMode", false);
+ await extension.awaitMessage("enterFailed");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_reload.js b/browser/components/extensions/test/browser/browser_ext_tabs_reload.js
new file mode 100644
index 0000000000..aed4a3822c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_reload.js
@@ -0,0 +1,53 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "tab.js": function () {
+ browser.runtime.sendMessage("tab-loaded");
+ },
+ "tab.html": `
+
+
+ `,
+ },
+
+ async background() {
+ let tabLoadedCount = 0;
+
+ let tab = await browser.tabs.create({ url: "tab.html", active: true });
+
+ 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");
+ }
+ }
+ });
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.reload");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js b/browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js
new file mode 100644
index 0000000000..ed3d8c7a14
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_reload_bypass_cache.js
@@ -0,0 +1,89 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", ""],
+ },
+
+ async background() {
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ 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 {
+ let 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",
+ });
+
+ let [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();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_remove.js b/browser/components/extensions/test/browser/browser_ext_tabs_remove.js
new file mode 100644
index 0000000000..8e51494ed1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_remove.js
@@ -0,0 +1,258 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function undoCloseAfterExtRemovesOneTab() {
+ let initialTab = gBrowser.selectedTab;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabs = await browser.tabs.query({});
+
+ browser.test.assertEq(3, tabs.length, "Should have 3 tabs");
+
+ let tabIdsToRemove = (
+ await browser.tabs.query({
+ url: "https://example.com/closeme/*",
+ })
+ ).map(tab => tab.id);
+
+ await browser.tabs.remove(tabIdsToRemove);
+ browser.test.sendMessage("removedtabs");
+ },
+ });
+
+ await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/1"),
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/closeme/2"
+ ),
+ ]);
+
+ await extension.startup();
+ await extension.awaitMessage("removedtabs");
+
+ is(
+ gBrowser.tabs.length,
+ 2,
+ "Once extension has closed a tab, there should be 2 tabs open"
+ );
+
+ // The tabs.remove API makes no promises about SessionStore's updates
+ // having been completed by the time it returns. So we need to wait separately
+ // for the closed tab count to be updated the correct value. This is OK because
+ // we can observe above that the tabs length has changed to reflect that
+ // some were closed.
+ await TestUtils.waitForCondition(
+ () => SessionStore.getLastClosedTabCount(window) == 1,
+ "SessionStore should know that one tab was closed"
+ );
+
+ undoCloseTab();
+
+ is(
+ gBrowser.tabs.length,
+ 3,
+ "All tabs should be restored for a total of 3 tabs"
+ );
+
+ await BrowserTestUtils.waitForEvent(gBrowser.tabs[2], "SSTabRestored");
+
+ is(
+ gBrowser.tabs[2].linkedBrowser.currentURI.spec,
+ "https://example.com/closeme/2",
+ "Restored tab at index 2 should have expected URL"
+ );
+
+ await extension.unload();
+ gBrowser.removeAllTabsBut(initialTab);
+});
+
+add_task(async function undoCloseAfterExtRemovesMultipleTabs() {
+ let initialTab = gBrowser.selectedTab;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let tabIds = (await browser.tabs.query({})).map(tab => tab.id);
+
+ browser.test.assertEq(
+ 8,
+ tabIds.length,
+ "Should have 8 total tabs (4 in each window: the initial blank tab and the 3 opened by this test)"
+ );
+
+ let tabIdsToRemove = (
+ await browser.tabs.query({
+ url: "https://example.com/closeme/*",
+ })
+ ).map(tab => tab.id);
+
+ await browser.tabs.remove(tabIdsToRemove);
+
+ browser.test.sendMessage("removedtabs");
+ },
+ });
+
+ await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/1"),
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/closeme/2"
+ ),
+ BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com/closeme/3"
+ ),
+ ]);
+
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await Promise.all([
+ BrowserTestUtils.openNewForegroundTab(
+ window2.gBrowser,
+ "https://example.com/4"
+ ),
+ BrowserTestUtils.openNewForegroundTab(
+ window2.gBrowser,
+ "https://example.com/closeme/5"
+ ),
+ BrowserTestUtils.openNewForegroundTab(
+ window2.gBrowser,
+ "https://example.com/closeme/6"
+ ),
+ ]);
+
+ await extension.startup();
+ await extension.awaitMessage("removedtabs");
+
+ is(
+ gBrowser.tabs.length,
+ 2,
+ "Original window should have 2 tabs still open, after closing tabs"
+ );
+
+ is(
+ window2.gBrowser.tabs.length,
+ 2,
+ "Second window should have 2 tabs still open, after closing tabs"
+ );
+
+ // The tabs.remove API makes no promises about SessionStore's updates
+ // having been completed by the time it returns. So we need to wait separately
+ // for the closed tab count to be updated the correct value. This is OK because
+ // we can observe above that the tabs length has changed to reflect that
+ // some were closed.
+ await TestUtils.waitForCondition(
+ () => SessionStore.getLastClosedTabCount(window) == 2,
+ "Last closed tab count is 2"
+ );
+
+ await TestUtils.waitForCondition(
+ () => SessionStore.getLastClosedTabCount(window2) == 2,
+ "Last closed tab count is 2"
+ );
+
+ undoCloseTab();
+ window2.undoCloseTab();
+
+ is(
+ gBrowser.tabs.length,
+ 4,
+ "All tabs in original window should be restored for a total of 4 tabs"
+ );
+
+ is(
+ window2.gBrowser.tabs.length,
+ 4,
+ "All tabs in second window should be restored for a total of 4 tabs"
+ );
+
+ await Promise.all([
+ BrowserTestUtils.waitForEvent(gBrowser.tabs[2], "SSTabRestored"),
+ BrowserTestUtils.waitForEvent(gBrowser.tabs[3], "SSTabRestored"),
+ BrowserTestUtils.waitForEvent(window2.gBrowser.tabs[2], "SSTabRestored"),
+ BrowserTestUtils.waitForEvent(window2.gBrowser.tabs[3], "SSTabRestored"),
+ ]);
+
+ is(
+ gBrowser.tabs[2].linkedBrowser.currentURI.spec,
+ "https://example.com/closeme/2",
+ "Original window restored tab at index 2 should have expected URL"
+ );
+
+ is(
+ gBrowser.tabs[3].linkedBrowser.currentURI.spec,
+ "https://example.com/closeme/3",
+ "Original window restored tab at index 3 should have expected URL"
+ );
+
+ is(
+ window2.gBrowser.tabs[2].linkedBrowser.currentURI.spec,
+ "https://example.com/closeme/5",
+ "Second window restored tab at index 2 should have expected URL"
+ );
+
+ is(
+ window2.gBrowser.tabs[3].linkedBrowser.currentURI.spec,
+ "https://example.com/closeme/6",
+ "Second window restored tab at index 3 should have expected URL"
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(window2);
+ gBrowser.removeAllTabsBut(initialTab);
+});
+
+add_task(async function closeWindowIfExtClosesAllTabs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.tabs.closeWindowWithLastTab", true],
+ ["browser.tabs.warnOnClose", true],
+ ],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async function () {
+ let tabsToRemove = await browser.tabs.query({ currentWindow: true });
+
+ let currentWindowId = tabsToRemove[0].windowId;
+
+ browser.test.assertEq(
+ 2,
+ tabsToRemove.length,
+ "Current window should have 2 tabs to remove"
+ );
+
+ await browser.tabs.remove(tabsToRemove.map(tab => tab.id));
+
+ await browser.test.assertRejects(
+ browser.windows.get(currentWindowId),
+ RegExp(`Invalid window ID: ${currentWindowId}`),
+ "After closing tabs, 2nd window should be closed and querying for it should be rejected"
+ );
+
+ browser.test.notifyPass("done");
+ },
+ });
+
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ await BrowserTestUtils.openNewForegroundTab(
+ window2.gBrowser,
+ "https://example.com/"
+ );
+
+ await extension.startup();
+ await extension.awaitFinish("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js b/browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js
new file mode 100644
index 0000000000..edaf2f61b4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js
@@ -0,0 +1,151 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testExecuteScript() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/",
+ true
+ );
+
+ async function background() {
+ let tasks = [
+ // Insert CSS file.
+ {
+ background: "rgba(0, 0, 0, 0)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs.insertCSS({
+ file: "file2.css",
+ });
+ },
+ },
+ // Insert CSS code.
+ {
+ background: "rgb(42, 42, 42)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs.insertCSS({
+ code: "* { background: rgb(42, 42, 42) }",
+ });
+ },
+ },
+ // Remove CSS code again.
+ {
+ background: "rgba(0, 0, 0, 0)",
+ foreground: "rgb(0, 113, 4)",
+ promise: () => {
+ return browser.tabs.removeCSS({
+ code: "* { background: rgb(42, 42, 42) }",
+ });
+ },
+ },
+ // Remove CSS file again.
+ {
+ background: "rgba(0, 0, 0, 0)",
+ foreground: "rgb(0, 0, 0)",
+ promise: () => {
+ return browser.tabs.removeCSS({
+ file: "file2.css",
+ });
+ },
+ },
+ // Insert CSS code.
+ {
+ background: "rgb(42, 42, 42)",
+ foreground: "rgb(0, 0, 0)",
+ promise: () => {
+ return browser.tabs.insertCSS({
+ code: "* { background: rgb(42, 42, 42) }",
+ cssOrigin: "user",
+ });
+ },
+ },
+ // Remove CSS code again.
+ {
+ background: "rgba(0, 0, 0, 0)",
+ foreground: "rgb(0, 0, 0)",
+ promise: () => {
+ return browser.tabs.removeCSS({
+ code: "* { background: rgb(42, 42, 42) }",
+ cssOrigin: "user",
+ });
+ },
+ },
+ ];
+
+ function checkCSS() {
+ let computedStyle = window.getComputedStyle(document.body);
+ return [computedStyle.backgroundColor, computedStyle.color];
+ }
+
+ try {
+ for (let { promise, background, foreground } of tasks) {
+ 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("removeCSS");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("removeCSS");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ },
+
+ background,
+
+ files: {
+ "file2.css": "* { color: rgb(0, 113, 4) }",
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitFinish("removeCSS");
+
+ // Verify that scripts created by tabs.removeCSS are not added to the content scripts
+ // that requires cleanup (Bug 1464711).
+ await SpecialPowers.spawn(tab.linkedBrowser, [extension.id], async extId => {
+ const { ExtensionContent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionContent.sys.mjs"
+ );
+
+ let contentScriptContext = ExtensionContent.getContextByExtensionId(
+ extId,
+ content.window
+ );
+
+ for (let script of contentScriptContext.scripts) {
+ if (script.matcher.removeCSS && script.requiresCleanup) {
+ throw new Error("tabs.removeCSS scripts should not require cleanup");
+ }
+ }
+ }).catch(err => {
+ // Log the error so that it is easy to see where the failure is coming from.
+ ok(false, err);
+ });
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_saveAsPDF.js b/browser/components/extensions/test/browser/browser_ext_tabs_saveAsPDF.js
new file mode 100644
index 0000000000..aae8e08ccc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_saveAsPDF.js
@@ -0,0 +1,203 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testReturnStatus(expectedStatus) {
+ // Test that tabs.saveAsPDF() returns the correct status
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+
+ let saveDir = FileUtils.getDir(
+ "TmpD",
+ [`testSaveDir-${Math.random()}`],
+ true
+ );
+
+ let saveFile = saveDir.clone();
+ saveFile.append("testSaveFile.pdf");
+ if (saveFile.exists()) {
+ saveFile.remove(false);
+ }
+
+ if (expectedStatus == "replaced") {
+ // Create file that can be replaced
+ saveFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ } else if (expectedStatus == "not_saved") {
+ // Create directory with same name as file - so that file cannot be saved
+ saveFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o666);
+ } else if (expectedStatus == "not_replaced") {
+ // Create file that cannot be replaced
+ saveFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o444);
+ }
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ if (expectedStatus == "replaced" || expectedStatus == "not_replaced") {
+ MockFilePicker.returnValue = MockFilePicker.returnReplace;
+ } else if (expectedStatus == "canceled") {
+ MockFilePicker.returnValue = MockFilePicker.returnCancel;
+ } else {
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+ }
+
+ MockFilePicker.displayDirectory = saveDir;
+
+ MockFilePicker.showCallback = fp => {
+ MockFilePicker.setFiles([saveFile]);
+ MockFilePicker.filterIndex = 0; // *.* - all file extensions
+ };
+
+ let manifest = {
+ description: expectedStatus,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: manifest,
+
+ background: async function () {
+ let pageSettings = {};
+
+ let expected = chrome.runtime.getManifest().description;
+
+ let status = await browser.tabs.saveAsPDF(pageSettings);
+
+ browser.test.assertEq(expected, status, "Got expected status");
+
+ browser.test.notifyPass("tabs.saveAsPDF");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.saveAsPDF");
+ await extension.unload();
+
+ if (expectedStatus == "saved" || expectedStatus == "replaced") {
+ // Check that first four bytes of saved PDF file are "%PDF"
+ let text = await IOUtils.read(saveFile.path, { maxBytes: 4 });
+ text = new TextDecoder().decode(text);
+ is(text, "%PDF", "Got correct magic number - %PDF");
+ }
+
+ MockFilePicker.cleanup();
+
+ if (expectedStatus == "not_saved" || expectedStatus == "not_replaced") {
+ saveFile.permissions = 0o666;
+ }
+
+ saveDir.remove(true);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testSaveAsPDF_saved() {
+ await testReturnStatus("saved");
+});
+
+add_task(async function testSaveAsPDF_replaced() {
+ await testReturnStatus("replaced");
+});
+
+add_task(async function testSaveAsPDF_canceled() {
+ await testReturnStatus("canceled");
+});
+
+add_task(async function testSaveAsPDF_not_saved() {
+ await testReturnStatus("not_saved");
+});
+
+add_task(async function testSaveAsPDF_not_replaced() {
+ await testReturnStatus("not_replaced");
+});
+
+async function testFileName(expectedFileName) {
+ // Test that tabs.saveAsPDF() saves with the correct filename
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+
+ let saveDir = FileUtils.getDir(
+ "TmpD",
+ [`testSaveDir-${Math.random()}`],
+ true
+ );
+
+ let saveFile = saveDir.clone();
+ saveFile.append(expectedFileName);
+ if (saveFile.exists()) {
+ saveFile.remove(false);
+ }
+
+ let MockFilePicker = SpecialPowers.MockFilePicker;
+ MockFilePicker.init(window);
+
+ MockFilePicker.returnValue = MockFilePicker.returnOK;
+
+ MockFilePicker.displayDirectory = saveDir;
+
+ MockFilePicker.showCallback = fp => {
+ is(
+ fp.defaultString,
+ expectedFileName,
+ "Got expected FilePicker defaultString"
+ );
+
+ is(fp.defaultExtension, "pdf", "Got expected FilePicker defaultExtension");
+
+ let file = saveDir.clone();
+ file.append(fp.defaultString);
+ MockFilePicker.setFiles([file]);
+ };
+
+ let manifest = {
+ description: expectedFileName,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: manifest,
+
+ background: async function () {
+ let pageSettings = {};
+
+ let expected = chrome.runtime.getManifest().description;
+
+ if (expected == "definedFileName") {
+ pageSettings.toFileName = expected;
+ }
+
+ let status = await browser.tabs.saveAsPDF(pageSettings);
+
+ browser.test.assertEq("saved", status, "Got expected status");
+
+ browser.test.notifyPass("tabs.saveAsPDF");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.saveAsPDF");
+ await extension.unload();
+
+ // Check that first four bytes of saved PDF file are "%PDF"
+ let text = await IOUtils.read(saveFile.path, { maxBytes: 4 });
+ text = new TextDecoder().decode(text);
+ is(text, "%PDF", "Got correct magic number - %PDF");
+
+ MockFilePicker.cleanup();
+
+ saveDir.remove(true);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testSaveAsPDF_defined_filename() {
+ await testFileName("definedFileName");
+});
+
+add_task(async function testSaveAsPDF_undefined_filename() {
+ // If pageSettings.toFileName is undefined, the expected filename will be
+ // the test page title "mochitest index /" with the "/" replaced by "_".
+ await testFileName("mochitest index _");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js b/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js
new file mode 100644
index 0000000000..8c420c2821
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_sendMessage.js
@@ -0,0 +1,385 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function tabsSendMessageReply() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+
+ content_scripts: [
+ {
+ matches: ["http://example.com/"],
+ js: ["content-script.js"],
+ run_at: "document_start",
+ },
+ ],
+ },
+
+ background: async function () {
+ let firstTab;
+ let promiseResponse = new Promise(resolve => {
+ browser.runtime.onMessage.addListener((msg, sender, respond) => {
+ if (msg == "content-script-ready") {
+ let 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]);
+ }
+ });
+ });
+
+ let 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);
+ }
+ });
+
+ let 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() {
+ let 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]);
+ }
+ });
+
+ let 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";
+
+ let 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.
+ let 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() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ async background() {
+ let url =
+ "http://example.com/mochitest/browser/browser/components/extensions/test/browser/file_dummy.html";
+ let tab = await browser.tabs.create({ url });
+
+ await browser.test.assertRejects(
+ browser.tabs.sendMessage(tab.id, "message"),
+ /Could not establish connection. Receiving end does not exist./,
+ "exception should be raised on tabs.sendMessage to nonexistent tab"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.sendMessage(tab.id + 100, "message"),
+ /Could not establish connection. Receiving end does not exist./,
+ "exception should be raised on tabs.sendMessage to nonexistent tab"
+ );
+
+ await browser.tabs.remove(tab.id);
+
+ browser.test.notifyPass("tabs.sendMessage");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.sendMessage");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_sharingState.js b/browser/components/extensions/test/browser/browser_ext_tabs_sharingState.js
new file mode 100644
index 0000000000..47f2006307
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_sharingState.js
@@ -0,0 +1,110 @@
+"use strict";
+
+add_task(async function test_tabs_mediaIndicators() {
+ let initialTab = gBrowser.selectedTab;
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com/#tab-sharing"
+ );
+
+ // Ensure that the tab to hide is not selected (otherwise
+ // it will not be hidden because it is selected).
+ gBrowser.selectedTab = initialTab;
+
+ // updateBrowserSharing is called when a request for media icons occurs. We're
+ // just testing that extension tabs get the info and are updated when it is
+ // called.
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, {
+ webRTC: {
+ sharing: "screen",
+ screen: "Window",
+ microphone: Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED,
+ camera: Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED,
+ },
+ });
+
+ async function background() {
+ let tabs = await browser.tabs.query({ url: "http://example.com/*" });
+ let testTab = tabs[0];
+
+ browser.test.assertEq(
+ testTab.url,
+ "http://example.com/#tab-sharing",
+ "Got the expected tab url"
+ );
+
+ browser.test.assertFalse(testTab.active, "test tab should not be selected");
+
+ let state = testTab.sharingState;
+ browser.test.assertTrue(state.camera, "sharing camera was turned on");
+ browser.test.assertTrue(state.microphone, "sharing mic was turned on");
+ browser.test.assertEq(state.screen, "Window", "sharing screen is window");
+
+ tabs = await browser.tabs.query({ screen: true });
+ browser.test.assertEq(tabs.length, 1, "screen sharing tab was found");
+
+ tabs = await browser.tabs.query({ screen: "Window" });
+ browser.test.assertEq(
+ tabs.length,
+ 1,
+ "screen sharing (window) tab was found"
+ );
+
+ tabs = await browser.tabs.query({ screen: "Screen" });
+ browser.test.assertEq(tabs.length, 0, "screen sharing tab was not found");
+
+ // Verify we cannot hide a sharing tab.
+ let hidden = await browser.tabs.hide(testTab.id);
+ browser.test.assertEq(hidden.length, 0, "unable to hide sharing tab");
+ tabs = await browser.tabs.query({ hidden: true });
+ browser.test.assertEq(tabs.length, 0, "unable to hide sharing tab");
+
+ browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
+ if (testTab.id !== tabId) {
+ return;
+ }
+ let state = changeInfo.sharingState;
+
+ // Ignore tab update events unrelated to the sharing state.
+ if (!state) {
+ return;
+ }
+
+ browser.test.assertFalse(state.camera, "sharing camera was turned off");
+ browser.test.assertFalse(state.microphone, "sharing mic was turned off");
+ browser.test.assertFalse(state.screen, "sharing screen was turned off");
+
+ // Verify we can hide the tab once it is not shared over webRTC anymore.
+ let hidden = await browser.tabs.hide(testTab.id);
+ browser.test.assertEq(hidden.length, 1, "tab hidden successfully");
+ tabs = await browser.tabs.query({ hidden: true });
+ browser.test.assertEq(tabs.length, 1, "hidden tab found");
+
+ browser.test.notifyPass("done");
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extdata = {
+ manifest: { permissions: ["tabs", "tabHide"] },
+ useAddonManager: "temporary",
+ background,
+ };
+ let extension = ExtensionTestUtils.loadExtension(extdata);
+ await extension.startup();
+
+ // Test that onUpdated is called after the sharing state is changed from
+ // chrome code.
+ await extension.awaitMessage("ready");
+
+ info("Updating browser sharing on the test tab");
+
+ // Clear only the webRTC part of the browser sharing state
+ // (used to test Bug 1577480 regression fix).
+ gBrowser.updateBrowserSharing(tab.linkedBrowser, { webRTC: null });
+
+ await extension.awaitFinish("done");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_successors.js b/browser/components/extensions/test/browser/browser_ext_tabs_successors.js
new file mode 100644
index 0000000000..77549c44d5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_successors.js
@@ -0,0 +1,396 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function background(tabCount, testFn) {
+ try {
+ const { TAB_ID_NONE } = browser.tabs;
+ const tabIds = await Promise.all(
+ Array.from({ length: tabCount }, () =>
+ browser.tabs.create({ url: "about:blank" }).then(t => t.id)
+ )
+ );
+
+ const toTabIds = i => tabIds[i];
+
+ const setSuccessors = mapping =>
+ Promise.all(
+ mapping.map((succ, i) =>
+ browser.tabs.update(tabIds[i], { successorTabId: tabIds[succ] })
+ )
+ );
+
+ const verifySuccessors = async function (mapping, name) {
+ const promises = [],
+ expected = [];
+ for (let i = 0; i < mapping.length; i++) {
+ if (mapping[i] !== undefined) {
+ promises.push(
+ browser.tabs.get(tabIds[i]).then(t => t.successorTabId)
+ );
+ expected.push(
+ mapping[i] === TAB_ID_NONE ? TAB_ID_NONE : tabIds[mapping[i]]
+ );
+ }
+ }
+ const results = await Promise.all(promises);
+ for (let i = 0; i < results.length; i++) {
+ browser.test.assertEq(
+ expected[i],
+ results[i],
+ `${name}: successorTabId of tab ${i} in mapping should be ${expected[i]}`
+ );
+ }
+ };
+
+ await testFn({
+ TAB_ID_NONE,
+ tabIds,
+ toTabIds,
+ setSuccessors,
+ verifySuccessors,
+ });
+
+ browser.test.notifyPass("background-script");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("background-script");
+ }
+}
+
+async function runTabTest(tabCount, testFn) {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ background: `(${background})(${tabCount}, ${testFn});`,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("background-script");
+ await extension.unload();
+}
+
+add_task(function testTabSuccessors() {
+ return runTabTest(3, async function ({ TAB_ID_NONE, tabIds }) {
+ const anotherWindow = await browser.windows.create({ url: "about:blank" });
+
+ browser.test.assertEq(
+ TAB_ID_NONE,
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "Tabs default to an undefined successor"
+ );
+
+ // Basic getting and setting
+
+ await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
+ browser.test.assertEq(
+ tabIds[1],
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "tabs.update assigned the correct successor"
+ );
+
+ await browser.tabs.update(tabIds[0], {
+ successorTabId: browser.tabs.TAB_ID_NONE,
+ });
+ browser.test.assertEq(
+ TAB_ID_NONE,
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "tabs.update cleared successor"
+ );
+
+ await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
+ await browser.tabs.update(tabIds[0], { successorTabId: tabIds[0] });
+ browser.test.assertEq(
+ TAB_ID_NONE,
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "Setting a tab as its own successor clears the successor instead"
+ );
+
+ // Validation tests
+
+ await browser.test.assertRejects(
+ browser.tabs.update(tabIds[0], { successorTabId: 1e8 }),
+ /Invalid successorTabId/,
+ "tabs.update should throw with an invalid successor tab ID"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.update(tabIds[0], {
+ successorTabId: anotherWindow.tabs[0].id,
+ }),
+ /Successor tab must be in the same window as the tab being updated/,
+ "tabs.update should throw with a successor tab ID from another window"
+ );
+
+ // Make sure the successor is truly being assigned
+
+ await browser.tabs.update(tabIds[0], {
+ successorTabId: tabIds[2],
+ active: true,
+ });
+ await browser.tabs.remove(tabIds[0]);
+ browser.test.assertEq(
+ tabIds[2],
+ (await browser.tabs.query({ active: true }))[0].id
+ );
+
+ return browser.tabs.remove([
+ tabIds[1],
+ tabIds[2],
+ anotherWindow.tabs[0].id,
+ ]);
+ });
+});
+
+add_task(function testMoveInSuccession_appendFalse() {
+ return runTabTest(
+ 8,
+ async function ({
+ TAB_ID_NONE,
+ tabIds,
+ toTabIds,
+ setSuccessors,
+ verifySuccessors,
+ }) {
+ await browser.tabs.moveInSuccession([1, 0].map(toTabIds), tabIds[0]);
+ await verifySuccessors([TAB_ID_NONE, 0], "scenario 1");
+
+ await browser.tabs.moveInSuccession(
+ [0, 1, 2, 3].map(toTabIds),
+ tabIds[0]
+ );
+ await verifySuccessors([1, 2, 3, 0], "scenario 2");
+
+ await browser.tabs.moveInSuccession([1, 0].map(toTabIds), tabIds[0]);
+ await verifySuccessors(
+ [TAB_ID_NONE, 0],
+ "scenario 1 after tab 0 has a successor"
+ );
+
+ await browser.tabs.update(tabIds[7], { successorTabId: tabIds[0] });
+ await browser.tabs.moveInSuccession([4, 5, 6, 7].map(toTabIds));
+ await verifySuccessors(
+ new Array(4).concat([5, 6, 7, TAB_ID_NONE]),
+ "scenario 4"
+ );
+
+ await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
+ await browser.tabs.moveInSuccession(
+ [4, 6, 3, 2].map(toTabIds),
+ tabIds[7]
+ );
+ await verifySuccessors([7, TAB_ID_NONE, 7, 2, 6, 7, 3, 5], "scenario 5");
+
+ await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
+ await browser.tabs.moveInSuccession(
+ [4, 6, 3, 2].map(toTabIds),
+ tabIds[7],
+ {
+ insert: true,
+ }
+ );
+ await verifySuccessors(
+ [4, TAB_ID_NONE, 7, 2, 6, 4, 3, 5],
+ "insert = true"
+ );
+
+ await setSuccessors([1, 2, 3, 4, 0]);
+ await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[0], {
+ insert: true,
+ });
+ await verifySuccessors([4, 2, 0, 1, 3], "insert = true, part 2");
+
+ await browser.tabs.moveInSuccession([
+ tabIds[0],
+ tabIds[1],
+ 1e8,
+ tabIds[2],
+ ]);
+ await verifySuccessors([1, 2, TAB_ID_NONE], "unknown tab ID");
+
+ browser.test.assertTrue(
+ await browser.tabs.moveInSuccession([1e8]).then(
+ () => true,
+ () => false
+ ),
+ "When all tab IDs are unknown, tabs.moveInSuccession should not throw"
+ );
+
+ // Validation tests
+
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([tabIds[0], tabIds[1], tabIds[0]]),
+ /IDs must not occur more than once in tabIds/,
+ "tabs.moveInSuccession should throw when a tab is referenced more than once in tabIds"
+ );
+
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([tabIds[0], tabIds[1]], tabIds[0], {
+ insert: true,
+ }),
+ /Value of tabId must not occur in tabIds if append or insert is true/,
+ "tabs.moveInSuccession should throw when tabId occurs in tabIds and insert is true"
+ );
+
+ return browser.tabs.remove(tabIds);
+ }
+ );
+});
+
+add_task(function testMoveInSuccession_appendTrue() {
+ return runTabTest(
+ 8,
+ async function ({
+ TAB_ID_NONE,
+ tabIds,
+ toTabIds,
+ setSuccessors,
+ verifySuccessors,
+ }) {
+ await browser.tabs.moveInSuccession([1].map(toTabIds), tabIds[0], {
+ append: true,
+ });
+ await verifySuccessors([1, TAB_ID_NONE], "scenario 1");
+
+ await browser.tabs.update(tabIds[3], { successorTabId: tabIds[4] });
+ await browser.tabs.moveInSuccession([1, 2, 3].map(toTabIds), tabIds[0], {
+ append: true,
+ });
+ await verifySuccessors([1, 2, 3, TAB_ID_NONE], "scenario 2");
+
+ await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
+ await browser.tabs.moveInSuccession([1e8], tabIds[0], { append: true });
+ browser.test.assertEq(
+ TAB_ID_NONE,
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "If no tabs get appended after the reference tab, it should lose its successor"
+ );
+
+ await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
+ await browser.tabs.moveInSuccession(
+ [4, 6, 3, 2].map(toTabIds),
+ tabIds[7],
+ {
+ append: true,
+ }
+ );
+ await verifySuccessors(
+ [7, TAB_ID_NONE, TAB_ID_NONE, 2, 6, 7, 3, 4],
+ "scenario 3"
+ );
+
+ await setSuccessors([7, 2, 3, 4, 3, 6, 7, 5]);
+ await browser.tabs.moveInSuccession(
+ [4, 6, 3, 2].map(toTabIds),
+ tabIds[7],
+ {
+ append: true,
+ insert: true,
+ }
+ );
+ await verifySuccessors(
+ [7, TAB_ID_NONE, 5, 2, 6, 7, 3, 4],
+ "insert = true"
+ );
+
+ await browser.tabs.moveInSuccession([0, 4].map(toTabIds), tabIds[7], {
+ append: true,
+ insert: true,
+ });
+ await verifySuccessors(
+ [4, undefined, undefined, undefined, 6, undefined, undefined, 0],
+ "insert = true, part 2"
+ );
+
+ await setSuccessors([1, 2, 3, 4, 0]);
+ await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[0], {
+ append: true,
+ insert: true,
+ });
+ await verifySuccessors([3, 2, 4, 1, 0], "insert = true, part 3");
+
+ await browser.tabs.update(tabIds[0], { successorTabId: tabIds[1] });
+ await browser.tabs.moveInSuccession([1e8], tabIds[0], {
+ append: true,
+ insert: true,
+ });
+ browser.test.assertEq(
+ tabIds[1],
+ (await browser.tabs.get(tabIds[0])).successorTabId,
+ "If no tabs get inserted after the reference tab, it should keep its successor"
+ );
+
+ // Validation tests
+
+ await browser.test.assertRejects(
+ browser.tabs.moveInSuccession([tabIds[0], tabIds[1]], tabIds[0], {
+ append: true,
+ }),
+ /Value of tabId must not occur in tabIds if append or insert is true/,
+ "tabs.moveInSuccession should throw when tabId occurs in tabIds and insert is true"
+ );
+
+ return browser.tabs.remove(tabIds);
+ }
+ );
+});
+
+add_task(function testMoveInSuccession_ignoreTabsInOtherWindows() {
+ return runTabTest(
+ 2,
+ async function ({
+ TAB_ID_NONE,
+ tabIds,
+ toTabIds,
+ setSuccessors,
+ verifySuccessors,
+ }) {
+ const anotherWindow = await browser.windows.create({
+ url: Array.from({ length: 3 }, () => "about:blank"),
+ });
+ tabIds.push(...anotherWindow.tabs.map(t => t.id));
+
+ await setSuccessors([1, 0, 3, 4, 2]);
+ await browser.tabs.moveInSuccession([1, 3, 2].map(toTabIds), tabIds[4]);
+ await verifySuccessors(
+ [1, 0, 4, 2, TAB_ID_NONE],
+ "first tab in another window"
+ );
+
+ await setSuccessors([1, 0, 3, 4, 2]);
+ await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[4]);
+ await verifySuccessors(
+ [1, 0, 4, 2, TAB_ID_NONE],
+ "middle tab in another window"
+ );
+
+ await setSuccessors([1, 0, 3, 4, 2]);
+ await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds));
+ await verifySuccessors(
+ [1, 0, TAB_ID_NONE, 2, TAB_ID_NONE],
+ "using the first tab to determine the window"
+ );
+
+ await setSuccessors([1, 0, 3, 4, 2]);
+ await browser.tabs.moveInSuccession([1, 3, 2].map(toTabIds), tabIds[4], {
+ append: true,
+ });
+ await verifySuccessors(
+ [1, 0, TAB_ID_NONE, 2, 3],
+ "first tab in another window, appending"
+ );
+
+ await setSuccessors([1, 0, 3, 4, 2]);
+ await browser.tabs.moveInSuccession([3, 1, 2].map(toTabIds), tabIds[4], {
+ append: true,
+ });
+ await verifySuccessors(
+ [1, 0, TAB_ID_NONE, 2, 3],
+ "middle tab in another window, appending"
+ );
+
+ return browser.tabs.remove(tabIds);
+ }
+ );
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_update.js b/browser/components/extensions/test/browser/browser_ext_tabs_update.js
new file mode 100644
index 0000000000..3963def8af
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_update.js
@@ -0,0 +1,54 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:robots"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:config"
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: function () {
+ browser.tabs.query(
+ {
+ lastFocusedWindow: true,
+ },
+ function (tabs) {
+ browser.test.assertEq(tabs.length, 3, "should have three tabs");
+
+ tabs.sort((tab1, tab2) => tab1.index - tab2.index);
+
+ browser.test.assertEq(tabs[0].url, "about:blank", "first tab blank");
+ tabs.shift();
+
+ browser.test.assertTrue(tabs[0].active, "tab 0 active");
+ browser.test.assertFalse(tabs[1].active, "tab 1 inactive");
+
+ browser.tabs.update(tabs[1].id, { active: true }, function () {
+ browser.test.sendMessage("check");
+ });
+ }
+ );
+ },
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("check")]);
+
+ ok(gBrowser.selectedTab == tab2, "correct tab selected");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js b/browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js
new file mode 100644
index 0000000000..0adb05e827
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_update_highlighted.js
@@ -0,0 +1,183 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_update_highlighted() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ const trackedEvents = ["onActivated", "onHighlighted"];
+ async function expectResults(fn, action) {
+ let resolve;
+ let reject;
+ let promise = new Promise((...args) => {
+ [resolve, reject] = args;
+ });
+ let expectedEvents;
+ let events = [];
+ let listeners = {};
+ for (let trackedEvent of trackedEvents) {
+ listeners[trackedEvent] = data => {
+ events.push([trackedEvent, data]);
+ if (expectedEvents && expectedEvents.length >= events.length) {
+ resolve();
+ }
+ };
+ browser.tabs[trackedEvent].addListener(listeners[trackedEvent]);
+ }
+ let expectedData = await fn();
+ let expectedHighlighted = expectedData.highlighted;
+ let expectedActive = expectedData.active;
+ expectedEvents = expectedData.events;
+ if (events.length < expectedEvents.length) {
+ // Wait up to 1000 ms for the expected number of events.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ setTimeout(reject, 1000);
+ await promise.catch(() => {
+ let numMissing = expectedEvents.length - events.length;
+ browser.test.fail(`${numMissing} missing events when ${action}`);
+ });
+ }
+ let [{ id: active }] = await browser.tabs.query({ active: true });
+ browser.test.assertEq(
+ expectedActive,
+ active,
+ `The expected tab is active when ${action}`
+ );
+ let highlighted = (await browser.tabs.query({ highlighted: true })).map(
+ ({ id }) => id
+ );
+ browser.test.assertEq(
+ JSON.stringify(expectedHighlighted),
+ JSON.stringify(highlighted),
+ `The expected tabs are highlighted when ${action}`
+ );
+ let unexpectedEvents = events.splice(expectedEvents.length);
+ browser.test.assertEq(
+ JSON.stringify(expectedEvents),
+ JSON.stringify(events),
+ `Should get expected events when ${action}`
+ );
+ if (unexpectedEvents.length) {
+ browser.test.fail(
+ `${unexpectedEvents.length} unexpected events when ${action}: ` +
+ JSON.stringify(unexpectedEvents)
+ );
+ }
+ for (let trackedEvent of trackedEvents) {
+ browser.tabs[trackedEvent].removeListener(listeners[trackedEvent]);
+ }
+ }
+
+ let { id: windowId } = await browser.windows.getCurrent();
+ let { id: tab1 } = await browser.tabs.create({ url: "about:blank?1" });
+ let { id: tab2 } = await browser.tabs.create({
+ url: "about:blank?2",
+ active: true,
+ });
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: true });
+ return { active: tab2, highlighted: [tab2], events: [] };
+ }, "highlighting active tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: false });
+ return { active: tab2, highlighted: [tab2], events: [] };
+ }, "unhighlighting active tab with no multiselection");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab1, { highlighted: true });
+ return {
+ active: tab1,
+ highlighted: [tab1, tab2],
+ events: [
+ ["onActivated", { tabId: tab1, previousTabId: tab2, windowId }],
+ ["onHighlighted", { tabIds: [tab1, tab2], windowId }],
+ ],
+ };
+ }, "highlighting non-highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: true });
+ return { active: tab1, highlighted: [tab1, tab2], events: [] };
+ }, "highlighting inactive highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab1, { highlighted: false });
+ return {
+ active: tab2,
+ highlighted: [tab2],
+ events: [
+ ["onActivated", { tabId: tab2, previousTabId: tab1, windowId }],
+ ["onHighlighted", { tabIds: [tab2], windowId }],
+ ],
+ };
+ }, "unhighlighting active tab with multiselection");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab1, { highlighted: true });
+ return {
+ active: tab1,
+ highlighted: [tab1, tab2],
+ events: [
+ ["onActivated", { tabId: tab1, previousTabId: tab2, windowId }],
+ ["onHighlighted", { tabIds: [tab1, tab2], windowId }],
+ ],
+ };
+ }, "highlighting non-highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: false });
+ return {
+ active: tab1,
+ highlighted: [tab1],
+ events: [["onHighlighted", { tabIds: [tab1], windowId }]],
+ };
+ }, "unhighlighting inactive highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: true, active: false });
+ return {
+ active: tab1,
+ highlighted: [tab1, tab2],
+ events: [["onHighlighted", { tabIds: [tab1, tab2], windowId }]],
+ };
+ }, "highlighting without activating non-highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab2, { highlighted: true, active: true });
+ return {
+ active: tab2,
+ highlighted: [tab2],
+ events: [
+ ["onActivated", { tabId: tab2, previousTabId: tab1, windowId }],
+ ["onHighlighted", { tabIds: [tab2], windowId }],
+ ],
+ };
+ }, "highlighting and activating inactive highlighted tab");
+
+ await expectResults(async () => {
+ await browser.tabs.update(tab1, { active: true, highlighted: true });
+ return {
+ active: tab1,
+ highlighted: [tab1],
+ events: [
+ ["onActivated", { tabId: tab1, previousTabId: tab2, windowId }],
+ ["onHighlighted", { tabIds: [tab1], windowId }],
+ ],
+ };
+ }, "highlighting and activating non-highlighted tab");
+
+ await browser.tabs.remove([tab1, tab2]);
+ browser.test.notifyPass("test-finished");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("test-finished");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js b/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js
new file mode 100644
index 0000000000..610415b66e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_update_url.js
@@ -0,0 +1,235 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
+});
+
+async function testTabsUpdateURL(
+ existentTabURL,
+ tabsUpdateURL,
+ isErrorExpected
+) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ files: {
+ "tab.html": `
+
+
+
+
+
+
+
tab page
+
+
+ `.trim(),
+ },
+ background: function () {
+ browser.test.sendMessage("ready", browser.runtime.getURL("tab.html"));
+
+ browser.test.onMessage.addListener(
+ async (msg, tabsUpdateURL, isErrorExpected) => {
+ let tabs = await browser.tabs.query({ lastFocusedWindow: true });
+
+ try {
+ let tab = await browser.tabs.update(tabs[1].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();
+
+ let mozExtTabURL = await extension.awaitMessage("ready");
+
+ if (tabsUpdateURL == "self") {
+ tabsUpdateURL = mozExtTabURL;
+ }
+
+ info(`tab.update URL "${tabsUpdateURL}" on tab with URL "${existentTabURL}"`);
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ existentTabURL
+ );
+
+ extension.sendMessage("start", tabsUpdateURL, isErrorExpected);
+ await extension.awaitMessage("done");
+
+ BrowserTestUtils.removeTab(tab1);
+ await extension.unload();
+}
+
+add_task(async function () {
+ info("Start testing tabs.update on javascript URLs");
+
+ let dataURLPage = `data:text/html,
+
+
+
+
+
+
+
data url page
+
+ `;
+
+ let 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,
+ },
+ ];
+
+ let testCases = checkList.map(check =>
+ Object.assign({}, check, { existentTabURL: "about:blank" })
+ );
+
+ for (let { existentTabURL, tabsUpdateURL, isErrorExpected } of testCases) {
+ await testTabsUpdateURL(existentTabURL, tabsUpdateURL, isErrorExpected);
+ }
+
+ info("done");
+});
+
+add_task(async function test_update_reload() {
+ const URL = "https://example.com/";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background() {
+ browser.test.onMessage.addListener(async (cmd, ...args) => {
+ const result = await browser.tabs[cmd](...args);
+ browser.test.sendMessage("result", result);
+ });
+
+ const filter = {
+ properties: ["status"],
+ };
+
+ browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+ if (changeInfo.status === "complete") {
+ browser.test.sendMessage("historyAdded");
+ }
+ }, filter);
+ },
+ });
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let tabBrowser = win.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURIString(tabBrowser, URL);
+ await BrowserTestUtils.browserLoaded(tabBrowser, false, URL);
+ let tab = win.gBrowser.selectedTab;
+
+ async function getTabHistory() {
+ await TabStateFlusher.flush(tabBrowser);
+ return JSON.parse(SessionStore.getTabState(tab));
+ }
+
+ await extension.startup();
+ extension.sendMessage("query", { url: URL });
+ let tabs = await extension.awaitMessage("result");
+ let tabId = tabs[0].id;
+
+ let history = await getTabHistory();
+ is(
+ history.entries.length,
+ 1,
+ "Tab history contains the expected number of entries."
+ );
+ is(
+ history.entries[0].url,
+ URL,
+ `Tab history contains the expected entry: URL.`
+ );
+
+ extension.sendMessage("update", tabId, { url: `${URL}1/` });
+ await Promise.all([
+ extension.awaitMessage("result"),
+ extension.awaitMessage("historyAdded"),
+ ]);
+
+ history = await getTabHistory();
+ is(
+ history.entries.length,
+ 2,
+ "Tab history contains the expected number of entries."
+ );
+ is(
+ history.entries[1].url,
+ `${URL}1/`,
+ `Tab history contains the expected entry: ${URL}1/.`
+ );
+
+ extension.sendMessage("update", tabId, {
+ url: `${URL}2/`,
+ loadReplace: true,
+ });
+ await Promise.all([
+ extension.awaitMessage("result"),
+ extension.awaitMessage("historyAdded"),
+ ]);
+
+ history = await getTabHistory();
+ is(
+ history.entries.length,
+ 2,
+ "Tab history contains the expected number of entries."
+ );
+ is(
+ history.entries[1].url,
+ `${URL}2/`,
+ `Tab history contains the expected entry: ${URL}2/.`
+ );
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_warmup.js b/browser/components/extensions/test/browser/browser_ext_tabs_warmup.js
new file mode 100644
index 0000000000..e9a5382de8
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_warmup.js
@@ -0,0 +1,40 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWarmupTab() {
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.net/"
+ );
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ Assert.ok(!tab1.linkedBrowser.renderLayers, "tab is not warm yet");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background: async function () {
+ let backgroundTab = (
+ await browser.tabs.query({
+ lastFocusedWindow: true,
+ url: "http://example.net/",
+ active: false,
+ })
+ )[0];
+ await browser.tabs.warmup(backgroundTab.id);
+ browser.test.notifyPass("tabs.warmup");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("tabs.warmup");
+ Assert.ok(tab1.linkedBrowser.renderLayers, "tab has been warmed up");
+ gBrowser.removeTab(tab1);
+ gBrowser.removeTab(tab2);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js b/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
new file mode 100644
index 0000000000..ad10324573
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
@@ -0,0 +1,346 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const SITE_SPECIFIC_PREF = "browser.zoom.siteSpecific";
+const FULL_ZOOM_PREF = "browser.content.full-zoom";
+
+let gContentPrefs = Cc["@mozilla.org/content-pref/service;1"].getService(
+ Ci.nsIContentPrefService2
+);
+
+// A single monitor for the tests. If it receives any
+// incognito data in event listeners it will fail.
+let monitor;
+add_task(async function startup() {
+ monitor = await startIncognitoMonitorExtension();
+});
+registerCleanupFunction(async function finish() {
+ await monitor.unload();
+});
+
+add_task(async function test_zoom_api() {
+ async function background() {
+ function promiseUpdated(tabId, attr) {
+ return new Promise(resolve => {
+ let onUpdated = (tabId_, changeInfo, tab) => {
+ if (tabId == tabId_ && attr in changeInfo) {
+ browser.tabs.onUpdated.removeListener(onUpdated);
+
+ resolve({ changeInfo, tab });
+ }
+ };
+ browser.tabs.onUpdated.addListener(onUpdated);
+ });
+ }
+
+ let deferred = {};
+ browser.test.onMessage.addListener((message, msg, result) => {
+ if (message == "msg-done" && deferred[msg]) {
+ deferred[msg].resolve(result);
+ }
+ });
+
+ let _id = 0;
+ function msg(...args) {
+ return new Promise((resolve, reject) => {
+ let id = ++_id;
+ deferred[id] = { resolve, reject };
+ browser.test.sendMessage("msg", id, ...args);
+ });
+ }
+
+ let lastZoomEvent = {};
+ let promiseZoomEvents = {};
+ browser.tabs.onZoomChange.addListener(info => {
+ lastZoomEvent[info.tabId] = info;
+ if (promiseZoomEvents[info.tabId]) {
+ promiseZoomEvents[info.tabId]();
+ promiseZoomEvents[info.tabId] = null;
+ }
+ });
+
+ let awaitZoom = async (tabId, newValue) => {
+ let listener;
+
+ // eslint-disable-next-line no-async-promise-executor
+ await new Promise(async resolve => {
+ listener = info => {
+ if (info.tabId == tabId && info.newZoomFactor == newValue) {
+ resolve();
+ }
+ };
+ browser.tabs.onZoomChange.addListener(listener);
+
+ let zoomFactor = await browser.tabs.getZoom(tabId);
+ if (zoomFactor == newValue) {
+ resolve();
+ }
+ });
+
+ browser.tabs.onZoomChange.removeListener(listener);
+ };
+
+ let checkZoom = async (tabId, newValue, oldValue = null) => {
+ let awaitEvent;
+ if (oldValue != null && !lastZoomEvent[tabId]) {
+ awaitEvent = new Promise(resolve => {
+ promiseZoomEvents[tabId] = resolve;
+ });
+ }
+
+ let [apiZoom, realZoom] = await Promise.all([
+ browser.tabs.getZoom(tabId),
+ msg("get-zoom", tabId),
+ awaitEvent,
+ ]);
+
+ browser.test.assertEq(
+ newValue,
+ apiZoom,
+ `Got expected zoom value from API`
+ );
+ browser.test.assertEq(
+ newValue,
+ realZoom,
+ `Got expected zoom value from parent`
+ );
+
+ if (oldValue != null) {
+ let event = lastZoomEvent[tabId];
+ lastZoomEvent[tabId] = null;
+ browser.test.assertEq(
+ tabId,
+ event.tabId,
+ `Got expected zoom event tab ID`
+ );
+ browser.test.assertEq(
+ newValue,
+ event.newZoomFactor,
+ `Got expected zoom event zoom factor`
+ );
+ browser.test.assertEq(
+ oldValue,
+ event.oldZoomFactor,
+ `Got expected zoom event old zoom factor`
+ );
+
+ browser.test.assertEq(
+ 3,
+ Object.keys(event.zoomSettings).length,
+ `Zoom settings should have 3 keys`
+ );
+ browser.test.assertEq(
+ "automatic",
+ event.zoomSettings.mode,
+ `Mode should be "automatic"`
+ );
+ browser.test.assertEq(
+ "per-origin",
+ event.zoomSettings.scope,
+ `Scope should be "per-origin"`
+ );
+ browser.test.assertEq(
+ 1,
+ event.zoomSettings.defaultZoomFactor,
+ `Default zoom should be 1`
+ );
+ }
+ };
+
+ try {
+ let tabs = await browser.tabs.query({});
+ browser.test.assertEq(tabs.length, 4, "We have 4 tabs");
+
+ let tabIds = tabs.splice(1).map(tab => tab.id);
+ await checkZoom(tabIds[0], 1);
+
+ await browser.tabs.setZoom(tabIds[0], 2);
+ await checkZoom(tabIds[0], 2, 1);
+
+ let zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]);
+ browser.test.assertEq(
+ 3,
+ Object.keys(zoomSettings).length,
+ `Zoom settings should have 3 keys`
+ );
+ browser.test.assertEq(
+ "automatic",
+ zoomSettings.mode,
+ `Mode should be "automatic"`
+ );
+ browser.test.assertEq(
+ "per-origin",
+ zoomSettings.scope,
+ `Scope should be "per-origin"`
+ );
+ browser.test.assertEq(
+ 1,
+ zoomSettings.defaultZoomFactor,
+ `Default zoom should be 1`
+ );
+
+ browser.test.log(`Switch to tab 2`);
+ await browser.tabs.update(tabIds[1], { active: true });
+ await checkZoom(tabIds[1], 1);
+
+ browser.test.log(`Navigate tab 2 to origin of tab 1`);
+ browser.tabs.update(tabIds[1], { url: "http://example.com" });
+ await promiseUpdated(tabIds[1], "url");
+ await checkZoom(tabIds[1], 2, 1);
+
+ browser.test.log(`Update zoom in tab 2, expect changes in both tabs`);
+ await browser.tabs.setZoom(tabIds[1], 1.5);
+ await checkZoom(tabIds[1], 1.5, 2);
+
+ browser.test.log(`Switch to tab 3, expect zoom to affect private window`);
+ await browser.tabs.setZoom(tabIds[2], 3);
+ await checkZoom(tabIds[2], 3, 1);
+
+ browser.test.log(
+ `Switch to tab 1, expect asynchronous zoom change just after the switch`
+ );
+ await Promise.all([
+ awaitZoom(tabIds[0], 1.5),
+ browser.tabs.update(tabIds[0], { active: true }),
+ ]);
+ await checkZoom(tabIds[0], 1.5, 2);
+
+ browser.test.log("Set zoom to 0, expect it set to 1");
+ await browser.tabs.setZoom(tabIds[0], 0);
+ await checkZoom(tabIds[0], 1, 1.5);
+
+ browser.test.log("Change zoom externally, expect changes reflected");
+ await msg("enlarge");
+ await checkZoom(tabIds[0], 1.1, 1);
+
+ await Promise.all([
+ browser.tabs.setZoom(tabIds[0], 0),
+ browser.tabs.setZoom(tabIds[1], 0),
+ browser.tabs.setZoom(tabIds[2], 0),
+ ]);
+ await Promise.all([
+ checkZoom(tabIds[0], 1, 1.1),
+ checkZoom(tabIds[1], 1, 1.5),
+ checkZoom(tabIds[2], 1, 3),
+ ]);
+
+ browser.test.log("Check that invalid zoom values throw an error");
+ await browser.test.assertRejects(
+ browser.tabs.setZoom(tabIds[0], 42),
+ /Zoom value 42 out of range/,
+ "Expected an out of range error"
+ );
+
+ browser.test.log("Disable site-specific zoom, expect correct scope");
+ await msg("site-specific", false);
+ zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]);
+
+ browser.test.assertEq(
+ "per-tab",
+ zoomSettings.scope,
+ `Scope should be "per-tab"`
+ );
+
+ await msg("site-specific", null);
+
+ browser.test.onMessage.addListener(async msg => {
+ if (msg === "set-global-zoom-done") {
+ zoomSettings = await browser.tabs.getZoomSettings(tabIds[0]);
+
+ browser.test.assertEq(
+ 5,
+ zoomSettings.defaultZoomFactor,
+ `Default zoom should be 5 after being changed`
+ );
+
+ browser.test.notifyPass("tab-zoom");
+ }
+ });
+ await msg("set-global-zoom");
+ } catch (e) {
+ browser.test.fail(`Error: ${e} :: ${e.stack}`);
+ browser.test.notifyFail("tab-zoom");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ },
+ incognitoOverride: "spanning",
+ background,
+ });
+
+ extension.onMessage("msg", (id, msg, ...args) => {
+ const {
+ Management: {
+ global: { tabTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let resp;
+ if (msg == "get-zoom") {
+ let tab = tabTracker.getTab(args[0]);
+ resp = ZoomManager.getZoomForBrowser(tab.linkedBrowser);
+ } else if (msg == "set-zoom") {
+ let tab = tabTracker.getTab(args[0]);
+ ZoomManager.setZoomForBrowser(tab.linkedBrowser);
+ } else if (msg == "set-global-zoom") {
+ resp = gContentPrefs.setGlobal(
+ FULL_ZOOM_PREF,
+ 5,
+ Cu.createLoadContext(),
+ {
+ handleCompletion() {
+ extension.sendMessage("set-global-zoom-done", id, resp);
+ },
+ }
+ );
+ } else if (msg == "enlarge") {
+ FullZoom.enlarge();
+ } else if (msg == "site-specific") {
+ if (args[0] == null) {
+ SpecialPowers.clearUserPref(SITE_SPECIFIC_PREF);
+ } else {
+ SpecialPowers.setBoolPref(SITE_SPECIFIC_PREF, args[0]);
+ }
+ }
+
+ extension.sendMessage("msg-done", id, resp);
+ });
+
+ let url = "https://example.com/";
+ let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ let tab2 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.org/"
+ );
+
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ let selectedBrowser = privateWindow.gBrowser.selectedBrowser;
+ BrowserTestUtils.loadURIString(selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(selectedBrowser, false, url);
+
+ gBrowser.selectedTab = tab1;
+
+ await extension.startup();
+
+ await extension.awaitFinish("tab-zoom");
+
+ await extension.unload();
+
+ await new Promise(resolve => {
+ gContentPrefs.setGlobal(FULL_ZOOM_PREF, null, Cu.createLoadContext(), {
+ handleCompletion() {
+ resolve();
+ },
+ });
+ });
+
+ privateWindow.close();
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_themes_validation.js b/browser/components/extensions/test/browser/browser_ext_themes_validation.js
new file mode 100644
index 0000000000..c004363a6b
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_themes_validation.js
@@ -0,0 +1,55 @@
+"use strict";
+
+PromiseTestUtils.allowMatchingRejectionsGlobally(/packaging errors/);
+
+/**
+ * Helper function for testing a theme with invalid properties.
+ *
+ * @param {object} invalidProps The invalid properties to load the theme with.
+ */
+async function testThemeWithInvalidProperties(invalidProps) {
+ let manifest = {
+ theme: {},
+ };
+
+ invalidProps.forEach(prop => {
+ // Some properties require additional information:
+ switch (prop) {
+ case "background":
+ manifest[prop] = { scripts: ["background.js"] };
+ break;
+ case "permissions":
+ manifest[prop] = ["tabs"];
+ break;
+ case "omnibox":
+ manifest[prop] = { keyword: "test" };
+ break;
+ default:
+ manifest[prop] = {};
+ }
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({ manifest });
+ await Assert.rejects(
+ extension.startup(),
+ /startup failed/,
+ "Theme should fail to load if it contains invalid properties"
+ );
+}
+
+add_task(
+ async function test_that_theme_with_invalid_properties_fails_to_load() {
+ let invalidProps = [
+ "page_action",
+ "browser_action",
+ "background",
+ "permissions",
+ "omnibox",
+ "commands",
+ ];
+ for (let prop in invalidProps) {
+ await testThemeWithInvalidProperties([prop]);
+ }
+ await testThemeWithInvalidProperties(invalidProps);
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_topSites.js b/browser/components/extensions/test/browser/browser_ext_topSites.js
new file mode 100644
index 0000000000..fe0edf2b8d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_topSites.js
@@ -0,0 +1,413 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+
+const {
+ ExtensionUtils: { makeDataURI },
+} = ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs");
+
+// A small 1x1 test png
+const IMAGE_1x1 =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+
+async function updateTopSites(condition) {
+ // Toggle the pref to clear the feed cache and force an update.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ ],
+ });
+
+ // Wait for the feed to be updated.
+ await TestUtils.waitForCondition(() => {
+ let sites = AboutNewTab.getTopSites();
+ return condition(sites);
+ }, "Waiting for top sites to be updated");
+}
+
+async function loadExtension() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["topSites"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async options => {
+ let sites = await browser.topSites.get(options);
+ browser.test.sendMessage("sites", sites);
+ });
+ },
+ });
+ await extension.startup();
+ return extension;
+}
+
+async function getSites(extension, options) {
+ extension.sendMessage(options);
+ return extension.awaitMessage("sites");
+}
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // The pref for TopSites is empty by default.
+ [
+ "browser.newtabpage.activity-stream.default.sites",
+ "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/",
+ ],
+ // Toggle the feed off and on as a workaround to read the new prefs.
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ [
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
+ true,
+ ],
+ ],
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:newtab",
+ false
+ );
+ registerCleanupFunction(() => {
+ BrowserTestUtils.removeTab(tab);
+ });
+});
+
+// Tests newtab links with an empty history.
+add_task(async function test_topSites_newtab_emptyHistory() {
+ let extension = await loadExtension();
+
+ let expectedResults = [
+ {
+ type: "search",
+ url: "https://amazon.com",
+ title: "@amazon",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.youtube.com/",
+ title: "youtube",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.facebook.com/",
+ title: "facebook",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.reddit.com/",
+ title: "reddit",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.wikipedia.org/",
+ title: "wikipedia",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://twitter.com/",
+ title: "twitter",
+ favicon: null,
+ },
+ ];
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites(extension, { newtab: true }),
+ "got topSites newtab links"
+ );
+
+ await extension.unload();
+});
+
+// Tests newtab links with some visits.
+add_task(async function test_topSites_newtab_visits() {
+ let extension = await loadExtension();
+
+ // Add some visits to a couple of URLs. We need to add at least two visits
+ // per URL for it to show up. Add some extra to be safe, and add one more to
+ // the first so that its frecency is larger.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example-1.com/",
+ "http://example-2.com/",
+ ]);
+ }
+ await PlacesTestUtils.addVisits("http://example-1.com/");
+
+ // Wait for example-1.com to be listed second, after the Amazon search link.
+ await updateTopSites(sites => {
+ return sites && sites[1] && sites[1].url == "http://example-1.com/";
+ });
+
+ let expectedResults = [
+ {
+ type: "search",
+ url: "https://amazon.com",
+ title: "@amazon",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "http://example-1.com/",
+ title: "test visit for http://example-1.com/",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "http://example-2.com/",
+ title: "test visit for http://example-2.com/",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.youtube.com/",
+ title: "youtube",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.facebook.com/",
+ title: "facebook",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.reddit.com/",
+ title: "reddit",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.wikipedia.org/",
+ title: "wikipedia",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://twitter.com/",
+ title: "twitter",
+ favicon: null,
+ },
+ ];
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites(extension, { newtab: true }),
+ "got topSites newtab links"
+ );
+
+ await extension.unload();
+ await PlacesUtils.history.clear();
+});
+
+// Tests that the newtab parameter is ignored if newtab Top Sites are disabled.
+add_task(async function test_topSites_newtab_ignored() {
+ let extension = await loadExtension();
+ // Add some visits to a couple of URLs. We need to add at least two visits
+ // per URL for it to show up. Add some extra to be safe, and add one more to
+ // the first so that its frecency is larger.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example-1.com/",
+ "http://example-2.com/",
+ ]);
+ }
+ await PlacesTestUtils.addVisits("http://example-1.com/");
+
+ // Wait for example-1.com to be listed second, after the Amazon search link.
+ await updateTopSites(sites => {
+ return sites && sites[1] && sites[1].url == "http://example-1.com/";
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.newtabpage.activity-stream.feeds.system.topsites", false]],
+ });
+
+ let expectedResults = [
+ {
+ type: "url",
+ url: "http://example-1.com/",
+ title: "test visit for http://example-1.com/",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "http://example-2.com/",
+ title: "test visit for http://example-2.com/",
+ favicon: null,
+ },
+ ];
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites(extension, { newtab: true }),
+ "Got top-frecency links from Places"
+ );
+
+ await SpecialPowers.popPrefEnv();
+ await extension.unload();
+ await PlacesUtils.history.clear();
+});
+
+// Tests newtab links with some visits and favicons.
+add_task(async function test_topSites_newtab_visits_favicons() {
+ let extension = await loadExtension();
+
+ // Add some visits to a couple of URLs. We need to add at least two visits
+ // per URL for it to show up. Add some extra to be safe, and add one more to
+ // the first so that its frecency is larger.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example-1.com/",
+ "http://example-2.com/",
+ ]);
+ }
+ await PlacesTestUtils.addVisits("http://example-1.com/");
+
+ // Give the first URL a favicon but not the second so that we can test links
+ // both with and without favicons.
+ let faviconData = new Map();
+ faviconData.set("http://example-1.com", IMAGE_1x1);
+ await PlacesTestUtils.addFavicons(faviconData);
+
+ // Wait for example-1.com to be listed second, after the Amazon search link.
+ await updateTopSites(sites => {
+ return sites && sites[1] && sites[1].url == "http://example-1.com/";
+ });
+
+ let base = "chrome://activity-stream/content/data/content/tippytop/images/";
+
+ let expectedResults = [
+ {
+ type: "search",
+ url: "https://amazon.com",
+ title: "@amazon",
+ favicon: await makeDataURI(`${base}amazon@2x.png`),
+ },
+ {
+ type: "url",
+ url: "http://example-1.com/",
+ title: "test visit for http://example-1.com/",
+ favicon: IMAGE_1x1,
+ },
+ {
+ type: "url",
+ url: "http://example-2.com/",
+ title: "test visit for http://example-2.com/",
+ favicon: null,
+ },
+ {
+ type: "url",
+ url: "https://www.youtube.com/",
+ title: "youtube",
+ favicon: await makeDataURI(`${base}youtube-com@2x.png`),
+ },
+ {
+ type: "url",
+ url: "https://www.facebook.com/",
+ title: "facebook",
+ favicon: await makeDataURI(`${base}facebook-com@2x.png`),
+ },
+ {
+ type: "url",
+ url: "https://www.reddit.com/",
+ title: "reddit",
+ favicon: await makeDataURI(`${base}reddit-com@2x.png`),
+ },
+ {
+ type: "url",
+ url: "https://www.wikipedia.org/",
+ title: "wikipedia",
+ favicon: await makeDataURI(`${base}wikipedia-org@2x.png`),
+ },
+ {
+ type: "url",
+ url: "https://twitter.com/",
+ title: "twitter",
+ favicon: await makeDataURI(`${base}twitter-com@2x.png`),
+ },
+ ];
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites(extension, { newtab: true, includeFavicon: true }),
+ "got topSites newtab links"
+ );
+
+ await extension.unload();
+ await PlacesUtils.history.clear();
+});
+
+// Tests newtab links with some visits, favicons, and the `limit` option.
+add_task(async function test_topSites_newtab_visits_favicons_limit() {
+ let extension = await loadExtension();
+
+ // Add some visits to a couple of URLs. We need to add at least two visits
+ // per URL for it to show up. Add some extra to be safe, and add one more to
+ // the first so that its frecency is larger.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example-1.com/",
+ "http://example-2.com/",
+ ]);
+ }
+ await PlacesTestUtils.addVisits("http://example-1.com/");
+
+ // Give the first URL a favicon but not the second so that we can test links
+ // both with and without favicons.
+ let faviconData = new Map();
+ faviconData.set("http://example-1.com", IMAGE_1x1);
+ await PlacesTestUtils.addFavicons(faviconData);
+
+ // Wait for example-1.com to be listed second, after the Amazon search link.
+ await updateTopSites(sites => {
+ return sites && sites[1] && sites[1].url == "http://example-1.com/";
+ });
+
+ let expectedResults = [
+ {
+ type: "search",
+ url: "https://amazon.com",
+ title: "@amazon",
+ favicon: await makeDataURI(
+ "chrome://activity-stream/content/data/content/tippytop/images/amazon@2x.png"
+ ),
+ },
+ {
+ type: "url",
+ url: "http://example-1.com/",
+ title: "test visit for http://example-1.com/",
+ favicon: IMAGE_1x1,
+ },
+ {
+ type: "url",
+ url: "http://example-2.com/",
+ title: "test visit for http://example-2.com/",
+ favicon: null,
+ },
+ ];
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites(extension, { newtab: true, includeFavicon: true, limit: 3 }),
+ "got topSites newtab links"
+ );
+
+ await extension.unload();
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js b/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
new file mode 100644
index 0000000000..7d3342044b
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_url_overrides_newtab.js
@@ -0,0 +1,785 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+requestLongerTimeout(4);
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionControlledPopup:
+ "resource:///modules/ExtensionControlledPopup.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "AboutNewTab",
+ "resource:///modules/AboutNewTab.jsm"
+);
+
+function getNotificationSetting(extensionId) {
+ return ExtensionSettingsStore.getSetting("newTabNotification", extensionId);
+}
+
+function getNewTabDoorhanger() {
+ ExtensionControlledPopup._getAndMaybeCreatePanel(document);
+ return document.getElementById("extension-new-tab-notification");
+}
+
+function clickKeepChanges(notification) {
+ notification.button.click();
+}
+
+function clickManage(notification) {
+ notification.secondaryButton.click();
+}
+
+async function promiseNewTab(expectUrl = AboutNewTab.newTabURL, win = window) {
+ let eventName = "browser-open-newtab-start";
+ let newTabStartPromise = new Promise(resolve => {
+ async function observer(subject) {
+ Services.obs.removeObserver(observer, eventName);
+ resolve(subject.wrappedJSObject);
+ }
+ Services.obs.addObserver(observer, eventName);
+ });
+
+ let newtabShown = TestUtils.waitForCondition(
+ () => win.gBrowser.currentURI.spec == expectUrl,
+ `Should open correct new tab url ${expectUrl}.`
+ );
+
+ win.BrowserOpenTab();
+ const newTabCreatedPromise = newTabStartPromise;
+ const browser = await newTabCreatedPromise;
+ await newtabShown;
+ const tab = win.gBrowser.selectedTab;
+
+ Assert.deepEqual(
+ browser,
+ tab.linkedBrowser,
+ "browser-open-newtab-start notified with the created browser"
+ );
+ return tab;
+}
+
+function waitForAddonDisabled(addon) {
+ return new Promise(resolve => {
+ let listener = {
+ onDisabled(disabledAddon) {
+ if (disabledAddon.id == addon.id) {
+ resolve();
+ AddonManager.removeAddonListener(listener);
+ }
+ },
+ };
+ AddonManager.addAddonListener(listener);
+ });
+}
+
+function waitForAddonEnabled(addon) {
+ return new Promise(resolve => {
+ let listener = {
+ onEnabled(enabledAddon) {
+ if (enabledAddon.id == addon.id) {
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ },
+ };
+ AddonManager.addAddonListener(listener);
+ });
+}
+
+// Default test extension data for newtab.
+const extensionData = {
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "newtaburl@mochi.test",
+ },
+ },
+ chrome_url_overrides: {
+ newtab: "newtab.html",
+ },
+ },
+ files: {
+ "newtab.html": "
New tab!
",
+ },
+ useAddonManager: "temporary",
+};
+
+add_task(async function test_new_tab_opens() {
+ let panel = getNewTabDoorhanger().closest("panel");
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ // Simulate opening the newtab open as a user would.
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab(extensionNewTabUrl);
+ await popupShown;
+
+ // This will show a confirmation doorhanger, make sure we don't leave it open.
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_new_tab_ignore_settings() {
+ await ExtensionSettingsStore.initialize();
+ let notification = getNewTabDoorhanger();
+ let panel = notification.closest("panel");
+ let extensionId = "newtabignore@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: extensionId } },
+ browser_action: {
+ default_popup: "ignore.html",
+ default_area: "navbar",
+ },
+ chrome_url_overrides: { newtab: "ignore.html" },
+ },
+ files: { "ignore.html": '
New Tab!
' },
+ useAddonManager: "temporary",
+ });
+
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is initially closed"
+ );
+
+ await extension.startup();
+
+ // Simulate opening the New Tab as a user would.
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab();
+ await popupShown;
+
+ // Ensure the doorhanger is shown and the setting isn't set yet.
+ is(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is open after opening New Tab"
+ );
+ is(gURLBar.focused, false, "The URL bar is not focused with a doorhanger");
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not set for this extension"
+ );
+ is(
+ panel.anchorNode.closest("toolbarbutton").id,
+ "newtabignore_mochi_test-BAP",
+ "The doorhanger is anchored to the browser action"
+ );
+
+ // Manually close the panel, as if the user ignored it.
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+
+ // Ensure panel is closed and the setting still isn't set.
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is closed"
+ );
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not set after ignoring the doorhanger"
+ );
+
+ // Close the first tab and open another new tab.
+ BrowserTestUtils.removeTab(tab);
+ tab = await promiseNewTab();
+
+ // Verify the doorhanger is not shown a second time.
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel doesn't open after ignoring the doorhanger"
+ );
+ is(gURLBar.focused, true, "The URL bar is focused with no doorhanger");
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_new_tab_keep_settings() {
+ await ExtensionSettingsStore.initialize();
+ let notification = getNewTabDoorhanger();
+ let panel = notification.closest("panel");
+ let extensionId = "newtabkeep@mochi.test";
+ let manifest = {
+ version: "1.0",
+ name: "New Tab Add-on",
+ browser_specific_settings: { gecko: { id: extensionId } },
+ chrome_url_overrides: { newtab: "newtab.html" },
+ };
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+ manifest,
+ useAddonManager: "permanent",
+ });
+
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is initially closed"
+ );
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ // Simulate opening the New Tab as a user would.
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab(extensionNewTabUrl);
+ await popupShown;
+
+ // Ensure the panel is open and the setting isn't saved yet.
+ is(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is open after opening New Tab"
+ );
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not set for this extension"
+ );
+ is(
+ panel.anchorNode.closest("toolbarbutton").id,
+ "PanelUI-menu-button",
+ "The doorhanger is anchored to the menu icon"
+ );
+ is(
+ panel.querySelector("#extension-new-tab-notification-description")
+ .textContent,
+ "An extension, New Tab Add-on, changed the page you see when you open a new tab.Learn more",
+ "The description includes the add-on name"
+ );
+
+ // Click the Keep Changes button.
+ let confirmationSaved = TestUtils.waitForCondition(() => {
+ return ExtensionSettingsStore.getSetting(
+ "newTabNotification",
+ extensionId,
+ extensionId
+ ).value;
+ });
+ clickKeepChanges(notification);
+ await confirmationSaved;
+
+ // Ensure panel is closed and setting is updated.
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is closed after click"
+ );
+ is(
+ getNotificationSetting(extensionId).value,
+ true,
+ "The New Tab notification is set after keeping the changes"
+ );
+
+ // Close the first tab and open another new tab.
+ BrowserTestUtils.removeTab(tab);
+ tab = await promiseNewTab(extensionNewTabUrl);
+
+ // Verify the doorhanger is not shown a second time.
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is not opened after keeping the changes"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ let upgradedExtension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+ manifest: Object.assign({}, manifest, { version: "2.0" }),
+ useAddonManager: "permanent",
+ });
+
+ await upgradedExtension.startup();
+ extensionNewTabUrl = `moz-extension://${upgradedExtension.uuid}/newtab.html`;
+
+ tab = await promiseNewTab(extensionNewTabUrl);
+
+ // Ensure panel is closed and setting is still set.
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is closed after click"
+ );
+ is(
+ getNotificationSetting(extensionId).value,
+ true,
+ "The New Tab notification is set after keeping the changes"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await upgradedExtension.unload();
+ await extension.unload();
+
+ let confirmation = ExtensionSettingsStore.getSetting(
+ "newTabNotification",
+ extensionId,
+ extensionId
+ );
+ is(confirmation, null, "The confirmation has been cleaned up");
+});
+
+add_task(async function test_new_tab_restore_settings() {
+ await ExtensionSettingsStore.initialize();
+ let notification = getNewTabDoorhanger();
+ let panel = notification.closest("panel");
+ let extensionId = "newtabrestore@mochi.test";
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: extensionId } },
+ chrome_url_overrides: { newtab: "restore.html" },
+ },
+ files: { "restore.html": '
New Tab!
' },
+ useAddonManager: "temporary",
+ });
+
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is initially closed"
+ );
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not initially set for this extension"
+ );
+
+ await extension.startup();
+
+ // Simulate opening the newtab open as a user would.
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab();
+ await popupShown;
+
+ // Verify that the panel is open and add-on is enabled.
+ let addon = await AddonManager.getAddonByID(extensionId);
+ is(addon.userDisabled, false, "The add-on is enabled at first");
+ is(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is open after opening New Tab"
+ );
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not set for this extension"
+ );
+
+ // Click the Manage button.
+ let preferencesShown = TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == "about:preferences#home",
+ "Should open about:preferences."
+ );
+
+ let popupHidden = promisePopupHidden(panel);
+ clickManage(notification);
+ await popupHidden;
+ await preferencesShown;
+
+ // Ensure panel is closed, settings haven't changed and add-on is disabled.
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is closed after click"
+ );
+
+ is(
+ getNotificationSetting(extensionId),
+ null,
+ "The New Tab notification is not set after clicking manage"
+ );
+
+ // Reopen a browser tab and verify that there's no doorhanger.
+ BrowserTestUtils.removeTab(tab);
+ tab = await promiseNewTab();
+
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is not opened after keeping the changes"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_new_tab_restore_settings_multiple() {
+ await ExtensionSettingsStore.initialize();
+ let notification = getNewTabDoorhanger();
+ let panel = notification.closest("panel");
+ let extensionOneId = "newtabrestoreone@mochi.test";
+ let extensionOne = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: extensionOneId } },
+ chrome_url_overrides: { newtab: "restore-one.html" },
+ },
+ files: {
+ "restore-one.html": `
+
' },
+ useAddonManager: "temporary",
+ });
+
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is initially closed"
+ );
+ is(
+ getNotificationSetting(extensionOneId),
+ null,
+ "The New Tab notification is not initially set for this extension"
+ );
+ is(
+ getNotificationSetting(extensionTwoId),
+ null,
+ "The New Tab notification is not initially set for this extension"
+ );
+
+ await extensionOne.startup();
+ await extensionTwo.startup();
+
+ // Simulate opening the newtab open as a user would.
+ let popupShown = promisePopupShown(panel);
+ let tab1 = await promiseNewTab();
+ await popupShown;
+
+ // Verify that the panel is open and add-on is enabled.
+ let addonTwo = await AddonManager.getAddonByID(extensionTwoId);
+ is(addonTwo.userDisabled, false, "The add-on is enabled at first");
+ is(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is open after opening New Tab"
+ );
+ is(
+ getNotificationSetting(extensionTwoId),
+ null,
+ "The New Tab notification is not set for this extension"
+ );
+
+ // Click the Manage button.
+ let popupHidden = promisePopupHidden(panel);
+ let preferencesShown = TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == "about:preferences#home",
+ "Should open about:preferences."
+ );
+ clickManage(notification);
+ await popupHidden;
+ await preferencesShown;
+
+ // Disable the second addon then refresh the new tab expect to see a new addon dropdown.
+ let addonDisabled = waitForAddonDisabled(addonTwo);
+ addonTwo.disable();
+ await addonDisabled;
+
+ // Ensure the panel opens again for the next add-on.
+ popupShown = promisePopupShown(panel);
+ let newtabShown = TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == AboutNewTab.newTabURL,
+ "Should open correct new tab url."
+ );
+ let tab2 = await promiseNewTab();
+ await newtabShown;
+ await popupShown;
+
+ is(
+ getNotificationSetting(extensionTwoId),
+ null,
+ "The New Tab notification is not set after restoring the settings"
+ );
+ let addonOne = await AddonManager.getAddonByID(extensionOneId);
+ is(
+ addonOne.userDisabled,
+ false,
+ "The extension is enabled before making a choice"
+ );
+ is(
+ getNotificationSetting(extensionOneId),
+ null,
+ "The New Tab notification is not set before making a choice"
+ );
+ is(
+ panel.getAttribute("panelopen"),
+ "true",
+ "The notification panel is open after opening New Tab"
+ );
+ is(
+ gBrowser.currentURI.spec,
+ AboutNewTab.newTabURL,
+ "The user is now on the next extension's New Tab page"
+ );
+
+ preferencesShown = TestUtils.waitForCondition(
+ () => gBrowser.currentURI.spec == "about:preferences#home",
+ "Should open about:preferences."
+ );
+ popupHidden = promisePopupHidden(panel);
+ clickManage(notification);
+ await popupHidden;
+ await preferencesShown;
+ // remove the extra preferences tab.
+ BrowserTestUtils.removeTab(tab2);
+
+ addonDisabled = waitForAddonDisabled(addonOne);
+ addonOne.disable();
+ await addonDisabled;
+ tab2 = await promiseNewTab();
+
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is closed after restoring the second time"
+ );
+ is(
+ getNotificationSetting(extensionOneId),
+ null,
+ "The New Tab notification is not set after restoring the settings"
+ );
+ is(
+ gBrowser.currentURI.spec,
+ "about:newtab",
+ "The user is now on the original New Tab URL since all extensions are disabled"
+ );
+
+ // Reopen a browser tab and verify that there's no doorhanger.
+ BrowserTestUtils.removeTab(tab2);
+ tab2 = await promiseNewTab();
+
+ ok(
+ panel.getAttribute("panelopen") != "true",
+ "The notification panel is not opened after keeping the changes"
+ );
+
+ // FIXME: We need to enable the add-on so it gets cleared from the
+ // ExtensionSettingsStore for now. See bug 1408226.
+ let addonsEnabled = Promise.all([
+ waitForAddonEnabled(addonOne),
+ waitForAddonEnabled(addonTwo),
+ ]);
+ await addonOne.enable();
+ await addonTwo.enable();
+ await addonsEnabled;
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+
+ await extensionOne.unload();
+ await extensionTwo.unload();
+});
+
+/**
+ * Ensure we don't show the extension URL in the URL bar temporarily in new tabs
+ * while we're switching remoteness (when the URL we're loading and the
+ * default content principal are different).
+ */
+add_task(async function dontTemporarilyShowAboutExtensionPath() {
+ await ExtensionSettingsStore.initialize();
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ let wpl = {
+ onLocationChange() {
+ is(gURLBar.value, "", "URL bar value should stay empty.");
+ },
+ };
+ gBrowser.addProgressListener(wpl);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: extensionNewTabUrl,
+ });
+
+ gBrowser.removeProgressListener(wpl);
+ is(gURLBar.value, "", "URL bar value should be empty.");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], function () {
+ is(
+ content.document.body.textContent,
+ "New tab!",
+ "New tab page is loaded."
+ );
+ });
+
+ BrowserTestUtils.removeTab(tab);
+ await extension.unload();
+});
+
+add_task(async function test_overriding_newtab_incognito_not_allowed() {
+ let panel = getNewTabDoorhanger().closest("panel");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+ useAddonManager: "permanent",
+ });
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab(extensionNewTabUrl);
+ await popupShown;
+
+ // This will show a confirmation doorhanger, make sure we don't leave it open.
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Verify a private window does not open the extension page. We would
+ // get an extra notification that we don't listen for if it gets loaded.
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow({ private: true });
+ await windowOpenedPromise;
+
+ await promiseNewTab("about:privatebrowsing", win);
+
+ is(win.gURLBar.value, "", "newtab not used in private window");
+
+ // Verify setting the pref directly doesn't bypass permissions.
+ let origUrl = AboutNewTab.newTabURL;
+ AboutNewTab.newTabURL = extensionNewTabUrl;
+ await promiseNewTab("about:privatebrowsing", win);
+
+ is(win.gURLBar.value, "", "directly set newtab not used in private window");
+
+ AboutNewTab.newTabURL = origUrl;
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_overriding_newtab_incognito_spanning() {
+ let extension = ExtensionTestUtils.loadExtension({
+ ...extensionData,
+ useAddonManager: "permanent",
+ incognitoOverride: "spanning",
+ });
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ let windowOpenedPromise = BrowserTestUtils.waitForNewWindow();
+ let win = OpenBrowserWindow({ private: true });
+ await windowOpenedPromise;
+ let panel = ExtensionControlledPopup._getAndMaybeCreatePanel(win.document);
+ let popupShown = promisePopupShown(panel);
+ await promiseNewTab(extensionNewTabUrl, win);
+ await popupShown;
+
+ // This will show a confirmation doorhanger, make sure we don't leave it open.
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
+
+// Test that prefs set by the newtab override code are
+// properly unset when all newtab extensions are gone.
+add_task(async function testNewTabPrefsReset() {
+ function isUndefinedPref(pref) {
+ try {
+ Services.prefs.getBoolPref(pref);
+ return false;
+ } catch (e) {
+ return true;
+ }
+ }
+
+ ok(
+ isUndefinedPref("browser.newtab.extensionControlled"),
+ "extensionControlled pref is not set"
+ );
+ ok(
+ isUndefinedPref("browser.newtab.privateAllowed"),
+ "privateAllowed pref is not set"
+ );
+});
+
+// This test ensures that an extension provided newtab
+// can be opened by another extension (e.g. tab manager)
+// regardless of whether the newtab url is made available
+// in web_accessible_resources.
+add_task(async function test_newtab_from_extension() {
+ let panel = getNewTabDoorhanger().closest("panel");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "newtaburl@mochi.test",
+ },
+ },
+ chrome_url_overrides: {
+ newtab: "newtab.html",
+ },
+ },
+ files: {
+ "newtab.html": `
New tab!
`,
+ "newtab.js": () => {
+ browser.test.sendMessage("newtab-loaded");
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ let extensionNewTabUrl = `moz-extension://${extension.uuid}/newtab.html`;
+
+ let popupShown = promisePopupShown(panel);
+ let tab = await promiseNewTab(extensionNewTabUrl);
+ await popupShown;
+
+ // This will show a confirmation doorhanger, make sure we don't leave it open.
+ let popupHidden = promisePopupHidden(panel);
+ panel.hidePopup();
+ await popupHidden;
+
+ BrowserTestUtils.removeTab(tab);
+
+ // extension to open the newtab
+ let opener = ExtensionTestUtils.loadExtension({
+ async background() {
+ let newtab = await browser.tabs.create({});
+ browser.test.assertTrue(
+ newtab.id !== browser.tabs.TAB_ID_NONE,
+ "New tab was created."
+ );
+ await browser.tabs.remove(newtab.id);
+ browser.test.sendMessage("complete");
+ },
+ });
+
+ function listener(msg) {
+ Assert.ok(!/may not load or link to moz-extension/.test(msg.message));
+ }
+ Services.console.registerListener(listener);
+ registerCleanupFunction(() => {
+ Services.console.unregisterListener(listener);
+ });
+
+ await opener.startup();
+ await opener.awaitMessage("complete");
+ await extension.awaitMessage("newtab-loaded");
+ await opener.unload();
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_urlbar.js b/browser/components/extensions/test/browser/browser_ext_urlbar.js
new file mode 100644
index 0000000000..5a2c84e5e7
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_urlbar.js
@@ -0,0 +1,606 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
+ const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/UrlbarTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+async function loadTipExtension(options = {}) {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ background() {
+ browser.test.onMessage.addListener(options => {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "restricting";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "tip",
+ source: "local",
+ heuristic: true,
+ payload: {
+ text: "Test",
+ buttonText: "OK",
+ buttonUrl: options.buttonUrl,
+ helpUrl: options.helpUrl,
+ },
+ },
+ ];
+ }, "test");
+ browser.urlbar.onResultPicked.addListener((payload, details) => {
+ browser.test.assertEq(payload.text, "Test", "payload.text");
+ browser.test.assertEq(payload.buttonText, "OK", "payload.buttonText");
+ browser.test.sendMessage("onResultPicked received", details);
+ }, "test");
+ });
+ },
+ });
+ await ext.startup();
+ ext.sendMessage(options);
+
+ // Wait for the provider to be registered before continuing. The provider
+ // will be registered once the parent process receives the first addListener
+ // call from the extension. There's no better way to do this, unfortunately.
+ // For example, if the extension sends a message to the test after it adds its
+ // listeners and then we wait here for that message, there's no guarantee that
+ // the addListener calls will have been received in the parent yet.
+ await BrowserTestUtils.waitForCondition(
+ () => UrlbarProvidersManager.getProvider("test"),
+ "Waiting for provider to be registered"
+ );
+
+ Assert.ok(
+ UrlbarProvidersManager.getProvider("test"),
+ "Provider should have been registered"
+ );
+ return ext;
+}
+
+/**
+ * Updates the Top Sites feed.
+ *
+ * @param {Function} condition
+ * A callback that returns true after Top Sites are successfully updated.
+ * @param {boolean} searchShortcuts
+ * True if Top Sites search shortcuts should be enabled.
+ */
+async function updateTopSites(condition, searchShortcuts = false) {
+ // Toggle the pref to clear the feed cache and force an update.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ [
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
+ searchShortcuts,
+ ],
+ ],
+ });
+
+ // Wait for the feed to be updated.
+ await TestUtils.waitForCondition(() => {
+ let sites = AboutNewTab.getTopSites();
+ return condition(sites);
+ }, "Waiting for top sites to be updated");
+}
+
+add_setup(async function () {
+ Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
+ });
+ // Set the notification timeout to a really high value to avoid intermittent
+ // failures due to the mock extensions not responding in time.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.extension.timeout", 5000]],
+ });
+});
+
+// Loads a tip extension without a main button URL and presses enter on the main
+// button.
+add_task(async function tip_onResultPicked_mainButton_noURL_enter() {
+ let ext = await loadTipExtension();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await ext.awaitMessage("onResultPicked received");
+ await ext.unload();
+});
+
+// Loads a tip extension without a main button URL and clicks the main button.
+add_task(async function tip_onResultPicked_mainButton_noURL_mouse() {
+ let ext = await loadTipExtension();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ });
+ let mainButton = gURLBar.querySelector(".urlbarView-button-tip");
+ Assert.ok(mainButton);
+ EventUtils.synthesizeMouseAtCenter(mainButton, {});
+ await ext.awaitMessage("onResultPicked received");
+ await ext.unload();
+});
+
+// Loads a tip extension with a main button URL and presses enter on the main
+// button.
+add_task(async function tip_onResultPicked_mainButton_url_enter() {
+ let ext = await loadTipExtension({ buttonUrl: "http://example.com/" });
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ });
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ ext.onMessage("onResultPicked received", () => {
+ Assert.ok(false, "onResultPicked should not be called");
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadedPromise;
+ Assert.equal(gBrowser.currentURI.spec, "http://example.com/");
+ });
+ await ext.unload();
+});
+
+// Loads a tip extension with a main button URL and clicks the main button.
+add_task(async function tip_onResultPicked_mainButton_url_mouse() {
+ let ext = await loadTipExtension({ buttonUrl: "http://example.com/" });
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ });
+ let mainButton = gURLBar.querySelector(".urlbarView-button-tip");
+ Assert.ok(mainButton);
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ ext.onMessage("onResultPicked received", () => {
+ Assert.ok(false, "onResultPicked should not be called");
+ });
+ EventUtils.synthesizeMouseAtCenter(mainButton, {});
+ await loadedPromise;
+ Assert.equal(gBrowser.currentURI.spec, "http://example.com/");
+ });
+ await ext.unload();
+});
+
+// Loads a tip extension with a help button URL and presses enter on the help
+// button.
+add_task(async function tip_onResultPicked_helpButton_url_enter() {
+ let ext = await loadTipExtension({ helpUrl: "http://example.com/" });
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ });
+ ext.onMessage("onResultPicked received", () => {
+ Assert.ok(false, "onResultPicked should not be called");
+ });
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ if (UrlbarPrefs.get("resultMenu")) {
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h");
+ } else {
+ EventUtils.synthesizeKey("KEY_Tab");
+ EventUtils.synthesizeKey("KEY_Enter");
+ }
+ info("Waiting for help URL to load");
+ await loadedPromise;
+ Assert.equal(gBrowser.currentURI.spec, "http://example.com/");
+ });
+ await ext.unload();
+});
+
+// Loads a tip extension with a help button URL and clicks the help button.
+add_task(async function tip_onResultPicked_helpButton_url_mouse() {
+ let ext = await loadTipExtension({ helpUrl: "http://example.com/" });
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ });
+ ext.onMessage("onResultPicked received", () => {
+ Assert.ok(false, "onResultPicked should not be called");
+ });
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ if (UrlbarPrefs.get("resultMenu")) {
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h", {
+ openByMouse: true,
+ });
+ } else {
+ let helpButton = gURLBar.querySelector(".urlbarView-button-help");
+ Assert.ok(helpButton);
+ EventUtils.synthesizeMouseAtCenter(helpButton, {});
+ }
+ info("Waiting for help URL to load");
+ await loadedPromise;
+ Assert.equal(gBrowser.currentURI.spec, "http://example.com/");
+ });
+ await ext.unload();
+});
+
+// Tests the search function with a non-empty string.
+add_task(async function search() {
+ gURLBar.blur();
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ background: () => {
+ browser.urlbar.search("test");
+ },
+ });
+ await ext.startup();
+
+ let context = await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(gURLBar.value, "test");
+ Assert.equal(context.searchString, "test");
+ Assert.ok(gURLBar.focused);
+ Assert.equal(gURLBar.getAttribute("focused"), "true");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await ext.unload();
+});
+
+// Tests the search function with an empty string.
+add_task(async function searchEmpty() {
+ gURLBar.blur();
+
+ // Searching for an empty string shows the history view, but there may be no
+ // history here since other tests may have cleared it or since this test is
+ // running in isolation. We want to make sure providers are called and their
+ // results are shown, so add a provider that returns a tip.
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "restricting";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "tip",
+ source: "local",
+ heuristic: true,
+ payload: {
+ text: "Test",
+ buttonText: "OK",
+ },
+ },
+ ];
+ }, "test");
+ browser.urlbar.search("");
+ },
+ });
+ await ext.startup();
+
+ await BrowserTestUtils.waitForCondition(
+ () => UrlbarProvidersManager.getProvider("test"),
+ "Waiting for provider to be registered"
+ );
+
+ let context = await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(gURLBar.value, "");
+ Assert.equal(context.searchString, "");
+ Assert.equal(context.results.length, 1);
+ Assert.equal(context.results[0].type, UrlbarUtils.RESULT_TYPE.TIP);
+ Assert.ok(gURLBar.focused);
+ Assert.equal(gURLBar.getAttribute("focused"), "true");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await ext.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests the search function with `focus: false`.
+add_task(async function searchFocusFalse() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesTestUtils.addVisits([
+ "http://example.com/test1",
+ "http://example.com/test2",
+ ]);
+
+ gURLBar.blur();
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ background: () => {
+ browser.urlbar.search("test", { focus: false });
+ },
+ });
+ await ext.startup();
+
+ let context = await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(gURLBar.value, "test");
+ Assert.equal(context.searchString, "test");
+ Assert.ok(!gURLBar.focused);
+ Assert.ok(!gURLBar.hasAttribute("focused"));
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.equal(resultCount, 3);
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(result.title, "test");
+
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(result.url, "http://example.com/test2");
+
+ result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(result.url, "http://example.com/test1");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await ext.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests the search function with `focus: false` and an empty string.
+add_task(async function searchFocusFalseEmpty() {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(["http://example.com/test1"]);
+ }
+ await updateTopSites(sites => sites.length == 1);
+ gURLBar.blur();
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ background: () => {
+ browser.urlbar.search("", { focus: false });
+ },
+ });
+ await ext.startup();
+
+ let context = await UrlbarTestUtils.promiseSearchComplete(window);
+ Assert.equal(gURLBar.value, "");
+ Assert.equal(context.searchString, "");
+ Assert.ok(!gURLBar.focused);
+ Assert.ok(!gURLBar.hasAttribute("focused"));
+
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+ Assert.equal(resultCount, 1);
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
+ Assert.equal(result.url, "http://example.com/test1");
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await ext.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests the focus function with select = false.
+add_task(async function focusSelectFalse() {
+ gURLBar.blur();
+ gURLBar.value = "test";
+ Assert.ok(!gURLBar.focused);
+ Assert.ok(!gURLBar.hasAttribute("focused"));
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ background: () => {
+ browser.urlbar.focus();
+ },
+ });
+ await ext.startup();
+
+ await TestUtils.waitForCondition(() => gURLBar.focused);
+ Assert.ok(gURLBar.focused);
+ Assert.ok(gURLBar.hasAttribute("focused"));
+ Assert.equal(gURLBar.selectionStart, gURLBar.selectionEnd);
+
+ await ext.unload();
+});
+
+// Tests the focus function with select = true.
+add_task(async function focusSelectTrue() {
+ gURLBar.blur();
+ gURLBar.value = "test";
+ Assert.ok(!gURLBar.focused);
+ Assert.ok(!gURLBar.hasAttribute("focused"));
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ background: () => {
+ browser.urlbar.focus(true);
+ },
+ });
+ await ext.startup();
+
+ await TestUtils.waitForCondition(() => gURLBar.focused);
+ Assert.ok(gURLBar.focused);
+ Assert.ok(gURLBar.hasAttribute("focused"));
+ Assert.equal(gURLBar.selectionStart, 0);
+ Assert.equal(gURLBar.selectionEnd, "test".length);
+
+ await ext.unload();
+});
+
+// Tests the closeView function.
+add_task(async function closeView() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ });
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ background: () => {
+ browser.urlbar.closeView();
+ },
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => ext.startup());
+ await ext.unload();
+});
+
+// Tests the onEngagement events.
+add_task(async function onEngagement() {
+ gURLBar.blur();
+
+ // Enable engagement telemetry.
+ Services.prefs.setBoolPref("browser.urlbar.eventTelemetry.enabled", true);
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ background() {
+ browser.urlbar.onEngagement.addListener(state => {
+ browser.test.sendMessage("onEngagement", state);
+ }, "test");
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "restricting";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "tip",
+ source: "local",
+ heuristic: true,
+ payload: {
+ text: "Test",
+ buttonText: "OK",
+ },
+ },
+ ];
+ }, "test");
+ browser.urlbar.search("");
+ },
+ });
+ await ext.startup();
+
+ // Start an engagement.
+ let messagePromise = ext.awaitMessage("onEngagement");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ fireInputEvent: true,
+ });
+ let state = await messagePromise;
+ Assert.equal(state, "start");
+
+ // Abandon the engagement.
+ messagePromise = ext.awaitMessage("onEngagement");
+ gURLBar.blur();
+ state = await messagePromise;
+ Assert.equal(state, "abandonment");
+
+ // Start an engagement.
+ messagePromise = ext.awaitMessage("onEngagement");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ fireInputEvent: true,
+ });
+ state = await messagePromise;
+ Assert.equal(state, "start");
+
+ // End the engagement by pressing enter on the extension's tip result.
+ messagePromise = ext.awaitMessage("onEngagement");
+ EventUtils.synthesizeKey("KEY_Enter");
+ state = await messagePromise;
+ Assert.equal(state, "engagement");
+
+ // We'll open about:preferences next. Since it won't open in a new tab if the
+ // current tab is blank, open a new tab now.
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Start an engagement.
+ messagePromise = ext.awaitMessage("onEngagement");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ fireInputEvent: true,
+ });
+ state = await messagePromise;
+ Assert.equal(state, "start");
+
+ // Press up and enter to pick the search settings button.
+ messagePromise = ext.awaitMessage("onEngagement");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ "about:preferences#search"
+ );
+ state = await messagePromise;
+ Assert.equal(state, "discard");
+ });
+
+ // Start a final engagement to make sure the previous discard didn't mess
+ // anything up.
+ messagePromise = ext.awaitMessage("onEngagement");
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value: "test",
+ fireInputEvent: true,
+ });
+ state = await messagePromise;
+ Assert.equal(state, "start");
+
+ // End the engagement by pressing enter on the extension's tip result.
+ messagePromise = ext.awaitMessage("onEngagement");
+ EventUtils.synthesizeKey("KEY_Enter");
+ state = await messagePromise;
+ Assert.equal(state, "engagement");
+
+ await ext.unload();
+ Services.prefs.clearUserPref("browser.urlbar.eventTelemetry.enabled");
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_user_events.js b/browser/components/extensions/test/browser/browser_ext_user_events.js
new file mode 100644
index 0000000000..4852ffd124
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_user_events.js
@@ -0,0 +1,271 @@
+"use strict";
+
+// Test that different types of events are all considered
+// "handling user input".
+add_task(async function testSources() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ async function request(perm) {
+ try {
+ let result = await browser.permissions.request({
+ permissions: [perm],
+ });
+ browser.test.sendMessage("request", { success: true, result, perm });
+ } catch (err) {
+ browser.test.sendMessage("request", {
+ success: false,
+ errmsg: err.message,
+ perm,
+ });
+ }
+ }
+
+ let tabs = await browser.tabs.query({
+ active: true,
+ currentWindow: true,
+ });
+ await browser.pageAction.show(tabs[0].id);
+
+ browser.pageAction.onClicked.addListener(() => request("bookmarks"));
+ browser.browserAction.onClicked.addListener(() => request("tabs"));
+ browser.commands.onCommand.addListener(() => request("downloads"));
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "contextMenus.update") {
+ browser.contextMenus.onClicked.addListener(() =>
+ request("webNavigation")
+ );
+ browser.contextMenus.update(
+ "menu",
+ {
+ title: "test user events in onClicked",
+ onclick: null,
+ },
+ () => browser.test.sendMessage("contextMenus.update-done")
+ );
+ }
+ if (msg === "openOptionsPage") {
+ browser.runtime.openOptionsPage();
+ }
+ });
+
+ browser.contextMenus.create(
+ {
+ id: "menu",
+ title: "test user events in onclick",
+ contexts: ["page"],
+ onclick() {
+ request("cookies");
+ },
+ },
+ () => {
+ browser.test.sendMessage("actions-ready");
+ }
+ );
+ },
+
+ files: {
+ "options.html": `
+
+
+
+
+
+
+
+ Link
+
+ `,
+
+ "options.js"() {
+ addEventListener("load", async () => {
+ let link = document.getElementById("link");
+ link.onclick = async event => {
+ link.onclick = null;
+ event.preventDefault();
+
+ browser.test.log("Calling permission.request from options page.");
+
+ let perm = "history";
+ try {
+ let result = await browser.permissions.request({
+ permissions: [perm],
+ });
+ browser.test.sendMessage("request", {
+ success: true,
+ result,
+ perm,
+ });
+ } catch (err) {
+ browser.test.sendMessage("request", {
+ success: false,
+ errmsg: err.message,
+ perm,
+ });
+ }
+ };
+
+ // Make a few trips through the event loop to make sure the
+ // options browser is fully visible. This is a bit dodgy, but
+ // we don't really have a reliable way to detect this from the
+ // options page side, and synthetic click events won't work
+ // until it is.
+ do {
+ browser.test.log(
+ "Waiting for the options browser to be visible..."
+ );
+ await new Promise(resolve => setTimeout(resolve, 0));
+ synthesizeMouseAtCenter(link, {});
+ } while (link.onclick !== null);
+ });
+ },
+ },
+
+ manifest: {
+ browser_action: {
+ default_title: "test",
+ default_area: "navbar",
+ },
+ page_action: { default_title: "test" },
+ permissions: ["contextMenus"],
+ optional_permissions: [
+ "bookmarks",
+ "tabs",
+ "webNavigation",
+ "history",
+ "cookies",
+ "downloads",
+ ],
+ options_ui: { page: "options.html" },
+ content_security_policy:
+ "script-src 'self' https://example.com; object-src 'none';",
+ commands: {
+ command: {
+ suggested_key: {
+ default: "Alt+Shift+J",
+ },
+ },
+ },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ async function testPermissionRequest(
+ { requestPermission, expectPrompt, perm },
+ what
+ ) {
+ info(`check request permission from '${what}'`);
+
+ let promptPromise = null;
+ if (expectPrompt) {
+ promptPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ ).then(panel => {
+ panel.button.click();
+ });
+ }
+
+ await requestPermission();
+ await promptPromise;
+
+ let result = await extension.awaitMessage("request");
+ ok(result.success, `request() did not throw when called from ${what}`);
+ is(result.result, true, `request() succeeded when called from ${what}`);
+ is(result.perm, perm, `requested permission ${what}`);
+ await promptPromise;
+ }
+
+ // Remove Sidebar button to prevent pushing extension button to overflow menu
+ CustomizableUI.removeWidgetFromArea("sidebar-button");
+
+ await extension.startup();
+ await extension.awaitMessage("actions-ready");
+
+ await testPermissionRequest(
+ {
+ requestPermission: () => clickPageAction(extension),
+ expectPrompt: true,
+ perm: "bookmarks",
+ },
+ "page action click"
+ );
+
+ await testPermissionRequest(
+ {
+ requestPermission: () => clickBrowserAction(extension),
+ expectPrompt: true,
+ perm: "tabs",
+ },
+ "browser action click"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ gBrowser.selectedTab = tab;
+
+ await testPermissionRequest(
+ {
+ requestPermission: async () => {
+ let menu = await openContextMenu("body");
+ let items = menu.getElementsByAttribute(
+ "label",
+ "test user events in onclick"
+ );
+ is(items.length, 1, "Found context menu item");
+ menu.activateItem(items[0]);
+ },
+ expectPrompt: false, // cookies permission has no prompt.
+ perm: "cookies",
+ },
+ "context menu in onclick"
+ );
+
+ extension.sendMessage("contextMenus.update");
+ await extension.awaitMessage("contextMenus.update-done");
+
+ await testPermissionRequest(
+ {
+ requestPermission: async () => {
+ let menu = await openContextMenu("body");
+ let items = menu.getElementsByAttribute(
+ "label",
+ "test user events in onClicked"
+ );
+ is(items.length, 1, "Found context menu item again");
+ menu.activateItem(items[0]);
+ },
+ expectPrompt: true,
+ perm: "webNavigation",
+ },
+ "context menu in onClicked"
+ );
+
+ await testPermissionRequest(
+ {
+ requestPermission: () => {
+ EventUtils.synthesizeKey("j", { altKey: true, shiftKey: true });
+ },
+ expectPrompt: true,
+ perm: "downloads",
+ },
+ "commands shortcut"
+ );
+
+ await testPermissionRequest(
+ {
+ requestPermission: () => {
+ extension.sendMessage("openOptionsPage");
+ },
+ expectPrompt: true,
+ perm: "history",
+ },
+ "options page link click"
+ );
+
+ await BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ await BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+
+ registerCleanupFunction(() => CustomizableUI.reset());
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_containerIsolation.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_containerIsolation.js
new file mode 100644
index 0000000000..3bf849b518
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_containerIsolation.js
@@ -0,0 +1,169 @@
+"use strict";
+
+add_task(async function containerIsolation_restricted() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.userContextIsolation.enabled", true],
+ ["privacy.userContext.enabled", true],
+ ],
+ });
+
+ let helperExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies", "webNavigation"],
+ },
+
+ async background() {
+ browser.webNavigation.onCompleted.addListener(details => {
+ browser.test.sendMessage("tabCreated", details.tabId);
+ });
+ browser.test.onMessage.addListener(async message => {
+ switch (message.subject) {
+ case "createTab": {
+ await browser.tabs.create({
+ url: message.data.url,
+ cookieStoreId: message.data.cookieStoreId,
+ });
+ break;
+ }
+ }
+ });
+ },
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+
+ async background() {
+ let eventNames = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ ];
+
+ const initialEmptyTabs = await browser.tabs.query({ active: true });
+ browser.test.assertEq(
+ 1,
+ initialEmptyTabs.length,
+ `Got one initial empty tab as expected: ${JSON.stringify(
+ initialEmptyTabs
+ )}`
+ );
+
+ for (let eventName of eventNames) {
+ browser.webNavigation[eventName].addListener(details => {
+ if (details.tabId === initialEmptyTabs[0].id) {
+ // Ignore webNavigation related to the initial about:blank tab, it may be technically
+ // still being loading when we start this test extension to run the test scenario.
+ return;
+ }
+ browser.test.assertEq(
+ "http://www.example.com/?allowed",
+ details.url,
+ `expected ${eventName} event`
+ );
+ browser.test.sendMessage(eventName, details.tabId);
+ });
+ }
+
+ const [restrictedTab, unrestrictedTab, noContainerTab] =
+ await new Promise(resolve => {
+ browser.test.onMessage.addListener(message => resolve(message));
+ });
+
+ await browser.test.assertRejects(
+ browser.webNavigation.getFrame({
+ tabId: restrictedTab,
+ frameId: 0,
+ }),
+ `Invalid tab ID: ${restrictedTab}`,
+ "getFrame rejected Promise should pass the expected error"
+ );
+
+ await browser.test.assertRejects(
+ browser.webNavigation.getAllFrames({ tabId: restrictedTab }),
+ `Invalid tab ID: ${restrictedTab}`,
+ "getAllFrames rejected Promise should pass the expected error"
+ );
+
+ await browser.tabs.remove(unrestrictedTab);
+ await browser.tabs.remove(noContainerTab);
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [`extensions.userContextIsolation.${extension.id}.restricted`, "[1]"],
+ ],
+ });
+
+ await helperExtension.startup();
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: {
+ url: "http://www.example.com/?restricted",
+ cookieStoreId: "firefox-container-1",
+ },
+ });
+
+ const restrictedTab = await helperExtension.awaitMessage("tabCreated");
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: {
+ url: "http://www.example.com/?allowed",
+ cookieStoreId: "firefox-container-2",
+ },
+ });
+
+ const unrestrictedTab = await helperExtension.awaitMessage("tabCreated");
+
+ helperExtension.sendMessage({
+ subject: "createTab",
+ data: {
+ url: "http://www.example.com/?allowed",
+ },
+ });
+
+ const noContainerTab = await helperExtension.awaitMessage("tabCreated");
+
+ let eventNames = [
+ "onBeforeNavigate",
+ "onCommitted",
+ "onDOMContentLoaded",
+ "onCompleted",
+ ];
+ for (let eventName of eventNames) {
+ let recTabId1 = await extension.awaitMessage(eventName);
+ let recTabId2 = await extension.awaitMessage(eventName);
+
+ Assert.equal(
+ recTabId1,
+ unrestrictedTab,
+ `Expected unrestricted tab with tabId: ${unrestrictedTab} from ${eventName} event`
+ );
+
+ Assert.equal(
+ recTabId2,
+ noContainerTab,
+ `Expected noContainer tab with tabId: ${noContainerTab} from ${eventName} event`
+ );
+ }
+
+ extension.sendMessage([restrictedTab, unrestrictedTab, noContainerTab]);
+
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+ await helperExtension.unload();
+ await SpecialPowers.popPrefEnv();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js
new file mode 100644
index 0000000000..c5b2c72778
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js
@@ -0,0 +1,43 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function webNavigation_getFrameId_of_existing_main_frame() {
+ const BASE =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/";
+ const DUMMY_URL = BASE + "file_dummy.html";
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ DUMMY_URL,
+ true
+ );
+
+ async function background(DUMMY_URL) {
+ let tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ let frames = await browser.webNavigation.getAllFrames({
+ tabId: tabs[0].id,
+ });
+ browser.test.assertEq(1, frames.length, "The dummy page has one frame");
+ browser.test.assertEq(0, frames[0].frameId, "Main frame's ID must be 0");
+ browser.test.assertEq(
+ DUMMY_URL,
+ frames[0].url,
+ "Main frame URL must match"
+ );
+ browser.test.notifyPass("frameId checked");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+
+ background: `(${background})(${JSON.stringify(DUMMY_URL)});`,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("frameId checked");
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
new file mode 100644
index 0000000000..f72713d8fc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js
@@ -0,0 +1,319 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWebNavigationGetNonExistentTab() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async function () {
+ // There is no "tabId = 0" because the id assigned by tabTracker (defined in ext-browser.js)
+ // starts from 1.
+ await browser.test.assertRejects(
+ browser.webNavigation.getAllFrames({ tabId: 0 }),
+ "Invalid tab ID: 0",
+ "getAllFrames rejected Promise should pass the expected error"
+ );
+
+ // There is no "tabId = 0" because the id assigned by tabTracker (defined in ext-browser.js)
+ // starts from 1, processId is currently marked as optional and it is ignored.
+ await browser.test.assertRejects(
+ browser.webNavigation.getFrame({
+ tabId: 0,
+ frameId: 15,
+ processId: 20,
+ }),
+ "Invalid tab ID: 0",
+ "getFrame rejected Promise should pass the expected error"
+ );
+
+ browser.test.sendMessage("getNonExistentTab.done");
+ },
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("getNonExistentTab.done");
+
+ await extension.unload();
+});
+
+add_task(async function testWebNavigationFrames() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async function () {
+ let tabId;
+ let collectedDetails = [];
+
+ browser.webNavigation.onCompleted.addListener(async details => {
+ collectedDetails.push(details);
+
+ if (details.frameId !== 0) {
+ // wait for the top level iframe to be complete
+ return;
+ }
+
+ let getAllFramesDetails = await browser.webNavigation.getAllFrames({
+ tabId,
+ });
+
+ let getFramePromises = getAllFramesDetails.map(({ frameId }) => {
+ // processId is currently marked as optional and it is ignored.
+ return browser.webNavigation.getFrame({
+ tabId,
+ frameId,
+ processId: 0,
+ });
+ });
+
+ let getFrameResults = await Promise.all(getFramePromises);
+ browser.test.sendMessage("webNavigationFrames.done", {
+ collectedDetails,
+ getAllFramesDetails,
+ getFrameResults,
+ });
+
+ // Pick a random frameId.
+ let nonExistentFrameId = Math.floor(Math.random() * 10000);
+
+ // Increment the picked random nonExistentFrameId until it doesn't exists.
+ while (
+ getAllFramesDetails.filter(
+ details => details.frameId == nonExistentFrameId
+ ).length
+ ) {
+ nonExistentFrameId += 1;
+ }
+
+ // Check that getFrame Promise is rejected with the expected error message on nonexistent frameId.
+ await browser.test.assertRejects(
+ browser.webNavigation.getFrame({
+ tabId,
+ frameId: nonExistentFrameId,
+ processId: 20,
+ }),
+ `No frame found with frameId: ${nonExistentFrameId}`,
+ "getFrame promise should be rejected with the expected error message on unexistent frameId"
+ );
+
+ await browser.tabs.remove(tabId);
+ browser.test.sendMessage("webNavigationFrames.done");
+ });
+
+ let tab = await browser.tabs.create({ url: "tab.html" });
+ tabId = tab.id;
+ },
+ manifest: {
+ permissions: ["webNavigation", "tabs"],
+ },
+ files: {
+ "tab.html": `
+
+
+
+
+
+
+
+
+
+
+ `,
+ "subframe.html": `
+
+
+
+
+
+
+ `,
+ },
+ });
+
+ await extension.startup();
+
+ let { collectedDetails, getAllFramesDetails, getFrameResults } =
+ await extension.awaitMessage("webNavigationFrames.done");
+
+ is(getAllFramesDetails.length, 3, "expected number of frames found");
+ is(
+ getAllFramesDetails.length,
+ collectedDetails.length,
+ "number of frames found should equal the number onCompleted events collected"
+ );
+
+ is(
+ getAllFramesDetails[0].frameId,
+ 0,
+ "the root frame has the expected frameId"
+ );
+ is(
+ getAllFramesDetails[0].parentFrameId,
+ -1,
+ "the root frame has the expected parentFrameId"
+ );
+
+ // ordered by frameId
+ let sortByFrameId = (el1, el2) => {
+ let val1 = el1 ? el1.frameId : -1;
+ let val2 = el2 ? el2.frameId : -1;
+ return val1 - val2;
+ };
+
+ collectedDetails = collectedDetails.sort(sortByFrameId);
+ getAllFramesDetails = getAllFramesDetails.sort(sortByFrameId);
+ getFrameResults = getFrameResults.sort(sortByFrameId);
+
+ info("check frame details content");
+
+ is(
+ getFrameResults.length,
+ getAllFramesDetails.length,
+ "getFrame and getAllFrames should return the same number of results"
+ );
+
+ Assert.deepEqual(
+ getFrameResults,
+ getAllFramesDetails,
+ "getFrame and getAllFrames should return the same results"
+ );
+
+ info(`check frame details collected and retrieved with getAllFrames`);
+
+ for (let [i, collected] of collectedDetails.entries()) {
+ let getAllFramesDetail = getAllFramesDetails[i];
+
+ is(getAllFramesDetail.frameId, collected.frameId, "frameId");
+ is(
+ getAllFramesDetail.parentFrameId,
+ collected.parentFrameId,
+ "parentFrameId"
+ );
+ is(getAllFramesDetail.tabId, collected.tabId, "tabId");
+
+ // This can be uncommented once Bug 1246125 has been fixed
+ // is(getAllFramesDetail.url, collected.url, "url");
+ }
+
+ info("frame details content checked");
+
+ await extension.awaitMessage("webNavigationFrames.done");
+
+ await extension.unload();
+});
+
+add_task(async function testWebNavigationGetFrameOnDiscardedTab() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+ async background() {
+ let tabs = await browser.tabs.query({ currentWindow: true });
+ browser.test.assertEq(2, tabs.length, "Expect 2 tabs open");
+
+ const tabId = tabs[1].id;
+
+ await browser.tabs.discard(tabId);
+ let tab = await browser.tabs.get(tabId);
+ browser.test.assertEq(true, tab.discarded, "Expect a discarded tab");
+
+ const allFrames = await browser.webNavigation.getAllFrames({ tabId });
+ browser.test.assertEq(
+ null,
+ allFrames,
+ "Expect null from calling getAllFrames on discarded tab"
+ );
+
+ tab = await browser.tabs.get(tabId);
+ browser.test.assertEq(
+ true,
+ tab.discarded,
+ "Expect tab to stay discarded"
+ );
+
+ const topFrame = await browser.webNavigation.getFrame({
+ tabId,
+ frameId: 0,
+ });
+ browser.test.assertEq(
+ null,
+ topFrame,
+ "Expect null from calling getFrame on discarded tab"
+ );
+
+ tab = await browser.tabs.get(tabId);
+ browser.test.assertEq(
+ true,
+ tab.discarded,
+ "Expect tab to stay discarded"
+ );
+
+ browser.test.sendMessage("get-frames-done");
+ },
+ });
+
+ const initialTab = gBrowser.selectedTab;
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://mochi.test:8888/?toBeDiscarded=true"
+ );
+ // Switch back to the initial tab to allow the new tab
+ // to be discarded.
+ await BrowserTestUtils.switchTab(gBrowser, initialTab);
+
+ ok(!!tab.linkedPanel, "Tab not initially discarded");
+
+ await extension.startup();
+ await extension.awaitMessage("get-frames-done");
+
+ ok(!tab.linkedPanel, "Tab should be discarded");
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
+
+add_task(async function testWebNavigationCrossOriginFrames() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ async background() {
+ let url =
+ "http://mochi.test:8888/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html";
+ let tab = await browser.tabs.create({ url });
+
+ await new Promise(resolve => {
+ browser.webNavigation.onCompleted.addListener(details => {
+ if (details.tabId === tab.id && details.frameId === 0) {
+ resolve();
+ }
+ });
+ });
+
+ let frames = await browser.webNavigation.getAllFrames({ tabId: tab.id });
+ browser.test.assertEq(frames[0].url, url, "Top is from mochi.test");
+
+ await browser.tabs.remove(tab.id);
+ browser.test.sendMessage("webNavigation.CrossOriginFrames", frames);
+ },
+ });
+
+ await extension.startup();
+
+ let frames = await extension.awaitMessage("webNavigation.CrossOriginFrames");
+ is(frames.length, 2, "getAllFrames() returns both frames.");
+
+ is(frames[0].frameId, 0, "Top frame has correct frameId.");
+ is(frames[0].parentFrameId, -1, "Top parentFrameId is correct.");
+
+ ok(frames[1].frameId > 0, "Cross-origin iframe has non-zero frameId.");
+ is(frames[1].parentFrameId, 0, "Iframe parentFrameId is correct.");
+ is(
+ frames[1].url,
+ "http://example.org/tests/toolkit/components/extensions/test/mochitest/file_contains_img.html",
+ "Irame is from example.org"
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget.js
new file mode 100644
index 0000000000..efe847c2b4
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget.js
@@ -0,0 +1,194 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_webNavigation.js");
+
+SpecialPowers.pushPrefEnv({
+ set: [["security.allow_eval_with_system_principal", true]],
+});
+
+async function background() {
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const sourceTabId = tabs[0].id;
+
+ const sourceTabFrames = await browser.webNavigation.getAllFrames({
+ tabId: sourceTabId,
+ });
+
+ browser.webNavigation.onCreatedNavigationTarget.addListener(msg => {
+ browser.test.sendMessage("webNavOnCreated", msg);
+ });
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ // NOTE: checking the url is currently necessary because of Bug 1252129
+ // ( Filter out webNavigation events related to new window initialization phase).
+ if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("webNavOnCompleted", msg);
+ }
+ });
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.sendMessage("tabsOnCreated", tab.id);
+ });
+
+ browser.test.sendMessage("expectedSourceTab", {
+ sourceTabId,
+ sourceTabFrames,
+ });
+}
+
+add_task(async function test_on_created_navigation_target_from_mouse_click() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("Open link in a new tab using Ctrl-click");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-tab-from-mouse-click",
+ { ctrlKey: true, metaKey: true },
+ tab.linkedBrowser
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-mouse-click`,
+ },
+ });
+
+ info("Open link in a new window using Shift-click");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-window-from-mouse-click",
+ { shiftKey: true },
+ tab.linkedBrowser
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-window-from-mouse-click`,
+ },
+ });
+
+ info('Open link with target="_blank" in a new tab using click');
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-tab-from-targetblank-click",
+ {},
+ tab.linkedBrowser
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-targetblank-click`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_on_created_navigation_target_from_mouse_click_subframe() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("Open a subframe link in a new tab using Ctrl-click");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-tab-from-mouse-click-subframe",
+ { ctrlKey: true, metaKey: true },
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-tab-from-mouse-click-subframe`,
+ },
+ });
+
+ info("Open a subframe link in a new window using Shift-click");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-window-from-mouse-click-subframe",
+ { shiftKey: true },
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-window-from-mouse-click-subframe`,
+ },
+ });
+
+ info('Open a subframe link with target="_blank" in a new tab using click');
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "#test-create-new-tab-from-targetblank-click-subframe",
+ {},
+ tab.linkedBrowser.browsingContext.children[0]
+ );
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-tab-from-targetblank-click-subframe`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_contextmenu.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_contextmenu.js
new file mode 100644
index 0000000000..8fd94af4f1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_contextmenu.js
@@ -0,0 +1,182 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_webNavigation.js");
+
+SpecialPowers.pushPrefEnv({
+ set: [["security.allow_eval_with_system_principal", true]],
+});
+
+async function clickContextMenuItem({
+ pageElementSelector,
+ contextMenuItemLabel,
+ frameIndex,
+}) {
+ let contentAreaContextMenu;
+ if (frameIndex == null) {
+ contentAreaContextMenu = await openContextMenu(pageElementSelector);
+ } else {
+ contentAreaContextMenu = await openContextMenuInFrame(
+ pageElementSelector,
+ frameIndex
+ );
+ }
+ const item = contentAreaContextMenu.getElementsByAttribute(
+ "label",
+ contextMenuItemLabel
+ );
+ is(item.length, 1, `found contextMenu item for "${contextMenuItemLabel}"`);
+ const closed = promiseContextMenuClosed(contentAreaContextMenu);
+ contentAreaContextMenu.activateItem(item[0]);
+ await closed;
+}
+
+async function background() {
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const sourceTabId = tabs[0].id;
+
+ const sourceTabFrames = await browser.webNavigation.getAllFrames({
+ tabId: sourceTabId,
+ });
+
+ browser.webNavigation.onCreatedNavigationTarget.addListener(msg => {
+ browser.test.sendMessage("webNavOnCreated", msg);
+ });
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ // NOTE: checking the url is currently necessary because of Bug 1252129
+ // ( Filter out webNavigation events related to new window initialization phase).
+ if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("webNavOnCompleted", msg);
+ }
+ });
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.sendMessage("tabsOnCreated", tab.id);
+ });
+
+ browser.test.sendMessage("expectedSourceTab", {
+ sourceTabId,
+ sourceTabFrames,
+ });
+}
+
+add_task(async function test_on_created_navigation_target_from_context_menu() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("Open link in a new tab from the context menu");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ async openNavTarget() {
+ await clickContextMenuItem({
+ pageElementSelector: "#test-create-new-tab-from-context-menu",
+ contextMenuItemLabel: "Open Link in New Tab",
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-context-menu`,
+ },
+ });
+
+ info("Open link in a new window from the context menu");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ async openNavTarget() {
+ await clickContextMenuItem({
+ pageElementSelector: "#test-create-new-window-from-context-menu",
+ contextMenuItemLabel: "Open Link in New Window",
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-window-from-context-menu`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_on_created_navigation_target_from_context_menu_subframe() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("Open a subframe link in a new tab from the context menu");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ async openNavTarget() {
+ await clickContextMenuItem({
+ pageElementSelector:
+ "#test-create-new-tab-from-context-menu-subframe",
+ contextMenuItemLabel: "Open Link in New Tab",
+ frameIndex: 0,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-tab-from-context-menu-subframe`,
+ },
+ });
+
+ info("Open a subframe link in a new window from the context menu");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ async openNavTarget() {
+ await clickContextMenuItem({
+ pageElementSelector:
+ "#test-create-new-window-from-context-menu-subframe",
+ contextMenuItemLabel: "Open Link in New Window",
+ frameIndex: 0,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-window-from-context-menu-subframe`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab);
+
+ await extension.unload();
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_named_window.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_named_window.js
new file mode 100644
index 0000000000..3a0a950319
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_named_window.js
@@ -0,0 +1,100 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_webNavigation.js");
+
+async function background() {
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const sourceTabId = tabs[0].id;
+
+ const sourceTabFrames = await browser.webNavigation.getAllFrames({
+ tabId: sourceTabId,
+ });
+
+ browser.webNavigation.onCreatedNavigationTarget.addListener(msg => {
+ browser.test.sendMessage("webNavOnCreated", msg);
+ });
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ // NOTE: checking the url is currently necessary because of Bug 1252129
+ // ( Filter out webNavigation events related to new window initialization phase).
+ if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("webNavOnCompleted", msg);
+ }
+ });
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.sendMessage("tabsOnCreated", tab.id);
+ });
+
+ browser.test.onMessage.addListener(({ type, code }) => {
+ if (type === "execute-contentscript") {
+ browser.tabs.executeScript(sourceTabId, { code: code });
+ }
+ });
+
+ browser.test.sendMessage("expectedSourceTab", {
+ sourceTabId,
+ sourceTabFrames,
+ });
+}
+
+add_task(async function test_window_open_in_named_win() {
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation", "tabs", ""],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("open a url in a new named window from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-named-window-open", "TestWinName"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-named-window-open`,
+ },
+ });
+
+ info("open a url in an existent named window from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#existent-named-window-open", "TestWinName"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#existent-named-window-open`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_subframe_window_open.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_subframe_window_open.js
new file mode 100644
index 0000000000..15439460a5
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_subframe_window_open.js
@@ -0,0 +1,168 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_webNavigation.js");
+
+async function background() {
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const sourceTabId = tabs[0].id;
+
+ const sourceTabFrames = await browser.webNavigation.getAllFrames({
+ tabId: sourceTabId,
+ });
+
+ browser.webNavigation.onCreatedNavigationTarget.addListener(msg => {
+ browser.test.sendMessage("webNavOnCreated", msg);
+ });
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ // NOTE: checking the url is currently necessary because of Bug 1252129
+ // ( Filter out webNavigation events related to new window initialization phase).
+ if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("webNavOnCompleted", msg);
+ }
+ });
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.sendMessage("tabsOnCreated", tab.id);
+ });
+
+ browser.test.onMessage.addListener(({ type, code }) => {
+ if (type === "execute-contentscript") {
+ browser.tabs.executeScript(sourceTabId, { code: code });
+ }
+ });
+
+ browser.test.sendMessage("expectedSourceTab", {
+ sourceTabId,
+ sourceTabFrames,
+ });
+}
+
+add_task(async function test_window_open_from_subframe() {
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation", "tabs", ""],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("open a url in a new tab from subframe window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `document.querySelector('iframe').contentWindow.open("${OPENED_PAGE}#new-tab-from-window-open-subframe"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-tab-from-window-open-subframe`,
+ },
+ });
+
+ info("open a url in a new window from subframe window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `document.querySelector('iframe').contentWindow.open("${OPENED_PAGE}#new-win-from-window-open-subframe", "_blank", "toolbar=0"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: expectedSourceTab.sourceTabFrames[1].frameId,
+ url: `${OPENED_PAGE}#new-win-from-window-open-subframe`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await extension.unload();
+});
+
+add_task(async function test_window_open_close_from_browserAction_popup() {
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ function popup() {
+ window.open("", "_self").close();
+
+ browser.test.sendMessage("browserAction_popup_executed");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ permissions: ["webNavigation", "tabs", ""],
+ },
+ files: {
+ "popup.html": `
+
+
+
+
+
+
+
+
+ `,
+ "popup.js": popup,
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ clickBrowserAction(extension);
+
+ await extension.awaitMessage("browserAction_popup_executed");
+
+ info("open a url in a new tab from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-tab-from-window-open"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-window-open`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js
new file mode 100644
index 0000000000..8a1c5ee82d
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_onCreatedNavigationTarget_window_open.js
@@ -0,0 +1,168 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+loadTestSubscript("head_webNavigation.js");
+
+async function background() {
+ const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+ const sourceTabId = tabs[0].id;
+
+ const sourceTabFrames = await browser.webNavigation.getAllFrames({
+ tabId: sourceTabId,
+ });
+
+ browser.webNavigation.onCreatedNavigationTarget.addListener(msg => {
+ browser.test.sendMessage("webNavOnCreated", msg);
+ });
+
+ browser.webNavigation.onCompleted.addListener(async msg => {
+ // NOTE: checking the url is currently necessary because of Bug 1252129
+ // ( Filter out webNavigation events related to new window initialization phase).
+ if (msg.tabId !== sourceTabId && msg.url !== "about:blank") {
+ await browser.tabs.remove(msg.tabId);
+ browser.test.sendMessage("webNavOnCompleted", msg);
+ }
+ });
+
+ browser.tabs.onCreated.addListener(tab => {
+ browser.test.sendMessage("tabsOnCreated", tab.id);
+ });
+
+ browser.test.onMessage.addListener(({ type, code }) => {
+ if (type === "execute-contentscript") {
+ browser.tabs.executeScript(sourceTabId, { code: code });
+ }
+ });
+
+ browser.test.sendMessage("expectedSourceTab", {
+ sourceTabId,
+ sourceTabFrames,
+ });
+}
+
+add_task(async function test_window_open() {
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["webNavigation", "tabs", ""],
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ info("open a url in a new tab from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-tab-from-window-open"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-window-open`,
+ },
+ });
+
+ info("open a url in a new window from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-win-from-window-open", "_blank", "toolbar=0"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-win-from-window-open`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await extension.unload();
+});
+
+add_task(async function test_window_open_close_from_browserAction_popup() {
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SOURCE_PAGE
+ );
+
+ gBrowser.selectedTab = tab1;
+
+ function popup() {
+ window.open("", "_self").close();
+
+ browser.test.sendMessage("browserAction_popup_executed");
+ }
+
+ const extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ default_area: "navbar",
+ },
+ permissions: ["webNavigation", "tabs", ""],
+ },
+ files: {
+ "popup.html": `
+
+
+
+
+
+
+
+
+ `,
+ "popup.js": popup,
+ },
+ });
+
+ await extension.startup();
+
+ const expectedSourceTab = await extension.awaitMessage("expectedSourceTab");
+
+ clickBrowserAction(extension);
+
+ await extension.awaitMessage("browserAction_popup_executed");
+
+ info("open a url in a new tab from a window.open call");
+
+ await runCreatedNavigationTargetTest({
+ extension,
+ openNavTarget() {
+ extension.sendMessage({
+ type: "execute-contentscript",
+ code: `window.open("${OPENED_PAGE}#new-tab-from-window-open"); true;`,
+ });
+ },
+ expectedWebNavProps: {
+ sourceTabId: expectedSourceTab.sourceTabId,
+ sourceFrameId: 0,
+ url: `${OPENED_PAGE}#new-tab-from-window-open`,
+ },
+ });
+
+ BrowserTestUtils.removeTab(tab1);
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
new file mode 100644
index 0000000000..e90a3c7ba1
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
@@ -0,0 +1,314 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
+});
+
+SearchTestUtils.init(this);
+
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+function promiseAutocompleteResultPopup(value) {
+ return UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ waitForFocus,
+ value,
+ });
+}
+
+async function addBookmark(bookmark) {
+ if (bookmark.keyword) {
+ await PlacesUtils.keywords.insert({
+ keyword: bookmark.keyword,
+ url: bookmark.url,
+ });
+ }
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: bookmark.url,
+ title: bookmark.title,
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+}
+
+async function prepareSearchEngine() {
+ let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ setAsDefault: true,
+ });
+
+ registerCleanupFunction(async function () {
+ Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
+
+ // Make sure the popup is closed for the next test.
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Clicking suggestions causes visits to search results pages, so clear that
+ // history now.
+ await PlacesUtils.history.clear();
+ });
+}
+
+add_task(async function test_webnavigation_urlbar_typed_transitions() {
+ function backgroundScript() {
+ browser.webNavigation.onCommitted.addListener(msg => {
+ browser.test.assertEq(
+ "http://example.com/?q=typed",
+ msg.url,
+ "Got the expected url"
+ );
+ // assert from_address_bar transition qualifier
+ browser.test.assertTrue(
+ msg.transitionQualifiers &&
+ msg.transitionQualifiers.includes("from_address_bar"),
+ "Got the expected from_address_bar transitionQualifier"
+ );
+ browser.test.assertEq(
+ "typed",
+ msg.transitionType,
+ "Got the expected transitionType"
+ );
+ browser.test.notifyPass("webNavigation.from_address_bar.typed");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+
+ await extension.awaitMessage("ready");
+
+ gURLBar.focus();
+ const inputValue = "http://example.com/?q=typed";
+ gURLBar.inputField.value = inputValue.slice(0, -1);
+ EventUtils.sendString(inputValue.slice(-1));
+ EventUtils.synthesizeKey("VK_RETURN", { altKey: true });
+
+ await extension.awaitFinish("webNavigation.from_address_bar.typed");
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_webnavigation_urlbar_typed_closed_popup_transitions() {
+ function backgroundScript() {
+ browser.webNavigation.onCommitted.addListener(msg => {
+ browser.test.assertEq(
+ "http://example.com/?q=typedClosed",
+ msg.url,
+ "Got the expected url"
+ );
+ // assert from_address_bar transition qualifier
+ browser.test.assertTrue(
+ msg.transitionQualifiers &&
+ msg.transitionQualifiers.includes("from_address_bar"),
+ "Got the expected from_address_bar transitionQualifier"
+ );
+ browser.test.assertEq(
+ "typed",
+ msg.transitionType,
+ "Got the expected transitionType"
+ );
+ browser.test.notifyPass("webNavigation.from_address_bar.typed");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+
+ await extension.awaitMessage("ready");
+ await promiseAutocompleteResultPopup("http://example.com/?q=typedClosed");
+ await UrlbarTestUtils.promiseSearchComplete(window);
+ // Closing the popup forces a different code route that handles no results
+ // being displayed.
+ await UrlbarTestUtils.promisePopupClose(window);
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ await extension.awaitFinish("webNavigation.from_address_bar.typed");
+
+ await extension.unload();
+ }
+);
+
+add_task(async function test_webnavigation_urlbar_bookmark_transitions() {
+ function backgroundScript() {
+ browser.webNavigation.onCommitted.addListener(msg => {
+ browser.test.assertEq(
+ "http://example.com/?q=bookmark",
+ msg.url,
+ "Got the expected url"
+ );
+
+ // assert from_address_bar transition qualifier
+ browser.test.assertTrue(
+ msg.transitionQualifiers &&
+ msg.transitionQualifiers.includes("from_address_bar"),
+ "Got the expected from_address_bar transitionQualifier"
+ );
+ browser.test.assertEq(
+ "auto_bookmark",
+ msg.transitionType,
+ "Got the expected transitionType"
+ );
+ browser.test.notifyPass("webNavigation.from_address_bar.auto_bookmark");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await addBookmark({
+ title: "Bookmark To Click",
+ url: "http://example.com/?q=bookmark",
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+
+ await extension.awaitMessage("ready");
+
+ await promiseAutocompleteResultPopup("Bookmark To Click");
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+ await extension.awaitFinish("webNavigation.from_address_bar.auto_bookmark");
+
+ await extension.unload();
+});
+
+add_task(async function test_webnavigation_urlbar_keyword_transition() {
+ function backgroundScript() {
+ browser.webNavigation.onCommitted.addListener(msg => {
+ browser.test.assertEq(
+ `http://example.com/?q=search`,
+ msg.url,
+ "Got the expected url"
+ );
+
+ // assert from_address_bar transition qualifier
+ browser.test.assertTrue(
+ msg.transitionQualifiers &&
+ msg.transitionQualifiers.includes("from_address_bar"),
+ "Got the expected from_address_bar transitionQualifier"
+ );
+ browser.test.assertEq(
+ "keyword",
+ msg.transitionType,
+ "Got the expected transitionType"
+ );
+ browser.test.notifyPass("webNavigation.from_address_bar.keyword");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await addBookmark({
+ title: "Test Keyword",
+ url: "http://example.com/?q=%s",
+ keyword: "testkw",
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+
+ await extension.awaitMessage("ready");
+
+ await promiseAutocompleteResultPopup("testkw search");
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+
+ await extension.awaitFinish("webNavigation.from_address_bar.keyword");
+
+ await extension.unload();
+});
+
+add_task(async function test_webnavigation_urlbar_search_transitions() {
+ function backgroundScript() {
+ browser.webNavigation.onCommitted.addListener(msg => {
+ browser.test.assertEq(
+ "http://mochi.test:8888/",
+ msg.url,
+ "Got the expected url"
+ );
+
+ // assert from_address_bar transition qualifier
+ browser.test.assertTrue(
+ msg.transitionQualifiers &&
+ msg.transitionQualifiers.includes("from_address_bar"),
+ "Got the expected from_address_bar transitionQualifier"
+ );
+ browser.test.assertEq(
+ "generated",
+ msg.transitionType,
+ "Got the expected 'generated' transitionType"
+ );
+ browser.test.notifyPass("webNavigation.from_address_bar.generated");
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: backgroundScript,
+ manifest: {
+ permissions: ["webNavigation"],
+ },
+ });
+
+ await extension.startup();
+ await SimpleTest.promiseFocus(window);
+
+ await extension.awaitMessage("ready");
+
+ await prepareSearchEngine();
+ await promiseAutocompleteResultPopup("foo");
+
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
+ EventUtils.synthesizeMouseAtCenter(result.element.row, {});
+
+ await extension.awaitFinish("webNavigation.from_address_bar.generated");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webRequest.js b/browser/components/extensions/test/browser/browser_ext_webRequest.js
new file mode 100644
index 0000000000..c2ff1d6c64
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webRequest.js
@@ -0,0 +1,142 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* import-globals-from ../../../../../toolkit/components/extensions/test/mochitest/head_webrequest.js */
+loadTestSubscript("head_webrequest.js");
+
+const { HiddenFrame } = ChromeUtils.importESModule(
+ "resource://gre/modules/HiddenFrame.sys.mjs"
+);
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+SimpleTest.requestCompleteLog();
+
+function createHiddenBrowser(url) {
+ let frame = new HiddenFrame();
+ return new Promise(resolve =>
+ frame.get().then(subframe => {
+ let doc = subframe.document;
+ let browser = doc.createElementNS(XUL_NS, "browser");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("remote", "true");
+ browser.setAttribute("disableglobalhistory", "true");
+ browser.setAttribute("src", url);
+
+ doc.documentElement.appendChild(browser);
+ resolve({ frame: frame, browser: browser });
+ })
+ );
+}
+
+let extension;
+let dummy =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/file_dummy.html";
+let headers = {
+ request: {
+ add: {
+ "X-WebRequest-request": "text",
+ "X-WebRequest-request-binary": "binary",
+ },
+ modify: {
+ "user-agent": "WebRequest",
+ },
+ remove: ["accept-encoding"],
+ },
+ response: {
+ add: {
+ "X-WebRequest-response": "text",
+ "X-WebRequest-response-binary": "binary",
+ },
+ modify: {
+ server: "WebRequest",
+ "content-type": "text/html; charset=utf-8",
+ },
+ remove: ["connection"],
+ },
+};
+
+let urls = ["http://mochi.test/browser/*"];
+let events = {
+ onBeforeRequest: [{ urls }, ["blocking"]],
+ onBeforeSendHeaders: [{ urls }, ["blocking", "requestHeaders"]],
+ onSendHeaders: [{ urls }, ["requestHeaders"]],
+ onHeadersReceived: [{ urls }, ["blocking", "responseHeaders"]],
+ onCompleted: [{ urls }, ["responseHeaders"]],
+};
+
+add_setup(async function () {
+ extension = makeExtension(events);
+ await extension.startup();
+});
+
+add_task(async function test_newWindow() {
+ let expect = {
+ "file_dummy.html": {
+ type: "main_frame",
+ headers,
+ },
+ };
+ // NOTE: When running solo, favicon will be loaded at some point during
+ // the tests in this file, so all tests ignore it. When running with
+ // other tests in this directory, favicon gets loaded at some point before
+ // we run, and we never see the request, thus it cannot be handled as part
+ // of expect above.
+ extension.sendMessage("set-expected", { expect, ignore: ["favicon.ico"] });
+ await extension.awaitMessage("continue");
+
+ let openedWindow = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab(
+ openedWindow.gBrowser,
+ `${dummy}?newWindow=${Math.random()}`
+ );
+
+ await extension.awaitMessage("done");
+ await BrowserTestUtils.closeWindow(openedWindow);
+});
+
+add_task(async function test_newTab() {
+ // again, in this window
+ let expect = {
+ "file_dummy.html": {
+ type: "main_frame",
+ headers,
+ },
+ };
+ extension.sendMessage("set-expected", { expect, ignore: ["favicon.ico"] });
+ await extension.awaitMessage("continue");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ `${dummy}?newTab=${Math.random()}`
+ );
+
+ await extension.awaitMessage("done");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_subframe() {
+ let expect = {
+ "file_dummy.html": {
+ type: "main_frame",
+ headers,
+ },
+ };
+ // test a content subframe attached to hidden window
+ extension.sendMessage("set-expected", { expect, ignore: ["favicon.ico"] });
+ info("*** waiting to continue");
+ await extension.awaitMessage("continue");
+ info("*** creating hidden browser");
+ let frameInfo = await createHiddenBrowser(
+ `${dummy}?subframe=${Math.random()}`
+ );
+ info("*** waiting for finish");
+ await extension.awaitMessage("done");
+ info("*** destroying hidden browser");
+ // cleanup
+ frameInfo.browser.remove();
+ frameInfo.frame.destroy();
+});
+
+add_task(async function teardown() {
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webRequest_error_after_stopped_or_closed.js b/browser/components/extensions/test/browser/browser_ext_webRequest_error_after_stopped_or_closed.js
new file mode 100644
index 0000000000..acf8b4446a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webRequest_error_after_stopped_or_closed.js
@@ -0,0 +1,110 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const SLOW_PAGE =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://www.example.com"
+ ) + "file_slowed_document.sjs";
+
+async function runTest(stopLoadFunc) {
+ async function background() {
+ let urls = ["http://www.example.com/*"];
+ browser.webRequest.onCompleted.addListener(
+ details => {
+ browser.test.sendMessage("done", {
+ msg: "onCompleted",
+ requestId: details.requestId,
+ });
+ },
+ { urls }
+ );
+
+ browser.webRequest.onBeforeRequest.addListener(
+ details => {
+ browser.test.sendMessage("onBeforeRequest", {
+ requestId: details.requestId,
+ });
+ },
+ { urls },
+ ["blocking"]
+ );
+
+ browser.webRequest.onErrorOccurred.addListener(
+ details => {
+ browser.test.sendMessage("done", {
+ msg: "onErrorOccurred",
+ requestId: details.requestId,
+ });
+ },
+ { urls }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: [
+ "webRequest",
+ "webRequestBlocking",
+ "http://www.example.com/*",
+ ],
+ },
+ background,
+ });
+ await extension.startup();
+
+ // Open a SLOW_PAGE and don't wait for it to load
+ let slowTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SLOW_PAGE,
+ false
+ );
+
+ stopLoadFunc(slowTab);
+
+ // Retrieve the requestId from onBeforeRequest
+ let requestIdOnBeforeRequest = await extension.awaitMessage(
+ "onBeforeRequest"
+ );
+
+ // Now verify that we got the correct event and request id
+ let doneMessage = await extension.awaitMessage("done");
+
+ // We shouldn't get the onCompleted message here
+ is(doneMessage.msg, "onErrorOccurred", "received onErrorOccurred message");
+ is(
+ requestIdOnBeforeRequest.requestId,
+ doneMessage.requestId,
+ "request Ids match"
+ );
+
+ BrowserTestUtils.removeTab(slowTab);
+ await extension.unload();
+}
+
+/**
+ * Check that after we cancel a slow page load, we get an error associated with
+ * our request.
+ */
+add_task(async function test_click_stop_button() {
+ await runTest(async slowTab => {
+ // Stop the load
+ let stopButton = document.getElementById("stop-button");
+ await TestUtils.waitForCondition(() => {
+ return !stopButton.disabled;
+ });
+ stopButton.click();
+ });
+});
+
+/**
+ * Check that after we close the tab corresponding to a slow page load,
+ * that we get an error associated with our request.
+ */
+add_task(async function test_remove_tab() {
+ await runTest(slowTab => {
+ // Remove the tab
+ BrowserTestUtils.removeTab(slowTab);
+ });
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_webrtc.js b/browser/components/extensions/test/browser/browser_ext_webrtc.js
new file mode 100644
index 0000000000..520cb9cd69
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webrtc.js
@@ -0,0 +1,131 @@
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.navigator.permission.fake", true]],
+ });
+});
+
+add_task(async function test_background_request() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {},
+ async background() {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg.type != "testGUM") {
+ browser.test.fail("unknown message");
+ }
+
+ await browser.test.assertRejects(
+ navigator.mediaDevices.getUserMedia({ audio: true }),
+ /The request is not allowed/,
+ "Calling gUM in background pages throws an error"
+ );
+ browser.test.notifyPass("done");
+ });
+ },
+ });
+
+ await extension.startup();
+
+ let policy = WebExtensionPolicy.getByID(extension.id);
+ let principal = policy.extension.principal;
+ // Add a permission for the extension to make sure that we throw even
+ // if permission was given.
+ PermissionTestUtils.add(principal, "microphone", Services.perms.ALLOW_ACTION);
+
+ let finished = extension.awaitFinish("done");
+ extension.sendMessage({ type: "testGUM" });
+ await finished;
+
+ PermissionTestUtils.remove(principal, "microphone");
+ await extension.unload();
+});
+
+let scriptPage = url =>
+ `${url}`;
+
+add_task(async function test_popup_request() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_action: {
+ default_popup: "popup.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "popup.html": scriptPage("popup.js"),
+ "popup.js": function () {
+ browser.test
+ .assertRejects(
+ navigator.mediaDevices.getUserMedia({ audio: true }),
+ /The request is not allowed/,
+ "Calling gUM in popup pages without permission throws an error"
+ )
+ .then(function () {
+ browser.test.notifyPass("done");
+ });
+ },
+ },
+ });
+
+ await extension.startup();
+ clickBrowserAction(extension);
+ await extension.awaitFinish("done");
+ await extension.unload();
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ // Use the same url for background page and browserAction popup,
+ // to double-check that the page url is not being used to decide
+ // if webRTC requests should be allowed or not.
+ background: { page: "page.html" },
+ browser_action: {
+ default_popup: "page.html",
+ browser_style: true,
+ },
+ },
+
+ files: {
+ "page.html": scriptPage("page.js"),
+ "page.js": async function () {
+ const isBackgroundPage =
+ window == (await browser.runtime.getBackgroundPage());
+
+ if (isBackgroundPage) {
+ await browser.test.assertRejects(
+ navigator.mediaDevices.getUserMedia({ audio: true }),
+ /The request is not allowed/,
+ "Calling gUM in background pages throws an error"
+ );
+ } else {
+ try {
+ await navigator.mediaDevices.getUserMedia({ audio: true });
+ browser.test.notifyPass("done");
+ } catch (err) {
+ browser.test.fail(`Failed with error ${err.message}`);
+ browser.test.notifyFail("done");
+ }
+ }
+ },
+ },
+ });
+
+ // Add a permission for the extension to make sure that we throw even
+ // if permission was given.
+ await extension.startup();
+
+ let policy = WebExtensionPolicy.getByID(extension.id);
+ let principal = policy.extension.principal;
+
+ PermissionTestUtils.add(principal, "microphone", Services.perms.ALLOW_ACTION);
+ clickBrowserAction(extension);
+
+ await extension.awaitFinish("done");
+ PermissionTestUtils.remove(principal, "microphone");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows.js b/browser/components/extensions/test/browser/browser_ext_windows.js
new file mode 100644
index 0000000000..5fc561ebdb
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows.js
@@ -0,0 +1,345 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Since we apply title localization asynchronously,
+// we'll use this helper to wait for the title to match
+// the condition and then test against it.
+async function verifyTitle(win, test, desc) {
+ await TestUtils.waitForCondition(test);
+ ok(true, desc);
+}
+
+add_task(async function testWindowGetAll() {
+ let raisedWin = Services.ww.openWindow(
+ null,
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ "chrome,dialog=no,all,alwaysRaised",
+ null
+ );
+
+ await TestUtils.topicObserved(
+ "browser-delayed-startup-finished",
+ subject => subject == raisedWin
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: async function () {
+ let wins = await browser.windows.getAll();
+ browser.test.assertEq(2, wins.length, "Expect two windows");
+
+ browser.test.assertEq(
+ false,
+ wins[0].alwaysOnTop,
+ "Expect first window not to be always on top"
+ );
+ browser.test.assertEq(
+ true,
+ wins[1].alwaysOnTop,
+ "Expect first window to be always on top"
+ );
+
+ let win = await browser.windows.create({
+ url: "http://example.com",
+ type: "popup",
+ });
+
+ wins = await browser.windows.getAll();
+ browser.test.assertEq(3, wins.length, "Expect three windows");
+
+ wins = await browser.windows.getAll({ windowTypes: ["popup"] });
+ browser.test.assertEq(1, wins.length, "Expect one window");
+ browser.test.assertEq("popup", wins[0].type, "Expect type to be popup");
+
+ wins = await browser.windows.getAll({ windowTypes: ["normal"] });
+ browser.test.assertEq(2, wins.length, "Expect two windows");
+ browser.test.assertEq("normal", wins[0].type, "Expect type to be normal");
+ browser.test.assertEq("normal", wins[1].type, "Expect type to be normal");
+
+ wins = await browser.windows.getAll({ windowTypes: ["popup", "normal"] });
+ browser.test.assertEq(3, wins.length, "Expect three windows");
+
+ await browser.windows.remove(win.id);
+
+ browser.test.notifyPass("getAll");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("getAll");
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(raisedWin);
+});
+
+add_task(async function testWindowTitle() {
+ const PREFACE1 = "My prefix1 - ";
+ const PREFACE2 = "My prefix2 - ";
+ const START_URL =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_dummy.html";
+ const START_TITLE = "Dummy test page";
+ const NEW_URL =
+ "http://example.com/browser/browser/components/extensions/test/browser/file_title.html";
+ const NEW_TITLE = "Different title test page";
+
+ async function background() {
+ browser.test.onMessage.addListener(
+ async (msg, options, windowId, expected) => {
+ if (msg === "create") {
+ let win = await browser.windows.create(options);
+ browser.test.sendMessage("created", win);
+ }
+ if (msg === "update") {
+ let win = await browser.windows.get(windowId);
+ browser.test.assertTrue(
+ win.title.startsWith(expected.before.preface),
+ "Window has the expected title preface before update."
+ );
+ browser.test.assertTrue(
+ win.title.includes(expected.before.text),
+ "Window has the expected title text before update."
+ );
+ win = await browser.windows.update(windowId, options);
+ browser.test.assertTrue(
+ win.title.startsWith(expected.after.preface),
+ "Window has the expected title preface after update."
+ );
+ browser.test.assertTrue(
+ win.title.includes(expected.after.text),
+ "Window has the expected title text after update."
+ );
+ browser.test.sendMessage("updated", win);
+ }
+ }
+ );
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["tabs"],
+ },
+ });
+
+ await extension.startup();
+ const {
+ Management: {
+ global: { windowTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ async function createApiWin(options) {
+ let promiseLoaded = BrowserTestUtils.waitForNewWindow({ url: START_URL });
+ extension.sendMessage("create", options);
+ let apiWin = await extension.awaitMessage("created");
+ let realWin = windowTracker.getWindow(apiWin.id);
+ await promiseLoaded;
+ let expectedPreface = options.titlePreface ? options.titlePreface : "";
+ await verifyTitle(
+ realWin,
+ () => {
+ return (
+ realWin.document.title.startsWith(expectedPreface || START_TITLE) &&
+ realWin.document.title.includes(START_TITLE)
+ );
+ },
+ "Created window starts with the expected preface and includes the right title text."
+ );
+ return apiWin;
+ }
+
+ async function updateWindow(options, apiWin, expected) {
+ extension.sendMessage("update", options, apiWin.id, expected);
+ await extension.awaitMessage("updated");
+ let realWin = windowTracker.getWindow(apiWin.id);
+ await verifyTitle(
+ realWin,
+ () => {
+ return (
+ realWin.document.title.startsWith(
+ expected.after.preface || expected.after.text
+ ) && realWin.document.title.includes(expected.after.text)
+ );
+ },
+ "Updated window starts with the expected preface and includes the right title text."
+ );
+ await BrowserTestUtils.closeWindow(realWin);
+ }
+
+ // Create a window without a preface.
+ let apiWin = await createApiWin({ url: START_URL });
+
+ // Add a titlePreface to the window.
+ let expected = {
+ before: {
+ preface: "",
+ text: START_TITLE,
+ },
+ after: {
+ preface: PREFACE1,
+ text: START_TITLE,
+ },
+ };
+ await updateWindow({ titlePreface: PREFACE1 }, apiWin, expected);
+
+ // Create a window with a preface.
+ apiWin = await createApiWin({ url: START_URL, titlePreface: PREFACE1 });
+
+ // Navigate to a different url and check that title is reflected.
+ let realWin = windowTracker.getWindow(apiWin.id);
+ let promiseLoaded = BrowserTestUtils.browserLoaded(
+ realWin.gBrowser.selectedBrowser
+ );
+ BrowserTestUtils.loadURIString(realWin.gBrowser.selectedBrowser, NEW_URL);
+ await promiseLoaded;
+ await verifyTitle(
+ realWin,
+ () => {
+ return (
+ realWin.document.title.startsWith(PREFACE1) &&
+ realWin.document.title.includes(NEW_TITLE)
+ );
+ },
+ "Updated window starts with the expected preface and includes the expected title."
+ );
+
+ // Update the titlePreface of the window.
+ expected = {
+ before: {
+ preface: PREFACE1,
+ text: NEW_TITLE,
+ },
+ after: {
+ preface: PREFACE2,
+ text: NEW_TITLE,
+ },
+ };
+ await updateWindow({ titlePreface: PREFACE2 }, apiWin, expected);
+
+ // Create a window with a preface.
+ apiWin = await createApiWin({ url: START_URL, titlePreface: PREFACE1 });
+ realWin = windowTracker.getWindow(apiWin.id);
+
+ // Update the titlePreface of the window with an empty string.
+ expected = {
+ before: {
+ preface: PREFACE1,
+ text: START_TITLE,
+ },
+ after: {
+ preface: "",
+ text: START_TITLE,
+ },
+ };
+ await verifyTitle(
+ realWin,
+ () => realWin.document.title.startsWith(expected.before.preface),
+ "Updated window has the expected title preface."
+ );
+ await updateWindow({ titlePreface: "" }, apiWin, expected);
+ await verifyTitle(
+ realWin,
+ () => !realWin.document.title.startsWith(expected.before.preface),
+ "Updated window doesn't not contain the preface after update."
+ );
+
+ // Create a window with a preface.
+ apiWin = await createApiWin({ url: START_URL, titlePreface: PREFACE1 });
+ realWin = windowTracker.getWindow(apiWin.id);
+
+ // Update the window without a titlePreface.
+ expected = {
+ before: {
+ preface: PREFACE1,
+ text: START_TITLE,
+ },
+ after: {
+ preface: PREFACE1,
+ text: START_TITLE,
+ },
+ };
+ await updateWindow({}, apiWin, expected);
+
+ await extension.unload();
+});
+
+// Test that the window title is only available with the correct tab
+// permissions.
+add_task(async function testWindowTitlePermissions() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "http://example.com/"
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ function awaitMessage(name) {
+ return new Promise(resolve => {
+ browser.test.onMessage.addListener(function listener(...msg) {
+ if (msg[0] === name) {
+ browser.test.onMessage.removeListener(listener);
+ resolve(msg[1]);
+ }
+ });
+ });
+ }
+
+ let window = await browser.windows.getCurrent();
+
+ browser.test.assertEq(
+ undefined,
+ window.title,
+ "Window title should be null without tab permission"
+ );
+
+ browser.test.sendMessage("grant-activeTab");
+ let expectedTitle = await awaitMessage("title");
+
+ window = await browser.windows.getCurrent();
+ browser.test.assertEq(
+ expectedTitle,
+ window.title,
+ "Window should have the expected title with tab permission granted"
+ );
+
+ await browser.test.notifyPass("window-title-permissions");
+ },
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+ });
+
+ await extension.startup();
+
+ await extension.awaitMessage("grant-activeTab");
+ await clickBrowserAction(extension);
+ extension.sendMessage("title", document.title);
+
+ await extension.awaitFinish("window-title-permissions");
+
+ await extension.unload();
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testInvalidWindowId() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.test.assertRejects(
+ // Assuming that this windowId does not exist.
+ browser.windows.get(123456789),
+ /Invalid window/,
+ "Should receive invalid window"
+ );
+ browser.test.notifyPass("windows.get.invalid");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("windows.get.invalid");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js b/browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js
new file mode 100644
index 0000000000..c89bcfce77
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_allowScriptsToClose.js
@@ -0,0 +1,69 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Tests allowScriptsToClose option
+add_task(async function test_allowScriptsToClose() {
+ const files = {
+ "dummy.html": "",
+ "close.js": function () {
+ window.close();
+ if (!window.closed) {
+ browser.test.sendMessage("close-failed");
+ }
+ },
+ };
+
+ function background() {
+ browser.test.onMessage.addListener((msg, options) => {
+ function listener(_, { status }, { url }) {
+ if (status == "complete" && url == options.url) {
+ browser.tabs.onUpdated.removeListener(listener);
+ browser.tabs.executeScript({ file: "close.js" });
+ }
+ }
+ options.url = browser.runtime.getURL(options.url);
+ browser.windows.create(options);
+ if (msg === "create+execute") {
+ browser.tabs.onUpdated.addListener(listener);
+ }
+ });
+ browser.test.notifyPass();
+ }
+
+ const example = "http://example.com/";
+ const manifest = { permissions: ["tabs", example] };
+
+ const extension = ExtensionTestUtils.loadExtension({
+ files,
+ background,
+ manifest,
+ });
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.allow_scripts_to_close_windows", false]],
+ });
+
+ await extension.startup();
+ await extension.awaitFinish();
+
+ extension.sendMessage("create", { url: "dummy.html" });
+ let win = await BrowserTestUtils.waitForNewWindow();
+ await BrowserTestUtils.windowClosed(win);
+ info("script allowed to close the window");
+
+ extension.sendMessage("create+execute", { url: example });
+ win = await BrowserTestUtils.waitForNewWindow();
+ await BrowserTestUtils.windowClosed(win);
+ info("script allowed to close the window");
+
+ extension.sendMessage("create+execute", {
+ url: example,
+ allowScriptsToClose: true,
+ });
+ win = await BrowserTestUtils.waitForNewWindow();
+ await BrowserTestUtils.windowClosed(win);
+ info("script allowed to close the window");
+
+ await SpecialPowers.popPrefEnv();
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create.js b/browser/components/extensions/test/browser/browser_ext_windows_create.js
new file mode 100644
index 0000000000..6c41abcd3e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create.js
@@ -0,0 +1,205 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWindowCreate() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let _checkWindowPromise;
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "checked-window") {
+ _checkWindowPromise.resolve();
+ _checkWindowPromise = null;
+ }
+ });
+
+ let os;
+
+ function checkWindow(expected) {
+ return new Promise(resolve => {
+ _checkWindowPromise = { resolve };
+ browser.test.sendMessage("check-window", expected);
+ });
+ }
+
+ async function createWindow(params, expected, keep = false) {
+ let window = await browser.windows.create(...params);
+ // params is null when testing create without createData
+ params = params[0] || {};
+
+ // Prevent frequent intermittent failures on macos where the newly created window
+ // may have not always got into the fullscreen state before browser.window.create
+ // resolves the windows details.
+ if (
+ os === "mac" &&
+ params.state === "fullscreen" &&
+ window.state !== params.state
+ ) {
+ browser.test.log(
+ "Wait for window.state for the newly create window to be set to fullscreen"
+ );
+ while (window.state !== params.state) {
+ window = await browser.windows.get(window.id, { populate: true });
+ }
+ browser.test.log(
+ "Newly created browser window got into fullscreen state"
+ );
+ }
+
+ for (let key of Object.keys(params)) {
+ if (key == "state" && os == "mac" && params.state == "normal") {
+ // OS-X doesn't have a hard distinction between "normal" and
+ // "maximized" states.
+ browser.test.assertTrue(
+ window.state == "normal" || window.state == "maximized",
+ `Expected window.state (currently ${window.state}) to be "normal" but will accept "maximized"`
+ );
+ } else {
+ browser.test.assertEq(
+ params[key],
+ window[key],
+ `Got expected value for window.${key}`
+ );
+ }
+ }
+
+ browser.test.assertEq(
+ 1,
+ window.tabs.length,
+ "tabs property got populated"
+ );
+
+ await checkWindow(expected);
+ if (keep) {
+ return window;
+ }
+
+ if (params.state == "fullscreen" && os == "win") {
+ // FIXME: Closing a fullscreen window causes a window leak in
+ // Windows tests.
+ await browser.windows.update(window.id, { state: "normal" });
+ }
+ await browser.windows.remove(window.id);
+ }
+
+ try {
+ ({ os } = await browser.runtime.getPlatformInfo());
+
+ // Set the current window to state: "normal" because the test is failing on Windows
+ // where the current window is maximized.
+ let currentWindow = await browser.windows.getCurrent();
+ await browser.windows.update(currentWindow.id, { state: "normal" });
+
+ await createWindow([], { state: "STATE_NORMAL" });
+ await createWindow([{ state: "maximized" }], {
+ state: "STATE_MAXIMIZED",
+ });
+ await createWindow([{ state: "minimized" }], {
+ state: "STATE_MINIMIZED",
+ });
+ await createWindow([{ state: "normal" }], {
+ state: "STATE_NORMAL",
+ hiddenChrome: [],
+ });
+ await createWindow([{ state: "fullscreen" }], {
+ state: "STATE_FULLSCREEN",
+ });
+
+ let window = await createWindow(
+ [{ type: "popup" }],
+ {
+ hiddenChrome: [
+ "menubar",
+ "toolbar",
+ "location",
+ "directories",
+ "status",
+ "extrachrome",
+ ],
+ chromeFlags: ["CHROME_OPENAS_DIALOG"],
+ },
+ true
+ );
+
+ let tabs = await browser.tabs.query({
+ windowType: "popup",
+ active: true,
+ });
+
+ browser.test.assertEq(1, tabs.length, "Expected only one popup");
+ browser.test.assertEq(
+ window.id,
+ tabs[0].windowId,
+ "Expected new window to be returned in query"
+ );
+
+ await browser.windows.remove(window.id);
+
+ browser.test.notifyPass("window-create");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-create");
+ }
+ },
+ });
+
+ let latestWindow;
+ let windowListener = (window, topic) => {
+ if (topic == "domwindowopened") {
+ latestWindow = window;
+ }
+ };
+ Services.ww.registerNotification(windowListener);
+
+ extension.onMessage("check-window", expected => {
+ if (expected.state != null) {
+ let { windowState } = latestWindow;
+ if (latestWindow.fullScreen) {
+ windowState = latestWindow.STATE_FULLSCREEN;
+ }
+
+ if (expected.state == "STATE_NORMAL") {
+ ok(
+ windowState == window.STATE_NORMAL ||
+ windowState == window.STATE_MAXIMIZED,
+ `Expected windowState (currently ${windowState}) to be STATE_NORMAL but will accept STATE_MAXIMIZED`
+ );
+ } else {
+ is(
+ windowState,
+ window[expected.state],
+ `Expected window state to be ${expected.state}`
+ );
+ }
+ }
+ if (expected.hiddenChrome) {
+ let chromeHidden =
+ latestWindow.document.documentElement.getAttribute("chromehidden");
+ is(
+ chromeHidden.trim().split(/\s+/).sort().join(" "),
+ expected.hiddenChrome.sort().join(" "),
+ "Got expected hidden chrome"
+ );
+ }
+ if (expected.chromeFlags) {
+ let { chromeFlags } = latestWindow.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow);
+ for (let flag of expected.chromeFlags) {
+ ok(
+ chromeFlags & Ci.nsIWebBrowserChrome[flag],
+ `Expected window to have the ${flag} flag`
+ );
+ }
+ }
+
+ extension.sendMessage("checked-window");
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-create");
+ await extension.unload();
+
+ Services.ww.unregisterNotification(windowListener);
+ latestWindow = null;
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_cookieStoreId.js b/browser/components/extensions/test/browser/browser_ext_windows_create_cookieStoreId.js
new file mode 100644
index 0000000000..dc7303c89e
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create_cookieStoreId.js
@@ -0,0 +1,344 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function no_cookies_permission() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-container-1" }),
+ /No permission for cookieStoreId/,
+ "cookieStoreId requires cookies permission"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function invalid_cookieStoreId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "not-firefox-container-1" }),
+ /Illegal cookieStoreId/,
+ "cookieStoreId must be valid"
+ );
+
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-private" }),
+ /Illegal to set private cookieStoreId in a non-private window/,
+ "cookieStoreId cannot be private in a non-private window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.create({
+ cookieStoreId: "firefox-default",
+ incognito: true,
+ }),
+ /Illegal to set non-private cookieStoreId in a private window/,
+ "cookieStoreId cannot be non-private in an private window"
+ );
+
+ await browser.test.assertRejects(
+ browser.windows.create({
+ cookieStoreId: "firefox-container-1",
+ incognito: true,
+ }),
+ /Illegal to set non-private cookieStoreId in a private window/,
+ "cookieStoreId cannot be a container tab ID in a private window"
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function perma_private_browsing_mode() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.privatebrowsing.autostart", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-container-1" }),
+ /Contextual identities are unavailable in permanent private browsing mode/,
+ "cookieStoreId cannot be a container tab ID in perma-private browsing mode"
+ );
+
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function userContext_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", false]],
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "cookies"],
+ },
+ async background() {
+ await browser.test.assertRejects(
+ browser.windows.create({ cookieStoreId: "firefox-container-1" }),
+ /Contextual identities are currently disabled/,
+ "cookieStoreId cannot be a container tab ID when contextual identities are disabled"
+ );
+ browser.test.sendMessage("done");
+ },
+ });
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function valid_cookieStoreId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ const testCases = [
+ {
+ description: "no explicit URL",
+ createParams: {
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreIds: ["firefox-container-1"],
+ expectedExecuteScriptResult: [
+ // Default URL is about:home, and extensions cannot run scripts in it.
+ "Missing host permission for the tab",
+ ],
+ },
+ {
+ description: "one URL",
+ createParams: {
+ url: "about:blank",
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreIds: ["firefox-container-1"],
+ expectedExecuteScriptResult: ["about:blank - null"],
+ },
+ {
+ description: "one URL in an array",
+ createParams: {
+ url: ["about:blank"],
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreIds: ["firefox-container-1"],
+ expectedExecuteScriptResult: ["about:blank - null"],
+ },
+ {
+ description: "two URLs in an array",
+ createParams: {
+ url: ["about:blank", "about:blank"],
+ cookieStoreId: "firefox-container-1",
+ },
+ expectedCookieStoreIds: ["firefox-container-1", "firefox-container-1"],
+ expectedExecuteScriptResult: ["about:blank - null", "about:blank - null"],
+ },
+ ];
+
+ async function background(testCases) {
+ let readyTabs = new Map();
+ let tabReadyCheckers = new Set();
+ browser.webNavigation.onCompleted.addListener(({ url, tabId, frameId }) => {
+ if (frameId === 0) {
+ readyTabs.set(tabId, url);
+ browser.test.log(`Detected navigation in tab ${tabId} to ${url}.`);
+
+ for (let check of tabReadyCheckers) {
+ check(tabId, url);
+ }
+ }
+ });
+ async function awaitTabReady(tabId, expectedUrl) {
+ if (readyTabs.get(tabId) === expectedUrl) {
+ browser.test.log(`Tab ${tabId} was ready with URL ${expectedUrl}.`);
+ return;
+ }
+ await new Promise(resolve => {
+ browser.test.log(
+ `Waiting for tab ${tabId} to load URL ${expectedUrl}...`
+ );
+ tabReadyCheckers.add(function check(completedTabId, completedUrl) {
+ if (completedTabId === tabId && completedUrl === expectedUrl) {
+ tabReadyCheckers.delete(check);
+ resolve();
+ }
+ });
+ });
+ browser.test.log(`Tab ${tabId} is ready with URL ${expectedUrl}.`);
+ }
+
+ async function executeScriptAndGetResult(tabId) {
+ try {
+ return (
+ await browser.tabs.executeScript(tabId, {
+ matchAboutBlank: true,
+ code: "`${document.URL} - ${origin}`",
+ })
+ )[0];
+ } catch (e) {
+ return e.message;
+ }
+ }
+ for (let {
+ description,
+ createParams,
+ expectedCookieStoreIds,
+ expectedExecuteScriptResult,
+ } of testCases) {
+ let win = await browser.windows.create(createParams);
+
+ browser.test.assertEq(
+ expectedCookieStoreIds.length,
+ win.tabs.length,
+ "Expected number of tabs"
+ );
+
+ for (let [i, expectedCookieStoreId] of Object.entries(
+ expectedCookieStoreIds
+ )) {
+ browser.test.assertEq(
+ expectedCookieStoreId,
+ win.tabs[i].cookieStoreId,
+ `expected cookieStoreId for tab ${i} (${description})`
+ );
+ }
+
+ for (let [i, expectedResult] of Object.entries(
+ expectedExecuteScriptResult
+ )) {
+ // Wait until the the tab can process the tabs.executeScript calls.
+ // TODO: Remove this when bug 1418655 and bug 1397667 are fixed.
+ let expectedUrl = Array.isArray(createParams.url)
+ ? createParams.url[i]
+ : createParams.url || "about:home";
+ await awaitTabReady(win.tabs[i].id, expectedUrl);
+
+ let result = await executeScriptAndGetResult(win.tabs[i].id);
+ browser.test.assertEq(
+ expectedResult,
+ result,
+ `expected executeScript result for tab ${i} (${description})`
+ );
+ }
+
+ await browser.windows.remove(win.id);
+ }
+ browser.test.sendMessage("done");
+ }
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["cookies", "webNavigation"],
+ },
+ background: `(${background})(${JSON.stringify(testCases)})`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
+
+add_task(async function cookieStoreId_and_tabId() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["privacy.userContext.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["cookies"],
+ },
+ async background() {
+ for (let cookieStoreId of ["firefox-default", "firefox-container-1"]) {
+ let { id: normalTabId } = await browser.tabs.create({ cookieStoreId });
+
+ await browser.test.assertRejects(
+ browser.windows.create({
+ cookieStoreId: "firefox-private",
+ tabId: normalTabId,
+ }),
+ /`cookieStoreId` must match the tab's cookieStoreId/,
+ "Cannot use cookieStoreId for pre-existing tabs with a different cookieStoreId"
+ );
+
+ let win = await browser.windows.create({
+ cookieStoreId,
+ tabId: normalTabId,
+ });
+ browser.test.assertEq(
+ cookieStoreId,
+ win.tabs[0].cookieStoreId,
+ "Adopted tab"
+ );
+ await browser.windows.remove(win.id);
+ }
+
+ {
+ let privateWindow = await browser.windows.create({ incognito: true });
+ let privateTabId = privateWindow.tabs[0].id;
+
+ await browser.test.assertRejects(
+ browser.windows.create({
+ cookieStoreId: "firefox-default",
+ tabId: privateTabId,
+ }),
+ /`cookieStoreId` must match the tab's cookieStoreId/,
+ "Cannot use cookieStoreId for pre-existing tab in a private window"
+ );
+ let win = await browser.windows.create({
+ cookieStoreId: "firefox-private",
+ tabId: privateTabId,
+ });
+ browser.test.assertEq(
+ "firefox-private",
+ win.tabs[0].cookieStoreId,
+ "Adopted private tab"
+ );
+ await browser.windows.remove(win.id);
+
+ await browser.test.assertRejects(
+ browser.windows.remove(privateWindow.id),
+ /Invalid window ID:/,
+ "The original private window should have been closed when its only tab was adopted."
+ );
+ }
+
+ browser.test.sendMessage("done");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_params.js b/browser/components/extensions/test/browser/browser_ext_windows_create_params.js
new file mode 100644
index 0000000000..6d80085433
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create_params.js
@@ -0,0 +1,249 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.initMochitest(this);
+
+// Tests that incompatible parameters can't be used together.
+add_task(async function testWindowCreateParams() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ try {
+ for (let state of ["minimized", "maximized", "fullscreen"]) {
+ for (let param of ["left", "top", "width", "height"]) {
+ let expected = `"state": "${state}" may not be combined with "left", "top", "width", or "height"`;
+
+ await browser.test.assertRejects(
+ browser.windows.create({ state, [param]: 100 }),
+ RegExp(expected),
+ `Got expected error from create(${param}=100)`
+ );
+ }
+ }
+
+ browser.test.notifyPass("window-create-params");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-create-params");
+ }
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-create-params");
+ await extension.unload();
+});
+
+// We do not support the focused param, however we do not want
+// to fail despite an error when it is passed. This provides
+// better code level compatibility with chrome.
+add_task(async function testWindowCreateFocused() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ async function doWaitForWindow(createOpts, resolve) {
+ let created;
+ browser.windows.onFocusChanged.addListener(async function listener(
+ wid
+ ) {
+ if (wid == browser.windows.WINDOW_ID_NONE) {
+ return;
+ }
+ let win = await created;
+ if (win.id !== wid) {
+ return;
+ }
+ browser.windows.onFocusChanged.removeListener(listener);
+ // update the window object
+ let window = await browser.windows.get(wid);
+ resolve(window);
+ });
+ created = browser.windows.create(createOpts);
+ }
+ async function awaitNewFocusedWindow(createOpts) {
+ return new Promise(resolve => {
+ // eslint doesn't like an async promise function, so
+ // we need to wrap it like this.
+ doWaitForWindow(createOpts, resolve);
+ });
+ }
+ try {
+ let window = await awaitNewFocusedWindow({});
+ browser.test.assertEq(
+ window.focused,
+ true,
+ "window is focused without focused param"
+ );
+ browser.test.log("removeWindow");
+ await browser.windows.remove(window.id);
+ window = await awaitNewFocusedWindow({ focused: true });
+ browser.test.assertEq(
+ window.focused,
+ true,
+ "window is focused with focused: true"
+ );
+ browser.test.log("removeWindow");
+ await browser.windows.remove(window.id);
+ window = await awaitNewFocusedWindow({ focused: false });
+ browser.test.assertEq(
+ window.focused,
+ true,
+ "window is focused with focused: false"
+ );
+ browser.test.log("removeWindow");
+ await browser.windows.remove(window.id);
+ browser.test.notifyPass("window-create-params");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-create-params");
+ }
+ },
+ });
+
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ let { messages } = await AddonTestUtils.promiseConsoleOutput(async () => {
+ await extension.startup();
+ await extension.awaitFinish("window-create-params");
+ await extension.unload();
+ });
+
+ AddonTestUtils.checkMessages(
+ messages,
+ {
+ expected: [
+ {
+ message:
+ /Warning processing focused: Opening inactive windows is not supported/,
+ },
+ ],
+ },
+ "Expected warning processing focused"
+ );
+
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+});
+
+add_task(async function testPopupTypeWithDimension() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ await browser.windows.create({
+ type: "popup",
+ left: 123,
+ top: 123,
+ width: 151,
+ height: 152,
+ });
+ await browser.windows.create({
+ type: "popup",
+ left: 123,
+ width: 152,
+ height: 153,
+ });
+ await browser.windows.create({
+ type: "popup",
+ top: 123,
+ width: 153,
+ height: 154,
+ });
+ await browser.windows.create({
+ type: "popup",
+ left: screen.availWidth * 100,
+ top: screen.availHeight * 100,
+ width: 154,
+ height: 155,
+ });
+ await browser.windows.create({
+ type: "popup",
+ left: -screen.availWidth * 100,
+ top: -screen.availHeight * 100,
+ width: 155,
+ height: 156,
+ });
+ browser.test.sendMessage("windows-created");
+ },
+ });
+
+ const baseWindow = await BrowserTestUtils.openNewBrowserWindow();
+ baseWindow.resizeTo(150, 150);
+ baseWindow.moveTo(50, 50);
+
+ let windows = [];
+ let windowListener = (window, topic) => {
+ if (topic == "domwindowopened") {
+ windows.push(window);
+ }
+ };
+ Services.ww.registerNotification(windowListener);
+
+ await extension.startup();
+ await extension.awaitMessage("windows-created");
+ await extension.unload();
+
+ const regularScreen = getScreenAt(0, 0, 150, 150);
+ const roundedX = roundCssPixcel(123, regularScreen);
+ const roundedY = roundCssPixcel(123, regularScreen);
+
+ const availRectLarge = getCssAvailRect(
+ getScreenAt(screen.width * 100, screen.height * 100, 150, 150)
+ );
+ const maxRight = availRectLarge.right;
+ const maxBottom = availRectLarge.bottom;
+
+ const availRectSmall = getCssAvailRect(
+ getScreenAt(-screen.width * 100, -screen.height * 100, 150, 150150)
+ );
+ const minLeft = availRectSmall.left;
+ const minTop = availRectSmall.top;
+
+ const actualCoordinates = windows
+ .slice(0, 3)
+ .map(window => `${window.screenX},${window.screenY}`);
+ const offsetFromBase = 10;
+ const expectedCoordinates = [
+ `${roundedX},${roundedY}`,
+ // Missing top should be +10 from the last browser window.
+ `${roundedX},${baseWindow.screenY + offsetFromBase}`,
+ // Missing left should be +10 from the last browser window.
+ `${baseWindow.screenX + offsetFromBase},${roundedY}`,
+ ];
+ is(
+ actualCoordinates.join(" / "),
+ expectedCoordinates.join(" / "),
+ "expected popup type windows are opened at given coordinates"
+ );
+
+ const actualSizes = windows
+ .slice(0, 3)
+ .map(window => `${window.outerWidth}x${window.outerHeight}`);
+ const expectedSizes = [`151x152`, `152x153`, `153x154`];
+ is(
+ actualSizes.join(" / "),
+ expectedSizes.join(" / "),
+ "expected popup type windows are opened with given size"
+ );
+
+ const actualRect = {
+ top: windows[4].screenY,
+ bottom: windows[3].screenY + windows[3].outerHeight,
+ left: windows[4].screenX,
+ right: windows[3].screenX + windows[3].outerWidth,
+ };
+ const maxRect = {
+ top: minTop,
+ bottom: maxBottom,
+ left: minLeft,
+ right: maxRight,
+ };
+ isRectContained(actualRect, maxRect);
+
+ for (const window of windows) {
+ window.close();
+ }
+
+ Services.ww.unregisterNotification(windowListener);
+ windows = null;
+ await BrowserTestUtils.closeWindow(baseWindow);
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js b/browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js
new file mode 100644
index 0000000000..83fda199b7
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create_tabId.js
@@ -0,0 +1,387 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function assertNoLeaksInTabTracker() {
+ // Check that no tabs have been leaked by the internal tabTracker helper class.
+ const { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+ );
+ const { tabTracker } = ExtensionParent.apiManager.global;
+
+ for (const [tabId, nativeTab] of tabTracker._tabIds) {
+ if (!nativeTab.ownerGlobal) {
+ ok(
+ false,
+ `A tab with tabId ${tabId} has been leaked in the tabTracker ("${nativeTab.title}")`
+ );
+ }
+ }
+}
+
+add_task(async function testWindowCreate() {
+ async function background() {
+ let promiseTabAttached = () => {
+ return new Promise(resolve => {
+ browser.tabs.onAttached.addListener(function listener() {
+ browser.tabs.onAttached.removeListener(listener);
+ resolve();
+ });
+ });
+ };
+
+ let promiseTabUpdated = expected => {
+ return new Promise(resolve => {
+ browser.tabs.onUpdated.addListener(function listener(
+ tabId,
+ changeInfo,
+ tab
+ ) {
+ if (changeInfo.url === expected) {
+ browser.tabs.onUpdated.removeListener(listener);
+ resolve();
+ }
+ });
+ });
+ };
+
+ try {
+ let window = await browser.windows.getCurrent();
+ let windowId = window.id;
+
+ browser.test.log("Create additional tab in window 1");
+ let tab = await browser.tabs.create({ windowId, url: "about:blank" });
+ let tabId = tab.id;
+
+ browser.test.log("Create a new window, adopting the new tab");
+
+ // Note that we want to check against actual boolean values for
+ // all of the `incognito` property tests.
+ browser.test.assertEq(false, tab.incognito, "Tab is not private");
+
+ {
+ let [, window] = await Promise.all([
+ promiseTabAttached(),
+ browser.windows.create({ tabId: tabId }),
+ ]);
+ browser.test.assertEq(
+ false,
+ window.incognito,
+ "New window is not private"
+ );
+ browser.test.assertEq(
+ tabId,
+ window.tabs[0].id,
+ "tabs property populated correctly"
+ );
+
+ browser.test.log("Close the new window");
+ await browser.windows.remove(window.id);
+ }
+
+ {
+ browser.test.log("Create a new private window");
+ let privateWindow = await browser.windows.create({ incognito: true });
+ browser.test.assertEq(
+ true,
+ privateWindow.incognito,
+ "Private window is private"
+ );
+
+ browser.test.log("Create additional tab in private window");
+ let privateTab = await browser.tabs.create({
+ windowId: privateWindow.id,
+ });
+ browser.test.assertEq(
+ true,
+ privateTab.incognito,
+ "Private tab is private"
+ );
+
+ browser.test.log("Create a new window, adopting the new private tab");
+ let [, newWindow] = await Promise.all([
+ promiseTabAttached(),
+ browser.windows.create({ tabId: privateTab.id }),
+ ]);
+ browser.test.assertEq(
+ true,
+ newWindow.incognito,
+ "New private window is private"
+ );
+
+ browser.test.log("Close the new private window");
+ await browser.windows.remove(newWindow.id);
+
+ browser.test.log("Close the private window");
+ await browser.windows.remove(privateWindow.id);
+ }
+
+ browser.test.log("Try to create a window with both a tab and a URL");
+ [tab] = await browser.tabs.query({ windowId, active: true });
+ await browser.test.assertRejects(
+ browser.windows.create({ tabId: tab.id, url: "http://example.com/" }),
+ /`tabId` may not be used in conjunction with `url`/,
+ "Create call failed as expected"
+ );
+
+ browser.test.log(
+ "Try to create a window with both a tab and an invalid incognito setting"
+ );
+ await browser.test.assertRejects(
+ browser.windows.create({ tabId: tab.id, incognito: true }),
+ /`incognito` property must match the incognito state of tab/,
+ "Create call failed as expected"
+ );
+
+ browser.test.log("Try to create a window with an invalid tabId");
+ await browser.test.assertRejects(
+ browser.windows.create({ tabId: 0 }),
+ /Invalid tab ID: 0/,
+ "Create call failed as expected"
+ );
+
+ browser.test.log("Try to create a window with two URLs");
+ let readyPromise = Promise.all([
+ // tabs.onUpdated can be invoked between the call of windows.create and
+ // the invocation of its callback/promise, so set up the listeners
+ // before creating the window.
+ promiseTabUpdated("http://example.com/"),
+ promiseTabUpdated("http://example.org/"),
+ ]);
+
+ window = await browser.windows.create({
+ url: ["http://example.com/", "http://example.org/"],
+ });
+ await readyPromise;
+
+ browser.test.assertEq(
+ 2,
+ window.tabs.length,
+ "2 tabs were opened in new window"
+ );
+ browser.test.assertEq(
+ "about:blank",
+ window.tabs[0].url,
+ "about:blank, page not loaded yet"
+ );
+ browser.test.assertEq(
+ "about:blank",
+ window.tabs[1].url,
+ "about:blank, page not loaded yet"
+ );
+
+ window = await browser.windows.get(window.id, { populate: true });
+
+ browser.test.assertEq(
+ 2,
+ window.tabs.length,
+ "2 tabs were opened in new window"
+ );
+ browser.test.assertEq(
+ "http://example.com/",
+ window.tabs[0].url,
+ "Correct URL was loaded in tab 1"
+ );
+ browser.test.assertEq(
+ "http://example.org/",
+ window.tabs[1].url,
+ "Correct URL was loaded in tab 2"
+ );
+
+ await browser.windows.remove(window.id);
+
+ browser.test.notifyPass("window-create");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-create");
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ incognitoOverride: "spanning",
+ manifest: {
+ permissions: ["tabs"],
+ },
+
+ background,
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-create");
+ await extension.unload();
+
+ assertNoLeaksInTabTracker();
+});
+
+add_task(async function testWebNavigationOnWindowCreateTabId() {
+ async function background() {
+ const webNavEvents = [];
+ const onceTabsAttached = [];
+
+ let promiseTabAttached = tab => {
+ return new Promise(resolve => {
+ browser.tabs.onAttached.addListener(function listener(tabId) {
+ if (tabId !== tab.id) {
+ return;
+ }
+ browser.tabs.onAttached.removeListener(listener);
+ resolve();
+ });
+ });
+ };
+
+ // Listen to webNavigation.onCompleted events to ensure that
+ // it is not going to be fired when we move the existent tabs
+ // to new windows.
+ browser.webNavigation.onCompleted.addListener(data => {
+ webNavEvents.push(data);
+ });
+
+ // Wait for the list of urls needed to select the test tabs,
+ // and then move these tabs to a new window and assert that
+ // no webNavigation.onCompleted events should be received
+ // while the tabs are being adopted into the new windows.
+ browser.test.onMessage.addListener(async (msg, testTabURLs) => {
+ if (msg !== "testTabURLs") {
+ return;
+ }
+
+ // Retrieve the tabs list and filter out the tabs that should
+ // not be moved into a new window.
+ let allTabs = await browser.tabs.query({});
+ let testTabs = allTabs.filter(tab => {
+ return testTabURLs.includes(tab.url);
+ });
+
+ browser.test.assertEq(
+ 2,
+ testTabs.length,
+ "Got the expected number of test tabs"
+ );
+
+ for (let tab of testTabs) {
+ onceTabsAttached.push(promiseTabAttached(tab));
+ await browser.windows.create({ tabId: tab.id });
+ }
+
+ // Wait the tabs to have been attached to the new window and then assert that no
+ // webNavigation.onCompleted event has been received.
+ browser.test.log("Waiting tabs move to new window to be attached");
+ await Promise.all(onceTabsAttached);
+
+ browser.test.assertEq(
+ "[]",
+ JSON.stringify(webNavEvents),
+ "No webNavigation.onCompleted event should have been received"
+ );
+
+ // Remove all the test tabs before exiting the test successfully.
+ for (let tab of testTabs) {
+ await browser.tabs.remove(tab.id);
+ }
+
+ browser.test.notifyPass("webNavigation-on-window-create-tabId");
+ });
+ }
+
+ const testURLs = ["http://example.com/", "http://example.org/"];
+
+ for (let url of testURLs) {
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ await extension.sendMessage("testTabURLs", testURLs);
+
+ await extension.awaitFinish("webNavigation-on-window-create-tabId");
+ await extension.unload();
+
+ assertNoLeaksInTabTracker();
+});
+
+add_task(async function testGetLastFocusedDoesNotLeakDuringTabAdoption() {
+ async function background() {
+ const allTabs = await browser.tabs.query({});
+
+ browser.test.onMessage.addListener(async (msg, testTabURL) => {
+ if (msg !== "testTabURL") {
+ return;
+ }
+
+ let tab = allTabs.filter(tab => tab.url === testTabURL).pop();
+
+ // Keep calling getLastFocused while browser.windows.create is creating
+ // a new window to adopt the test tab, so that the test recreates
+ // conditions similar to the extension that has been triggered this leak
+ // (See Bug 1458918 for a rationale).
+ // The while loop is explicited exited right before the notifyPass
+ // (but unloading the extension will stop it in any case).
+ let stopGetLastFocusedLoop = false;
+ Promise.resolve().then(async () => {
+ while (!stopGetLastFocusedLoop) {
+ browser.windows.getLastFocused({ populate: true });
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 50));
+ }
+ });
+
+ // Create a new window which adopt an existent tab and wait the tab to
+ // be fully attached to the new window.
+ await Promise.all([
+ new Promise(resolve => {
+ const listener = () => {
+ browser.tabs.onAttached.removeListener(listener);
+ resolve();
+ };
+ browser.tabs.onAttached.addListener(listener);
+ }),
+ browser.windows.create({ tabId: tab.id }),
+ ]);
+
+ // Check that getLastFocused populate the tabs property once the tab adoption
+ // has been completed.
+ const lastFocusedPopulate = await browser.windows.getLastFocused({
+ populate: true,
+ });
+ browser.test.assertEq(
+ 1,
+ lastFocusedPopulate.tabs.length,
+ "Got the expected number of tabs from windows.getLastFocused"
+ );
+
+ // Remove the test tab.
+ await browser.tabs.remove(tab.id);
+
+ stopGetLastFocusedLoop = true;
+
+ browser.test.notifyPass("tab-adopted");
+ });
+ }
+
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs", "webNavigation"],
+ },
+ background,
+ });
+
+ await extension.startup();
+
+ extension.sendMessage("testTabURL", "http://example.com/");
+
+ await extension.awaitFinish("tab-adopted");
+
+ await extension.unload();
+
+ assertNoLeaksInTabTracker();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_create_url.js b/browser/components/extensions/test/browser/browser_ext_windows_create_url.js
new file mode 100644
index 0000000000..0ee3a50dff
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create_url.js
@@ -0,0 +1,242 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWindowCreate() {
+ let pageExt = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: "page@mochitest" } },
+ protocol_handlers: [
+ {
+ protocol: "ext+foo",
+ name: "a foo protocol handler",
+ uriTemplate: "page.html?val=%s",
+ },
+ ],
+ },
+ files: {
+ "page.html": `
+
+ `,
+ },
+ });
+ await pageExt.startup();
+
+ async function background(OTHER_PAGE) {
+ browser.test.log(`== using ${OTHER_PAGE}`);
+ const EXTENSION_URL = browser.runtime.getURL("test.html");
+ const EXT_PROTO = "ext+bar:foo";
+ const OTHER_PROTO = "ext+foo:bar";
+
+ let windows = new (class extends Map {
+ // eslint-disable-line new-parens
+ get(id) {
+ if (!this.has(id)) {
+ let window = {
+ tabs: new Map(),
+ };
+ window.promise = new Promise(resolve => {
+ window.resolvePromise = resolve;
+ });
+
+ this.set(id, window);
+ }
+
+ return super.get(id);
+ }
+ })();
+
+ browser.tabs.onUpdated.addListener((tabId, changed, tab) => {
+ if (changed.status == "complete" && tab.url !== "about:blank") {
+ let window = windows.get(tab.windowId);
+ window.tabs.set(tab.index, tab);
+
+ if (window.tabs.size === window.expectedTabs) {
+ browser.test.log("resolving a window load");
+ window.resolvePromise(window);
+ }
+ }
+ });
+
+ async function create(options) {
+ browser.test.log(`creating window for ${options.url}`);
+ // Note: may reject
+ let window = await browser.windows.create(options);
+ let win = windows.get(window.id);
+ win.id = window.id;
+
+ win.expectedTabs = Array.isArray(options.url) ? options.url.length : 1;
+
+ return win.promise;
+ }
+
+ let TEST_SETS = [
+ {
+ name: "Single protocol URL in this extension",
+ url: EXT_PROTO,
+ expect: [`${EXTENSION_URL}?val=ext%2Bbar%3Afoo`],
+ },
+ {
+ name: "Single, relative URL",
+ url: "test.html",
+ expect: [EXTENSION_URL],
+ },
+ {
+ name: "Single, absolute, extension URL",
+ url: EXTENSION_URL,
+ expect: [EXTENSION_URL],
+ },
+ {
+ // This is primarily for backwards-compatibility, to allow extensions
+ // to open other home pages. This test case opens the home page
+ // explicitly; the implicit case (windows.create({}) without URL) is at
+ // browser_ext_chrome_settings_overrides_home.js.
+ name: "Single, absolute, other extension URL",
+ url: OTHER_PAGE,
+ expect: [OTHER_PAGE],
+ },
+ {
+ // This is oddly inconsistent with the non-array case, but here we are
+ // intentionally stricter because of lesser backwards-compatibility
+ // concerns.
+ name: "Array, absolute, other extension URL",
+ url: [OTHER_PAGE],
+ expectError: `Illegal URL: ${OTHER_PAGE}`,
+ },
+ {
+ name: "Single protocol URL in other extension",
+ url: OTHER_PROTO,
+ expect: [`${OTHER_PAGE}?val=ext%2Bfoo%3Abar`],
+ },
+ {
+ name: "Single, about:blank",
+ // Added "?" after "about:blank" because the test's tab load detection
+ // ignores about:blank.
+ url: "about:blank?",
+ expect: ["about:blank?"],
+ },
+ {
+ name: "multiple urls",
+ url: [EXT_PROTO, "test.html", EXTENSION_URL, OTHER_PROTO],
+ expect: [
+ `${EXTENSION_URL}?val=ext%2Bbar%3Afoo`,
+ EXTENSION_URL,
+ EXTENSION_URL,
+ `${OTHER_PAGE}?val=ext%2Bfoo%3Abar`,
+ ],
+ },
+ {
+ name: "Reject array of own allowed URLs and other moz-extension:-URL",
+ url: [EXTENSION_URL, EXT_PROTO, "about:blank?#", OTHER_PAGE],
+ expectError: `Illegal URL: ${OTHER_PAGE}`,
+ },
+ {
+ name: "Single, about:robots",
+ url: "about:robots",
+ expectError: "Illegal URL: about:robots",
+ },
+ {
+ name: "Array containing about:robots",
+ url: ["about:robots"],
+ expectError: "Illegal URL: about:robots",
+ },
+ ];
+ async function checkCreateResult({ status, value, reason }, testCase) {
+ const window = status === "fulfilled" ? value : null;
+ try {
+ if (testCase.expectError) {
+ let error = reason?.message;
+ browser.test.assertEq(testCase.expectError, error, testCase.name);
+ } else {
+ let tabUrls = [];
+ for (let [tabIndex, tab] of window.tabs) {
+ tabUrls[tabIndex] = tab.url;
+ }
+ browser.test.assertDeepEq(testCase.expect, tabUrls, testCase.name);
+ }
+ } catch (e) {
+ browser.test.fail(`Unexpected failure in ${testCase.name} :: ${e}`);
+ } finally {
+ // Close opened windows, whether they were expected or not.
+ if (window) {
+ await browser.windows.remove(window.id);
+ }
+ }
+ }
+ try {
+ // First collect all results, in parallel.
+ const results = await Promise.allSettled(
+ TEST_SETS.map(t => create({ url: t.url }))
+ );
+ // Then check the results sequentially
+ await Promise.all(
+ TEST_SETS.map((t, i) => checkCreateResult(results[i], t))
+ );
+ browser.test.notifyPass("window-create-url");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-create-url");
+ }
+ }
+
+ // Watch for any permission prompts to show up and accept them.
+ let dialogCount = 0;
+ let windowObserver = window => {
+ // This listener will go away when the window is closed so there is no need
+ // to explicitely remove it.
+ // eslint-disable-next-line mozilla/balanced-listeners
+ window.addEventListener("dialogopen", event => {
+ dialogCount++;
+ let { dialog } = event.detail;
+ Assert.equal(
+ dialog?._openedURL,
+ "chrome://mozapps/content/handling/permissionDialog.xhtml",
+ "Should only ever see the permission dialog"
+ );
+ let dialogEl = dialog._frame.contentDocument.querySelector("dialog");
+ Assert.ok(dialogEl, "Dialog element should exist");
+ dialogEl.setAttribute("buttondisabledaccept", false);
+ dialogEl.acceptDialog();
+ });
+ };
+ Services.obs.addObserver(windowObserver, "browser-delayed-startup-finished");
+ registerCleanupFunction(() => {
+ Services.obs.removeObserver(
+ windowObserver,
+ "browser-delayed-startup-finished"
+ );
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["tabs"],
+ protocol_handlers: [
+ {
+ protocol: "ext+bar",
+ name: "a bar protocol handler",
+ uriTemplate: "test.html?val=%s",
+ },
+ ],
+ },
+
+ background: `(${background})("moz-extension://${pageExt.uuid}/page.html")`,
+
+ files: {
+ "test.html": ``,
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-create-url");
+ await extension.unload();
+ await pageExt.unload();
+
+ Assert.equal(
+ dialogCount,
+ 2,
+ "Expected to see the right number of permission prompts."
+ );
+
+ // Make sure windows have been released before finishing.
+ Cu.forceGC();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_events.js b/browser/components/extensions/test/browser/browser_ext_windows_events.js
new file mode 100644
index 0000000000..aa8a2655ce
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_events.js
@@ -0,0 +1,222 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+SimpleTest.requestCompleteLog();
+
+add_task(async function test_windows_events_not_allowed() {
+ let monitor = await startIncognitoMonitorExtension();
+
+ function background() {
+ browser.windows.onCreated.addListener(window => {
+ browser.test.log(`onCreated: windowId=${window.id}`);
+
+ browser.test.assertTrue(
+ Number.isInteger(window.id),
+ "Window object's id is an integer"
+ );
+ browser.test.assertEq(
+ "normal",
+ window.type,
+ "Window object returned with the correct type"
+ );
+ browser.test.sendMessage("window-created", window.id);
+ });
+
+ let lastWindowId;
+ browser.windows.onFocusChanged.addListener(async eventWindowId => {
+ browser.test.log(
+ `onFocusChange: windowId=${eventWindowId} lastWindowId=${lastWindowId}`
+ );
+
+ browser.test.assertTrue(
+ lastWindowId !== eventWindowId,
+ "onFocusChanged fired once for the given window"
+ );
+ lastWindowId = eventWindowId;
+
+ browser.test.assertTrue(
+ Number.isInteger(eventWindowId),
+ "windowId is an integer"
+ );
+ let window = await browser.windows.getLastFocused();
+ browser.test.sendMessage("window-focus-changed", {
+ winId: eventWindowId,
+ lastFocusedWindowId: window.id,
+ });
+ });
+
+ browser.windows.onRemoved.addListener(windowId => {
+ browser.test.log(`onRemoved: windowId=${windowId}`);
+
+ browser.test.assertTrue(
+ Number.isInteger(windowId),
+ "windowId is an integer"
+ );
+ browser.test.sendMessage("window-removed", windowId);
+ browser.test.notifyPass("windows.events");
+ });
+
+ browser.test.sendMessage("ready", browser.windows.WINDOW_ID_NONE);
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {},
+ background,
+ incognitoOverride: "spanning",
+ });
+
+ await extension.startup();
+ const WINDOW_ID_NONE = await extension.awaitMessage("ready");
+
+ async function awaitFocusChanged() {
+ let windowInfo = await extension.awaitMessage("window-focus-changed");
+ if (windowInfo.winId === WINDOW_ID_NONE) {
+ info("Ignoring a superfluous WINDOW_ID_NONE (blur) event.");
+ windowInfo = await extension.awaitMessage("window-focus-changed");
+ }
+ is(
+ windowInfo.winId,
+ windowInfo.lastFocusedWindowId,
+ "Last focused window has the correct id"
+ );
+ return windowInfo.winId;
+ }
+
+ const {
+ Management: {
+ global: { windowTracker },
+ },
+ } = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");
+
+ let currentWindow = window;
+ let currentWindowId = windowTracker.getId(currentWindow);
+ info(`Current window ID: ${currentWindowId}`);
+
+ info("Create browser window 1");
+ let win1 = await BrowserTestUtils.openNewBrowserWindow();
+ let win1Id = await extension.awaitMessage("window-created");
+ info(`Window 1 ID: ${win1Id}`);
+
+ // This shouldn't be necessary, but tests intermittently fail, so let's give
+ // it a try.
+ win1.focus();
+
+ let winId = await awaitFocusChanged();
+ is(winId, win1Id, "Got focus change event for the correct window ID.");
+
+ info("Create browser window 2");
+ let win2 = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ let win2Id = await extension.awaitMessage("window-created");
+ info(`Window 2 ID: ${win2Id}`);
+
+ win2.focus();
+
+ winId = await awaitFocusChanged();
+ is(winId, win2Id, "Got focus change event for the correct window ID.");
+
+ info("Focus browser window 1");
+ await focusWindow(win1);
+
+ winId = await awaitFocusChanged();
+ is(winId, win1Id, "Got focus change event for the correct window ID.");
+
+ info("Close browser window 2");
+ await BrowserTestUtils.closeWindow(win2);
+
+ winId = await extension.awaitMessage("window-removed");
+ is(winId, win2Id, "Got removed event for the correct window ID.");
+
+ info("Close browser window 1");
+ await BrowserTestUtils.closeWindow(win1);
+
+ currentWindow.focus();
+
+ winId = await extension.awaitMessage("window-removed");
+ is(winId, win1Id, "Got removed event for the correct window ID.");
+
+ winId = await awaitFocusChanged();
+ is(
+ winId,
+ currentWindowId,
+ "Got focus change event for the correct window ID."
+ );
+
+ await extension.awaitFinish("windows.events");
+ await extension.unload();
+ await monitor.unload();
+});
+
+add_task(async function test_windows_event_page() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.eventPages.enabled", true]],
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@windows" } },
+ background: { persistent: false },
+ },
+ background() {
+ let removed;
+ browser.windows.onCreated.addListener(window => {
+ browser.test.sendMessage("onCreated", window.id);
+ });
+ browser.windows.onRemoved.addListener(wid => {
+ removed = wid;
+ browser.test.sendMessage("onRemoved", wid);
+ });
+ browser.windows.onFocusChanged.addListener(wid => {
+ if (wid != browser.windows.WINDOW_ID_NONE && wid != removed) {
+ browser.test.sendMessage("onFocusChanged", wid);
+ }
+ });
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const EVENTS = ["onCreated", "onRemoved", "onFocusChanged"];
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "windows", event, {
+ primed: false,
+ });
+ }
+
+ // test events waken background
+ await extension.terminateBackground();
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "windows", event, {
+ primed: true,
+ });
+ }
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await extension.awaitMessage("ready");
+ let windowId = await extension.awaitMessage("onCreated");
+ ok(true, "persistent event woke background");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "windows", event, {
+ primed: false,
+ });
+ }
+ // focus returns the new window
+ let focusedId = await extension.awaitMessage("onFocusChanged");
+ Assert.equal(windowId, focusedId, "new window was focused");
+ await extension.terminateBackground();
+
+ await BrowserTestUtils.closeWindow(win);
+ await extension.awaitMessage("ready");
+ let removedId = await extension.awaitMessage("onRemoved");
+ Assert.equal(windowId, removedId, "window was removed");
+ // focus returns the window focus was passed to
+ focusedId = await extension.awaitMessage("onFocusChanged");
+ Assert.notEqual(windowId, focusedId, "old window was focused");
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_incognito.js b/browser/components/extensions/test/browser/browser_ext_windows_incognito.js
new file mode 100644
index 0000000000..ef6d8a8eae
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_incognito.js
@@ -0,0 +1,84 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_window_incognito() {
+ const url =
+ "http://mochi.test:8888/browser/browser/components/extensions/test/browser/file_iframe_document.html";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["http://mochi.test/"],
+ },
+ background() {
+ let lastFocusedWindowId = null;
+ // Catch focus change events to power the test below.
+ browser.windows.onFocusChanged.addListener(function listener(
+ eventWindowId
+ ) {
+ lastFocusedWindowId = eventWindowId;
+ browser.windows.onFocusChanged.removeListener(listener);
+ });
+
+ browser.test.onMessage.addListener(async pbw => {
+ browser.test.assertEq(
+ browser.windows.WINDOW_ID_NONE,
+ lastFocusedWindowId,
+ "Focus on private window sends the event, but doesn't reveal windowId (without permissions)"
+ );
+
+ await browser.test.assertRejects(
+ browser.windows.get(pbw.windowId),
+ /Invalid window ID/,
+ "should not be able to get incognito window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.remove(pbw.windowId),
+ /Invalid window ID/,
+ "should not be able to remove incognito window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.getCurrent(),
+ /Invalid window/,
+ "should not be able to get incognito top window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.getLastFocused(),
+ /Invalid window/,
+ "should not be able to get incognito focused window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.create({ incognito: true }),
+ /Extension does not have permission for incognito mode/,
+ "should not be able to create incognito window"
+ );
+ await browser.test.assertRejects(
+ browser.windows.update(pbw.windowId, { focused: true }),
+ /Invalid window ID/,
+ "should not be able to update incognito window"
+ );
+
+ let windows = await browser.windows.getAll();
+ browser.test.assertEq(
+ 1,
+ windows.length,
+ "unable to get incognito window"
+ );
+
+ browser.test.notifyPass("pass");
+ });
+ },
+ });
+
+ await extension.startup();
+
+ // The tests expect the incognito window to be
+ // created after the extension is started, so think
+ // carefully when moving this line.
+ let winData = await getIncognitoWindow(url);
+
+ extension.sendMessage(winData.details);
+ await extension.awaitFinish("pass");
+ await BrowserTestUtils.closeWindow(winData.win);
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_remove.js b/browser/components/extensions/test/browser/browser_ext_windows_remove.js
new file mode 100644
index 0000000000..455987a908
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_remove.js
@@ -0,0 +1,53 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function testWindowRemove() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ async function closeWindow(id) {
+ let window = await browser.windows.get(id);
+ return new Promise(function (resolve) {
+ browser.windows.onRemoved.addListener(async function listener(
+ windowId
+ ) {
+ browser.windows.onRemoved.removeListener(listener);
+ await browser.test.assertEq(
+ windowId,
+ window.id,
+ "The right window was closed"
+ );
+ await browser.test.assertRejects(
+ browser.windows.get(windowId),
+ new RegExp(`Invalid window ID: ${windowId}`),
+ "The window was really closed."
+ );
+ resolve();
+ });
+ browser.windows.remove(id);
+ });
+ }
+
+ browser.test.log("Create a new window and close it by its ID");
+ let newWindow = await browser.windows.create();
+ await closeWindow(newWindow.id);
+
+ browser.test.log("Create a new window and close it by WINDOW_ID_CURRENT");
+ await browser.windows.create();
+ await closeWindow(browser.windows.WINDOW_ID_CURRENT);
+
+ browser.test.log("Assert failure for bad parameter.");
+ await browser.test.assertThrows(
+ () => browser.windows.remove(-3),
+ /-3 is too small \(must be at least -2\)/,
+ "Invalid windowId throws"
+ );
+
+ browser.test.notifyPass("window-remove");
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-remove");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_size.js b/browser/components/extensions/test/browser/browser_ext_windows_size.js
new file mode 100644
index 0000000000..4a4f0d8a0c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_size.js
@@ -0,0 +1,122 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+add_task(async function testWindowCreate() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let _checkWindowPromise;
+ browser.test.onMessage.addListener((msg, arg) => {
+ if (msg == "checked-window") {
+ _checkWindowPromise.resolve(arg);
+ _checkWindowPromise = null;
+ }
+ });
+
+ let getWindowSize = () => {
+ return new Promise(resolve => {
+ _checkWindowPromise = { resolve };
+ browser.test.sendMessage("check-window");
+ });
+ };
+
+ const KEYS = ["left", "top", "width", "height"];
+ function checkGeom(expected, actual) {
+ for (let key of KEYS) {
+ browser.test.assertEq(
+ expected[key],
+ actual[key],
+ `Expected '${key}' value`
+ );
+ }
+ }
+
+ let windowId;
+ async function checkWindow(expected, retries = 5) {
+ let geom = await getWindowSize();
+
+ if (retries && KEYS.some(key => expected[key] != geom[key])) {
+ browser.test.log(
+ `Got mismatched size (${JSON.stringify(
+ expected
+ )} != ${JSON.stringify(geom)}). Retrying after a short delay.`
+ );
+
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ return checkWindow(expected, retries - 1);
+ }
+
+ browser.test.log(`Check actual window size`);
+ checkGeom(expected, geom);
+
+ browser.test.log("Check API-reported window size");
+
+ geom = await browser.windows.get(windowId);
+
+ checkGeom(expected, geom);
+ }
+
+ try {
+ let geom = { left: 100, top: 100, width: 500, height: 300 };
+
+ let window = await browser.windows.create(geom);
+ windowId = window.id;
+
+ await checkWindow(geom);
+
+ let update = { left: 150, width: 600 };
+ Object.assign(geom, update);
+ await browser.windows.update(windowId, update);
+ await checkWindow(geom);
+
+ update = { top: 150, height: 400 };
+ Object.assign(geom, update);
+ await browser.windows.update(windowId, update);
+ await checkWindow(geom);
+
+ geom = { left: 200, top: 200, width: 800, height: 600 };
+ await browser.windows.update(windowId, geom);
+ await checkWindow(geom);
+
+ let platformInfo = await browser.runtime.getPlatformInfo();
+ if (platformInfo.os != "linux") {
+ geom = { left: -50, top: -50, width: 800, height: 600 };
+ await browser.windows.update(windowId, geom);
+ await checkWindow({ ...geom, left: 0, top: 0 });
+ }
+
+ await browser.windows.remove(windowId);
+ browser.test.notifyPass("window-size");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-size");
+ }
+ },
+ });
+
+ let latestWindow;
+ let windowListener = (window, topic) => {
+ if (topic == "domwindowopened") {
+ latestWindow = window;
+ }
+ };
+ Services.ww.registerNotification(windowListener);
+
+ extension.onMessage("check-window", () => {
+ extension.sendMessage("checked-window", {
+ top: latestWindow.screenY,
+ left: latestWindow.screenX,
+ width: latestWindow.outerWidth,
+ height: latestWindow.outerHeight,
+ });
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-size");
+ await extension.unload();
+
+ Services.ww.unregisterNotification(windowListener);
+ latestWindow = null;
+});
diff --git a/browser/components/extensions/test/browser/browser_ext_windows_update.js b/browser/components/extensions/test/browser/browser_ext_windows_update.js
new file mode 100644
index 0000000000..0e02f30cbc
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_update.js
@@ -0,0 +1,386 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function () {
+ function promiseWaitForFocus(window) {
+ return new Promise(resolve => {
+ waitForFocus(function () {
+ ok(Services.focus.activeWindow === window, "correct window focused");
+ resolve();
+ }, window);
+ });
+ }
+
+ let window1 = window;
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ window2.focus();
+ await promiseWaitForFocus(window2);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function () {
+ browser.windows.getAll(undefined, function (wins) {
+ browser.test.assertEq(wins.length, 2, "should have two windows");
+
+ // Sort the unfocused window to the lower index.
+ wins.sort(function (win1, win2) {
+ if (win1.focused === win2.focused) {
+ return 0;
+ }
+
+ return win1.focused ? 1 : -1;
+ });
+
+ browser.windows.update(wins[0].id, { focused: true }, function () {
+ browser.test.sendMessage("check");
+ });
+ });
+ },
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("check")]);
+
+ await promiseWaitForFocus(window1);
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(window2);
+});
+
+add_task(async function testWindowUpdate() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ let _checkWindowPromise;
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "checked-window") {
+ _checkWindowPromise.resolve();
+ _checkWindowPromise = null;
+ }
+ });
+
+ let os;
+ function checkWindow(expected) {
+ return new Promise(resolve => {
+ _checkWindowPromise = { resolve };
+ browser.test.sendMessage("check-window", expected);
+ });
+ }
+
+ let currentWindowId;
+ async function updateWindow(windowId, params, expected, otherChecks) {
+ let window = await browser.windows.update(windowId, params);
+
+ browser.test.assertEq(
+ currentWindowId,
+ window.id,
+ "Expected WINDOW_ID_CURRENT to refer to the same window"
+ );
+ for (let key of Object.keys(params)) {
+ if (key == "state" && os == "mac" && params.state == "normal") {
+ // OS-X doesn't have a hard distinction between "normal" and
+ // "maximized" states.
+ browser.test.assertTrue(
+ window.state == "normal" || window.state == "maximized",
+ `Expected window.state (currently ${window.state}) to be "normal" but will accept "maximized"`
+ );
+ } else {
+ browser.test.assertEq(
+ params[key],
+ window[key],
+ `Got expected value for window.${key}`
+ );
+ }
+ }
+ if (otherChecks) {
+ for (let key of Object.keys(otherChecks)) {
+ browser.test.assertEq(
+ otherChecks[key],
+ window[key],
+ `Got expected value for window.${key}`
+ );
+ }
+ }
+
+ return checkWindow(expected);
+ }
+
+ try {
+ let windowId = browser.windows.WINDOW_ID_CURRENT;
+
+ ({ os } = await browser.runtime.getPlatformInfo());
+
+ let window = await browser.windows.getCurrent();
+ currentWindowId = window.id;
+
+ // Store current, "normal" width and height to compare against
+ // window width and height after updating to "normal" state.
+ let normalWidth = window.width;
+ let normalHeight = window.height;
+
+ await updateWindow(
+ windowId,
+ { state: "maximized" },
+ { state: "STATE_MAXIMIZED" }
+ );
+ await updateWindow(
+ windowId,
+ { state: "normal" },
+ { state: "STATE_NORMAL" },
+ { width: normalWidth, height: normalHeight }
+ );
+ await updateWindow(
+ windowId,
+ { state: "minimized" },
+ { state: "STATE_MINIMIZED" }
+ );
+ await updateWindow(
+ windowId,
+ { state: "normal" },
+ { state: "STATE_NORMAL" },
+ { width: normalWidth, height: normalHeight }
+ );
+ await updateWindow(
+ windowId,
+ { state: "fullscreen" },
+ { state: "STATE_FULLSCREEN" }
+ );
+ await updateWindow(
+ windowId,
+ { state: "normal" },
+ { state: "STATE_NORMAL" },
+ { width: normalWidth, height: normalHeight }
+ );
+
+ browser.test.notifyPass("window-update");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-update");
+ }
+ },
+ });
+
+ extension.onMessage("check-window", expected => {
+ if (expected.state != null) {
+ let { windowState } = window;
+ if (window.fullScreen) {
+ windowState = window.STATE_FULLSCREEN;
+ }
+
+ // Temporarily accepting STATE_MAXIMIZED on Linux because of bug 1307759.
+ if (
+ expected.state == "STATE_NORMAL" &&
+ (AppConstants.platform == "macosx" || AppConstants.platform == "linux")
+ ) {
+ ok(
+ windowState == window.STATE_NORMAL ||
+ windowState == window.STATE_MAXIMIZED,
+ `Expected windowState (currently ${windowState}) to be STATE_NORMAL but will accept STATE_MAXIMIZED`
+ );
+ } else {
+ is(
+ windowState,
+ window[expected.state],
+ `Expected window state to be ${expected.state}`
+ );
+ }
+ }
+
+ extension.sendMessage("checked-window");
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-update");
+ await extension.unload();
+});
+
+add_task(async function () {
+ let window2 = await BrowserTestUtils.openNewBrowserWindow();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background: function () {
+ browser.windows.getAll(undefined, function (wins) {
+ browser.test.assertEq(wins.length, 2, "should have two windows");
+
+ let unfocused = wins.find(win => !win.focused);
+ browser.windows.update(
+ unfocused.id,
+ { drawAttention: true },
+ function () {
+ browser.test.sendMessage("check");
+ }
+ );
+ });
+ },
+ });
+
+ await Promise.all([extension.startup(), extension.awaitMessage("check")]);
+
+ await extension.unload();
+
+ await BrowserTestUtils.closeWindow(window2);
+});
+
+// Tests that incompatible parameters can't be used together.
+add_task(async function testWindowUpdateParams() {
+ let extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ try {
+ for (let state of ["minimized", "maximized", "fullscreen"]) {
+ for (let param of ["left", "top", "width", "height"]) {
+ let expected = `"state": "${state}" may not be combined with "left", "top", "width", or "height"`;
+
+ let windowId = browser.windows.WINDOW_ID_CURRENT;
+ await browser.test.assertRejects(
+ browser.windows.update(windowId, { state, [param]: 100 }),
+ RegExp(expected),
+ `Got expected error for create(${param}=100`
+ );
+ }
+ }
+
+ browser.test.notifyPass("window-update-params");
+ } catch (e) {
+ browser.test.fail(`${e} :: ${e.stack}`);
+ browser.test.notifyFail("window-update-params");
+ }
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("window-update-params");
+ await extension.unload();
+});
+
+add_task(async function testPositionBoundaryCheck() {
+ const extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ function waitMessage() {
+ return new Promise((resolve, reject) => {
+ const onMessage = message => {
+ if (message == "continue") {
+ browser.test.onMessage.removeListener(onMessage);
+ resolve();
+ }
+ };
+ browser.test.onMessage.addListener(onMessage);
+ });
+ }
+ const win = await browser.windows.create({
+ type: "popup",
+ left: 50,
+ top: 50,
+ width: 150,
+ height: 150,
+ });
+ await browser.test.sendMessage("ready");
+ await waitMessage();
+ await browser.windows.update(win.id, {
+ left: 123,
+ top: 123,
+ });
+ await browser.test.sendMessage("regular");
+ await waitMessage();
+ await browser.windows.update(win.id, {
+ left: 123,
+ });
+ await browser.test.sendMessage("only-left");
+ await waitMessage();
+ await browser.windows.update(win.id, {
+ top: 123,
+ });
+ await browser.test.sendMessage("only-top");
+ await waitMessage();
+ await browser.windows.update(win.id, {
+ left: screen.availWidth * 100,
+ top: screen.availHeight * 100,
+ });
+ await browser.test.sendMessage("too-large");
+ await waitMessage();
+ await browser.windows.update(win.id, {
+ left: -screen.availWidth * 100,
+ top: -screen.availHeight * 100,
+ });
+ await browser.test.sendMessage("too-small");
+ },
+ });
+
+ const promisedWin = new Promise((resolve, reject) => {
+ const windowListener = (window, topic) => {
+ if (topic == "domwindowopened") {
+ Services.ww.unregisterNotification(windowListener);
+ resolve(window);
+ }
+ };
+ Services.ww.registerNotification(windowListener);
+ });
+
+ await extension.startup();
+
+ const win = await promisedWin;
+
+ const regularScreen = getScreenAt(0, 0, 150, 150);
+ const roundedX = roundCssPixcel(123, regularScreen);
+ const roundedY = roundCssPixcel(123, regularScreen);
+
+ const availRectLarge = getCssAvailRect(
+ getScreenAt(screen.width * 100, screen.height * 100, 150, 150)
+ );
+ const maxRight = availRectLarge.right;
+ const maxBottom = availRectLarge.bottom;
+
+ const availRectSmall = getCssAvailRect(
+ getScreenAt(-screen.width * 100, -screen.height * 100, 150, 150)
+ );
+ const minLeft = availRectSmall.left;
+ const minTop = availRectSmall.top;
+
+ const expectedCoordinates = [
+ `${roundedX},${roundedY}`,
+ `${roundedX},${win.screenY}`,
+ `${win.screenX},${roundedY}`,
+ ];
+
+ await extension.awaitMessage("ready");
+
+ const actualCoordinates = [];
+ extension.sendMessage("continue");
+ await extension.awaitMessage("regular");
+ actualCoordinates.push(`${win.screenX},${win.screenY}`);
+ win.moveTo(50, 50);
+ extension.sendMessage("continue");
+ await extension.awaitMessage("only-left");
+ actualCoordinates.push(`${win.screenX},${win.screenY}`);
+ win.moveTo(50, 50);
+ extension.sendMessage("continue");
+ await extension.awaitMessage("only-top");
+ actualCoordinates.push(`${win.screenX},${win.screenY}`);
+ is(
+ actualCoordinates.join(" / "),
+ expectedCoordinates.join(" / "),
+ "expected window is placed at given coordinates"
+ );
+
+ const actualRect = {};
+ const maxRect = {
+ top: minTop,
+ bottom: maxBottom,
+ left: minLeft,
+ right: maxRight,
+ };
+
+ extension.sendMessage("continue");
+ await extension.awaitMessage("too-large");
+ actualRect.right = win.screenX + win.outerWidth;
+ actualRect.bottom = win.screenY + win.outerHeight;
+
+ extension.sendMessage("continue");
+ await extension.awaitMessage("too-small");
+ actualRect.top = win.screenY;
+ actualRect.left = win.screenX;
+
+ isRectContained(actualRect, maxRect);
+
+ await extension.unload();
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/browser_toolbar_prefers_color_scheme.js b/browser/components/extensions/test/browser/browser_toolbar_prefers_color_scheme.js
new file mode 100644
index 0000000000..ae7f488f0a
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_toolbar_prefers_color_scheme.js
@@ -0,0 +1,266 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kDark = 0;
+const kLight = 1;
+const kSystem = 2;
+
+// The above tests should be enough to make sure that the prefs behave as
+// expected, the following ones test various edge cases in a simpler way.
+async function testTheme(description, toolbar, content, themeManifestData) {
+ info(description);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "dummy@mochi.test",
+ },
+ },
+ ...themeManifestData,
+ },
+ });
+
+ await Promise.all([
+ TestUtils.topicObserved("lightweight-theme-styling-update"),
+ extension.startup(),
+ ]);
+
+ is(
+ SpecialPowers.getIntPref("browser.theme.toolbar-theme"),
+ toolbar,
+ "Toolbar theme expected"
+ );
+ is(
+ SpecialPowers.getIntPref("browser.theme.content-theme"),
+ content,
+ "Content theme expected"
+ );
+
+ await Promise.all([
+ TestUtils.topicObserved("lightweight-theme-styling-update"),
+ extension.unload(),
+ ]);
+}
+
+add_task(async function test_dark_toolbar_dark_text() {
+ // Bug 1743010
+ await testTheme(
+ "Dark toolbar color, dark toolbar background",
+ kDark,
+ kSystem,
+ {
+ theme: {
+ colors: {
+ toolbar: "rgb(20, 17, 26)",
+ toolbar_text: "rgb(251, 29, 78)",
+ },
+ },
+ }
+ );
+
+ // Dark frame text is ignored as it might be overlaid with an image,
+ // see bug 1741931.
+ await testTheme("Dark frame is ignored", kLight, kSystem, {
+ theme: {
+ colors: {
+ frame: "#000000",
+ tab_background_text: "#000000",
+ },
+ },
+ });
+
+ await testTheme(
+ "Semi-transparent toolbar backgrounds are ignored.",
+ kLight,
+ kSystem,
+ {
+ theme: {
+ colors: {
+ toolbar: "rgba(0, 0, 0, .2)",
+ toolbar_text: "#000",
+ },
+ },
+ }
+ );
+});
+
+add_task(async function dark_theme_presence_overrides_heuristics() {
+ const systemScheme = window.matchMedia("(-moz-system-dark-theme)").matches
+ ? kDark
+ : kLight;
+ await testTheme(
+ "darkTheme presence overrides heuristics",
+ systemScheme,
+ kSystem,
+ {
+ theme: {
+ colors: {
+ toolbar: "#000",
+ toolbar_text: "#fff",
+ },
+ },
+ dark_theme: {
+ colors: {
+ toolbar: "#000",
+ toolbar_text: "#fff",
+ },
+ },
+ }
+ );
+});
+
+add_task(async function color_scheme_override() {
+ await testTheme(
+ "color_scheme overrides toolbar / toolbar_text pair (dark)",
+ kDark,
+ kDark,
+ {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ },
+ properties: {
+ color_scheme: "dark",
+ },
+ },
+ }
+ );
+
+ await testTheme(
+ "color_scheme overrides toolbar / toolbar_text pair (light)",
+ kLight,
+ kLight,
+ {
+ theme: {
+ colors: {
+ toolbar: "#000",
+ toolbar_text: "#fff",
+ },
+ properties: {
+ color_scheme: "light",
+ },
+ },
+ }
+ );
+
+ await testTheme(
+ "content_color_scheme overrides ntp_text / ntp_background (dark)",
+ kLight,
+ kDark,
+ {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ ntp_background: "#fff",
+ ntp_text: "#000",
+ },
+ properties: {
+ content_color_scheme: "dark",
+ },
+ },
+ }
+ );
+
+ await testTheme(
+ "content_color_scheme overrides ntp_text / ntp_background (light)",
+ kLight,
+ kLight,
+ {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ ntp_background: "#000",
+ ntp_text: "#fff",
+ },
+ properties: {
+ content_color_scheme: "light",
+ },
+ },
+ }
+ );
+
+ await testTheme(
+ "content_color_scheme overrides color_scheme only for content",
+ kLight,
+ kDark,
+ {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ ntp_background: "#fff",
+ ntp_text: "#000",
+ },
+ properties: {
+ content_color_scheme: "dark",
+ },
+ },
+ }
+ );
+
+ await testTheme(
+ "content_color_scheme sytem overrides color_scheme only for content",
+ kLight,
+ kSystem,
+ {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ ntp_background: "#fff",
+ ntp_text: "#000",
+ },
+ properties: {
+ content_color_scheme: "system",
+ },
+ },
+ }
+ );
+
+ await testTheme("color_scheme: sytem override", kSystem, kSystem, {
+ theme: {
+ colors: {
+ toolbar: "#fff",
+ toolbar_text: "#000",
+ ntp_background: "#fff",
+ ntp_text: "#000",
+ },
+ properties: {
+ color_scheme: "system",
+ content_color_scheme: "system",
+ },
+ },
+ });
+});
+
+add_task(async function unified_theme() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.theme.unified-color-scheme", true]],
+ });
+
+ await testTheme("Dark toolbar color", kDark, kDark, {
+ theme: {
+ colors: {
+ toolbar: "rgb(20, 17, 26)",
+ toolbar_text: "rgb(251, 29, 78)",
+ },
+ },
+ });
+
+ await testTheme("Light toolbar color", kLight, kLight, {
+ theme: {
+ colors: {
+ toolbar: "white",
+ toolbar_text: "black",
+ },
+ },
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions.js b/browser/components/extensions/test/browser/browser_unified_extensions.js
new file mode 100644
index 0000000000..0a889b7b56
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions.js
@@ -0,0 +1,1543 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+/* import-globals-from ../../../../../toolkit/mozapps/extensions/test/browser/head.js */
+
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+loadTestSubscript("head_unified_extensions.js");
+
+const openCustomizationUI = async () => {
+ const customizationReady = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "customizationready"
+ );
+ gCustomizeMode.enter();
+ await customizationReady;
+ ok(
+ CustomizationHandler.isCustomizing(),
+ "expected customizing mode to be enabled"
+ );
+};
+
+const closeCustomizationUI = async () => {
+ const afterCustomization = BrowserTestUtils.waitForEvent(
+ gNavToolbox,
+ "aftercustomization"
+ );
+ gCustomizeMode.exit();
+ await afterCustomization;
+ ok(
+ !CustomizationHandler.isCustomizing(),
+ "expected customizing mode to be disabled"
+ );
+};
+
+add_setup(async function () {
+ // Make sure extension buttons added to the navbar will not overflow in the
+ // panel, which could happen when a previous test file resizes the current
+ // window.
+ await ensureMaximizedWindow(window);
+});
+
+add_task(async function test_button_enabled_by_pref() {
+ const { button } = gUnifiedExtensions;
+ is(button.hidden, false, "expected button to be visible");
+ is(
+ document
+ .getElementById("nav-bar")
+ .getAttribute("unifiedextensionsbuttonshown"),
+ "true",
+ "expected attribute on nav-bar"
+ );
+});
+
+add_task(async function test_open_panel_on_button_click() {
+ const extensions = createExtensions([
+ { name: "Extension #1" },
+ { name: "Another extension", icons: { 16: "test-icon-16.png" } },
+ {
+ name: "Yet another extension with an icon",
+ icons: {
+ 32: "test-icon-32.png",
+ },
+ },
+ ]);
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ await openExtensionsPanel();
+
+ let item = getUnifiedExtensionsItem(extensions[0].id);
+ is(
+ item.querySelector(".unified-extensions-item-name").textContent,
+ "Extension #1",
+ "expected name of the first extension"
+ );
+ is(
+ item.querySelector(".unified-extensions-item-icon").src,
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg",
+ "expected generic icon for the first extension"
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(
+ item.querySelector(".unified-extensions-item-menu-button")
+ ),
+ {
+ id: "unified-extensions-item-open-menu",
+ args: { extensionName: "Extension #1" },
+ },
+ "expected l10n attributes for the first extension"
+ );
+
+ item = getUnifiedExtensionsItem(extensions[1].id);
+ is(
+ item.querySelector(".unified-extensions-item-name").textContent,
+ "Another extension",
+ "expected name of the second extension"
+ );
+ ok(
+ item
+ .querySelector(".unified-extensions-item-icon")
+ .src.endsWith("/test-icon-16.png"),
+ "expected custom icon for the second extension"
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(
+ item.querySelector(".unified-extensions-item-menu-button")
+ ),
+ {
+ id: "unified-extensions-item-open-menu",
+ args: { extensionName: "Another extension" },
+ },
+ "expected l10n attributes for the second extension"
+ );
+
+ item = getUnifiedExtensionsItem(extensions[2].id);
+ is(
+ item.querySelector(".unified-extensions-item-name").textContent,
+ "Yet another extension with an icon",
+ "expected name of the third extension"
+ );
+ ok(
+ item
+ .querySelector(".unified-extensions-item-icon")
+ .src.endsWith("/test-icon-32.png"),
+ "expected custom icon for the third extension"
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(
+ item.querySelector(".unified-extensions-item-menu-button")
+ ),
+ {
+ id: "unified-extensions-item-open-menu",
+ args: { extensionName: "Yet another extension with an icon" },
+ },
+ "expected l10n attributes for the third extension"
+ );
+
+ await closeExtensionsPanel();
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+});
+
+// Verify that the context click doesn't open the panel in addition to the
+// context menu.
+add_task(async function test_clicks_on_unified_extension_button() {
+ const extensions = createExtensions([{ name: "Extension #1" }]);
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ const { button, panel } = gUnifiedExtensions;
+ ok(button, "expected button");
+ ok(panel, "expected panel");
+
+ info("open panel with primary click");
+ await openExtensionsPanel();
+ ok(
+ panel.getAttribute("panelopen") === "true",
+ "expected panel to be visible"
+ );
+ await closeExtensionsPanel();
+ ok(!panel.hasAttribute("panelopen"), "expected panel to be hidden");
+
+ info("open context menu with non-primary click");
+ const contextMenu = document.getElementById("toolbar-context-menu");
+ const popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(button, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await popupShownPromise;
+ ok(!panel.hasAttribute("panelopen"), "expected panel to remain hidden");
+ await closeChromeContextMenu(contextMenu.id, null);
+
+ // On MacOS, ctrl-click shouldn't open the panel because this normally opens
+ // the context menu. We can't test anything on MacOS...
+ if (AppConstants.platform !== "macosx") {
+ info("open panel with ctrl-click");
+ const listView = getListView();
+ const viewShown = BrowserTestUtils.waitForEvent(listView, "ViewShown");
+ EventUtils.synthesizeMouseAtCenter(button, { ctrlKey: true });
+ await viewShown;
+ ok(
+ panel.getAttribute("panelopen") === "true",
+ "expected panel to be visible"
+ );
+ await closeExtensionsPanel();
+ ok(!panel.hasAttribute("panelopen"), "expected panel to be hidden");
+ }
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+});
+
+add_task(async function test_item_shows_the_best_addon_icon() {
+ const extensions = createExtensions([
+ {
+ name: "Extension with different icons",
+ icons: {
+ 16: "test-icon-16.png",
+ 32: "test-icon-32.png",
+ 64: "test-icon-64.png",
+ 96: "test-icon-96.png",
+ 128: "test-icon-128.png",
+ },
+ },
+ ]);
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ for (const { resolution, expectedIcon } of [
+ { resolution: 2, expectedIcon: "test-icon-64.png" },
+ { resolution: 1, expectedIcon: "test-icon-32.png" },
+ ]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["layout.css.devPixelsPerPx", String(resolution)]],
+ });
+ is(
+ window.devicePixelRatio,
+ resolution,
+ "window has the required resolution"
+ );
+
+ await openExtensionsPanel();
+
+ const item = getUnifiedExtensionsItem(extensions[0].id);
+ const iconSrc = item.querySelector(".unified-extensions-item-icon").src;
+ ok(
+ iconSrc.endsWith(expectedIcon),
+ `expected ${expectedIcon}, got: ${iconSrc}`
+ );
+
+ await closeExtensionsPanel();
+ await SpecialPowers.popPrefEnv();
+ }
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+});
+
+add_task(async function test_panel_has_a_manage_extensions_button() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots" },
+ async () => {
+ await openExtensionsPanel();
+
+ const manageExtensionsButton = getListView().querySelector(
+ "#unified-extensions-manage-extensions"
+ );
+ ok(manageExtensionsButton, "expected a 'manage extensions' button");
+
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons",
+ true
+ );
+ const popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ document,
+ "popuphidden",
+ true
+ );
+
+ manageExtensionsButton.click();
+
+ const [tab] = await Promise.all([tabPromise, popupHiddenPromise]);
+ is(
+ gBrowser.currentURI.spec,
+ "about:addons",
+ "Manage opened about:addons"
+ );
+ is(
+ gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId,
+ "addons://list/extension",
+ "expected about:addons to show the list of extensions"
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_list_active_extensions_only() {
+ const arrayOfManifestData = [
+ {
+ name: "hidden addon",
+ browser_specific_settings: { gecko: { id: "ext1@test" } },
+ hidden: true,
+ },
+ {
+ name: "regular addon",
+ browser_specific_settings: { gecko: { id: "ext2@test" } },
+ hidden: false,
+ },
+ {
+ name: "disabled addon",
+ browser_specific_settings: { gecko: { id: "ext3@test" } },
+ hidden: false,
+ },
+ {
+ name: "regular addon with browser action",
+ browser_specific_settings: { gecko: { id: "ext4@test" } },
+ hidden: false,
+ browser_action: {
+ default_area: "navbar",
+ },
+ },
+ {
+ manifest_version: 3,
+ name: "regular mv3 addon with browser action",
+ browser_specific_settings: { gecko: { id: "ext5@test" } },
+ hidden: false,
+ action: {
+ default_area: "navbar",
+ },
+ },
+ {
+ name: "regular addon with page action",
+ browser_specific_settings: { gecko: { id: "ext6@test" } },
+ hidden: false,
+ page_action: {},
+ },
+ ];
+ const extensions = createExtensions(arrayOfManifestData, {
+ useAddonManager: "temporary",
+ // Allow all extensions in PB mode by default.
+ incognitoOverride: "spanning",
+ });
+ // This extension is loaded with a different `incognitoOverride` value to
+ // make sure it won't show up in a private window.
+ extensions.push(
+ ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: "ext7@test" } },
+ name: "regular addon with private browsing disabled",
+ },
+ useAddonManager: "temporary",
+ incognitoOverride: "not_allowed",
+ })
+ );
+
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ // Disable the "disabled addon".
+ let addon2 = await AddonManager.getAddonByID(extensions[2].id);
+ await addon2.disable();
+
+ for (const isPrivate of [false, true]) {
+ info(
+ `verifying extensions listed in the panel with private browsing ${
+ isPrivate ? "enabled" : "disabled"
+ }`
+ );
+ const aWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: isPrivate,
+ });
+ // Make sure extension buttons added to the navbar will not overflow in the
+ // panel, which could happen when a previous test file resizes the current
+ // window.
+ await ensureMaximizedWindow(aWin);
+
+ await openExtensionsPanel(aWin);
+
+ ok(
+ aWin.gUnifiedExtensions._button.open,
+ "Expected unified extension panel to be open"
+ );
+
+ const hiddenAddonItem = getUnifiedExtensionsItem(extensions[0].id, aWin);
+ is(hiddenAddonItem, null, "didn't expect an item for a hidden add-on");
+
+ const regularAddonItem = getUnifiedExtensionsItem(extensions[1].id, aWin);
+ is(
+ regularAddonItem.querySelector(".unified-extensions-item-name")
+ .textContent,
+ "regular addon",
+ "expected an item for a regular add-on"
+ );
+
+ const disabledAddonItem = getUnifiedExtensionsItem(extensions[2].id, aWin);
+ is(disabledAddonItem, null, "didn't expect an item for a disabled add-on");
+
+ const browserActionItem = getUnifiedExtensionsItem(extensions[3].id, aWin);
+ is(
+ browserActionItem,
+ null,
+ "didn't expect an item for an add-on with browser action placed in the navbar"
+ );
+
+ const mv3BrowserActionItem = getUnifiedExtensionsItem(
+ extensions[4].id,
+ aWin
+ );
+ is(
+ mv3BrowserActionItem,
+ null,
+ "didn't expect an item for a MV3 add-on with browser action placed in the navbar"
+ );
+
+ const pageActionItem = getUnifiedExtensionsItem(extensions[5].id, aWin);
+ is(
+ pageActionItem.querySelector(".unified-extensions-item-name").textContent,
+ "regular addon with page action",
+ "expected an item for a regular add-on with page action"
+ );
+
+ const privateBrowsingDisabledItem = getUnifiedExtensionsItem(
+ extensions[6].id,
+ aWin
+ );
+ if (isPrivate) {
+ is(
+ privateBrowsingDisabledItem,
+ null,
+ "didn't expect an item for a regular add-on with private browsing enabled"
+ );
+ } else {
+ is(
+ privateBrowsingDisabledItem.querySelector(
+ ".unified-extensions-item-name"
+ ).textContent,
+ "regular addon with private browsing disabled",
+ "expected an item for a regular add-on with private browsing disabled"
+ );
+ }
+
+ await closeExtensionsPanel(aWin);
+
+ await BrowserTestUtils.closeWindow(aWin);
+ }
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+});
+
+add_task(async function test_button_opens_discopane_when_no_extension() {
+ // The test harness registers regular extensions so we need to mock the
+ // `getActivePolicies` extension to simulate zero extensions installed.
+ const origGetActivePolicies = gUnifiedExtensions.getActivePolicies;
+ gUnifiedExtensions.getActivePolicies = () => [];
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots" },
+ async () => {
+ const { button } = gUnifiedExtensions;
+ ok(button, "expected button");
+
+ // Primary click should open about:addons.
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons",
+ true
+ );
+
+ button.click();
+
+ const tab = await tabPromise;
+ is(
+ gBrowser.currentURI.spec,
+ "about:addons",
+ "expected about:addons to be open"
+ );
+ is(
+ gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId,
+ "addons://discover/",
+ "expected about:addons to show the recommendations"
+ );
+ BrowserTestUtils.removeTab(tab);
+
+ // "Right-click" should open the context menu only.
+ const contextMenu = document.getElementById("toolbar-context-menu");
+ const popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(button, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await popupShownPromise;
+ await closeChromeContextMenu(contextMenu.id, null);
+ }
+ );
+
+ gUnifiedExtensions.getActivePolicies = origGetActivePolicies;
+});
+
+add_task(
+ async function test_button_opens_extlist_when_no_extension_and_pane_disabled() {
+ // If extensions.getAddons.showPane is set to false, there is no "Recommended" tab,
+ // so we need to make sure we don't navigate to it.
+
+ // The test harness registers regular extensions so we need to mock the
+ // `getActivePolicies` extension to simulate zero extensions installed.
+ const origGetActivePolicies = gUnifiedExtensions.getActivePolicies;
+ gUnifiedExtensions.getActivePolicies = () => [];
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Set this to another value to make sure not to "accidentally" land on the right page
+ ["extensions.ui.lastCategory", "addons://list/theme"],
+ ["extensions.getAddons.showPane", false],
+ ],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots" },
+ async () => {
+ const { button } = gUnifiedExtensions;
+ ok(button, "expected button");
+
+ // Primary click should open about:addons.
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons",
+ true
+ );
+
+ button.click();
+
+ const tab = await tabPromise;
+ is(
+ gBrowser.currentURI.spec,
+ "about:addons",
+ "expected about:addons to be open"
+ );
+ is(
+ gBrowser.selectedBrowser.contentWindow.gViewController.currentViewId,
+ "addons://list/extension",
+ "expected about:addons to show the extension list"
+ );
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+
+ gUnifiedExtensions.getActivePolicies = origGetActivePolicies;
+ }
+);
+
+add_task(
+ async function test_unified_extensions_panel_not_open_in_customization_mode() {
+ const listView = getListView();
+ ok(listView, "expected list view");
+ const throwIfExecuted = () => {
+ throw new Error("panel should not have been shown");
+ };
+ listView.addEventListener("ViewShown", throwIfExecuted);
+
+ await openCustomizationUI();
+
+ const unifiedExtensionsButtonToggled = BrowserTestUtils.waitForEvent(
+ window,
+ "UnifiedExtensionsTogglePanel"
+ );
+ const button = document.getElementById("unified-extensions-button");
+
+ button.click();
+ await unifiedExtensionsButtonToggled;
+
+ await closeCustomizationUI();
+
+ listView.removeEventListener("ViewShown", throwIfExecuted);
+ }
+);
+
+const NO_ACCESS = { id: "origin-controls-state-no-access", args: null };
+const QUARANTINED = { id: "origin-controls-state-quarantined", args: null };
+
+const ALWAYS_ON = { id: "origin-controls-state-always-on", args: null };
+const WHEN_CLICKED = { id: "origin-controls-state-when-clicked", args: null };
+const TEMP_ACCESS = {
+ id: "origin-controls-state-temporary-access",
+ args: null,
+};
+
+const HOVER_RUN_VISIT_ONLY = {
+ id: "origin-controls-state-hover-run-visit-only",
+ args: null,
+};
+const HOVER_RUNNABLE_RUN_EXT = {
+ id: "origin-controls-state-runnable-hover-run",
+ args: null,
+};
+const HOVER_RUNNABLE_OPEN_EXT = {
+ id: "origin-controls-state-runnable-hover-open",
+ args: null,
+};
+
+add_task(async function test_messages_origin_controls() {
+ const TEST_CASES = [
+ {
+ title: "MV2 - no access",
+ manifest: {
+ manifest_version: 2,
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - always on",
+ manifest: {
+ manifest_version: 2,
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: ALWAYS_ON,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - content script",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: ALWAYS_ON,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - non-matching content script",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://foobar.net/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - all_urls content script",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: [""],
+ },
+ ],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: ALWAYS_ON,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - activeTab without browser action",
+ manifest: {
+ manifest_version: 2,
+ permissions: ["activeTab"],
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - when clicked: activeTab with browser action",
+ manifest: {
+ manifest_version: 2,
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "MV3 - when clicked: activeTab with action",
+ manifest: {
+ manifest_version: 3,
+ permissions: ["activeTab"],
+ action: {},
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "MV2 - browser action - click event - always on",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "MV2 - browser action - popup - always on",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "MV2 - browser action - click event - content script",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "MV2 - browser action - popup - content script",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {
+ default_popup: "popup.html",
+ },
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "no access",
+ manifest: {
+ manifest_version: 3,
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "when clicked with host permissions",
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "when clicked with host permissions already granted",
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: ALWAYS_ON,
+ expectedActionButtonDisabled: true,
+ grantHostPermissions: true,
+ },
+ {
+ title: "when clicked",
+ manifest: {
+ manifest_version: 3,
+ permissions: ["activeTab"],
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "page action - no access",
+ manifest: {
+ manifest_version: 3,
+ page_action: {},
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "page action - when clicked with host permissions",
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*"],
+ page_action: {},
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "page action - when clicked with host permissions already granted",
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*"],
+ page_action: {},
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: ALWAYS_ON,
+ expectedActionButtonDisabled: true,
+ grantHostPermissions: true,
+ },
+ {
+ title: "page action - when clicked",
+ manifest: {
+ manifest_version: 3,
+ permissions: ["activeTab"],
+ page_action: {},
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "browser action - click event - no access",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "browser action - popup - no access",
+ manifest: {
+ manifest_version: 3,
+ action: {
+ default_popup: "popup.html",
+ },
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "browser action - click event - when clicked",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title: "browser action - popup - when clicked",
+ manifest: {
+ manifest_version: 3,
+ action: {
+ default_popup: "popup.html",
+ },
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: WHEN_CLICKED,
+ expectedHoverMessage: HOVER_RUN_VISIT_ONLY,
+ expectedActionButtonDisabled: false,
+ },
+ {
+ title:
+ "browser action - click event - when clicked with host permissions already granted",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT,
+ expectedActionButtonDisabled: false,
+ grantHostPermissions: true,
+ },
+ {
+ title:
+ "browser action - popup - when clicked with host permissions already granted",
+ manifest: {
+ manifest_version: 3,
+ action: {
+ default_popup: "popup.html",
+ },
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: ALWAYS_ON,
+ expectedHoverMessage: HOVER_RUNNABLE_OPEN_EXT,
+ expectedActionButtonDisabled: false,
+ grantHostPermissions: true,
+ },
+ ];
+
+ async function runTestCases(testCases) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "https://example.com/" },
+ async () => {
+ let count = 0;
+
+ for (const {
+ title,
+ manifest,
+ expectedDefaultMessage,
+ expectedHoverMessage,
+ expectedActionButtonDisabled,
+ grantHostPermissions,
+ } of testCases) {
+ info(`case: ${title}`);
+
+ const id = `test-origin-controls-${count++}@ext`;
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: title,
+ browser_specific_settings: { gecko: { id } },
+ ...manifest,
+ },
+ files: {
+ "script.js": "",
+ "popup.html": "",
+ },
+ useAddonManager: "permanent",
+ });
+
+ if (grantHostPermissions) {
+ info("Granting initial permissions.");
+ await ExtensionPermissions.add(id, {
+ permissions: [],
+ origins: manifest.host_permissions,
+ });
+ }
+
+ await extension.startup();
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ const item = getUnifiedExtensionsItem(extension.id);
+ ok(item, `expected item for ${extension.id}`);
+
+ const messageDeck = item.querySelector(
+ ".unified-extensions-item-message-deck"
+ );
+ ok(messageDeck, "expected a message deck element");
+
+ // 1. Verify the default message displayed below the extension's name.
+ const defaultMessage = item.querySelector(
+ ".unified-extensions-item-message-default"
+ );
+ ok(defaultMessage, "expected a default message element");
+
+ Assert.deepEqual(
+ document.l10n.getAttributes(defaultMessage),
+ expectedDefaultMessage,
+ "expected l10n attributes for the default message"
+ );
+
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+
+ // 2. Verify the action button state.
+ const actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(actionButton, "expected an action button");
+ is(
+ actionButton.disabled,
+ expectedActionButtonDisabled,
+ `expected action button to be ${
+ expectedActionButtonDisabled ? "disabled" : "enabled"
+ }`
+ );
+
+ // 3. Verify the message displayed on hover but only when the action
+ // button isn't disabled to avoid some test failures.
+ if (!expectedActionButtonDisabled) {
+ const hovered = BrowserTestUtils.waitForEvent(
+ actionButton,
+ "mouseover"
+ );
+ EventUtils.synthesizeMouseAtCenter(actionButton, {
+ type: "mouseover",
+ });
+ await hovered;
+
+ const hoverMessage = item.querySelector(
+ ".unified-extensions-item-message-hover"
+ );
+ ok(hoverMessage, "expected a hover message element");
+
+ Assert.deepEqual(
+ document.l10n.getAttributes(hoverMessage),
+ expectedHoverMessage,
+ "expected l10n attributes for the message on hover"
+ );
+
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+ }
+
+ await closeExtensionsPanel();
+
+ // Move cursor elsewhere to avoid issues with previous "hovering".
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {});
+
+ await extension.unload();
+ }
+ }
+ );
+ }
+
+ await runTestCases(TEST_CASES);
+
+ info("Testing again with example.com quarantined.");
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.quarantinedDomains.list", "example.com"]],
+ });
+
+ await runTestCases([
+ {
+ title: "MV2 - no access",
+ manifest: {
+ manifest_version: 2,
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - host permission but quarantined",
+ manifest: {
+ manifest_version: 2,
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: QUARANTINED,
+ expectedHoverMessage: QUARANTINED,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - content script but quarantined",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: QUARANTINED,
+ expectedHoverMessage: QUARANTINED,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV2 - non-matching content script",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://foobar.net/*"],
+ },
+ ],
+ },
+ expectedDefaultMessage: NO_ACCESS,
+ expectedHoverMessage: NO_ACCESS,
+ expectedActionButtonDisabled: true,
+ },
+ {
+ title: "MV3 - content script but quarantined",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: QUARANTINED,
+ expectedHoverMessage: QUARANTINED,
+ expectedActionButtonDisabled: true,
+ grantHostPermissions: true,
+ },
+ {
+ title: "MV3 host permissions already granted but quarantined",
+ manifest: {
+ manifest_version: 3,
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: QUARANTINED,
+ expectedHoverMessage: QUARANTINED,
+ expectedActionButtonDisabled: true,
+ grantHostPermissions: true,
+ },
+ {
+ title: "browser action, host permissions already granted, quarantined",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ expectedDefaultMessage: QUARANTINED,
+ expectedHoverMessage: HOVER_RUNNABLE_RUN_EXT,
+ expectedActionButtonDisabled: false,
+ grantHostPermissions: true,
+ },
+ ]);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_hover_message_when_button_updates_itself() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ name: "an extension that refreshes its title",
+ action: {},
+ },
+ background() {
+ browser.test.onMessage.addListener(async msg => {
+ browser.test.assertEq(
+ "update-button",
+ msg,
+ "expected 'update-button' message"
+ );
+
+ browser.action.setTitle({ title: "a title" });
+
+ browser.test.sendMessage(`${msg}-done`);
+ });
+
+ browser.test.sendMessage("background-ready");
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("background-ready");
+
+ await openExtensionsPanel();
+
+ const item = getUnifiedExtensionsItem(extension.id);
+ ok(item, "expected item in the panel");
+
+ const actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(actionButton, "expected an action button");
+
+ const menuButton = item.querySelector(".unified-extensions-item-menu-button");
+ ok(menuButton, "expected a menu button");
+
+ const hovered = BrowserTestUtils.waitForEvent(actionButton, "mouseover");
+ EventUtils.synthesizeMouseAtCenter(actionButton, { type: "mouseover" });
+ await hovered;
+
+ const messageDeck = item.querySelector(
+ ".unified-extensions-item-message-deck"
+ );
+ ok(messageDeck, "expected a message deck element");
+
+ const hoverMessage = item.querySelector(
+ ".unified-extensions-item-message-hover"
+ );
+ ok(hoverMessage, "expected a hover message element");
+
+ const expectedL10nAttributes = {
+ id: "origin-controls-state-runnable-hover-run",
+ args: null,
+ };
+ Assert.deepEqual(
+ document.l10n.getAttributes(hoverMessage),
+ expectedL10nAttributes,
+ "expected l10n attributes for the hover message"
+ );
+
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ extension.sendMessage("update-button");
+ await extension.awaitMessage("update-button-done");
+
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to remain the same"
+ );
+
+ const menuButtonHovered = BrowserTestUtils.waitForEvent(
+ menuButton,
+ "mouseover"
+ );
+ EventUtils.synthesizeMouseAtCenter(menuButton, { type: "mouseover" });
+ await menuButtonHovered;
+
+ await closeExtensionsPanel();
+
+ // Move cursor to the center of the entire browser UI to avoid issues with
+ // other focus/hover checks. We do this to avoid intermittent test failures.
+ EventUtils.synthesizeMouseAtCenter(document.documentElement, {});
+
+ await extension.unload();
+});
+
+// Test the temporary access state messages and attention indicator.
+add_task(async function test_temporary_access() {
+ const TEST_CASES = [
+ {
+ title: "mv3 with active scripts and browser action",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ before: {
+ attention: true,
+ state: WHEN_CLICKED,
+ disabled: false,
+ },
+ messages: ["action-onClicked", "cs-injected"],
+ after: {
+ attention: false,
+ state: TEMP_ACCESS,
+ disabled: false,
+ },
+ },
+ {
+ title: "mv3 with active scripts and no browser action",
+ manifest: {
+ manifest_version: 3,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ before: {
+ attention: true,
+ state: WHEN_CLICKED,
+ disabled: false,
+ },
+ messages: ["cs-injected"],
+ after: {
+ attention: false,
+ state: TEMP_ACCESS,
+ // TODO: This will need updating for bug 1807835.
+ disabled: false,
+ },
+ },
+ {
+ title: "mv3 with browser action and host_permission",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ before: {
+ attention: true,
+ state: WHEN_CLICKED,
+ disabled: false,
+ },
+ messages: ["action-onClicked"],
+ after: {
+ attention: false,
+ state: TEMP_ACCESS,
+ disabled: false,
+ },
+ },
+ {
+ title: "mv3 with browser action no host_permissions",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ },
+ before: {
+ attention: false,
+ state: NO_ACCESS,
+ disabled: false,
+ },
+ messages: ["action-onClicked"],
+ after: {
+ attention: false,
+ state: NO_ACCESS,
+ disabled: false,
+ },
+ },
+ // MV2 tests.
+ {
+ title: "mv2 with content scripts and browser action",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ before: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: false,
+ },
+ messages: ["action-onClicked", "cs-injected"],
+ after: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: false,
+ },
+ },
+ {
+ title: "mv2 with content scripts and no browser action",
+ manifest: {
+ manifest_version: 2,
+ content_scripts: [
+ {
+ js: ["script.js"],
+ matches: ["*://example.com/*"],
+ },
+ ],
+ },
+ before: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: true,
+ },
+ messages: ["cs-injected"],
+ after: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: true,
+ },
+ },
+ {
+ title: "mv2 with browser action and host_permission",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ host_permissions: ["*://example.com/*"],
+ },
+ before: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: false,
+ },
+ messages: ["action-onClicked"],
+ after: {
+ attention: false,
+ state: ALWAYS_ON,
+ disabled: false,
+ },
+ },
+ {
+ title: "mv2 with browser action no host_permissions",
+ manifest: {
+ manifest_version: 2,
+ browser_action: {},
+ },
+ before: {
+ attention: false,
+ state: NO_ACCESS,
+ disabled: false,
+ },
+ messages: ["action-onClicked"],
+ after: {
+ attention: false,
+ state: NO_ACCESS,
+ disabled: false,
+ },
+ },
+ ];
+
+ let count = 1;
+ await Promise.all(
+ TEST_CASES.map(test => {
+ let id = `test-temp-access-${count++}@ext`;
+ test.extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: test.title,
+ browser_specific_settings: { gecko: { id } },
+ ...test.manifest,
+ },
+ files: {
+ "popup.html": "",
+ "script.js"() {
+ browser.test.sendMessage("cs-injected");
+ },
+ },
+ background() {
+ let action = browser.action ?? browser.browserAction;
+ action?.onClicked.addListener(() => {
+ browser.test.sendMessage("action-onClicked");
+ });
+ },
+ useAddonManager: "temporary",
+ });
+
+ return test.extension.startup();
+ })
+ );
+
+ async function checkButton(extension, expect, click = false) {
+ await openExtensionsPanel();
+
+ let item = getUnifiedExtensionsItem(extension.id);
+ ok(item, `Expected item for ${extension.id}.`);
+
+ let state = item.querySelector(".unified-extensions-item-message-default");
+ ok(state, "Expected a default state message element.");
+
+ is(
+ item.hasAttribute("attention"),
+ !!expect.attention,
+ "Expected attention badge."
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(state),
+ expect.state,
+ "Expected l10n attributes for the message."
+ );
+
+ let button = item.querySelector(".unified-extensions-item-action-button");
+ is(button.disabled, !!expect.disabled, "Expect disabled item.");
+
+ // If we should click, and button is not disabled.
+ if (click && !expect.disabled) {
+ let onClick = BrowserTestUtils.waitForEvent(button, "click");
+ button.click();
+ await onClick;
+ } else {
+ // Otherwise, just close the panel.
+ await closeExtensionsPanel();
+ }
+ }
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "https://example.com/" },
+ async () => {
+ for (let { title, extension, before, messages, after } of TEST_CASES) {
+ info(`Test case: ${title}`);
+ await checkButton(extension, before, true);
+
+ await Promise.all(
+ messages.map(msg => {
+ info(`Waiting for ${msg} from clicking the button.`);
+ return extension.awaitMessage(msg);
+ })
+ );
+
+ await checkButton(extension, after);
+ await extension.unload();
+ }
+ }
+ );
+});
+
+add_task(
+ async function test_action_and_menu_buttons_css_class_with_new_window() {
+ const [extension] = createExtensions([
+ {
+ name: "an extension placed in the extensions panel",
+ browser_action: {
+ default_area: "menupanel",
+ },
+ },
+ ]);
+ await extension.startup();
+
+ let aSecondWindow = await BrowserTestUtils.openNewBrowserWindow();
+ await ensureMaximizedWindow(aSecondWindow);
+
+ // Open and close the extensions panel in the newly created window to build
+ // the extensions panel and add the extension widget(s) to it.
+ await openExtensionsPanel(aSecondWindow);
+ await closeExtensionsPanel(aSecondWindow);
+
+ for (const { title, win } of [
+ { title: "current window", win: window },
+ { title: "second window", win: aSecondWindow },
+ ]) {
+ const node = CustomizableUI.getWidget(
+ AppUiTestInternals.getBrowserActionWidgetId(extension.id)
+ ).forWindow(win).node;
+
+ let actionButton = node.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ `${title} - expected .subviewbutton CSS class on the action button`
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ `${title} - expected no .toolbarbutton-1 CSS class on the action button`
+ );
+ let menuButton = node.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ `${title} - expected .subviewbutton CSS class on the menu button`
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ `${title} - expected no .toolbarbutton-1 CSS class on the menu button`
+ );
+ }
+
+ await BrowserTestUtils.closeWindow(aSecondWindow);
+
+ await extension.unload();
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js b/browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js
new file mode 100644
index 0000000000..fccc77b8a9
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_accessibility.js
@@ -0,0 +1,302 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+loadTestSubscript("head_unified_extensions.js");
+
+add_task(async function test_keyboard_navigation_activeScript() {
+ const extension1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ name: "1",
+ content_scripts: [
+ {
+ matches: ["*://*/*"],
+ js: ["script.js"],
+ },
+ ],
+ },
+ files: {
+ "script.js": () => {
+ browser.test.fail("this script should NOT have been executed");
+ },
+ },
+ useAddonManager: "temporary",
+ });
+ const extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ name: "2",
+ content_scripts: [
+ {
+ matches: ["*://*/*"],
+ js: ["script.js"],
+ },
+ ],
+ },
+ files: {
+ "script.js": () => {
+ browser.test.sendMessage("script executed");
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ BrowserTestUtils.loadURIString(
+ gBrowser.selectedBrowser,
+ "https://example.org/"
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ await Promise.all([extension1.startup(), extension2.startup()]);
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ let item = getUnifiedExtensionsItem(extension1.id);
+ ok(item, `expected item for ${extension1.id}`);
+
+ info("moving focus to first item in the unified extensions panel");
+ let actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ let focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ await focused;
+ is(
+ actionButton,
+ document.activeElement,
+ "expected action button of first extension item to be focused"
+ );
+
+ item = getUnifiedExtensionsItem(extension2.id);
+ ok(item, `expected item for ${extension2.id}`);
+
+ info("moving focus to second item in the unified extensions panel");
+ actionButton = item.querySelector(".unified-extensions-item-action-button");
+ focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ await focused;
+ is(
+ actionButton,
+ document.activeElement,
+ "expected action button of second extension item to be focused"
+ );
+
+ info("granting permission");
+ const popupHidden = BrowserTestUtils.waitForEvent(
+ document,
+ "popuphidden",
+ true
+ );
+ EventUtils.synthesizeKey(" ", {});
+ await Promise.all([popupHidden, extension2.awaitMessage("script executed")]);
+
+ await Promise.all([extension1.unload(), extension2.unload()]);
+});
+
+add_task(async function test_keyboard_navigation_opens_menu() {
+ const extension1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "1",
+ // activeTab and browser_action needed to enable the action button in mv2.
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ useAddonManager: "temporary",
+ });
+ const extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "2",
+ },
+ useAddonManager: "temporary",
+ });
+ const extension3 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ manifest_version: 3,
+ name: "3",
+ // activeTab enables the action button without a browser action in mv3.
+ permissions: ["activeTab"],
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension1.startup();
+ await extension2.startup();
+ await extension3.startup();
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ let item = getUnifiedExtensionsItem(extension1.id);
+ ok(item, `expected item for ${extension1.id}`);
+
+ let messageDeck = item.querySelector(".unified-extensions-item-message-deck");
+ ok(messageDeck, "expected a message deck element");
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+
+ info("moving focus to first item in the unified extensions panel");
+ let actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ let focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ await focused;
+ is(
+ actionButton,
+ document.activeElement,
+ "expected action button of the first extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ info(
+ "moving focus to menu button of the first item in the unified extensions panel"
+ );
+ let menuButton = item.querySelector(".unified-extensions-item-menu-button");
+ focused = BrowserTestUtils.waitForEvent(menuButton, "focus");
+ ok(menuButton, "expected menu button");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ await focused;
+ is(
+ menuButton,
+ document.activeElement,
+ "expected menu button in first extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER,
+ "expected selected message in the deck to be the message when hovering the menu button"
+ );
+
+ info("opening menu of the first item");
+ const contextMenu = document.getElementById(
+ "unified-extensions-context-menu"
+ );
+ ok(contextMenu, "expected menu");
+ const shown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeKey(" ", {});
+ await shown;
+
+ await closeChromeContextMenu(contextMenu.id, null);
+
+ info("moving focus back to the action button of the first item");
+ focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ await focused;
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ // Moving to the third extension directly because the second extension cannot
+ // do anything on the current page and its action button is disabled. Note
+ // that this third extension does not have a browser action but it has
+ // "activeTab", which makes the extension "clickable". This allows us to
+ // verify the focus/blur behavior of custom elments.
+ info("moving focus to third item in the panel");
+ item = getUnifiedExtensionsItem(extension3.id);
+ ok(item, `expected item for ${extension3.id}`);
+ actionButton = item.querySelector(".unified-extensions-item-action-button");
+ ok(actionButton, `expected action button for ${extension3.id}`);
+ messageDeck = item.querySelector(".unified-extensions-item-message-deck");
+ ok(messageDeck, `expected message deck for ${extension3.id}`);
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+ // Now that we checked everything on this third extension, let's actually
+ // focus it with the arrow down key.
+ focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("KEY_ArrowDown", {});
+ await focused;
+ is(
+ actionButton,
+ document.activeElement,
+ "expected action button of the third extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ info(
+ "moving focus to menu button of the third item in the unified extensions panel"
+ );
+ menuButton = item.querySelector(".unified-extensions-item-menu-button");
+ focused = BrowserTestUtils.waitForEvent(menuButton, "focus");
+ ok(menuButton, "expected menu button");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ await focused;
+ is(
+ menuButton,
+ document.activeElement,
+ "expected menu button in third extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER,
+ "expected selected message in the deck to be the message when hovering the menu button"
+ );
+
+ info("moving focus back to the action button of the third item");
+ focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true });
+ await focused;
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ await closeExtensionsPanel();
+
+ await extension1.unload();
+ await extension2.unload();
+ await extension3.unload();
+});
+
+add_task(async function test_open_panel_with_keyboard_navigation() {
+ const { button, panel } = gUnifiedExtensions;
+ ok(button, "expected button");
+ ok(panel, "expected panel");
+
+ const listView = getListView();
+ ok(listView, "expected list view");
+
+ // Force focus on the unified extensions button.
+ const forceFocusUnifiedExtensionsButton = () => {
+ button.setAttribute("tabindex", "-1");
+ button.focus();
+ button.removeAttribute("tabindex");
+ };
+ forceFocusUnifiedExtensionsButton();
+
+ // Use the "space" key to open the panel.
+ let viewShown = BrowserTestUtils.waitForEvent(listView, "ViewShown");
+ EventUtils.synthesizeKey(" ", {});
+ await viewShown;
+
+ await closeExtensionsPanel();
+
+ // Force focus on the unified extensions button again.
+ forceFocusUnifiedExtensionsButton();
+
+ // Use the "return" key to open the panel.
+ viewShown = BrowserTestUtils.waitForEvent(listView, "ViewShown");
+ EventUtils.synthesizeKey("KEY_Enter", {});
+ await viewShown;
+
+ await closeExtensionsPanel();
+});
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js b/browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js
new file mode 100644
index 0000000000..cf43401c0c
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_context_menu.js
@@ -0,0 +1,938 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AbuseReporter: "resource://gre/modules/AbuseReporter.sys.mjs",
+});
+
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+loadTestSubscript("head_unified_extensions.js");
+
+// We expect this rejection when the abuse report dialog window is
+// being forcefully closed as part of the related test task.
+PromiseTestUtils.allowMatchingRejectionsGlobally(/report dialog closed/);
+
+const promiseExtensionUninstalled = extensionId => {
+ return new Promise(resolve => {
+ let listener = {};
+ listener.onUninstalled = addon => {
+ if (addon.id == extensionId) {
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ });
+};
+
+function waitClosedWindow(win) {
+ return new Promise(resolve => {
+ function onWindowClosed() {
+ if (win && !win.closed) {
+ // If a specific window reference has been passed, then check
+ // that the window is closed before resolving the promise.
+ return;
+ }
+ Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed");
+ resolve();
+ }
+ Services.obs.addObserver(onWindowClosed, "xul-window-destroyed");
+ });
+}
+
+function assertVisibleContextMenuItems(contextMenu, expected) {
+ let visibleItems = contextMenu.querySelectorAll(
+ ":is(menuitem, menuseparator):not([hidden])"
+ );
+ is(visibleItems.length, expected, `expected ${expected} visible menu items`);
+}
+
+function assertOrderOfWidgetsInPanel(extensions, win = window) {
+ const widgetIds = CustomizableUI.getWidgetIdsInArea(
+ CustomizableUI.AREA_ADDONS
+ ).filter(
+ widgetId => !!CustomizableUI.getWidget(widgetId).forWindow(win).node
+ );
+ const widgetIdsFromExtensions = extensions.map(ext =>
+ AppUiTestInternals.getBrowserActionWidgetId(ext.id)
+ );
+
+ Assert.deepEqual(
+ widgetIds,
+ widgetIdsFromExtensions,
+ "expected extensions to be ordered"
+ );
+}
+
+async function moveWidgetUp(extension, win = window) {
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id, win);
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(
+ contextMenu.querySelector(".unified-extensions-context-menu-move-widget-up")
+ );
+ await hidden;
+}
+
+async function moveWidgetDown(extension, win = window) {
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id, win);
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(
+ contextMenu.querySelector(
+ ".unified-extensions-context-menu-move-widget-down"
+ )
+ );
+ await hidden;
+}
+
+async function pinToToolbar(extension, win = window) {
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id, win);
+ const pinToToolbarItem = contextMenu.querySelector(
+ ".unified-extensions-context-menu-pin-to-toolbar"
+ );
+ ok(pinToToolbarItem, "expected 'pin to toolbar' menu item");
+
+ const hidden = BrowserTestUtils.waitForEvent(
+ win.gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ contextMenu.activateItem(pinToToolbarItem);
+ await hidden;
+}
+
+async function assertMoveContextMenuItems(
+ ext,
+ { expectMoveUpHidden, expectMoveDownHidden, expectOrder },
+ win = window
+) {
+ const extName = WebExtensionPolicy.getByID(ext.id).name;
+ info(`Assert Move context menu items visibility for ${extName}`);
+ const contextMenu = await openUnifiedExtensionsContextMenu(ext.id, win);
+ const moveUp = contextMenu.querySelector(
+ ".unified-extensions-context-menu-move-widget-up"
+ );
+ const moveDown = contextMenu.querySelector(
+ ".unified-extensions-context-menu-move-widget-down"
+ );
+ ok(moveUp, "expected 'move up' item in the context menu");
+ ok(moveDown, "expected 'move down' item in the context menu");
+
+ is(
+ BrowserTestUtils.is_hidden(moveUp),
+ expectMoveUpHidden,
+ `expected 'move up' item to be ${expectMoveUpHidden ? "hidden" : "visible"}`
+ );
+ is(
+ BrowserTestUtils.is_hidden(moveDown),
+ expectMoveDownHidden,
+ `expected 'move down' item to be ${
+ expectMoveDownHidden ? "hidden" : "visible"
+ }`
+ );
+ const expectedVisibleItems =
+ 5 + (+(expectMoveUpHidden ? 0 : 1) + (expectMoveDownHidden ? 0 : 1));
+ assertVisibleContextMenuItems(contextMenu, expectedVisibleItems);
+ if (expectOrder) {
+ assertOrderOfWidgetsInPanel(expectOrder, win);
+ }
+ await closeChromeContextMenu(contextMenu.id, null, win);
+}
+
+add_task(async function test_context_menu() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ // Get the menu button of the extension and verify the mouseover/mouseout
+ // behavior. We expect a help message (in the message deck) to be selected
+ // (and therefore displayed) when the menu button is hovered/focused.
+ const item = getUnifiedExtensionsItem(extension.id);
+ ok(item, "expected an item for the extension");
+
+ const messageDeck = item.querySelector(
+ ".unified-extensions-item-message-deck"
+ );
+ ok(messageDeck, "expected message deck");
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+
+ const hoverMenuButtonMessage = item.querySelector(
+ ".unified-extensions-item-message-hover-menu-button"
+ );
+ Assert.deepEqual(
+ document.l10n.getAttributes(hoverMenuButtonMessage),
+ { id: "unified-extensions-item-message-manage", args: null },
+ "expected correct l10n attributes for the hover message"
+ );
+
+ const menuButton = item.querySelector(".unified-extensions-item-menu-button");
+ ok(menuButton, "expected menu button");
+
+ let hovered = BrowserTestUtils.waitForEvent(menuButton, "mouseover");
+ EventUtils.synthesizeMouseAtCenter(menuButton, { type: "mouseover" });
+ await hovered;
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER,
+ "expected selected message in the deck to be the message when hovering the menu button"
+ );
+
+ let notHovered = BrowserTestUtils.waitForEvent(menuButton, "mouseout");
+ // Move mouse somewhere else...
+ EventUtils.synthesizeMouseAtCenter(item, { type: "mouseover" });
+ await notHovered;
+ is(
+ messageDeck.selectedIndex,
+ gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ // Open the context menu for the extension.
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+ const doc = contextMenu.ownerDocument;
+
+ const manageButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-manage-extension"
+ );
+ ok(manageButton, "expected manage button");
+ is(manageButton.hidden, false, "expected manage button to be visible");
+ is(manageButton.disabled, false, "expected manage button to be enabled");
+ Assert.deepEqual(
+ doc.l10n.getAttributes(manageButton),
+ { id: "unified-extensions-context-menu-manage-extension", args: null },
+ "expected correct l10n attributes for manage button"
+ );
+
+ const removeButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-remove-extension"
+ );
+ ok(removeButton, "expected remove button");
+ is(removeButton.hidden, false, "expected remove button to be visible");
+ is(removeButton.disabled, false, "expected remove button to be enabled");
+ Assert.deepEqual(
+ doc.l10n.getAttributes(removeButton),
+ { id: "unified-extensions-context-menu-remove-extension", args: null },
+ "expected correct l10n attributes for remove button"
+ );
+
+ const reportButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-report-extension"
+ );
+ ok(reportButton, "expected report button");
+ is(reportButton.hidden, false, "expected report button to be visible");
+ is(reportButton.disabled, false, "expected report button to be enabled");
+ Assert.deepEqual(
+ doc.l10n.getAttributes(reportButton),
+ { id: "unified-extensions-context-menu-report-extension", args: null },
+ "expected correct l10n attributes for report button"
+ );
+
+ await closeChromeContextMenu(contextMenu.id, null);
+ await closeExtensionsPanel();
+
+ await extension.unload();
+});
+
+add_task(
+ async function test_context_menu_report_button_hidden_when_abuse_report_disabled() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.enabled", false]],
+ });
+
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel, then open the contextMenu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const reportButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-report-extension"
+ );
+ ok(reportButton, "expected report button");
+ is(reportButton.hidden, true, "expected report button to be hidden");
+
+ await closeChromeContextMenu(contextMenu.id, null);
+ await closeExtensionsPanel();
+
+ await extension.unload();
+ }
+);
+
+add_task(
+ async function test_context_menu_remove_button_disabled_when_extension_cannot_be_uninstalled() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ Extensions: {
+ Locked: [extension.id],
+ },
+ },
+ });
+
+ // Open the extension panel, then open the context menu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const removeButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-remove-extension"
+ );
+ ok(removeButton, "expected remove button");
+ is(removeButton.disabled, true, "expected remove button to be disabled");
+
+ await closeChromeContextMenu(contextMenu.id, null);
+ await closeExtensionsPanel();
+
+ await extension.unload();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+ }
+);
+
+add_task(async function test_manage_extension() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:robots" },
+ async () => {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel, then open the context menu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const manageButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-manage-extension"
+ );
+ ok(manageButton, "expected manage button");
+
+ // Click the "manage extension" context menu item, and wait until the menu is
+ // closed and about:addons is open.
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ const aboutAddons = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons",
+ true
+ );
+ contextMenu.activateItem(manageButton);
+ const [aboutAddonsTab] = await Promise.all([aboutAddons, hidden]);
+
+ // Close the tab containing about:addons because we don't need it anymore.
+ BrowserTestUtils.removeTab(aboutAddonsTab);
+
+ await extension.unload();
+ }
+ );
+});
+
+add_task(async function test_report_extension() {
+ SpecialPowers.pushPrefEnv({
+ set: [["extensions.abuseReport.enabled", true]],
+ });
+
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ // Open the extension panel, then open the context menu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const reportButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-report-extension"
+ );
+ ok(reportButton, "expected report button");
+
+ // Click the "report extension" context menu item, and wait until the menu is
+ // closed and about:addons is open with the "abuse report dialog".
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ const abuseReportOpen = BrowserTestUtils.waitForCondition(
+ () => AbuseReporter.getOpenDialog(),
+ "wait for the abuse report dialog to have been opened"
+ );
+ contextMenu.activateItem(reportButton);
+ const [reportDialogWindow] = await Promise.all([abuseReportOpen, hidden]);
+
+ const reportDialogParams = reportDialogWindow.arguments[0].wrappedJSObject;
+ is(
+ reportDialogParams.report.addon.id,
+ extension.id,
+ "abuse report dialog has the expected addon id"
+ );
+ is(
+ reportDialogParams.report.reportEntryPoint,
+ "unified_context_menu",
+ "abuse report dialog has the expected reportEntryPoint"
+ );
+
+ let promiseClosedWindow = waitClosedWindow();
+ reportDialogWindow.close();
+ // Wait for the report dialog window to be completely closed
+ // (to prevent an intermittent failure due to a race between
+ // the dialog window being closed and the test tasks that follows
+ // opening the unified extensions button panel to not lose the
+ // focus and be suddently closed before the task has done with
+ // its assertions, see Bug 1782304).
+ await promiseClosedWindow;
+ });
+
+ await extension.unload();
+});
+
+add_task(async function test_remove_extension() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel, then open the context menu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const removeButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-remove-extension"
+ );
+ ok(removeButton, "expected remove button");
+
+ // Set up a mock prompt service that returns 0 to indicate that the user
+ // pressed the OK button.
+ const { prompt } = Services;
+ const promptService = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ confirmEx() {
+ return 0;
+ },
+ };
+ Services.prompt = promptService;
+ registerCleanupFunction(() => {
+ Services.prompt = prompt;
+ });
+
+ // Click the "remove extension" context menu item, and wait until the menu is
+ // closed and the extension is uninstalled.
+ const uninstalled = promiseExtensionUninstalled(extension.id);
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(removeButton);
+ await Promise.all([uninstalled, hidden]);
+
+ await extension.unload();
+ // Restore prompt service.
+ Services.prompt = prompt;
+});
+
+add_task(async function test_remove_extension_cancelled() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel, then open the context menu for the extension.
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const removeButton = contextMenu.querySelector(
+ ".unified-extensions-context-menu-remove-extension"
+ );
+ ok(removeButton, "expected remove button");
+
+ // Set up a mock prompt service that returns 1 to indicate that the user
+ // refused to uninstall the extension.
+ const { prompt } = Services;
+ const promptService = {
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ confirmEx() {
+ return 1;
+ },
+ };
+ Services.prompt = promptService;
+ registerCleanupFunction(() => {
+ Services.prompt = prompt;
+ });
+
+ // Click the "remove extension" context menu item, and wait until the menu is
+ // closed.
+ const hidden = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+ contextMenu.activateItem(removeButton);
+ await hidden;
+
+ // Re-open the panel to make sure the extension is still there.
+ await openExtensionsPanel();
+ const item = getUnifiedExtensionsItem(extension.id);
+ is(
+ item.querySelector(".unified-extensions-item-name").textContent,
+ "an extension",
+ "expected extension to still be listed"
+ );
+ await closeExtensionsPanel();
+
+ await extension.unload();
+ // Restore prompt service.
+ Services.prompt = prompt;
+});
+
+add_task(async function test_open_context_menu_on_click() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ const button = getUnifiedExtensionsItem(extension.id).querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(button, "expected menu button");
+
+ const contextMenu = document.getElementById(
+ "unified-extensions-context-menu"
+ );
+ ok(contextMenu, "expected menu");
+
+ // Open the context menu with a "right-click".
+ const shown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(button, { type: "contextmenu" });
+ await shown;
+
+ await closeChromeContextMenu(contextMenu.id, null);
+ await closeExtensionsPanel();
+
+ await extension.unload();
+});
+
+add_task(async function test_open_context_menu_with_keyboard() {
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel.
+ await openExtensionsPanel();
+
+ const button = getUnifiedExtensionsItem(extension.id).querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(button, "expected menu button");
+ // Make this button focusable because those (toolbar) buttons are only made
+ // focusable when a user is navigating with the keyboard, which isn't exactly
+ // what we are doing in this test.
+ button.setAttribute("tabindex", "-1");
+
+ const contextMenu = document.getElementById(
+ "unified-extensions-context-menu"
+ );
+ ok(contextMenu, "expected menu");
+
+ // Open the context menu by focusing the button and pressing the SPACE key.
+ let shown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ button.focus();
+ is(button, document.activeElement, "expected button to be focused");
+ EventUtils.synthesizeKey(" ", {});
+ await shown;
+
+ await closeChromeContextMenu(contextMenu.id, null);
+
+ if (AppConstants.platform != "macosx") {
+ // Open the context menu by focusing the button and pressing the ENTER key.
+ // TODO(emilio): Maybe we should harmonize this behavior across platforms,
+ // we're inconsistent right now.
+ shown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ button.focus();
+ is(button, document.activeElement, "expected button to be focused");
+ EventUtils.synthesizeKey("KEY_Enter", {});
+ await shown;
+ await closeChromeContextMenu(contextMenu.id, null);
+ }
+
+ await closeExtensionsPanel();
+
+ await extension.unload();
+});
+
+add_task(async function test_context_menu_without_browserActionFor_global() {
+ const { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+ );
+ const { browserActionFor } = ExtensionParent.apiManager.global;
+ const cleanup = () => {
+ ExtensionParent.apiManager.global.browserActionFor = browserActionFor;
+ };
+ registerCleanupFunction(cleanup);
+ // This is needed to simulate the case where the browserAction API hasn't
+ // been loaded yet (since it is lazy-loaded). That could happen when only
+ // extensions without browser actions are installed. In which case, the
+ // `global.browserActionFor()` function would not be defined yet.
+ delete ExtensionParent.apiManager.global.browserActionFor;
+
+ const [extension] = createExtensions([{ name: "an extension" }]);
+ await extension.startup();
+
+ // Open the extension panel and then the context menu for the extension that
+ // has been loaded above. We expect the context menu to be displayed and no
+ // error caused by the lack of `global.browserActionFor()`.
+ await openExtensionsPanel();
+ // This promise rejects with an error if the implementation does not handle
+ // the case where `global.browserActionFor()` is undefined.
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+ assertVisibleContextMenuItems(contextMenu, 3);
+
+ await closeChromeContextMenu(contextMenu.id, null);
+ await closeExtensionsPanel();
+
+ await extension.unload();
+
+ cleanup();
+});
+
+add_task(async function test_page_action_context_menu() {
+ const extWithMenuPageAction = ExtensionTestUtils.loadExtension({
+ manifest: {
+ page_action: {},
+ permissions: ["contextMenus"],
+ },
+ background() {
+ browser.contextMenus.create(
+ {
+ id: "some-menu-id",
+ title: "Click me!",
+ contexts: ["all"],
+ },
+ () => browser.test.sendMessage("menu-created")
+ );
+ },
+ useAddonManager: "temporary",
+ });
+ const extWithoutMenu1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "extension without any menu",
+ },
+ useAddonManager: "temporary",
+ });
+
+ const extensions = [extWithMenuPageAction, extWithoutMenu1];
+
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ await extWithMenuPageAction.awaitMessage("menu-created");
+
+ await openExtensionsPanel();
+
+ info("extension with page action and a menu");
+ // This extension declares a page action so its menu shouldn't be added to
+ // the unified extensions context menu.
+ let contextMenu = await openUnifiedExtensionsContextMenu(
+ extWithMenuPageAction.id
+ );
+ assertVisibleContextMenuItems(contextMenu, 3);
+ await closeChromeContextMenu(contextMenu.id, null);
+
+ info("extension with no browser action and no menu");
+ // There is no context menu created by this extension, so there should only
+ // be 3 menu items corresponding to the default manage/remove/report items.
+ contextMenu = await openUnifiedExtensionsContextMenu(extWithoutMenu1.id);
+ assertVisibleContextMenuItems(contextMenu, 3);
+ await closeChromeContextMenu(contextMenu.id, null);
+
+ await closeExtensionsPanel();
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+});
+
+add_task(async function test_pin_to_toolbar() {
+ const [extension] = createExtensions([
+ { name: "an extension", browser_action: {} },
+ ]);
+ await extension.startup();
+
+ // Open the extension panel, then open the context menu for the extension and
+ // pin the extension to the toolbar.
+ await openExtensionsPanel();
+ await pinToToolbar(extension);
+
+ // Undo the 'pin to toolbar' action.
+ await CustomizableUI.reset();
+ await extension.unload();
+});
+
+add_task(async function test_contextmenu_command_closes_panel() {
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "an extension",
+ browser_action: {},
+ permissions: ["contextMenus"],
+ },
+ background() {
+ browser.contextMenus.create(
+ {
+ id: "some-menu-id",
+ title: "Click me!",
+ contexts: ["all"],
+ },
+ () => browser.test.sendMessage("menu-created")
+ );
+ },
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+ await extension.awaitMessage("menu-created");
+
+ await openExtensionsPanel();
+ const contextMenu = await openUnifiedExtensionsContextMenu(extension.id);
+
+ const firstMenuItem = contextMenu.querySelector("menuitem");
+ is(
+ firstMenuItem?.getAttribute("label"),
+ "Click me!",
+ "expected custom menu item as first child"
+ );
+
+ const hidden = BrowserTestUtils.waitForEvent(
+ gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ contextMenu.activateItem(firstMenuItem);
+ await hidden;
+
+ await extension.unload();
+});
+
+add_task(async function test_contextmenu_reorder_extensions() {
+ const [ext1, ext2, ext3] = createExtensions([
+ { name: "ext1", browser_action: {} },
+ { name: "ext2", browser_action: {} },
+ { name: "ext3", browser_action: {} },
+ ]);
+ await Promise.all([ext1.startup(), ext2.startup(), ext3.startup()]);
+
+ await openExtensionsPanel();
+
+ // First extension in the list should only have "Move Down".
+ await assertMoveContextMenuItems(ext1, {
+ expectMoveUpHidden: true,
+ expectMoveDownHidden: false,
+ });
+
+ // Second extension in the list should have "Move Up" and "Move Down".
+ await assertMoveContextMenuItems(ext2, {
+ expectMoveUpHidden: false,
+ expectMoveDownHidden: false,
+ });
+
+ // Third extension in the list should only have "Move Up".
+ await assertMoveContextMenuItems(ext3, {
+ expectMoveUpHidden: false,
+ expectMoveDownHidden: true,
+ expectOrder: [ext1, ext2, ext3],
+ });
+
+ // Let's move some extensions now. We'll start by moving ext1 down until it
+ // is positioned at the end of the list.
+ info("Move down ext1 action to the bottom of the list");
+ await moveWidgetDown(ext1);
+ assertOrderOfWidgetsInPanel([ext2, ext1, ext3]);
+ await moveWidgetDown(ext1);
+
+ // Verify that the extension 1 has the right context menu items now that it
+ // is located at the end of the list.
+ await assertMoveContextMenuItems(ext1, {
+ expectMoveUpHidden: false,
+ expectMoveDownHidden: true,
+ expectOrder: [ext2, ext3, ext1],
+ });
+
+ info("Move up ext1 action to the top of the list");
+ await moveWidgetUp(ext1);
+ assertOrderOfWidgetsInPanel([ext2, ext1, ext3]);
+
+ await moveWidgetUp(ext1);
+ assertOrderOfWidgetsInPanel([ext1, ext2, ext3]);
+
+ // Move the last extension up.
+ info("Move up ext3 action");
+ await moveWidgetUp(ext3);
+ assertOrderOfWidgetsInPanel([ext1, ext3, ext2]);
+
+ // Move the last extension up (again).
+ info("Move up ext2 action to the top of the list");
+ await moveWidgetUp(ext2);
+ assertOrderOfWidgetsInPanel([ext1, ext2, ext3]);
+
+ // Move the second extension up.
+ await moveWidgetUp(ext2);
+ assertOrderOfWidgetsInPanel([ext2, ext1, ext3]);
+
+ // Pin an extension to the toolbar, which should remove it from the panel.
+ info("Pin ext1 action to the toolbar");
+ await pinToToolbar(ext1);
+ await openExtensionsPanel();
+ assertOrderOfWidgetsInPanel([ext2, ext3]);
+ await closeExtensionsPanel();
+
+ await Promise.all([ext1.unload(), ext2.unload(), ext3.unload()]);
+ await CustomizableUI.reset();
+});
+
+add_task(async function test_contextmenu_only_one_widget() {
+ const [extension] = createExtensions([{ name: "ext1", browser_action: {} }]);
+ await extension.startup();
+
+ await openExtensionsPanel();
+ await assertMoveContextMenuItems(extension, {
+ expectMoveUpHidden: true,
+ expectMoveDownHidden: true,
+ });
+ await closeExtensionsPanel();
+
+ await extension.unload();
+ await CustomizableUI.reset();
+});
+
+add_task(
+ async function test_contextmenu_reorder_extensions_with_private_window() {
+ // We want a panel in private mode that looks like this one (ext2 is not
+ // allowed in PB mode):
+ //
+ // - ext1
+ // - ext3
+ //
+ // But if we ask CUI to list the widgets in the panel, it would list:
+ //
+ // - ext1
+ // - ext2
+ // - ext3
+ //
+ const ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "ext1",
+ browser_specific_settings: { gecko: { id: "ext1@reorder-private" } },
+ browser_action: {},
+ },
+ useAddonManager: "temporary",
+ incognitoOverride: "spanning",
+ });
+ await ext1.startup();
+
+ const ext2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "ext2",
+ browser_specific_settings: { gecko: { id: "ext2@reorder-private" } },
+ browser_action: {},
+ },
+ useAddonManager: "temporary",
+ incognitoOverride: "not_allowed",
+ });
+ await ext2.startup();
+
+ const ext3 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "ext3",
+ browser_specific_settings: { gecko: { id: "ext3@reorder-private" } },
+ browser_action: {},
+ },
+ useAddonManager: "temporary",
+ incognitoOverride: "spanning",
+ });
+ await ext3.startup();
+
+ // Make sure all extension widgets are in the correct order.
+ assertOrderOfWidgetsInPanel([ext1, ext2, ext3]);
+
+ const privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ await openExtensionsPanel(privateWin);
+
+ // First extension in the list should only have "Move Down".
+ await assertMoveContextMenuItems(
+ ext1,
+ {
+ expectMoveUpHidden: true,
+ expectMoveDownHidden: false,
+ expectOrder: [ext1, ext3],
+ },
+ privateWin
+ );
+
+ // Second extension in the list (which is ext3) should only have "Move Up".
+ await assertMoveContextMenuItems(
+ ext3,
+ {
+ expectMoveUpHidden: false,
+ expectMoveDownHidden: true,
+ expectOrder: [ext1, ext3],
+ },
+ privateWin
+ );
+
+ // In private mode, we should only have two CUI widget nodes in the panel.
+ assertOrderOfWidgetsInPanel([ext1, ext3], privateWin);
+
+ info("Move ext1 down");
+ await moveWidgetDown(ext1, privateWin);
+ // The new order in a regular window should be:
+ assertOrderOfWidgetsInPanel([ext2, ext3, ext1]);
+ // ... while the order in the private window should be:
+ assertOrderOfWidgetsInPanel([ext3, ext1], privateWin);
+
+ // Verify that the extension 1 has the right context menu items now that it
+ // is located at the end of the list in PB mode.
+ await assertMoveContextMenuItems(
+ ext1,
+ {
+ expectMoveUpHidden: false,
+ expectMoveDownHidden: true,
+ expectOrder: [ext3, ext1],
+ },
+ privateWin
+ );
+
+ // Verify that the extension 3 has the right context menu items now that it
+ // is located at the top of the list in PB mode.
+ await assertMoveContextMenuItems(
+ ext3,
+ {
+ expectMoveUpHidden: true,
+ expectMoveDownHidden: false,
+ expectOrder: [ext3, ext1],
+ },
+ privateWin
+ );
+
+ info("Move ext3 extension down");
+ await moveWidgetDown(ext3, privateWin);
+ // The new order in a regular window should be:
+ assertOrderOfWidgetsInPanel([ext2, ext1, ext3]);
+ // ... while the order in the private window should be:
+ assertOrderOfWidgetsInPanel([ext1, ext3], privateWin);
+
+ // Pin an extension to the toolbar, which should remove it from the panel.
+ info("Pin ext1 to the toolbar");
+ await pinToToolbar(ext1, privateWin);
+ await openExtensionsPanel(privateWin);
+
+ // The new order in a regular window should be:
+ assertOrderOfWidgetsInPanel([ext2, ext3]);
+ await assertMoveContextMenuItems(
+ ext3,
+ {
+ expectMoveUpHidden: true,
+ expectMoveDownHidden: true,
+ // ... while the order in the private window should be:
+ expectOrder: [ext3],
+ },
+ privateWin
+ );
+
+ await closeExtensionsPanel(privateWin);
+
+ await Promise.all([ext1.unload(), ext2.unload(), ext3.unload()]);
+ await CustomizableUI.reset();
+
+ await BrowserTestUtils.closeWindow(privateWin);
+ }
+);
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_cui.js b/browser/components/extensions/test/browser/browser_unified_extensions_cui.js
new file mode 100644
index 0000000000..dc02623452
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_cui.js
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+loadTestSubscript("head_unified_extensions.js");
+
+/**
+ * Tests that if the addons panel is somehow open when customization mode is
+ * invoked, that the panel is hidden.
+ */
+add_task(async function test_hide_panel_when_customizing() {
+ await openExtensionsPanel();
+
+ let panel = gUnifiedExtensions.panel;
+ Assert.equal(panel.state, "open");
+
+ let panelHidden = BrowserTestUtils.waitForPopupEvent(panel, "hidden");
+ CustomizableUI.dispatchToolboxEvent("customizationstarting", {});
+ await panelHidden;
+ Assert.equal(panel.state, "closed");
+ CustomizableUI.dispatchToolboxEvent("aftercustomization", {});
+});
+
+/**
+ * Tests that if a browser action is in a collapsed toolbar area, like the
+ * bookmarks toolbar, that its DOM node is overflowed in the extensions panel.
+ */
+add_task(async function test_extension_in_collapsed_area() {
+ const extensions = createExtensions(
+ [
+ {
+ name: "extension1",
+ browser_action: { default_area: "navbar", default_popup: "popup.html" },
+ browser_specific_settings: {
+ gecko: { id: "unified-extensions-cui@ext-1" },
+ },
+ },
+ {
+ name: "extension2",
+ browser_action: { default_area: "navbar" },
+ browser_specific_settings: {
+ gecko: { id: "unified-extensions-cui@ext-2" },
+ },
+ },
+ ],
+ {
+ files: {
+ "popup.html": `
+
+
+
test popup
+
+
+
+ `,
+ "popup.js": function () {
+ browser.test.sendMessage("test-popup-opened");
+ },
+ },
+ }
+ );
+ await Promise.all(extensions.map(extension => extension.startup()));
+
+ await openExtensionsPanel();
+ for (const extension of extensions) {
+ let item = getUnifiedExtensionsItem(extension.id);
+ Assert.ok(
+ !item,
+ `extension with ID=${extension.id} should not appear in the panel`
+ );
+ }
+ await closeExtensionsPanel();
+
+ // Move an extension to the bookmarks toolbar.
+ const bookmarksToolbar = document.getElementById(
+ CustomizableUI.AREA_BOOKMARKS
+ );
+ const firstExtensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extensions[0].id
+ );
+ CustomizableUI.addWidgetToArea(
+ firstExtensionWidgetID,
+ CustomizableUI.AREA_BOOKMARKS
+ );
+
+ // Ensure that the toolbar is currently collapsed.
+ await promiseSetToolbarVisibility(bookmarksToolbar, false);
+
+ await openExtensionsPanel();
+ let item = getUnifiedExtensionsItem(extensions[0].id);
+ Assert.ok(
+ item,
+ "extension placed in the collapsed toolbar should appear in the panel"
+ );
+
+ // NOTE: ideally we would simply call `AppUiTestDelegate.clickBrowserAction()`
+ // but, unfortunately, that does internally call `showBrowserAction()`, which
+ // explicitly assert the group areaType that would hit a failure in this test
+ // because we are moving it to AREA_BOOKMARKS.
+ let widget = getBrowserActionWidget(extensions[0]).forWindow(window);
+ ok(widget, "Got a widget for the extension button overflowed into the panel");
+ widget.node.firstElementChild.click();
+
+ const promisePanelBrowser = AppUiTestDelegate.awaitExtensionPanel(
+ window,
+ extensions[0].id,
+ true
+ );
+ await extensions[0].awaitMessage("test-popup-opened");
+ const extPanelBrowser = await promisePanelBrowser;
+ ok(extPanelBrowser, "Got a action panel browser");
+ closeBrowserAction(extensions[0]);
+
+ // Now, make the toolbar visible.
+ await promiseSetToolbarVisibility(bookmarksToolbar, true);
+
+ await openExtensionsPanel();
+ for (const extension of extensions) {
+ let item = getUnifiedExtensionsItem(extension.id);
+ Assert.ok(
+ !item,
+ `extension with ID=${extension.id} should not appear in the panel`
+ );
+ }
+ await closeExtensionsPanel();
+
+ // Hide the bookmarks toolbar again.
+ await promiseSetToolbarVisibility(bookmarksToolbar, false);
+
+ await openExtensionsPanel();
+ item = getUnifiedExtensionsItem(extensions[0].id);
+ Assert.ok(item, "extension should reappear in the panel");
+ await closeExtensionsPanel();
+
+ // We now empty the bookmarks toolbar but we keep the extension widget.
+ for (const widgetId of CustomizableUI.getWidgetIdsInArea(
+ CustomizableUI.AREA_BOOKMARKS
+ ).filter(widgetId => widgetId !== firstExtensionWidgetID)) {
+ CustomizableUI.removeWidgetFromArea(widgetId);
+ }
+
+ // We make the bookmarks toolbar visible again. At this point, the extension
+ // widget should be re-inserted in this toolbar.
+ await promiseSetToolbarVisibility(bookmarksToolbar, true);
+
+ await openExtensionsPanel();
+ for (const extension of extensions) {
+ let item = getUnifiedExtensionsItem(extension.id);
+ Assert.ok(
+ !item,
+ `extension with ID=${extension.id} should not appear in the panel`
+ );
+ }
+ await closeExtensionsPanel();
+
+ await Promise.all(extensions.map(extension => extension.unload()));
+ await CustomizableUI.reset();
+});
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js b/browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js
new file mode 100644
index 0000000000..8603928894
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_doorhangers.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+loadTestSubscript("head_unified_extensions.js");
+
+const verifyPermissionsPrompt = async expectedAnchorID => {
+ const ext = ExtensionTestUtils.loadExtension({
+ useAddonManager: "temporary",
+
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "some search name",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ optional_permissions: ["history"],
+ },
+
+ background: () => {
+ browser.test.onMessage.addListener(async msg => {
+ if (msg !== "create-tab") {
+ return;
+ }
+
+ await browser.tabs.create({
+ url: browser.runtime.getURL("content.html"),
+ active: true,
+ });
+ });
+ },
+
+ files: {
+ "content.html": ``,
+ "content.js": async () => {
+ browser.test.onMessage.addListener(async msg => {
+ browser.test.assertEq(
+ msg,
+ "grant-permission",
+ "expected message to grant permission"
+ );
+
+ const granted = await new Promise(resolve => {
+ browser.test.withHandlingUserInput(() => {
+ resolve(
+ browser.permissions.request({ permissions: ["history"] })
+ );
+ });
+ });
+ browser.test.assertTrue(granted, "permission request succeeded");
+
+ browser.test.sendMessage("ok");
+ });
+
+ browser.test.sendMessage("ready");
+ },
+ },
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async () => {
+ const defaultSearchPopupPromise = promisePopupNotificationShown(
+ "addon-webext-defaultsearch"
+ );
+ let [panel] = await Promise.all([defaultSearchPopupPromise, ext.startup()]);
+ ok(panel, "expected panel");
+ let notification = PopupNotifications.getNotification(
+ "addon-webext-defaultsearch"
+ );
+ ok(notification, "expected notification");
+ // We always want the defaultsearch popup to be anchored on the urlbar (the
+ // ID below) because the post-install popup would be displayed on top of
+ // this one otherwise, see Bug 1789407.
+ is(
+ notification?.anchorElement?.id,
+ "addons-notification-icon",
+ "expected the right anchor ID for the defaultsearch popup"
+ );
+ // Accept to override the search.
+ panel.button.click();
+ await TestUtils.topicObserved("webextension-defaultsearch-prompt-response");
+
+ ext.sendMessage("create-tab");
+ await ext.awaitMessage("ready");
+
+ const popupPromise = promisePopupNotificationShown(
+ "addon-webext-permissions"
+ );
+ ext.sendMessage("grant-permission");
+ panel = await popupPromise;
+ ok(panel, "expected panel");
+ notification = PopupNotifications.getNotification(
+ "addon-webext-permissions"
+ );
+ ok(notification, "expected notification");
+ is(
+ // We access the parent element because the anchor is on the icon (inside
+ // the button), not on the unified extensions button itself.
+ notification.anchorElement.id ||
+ notification.anchorElement.parentElement.id,
+ expectedAnchorID,
+ "expected the right anchor ID"
+ );
+
+ panel.button.click();
+ await ext.awaitMessage("ok");
+
+ await ext.unload();
+ });
+};
+
+add_task(async function test_permissions_prompt() {
+ await verifyPermissionsPrompt("unified-extensions-button");
+});
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_messages.js b/browser/components/extensions/test/browser/browser_unified_extensions_messages.js
new file mode 100644
index 0000000000..2c13e08727
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_messages.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+loadTestSubscript("head_unified_extensions.js");
+
+const verifyMessageBar = message => {
+ Assert.equal(
+ message.getAttribute("type"),
+ "warning",
+ "expected warning message"
+ );
+ Assert.ok(
+ !message.hasAttribute("dismissable"),
+ "expected message to not be dismissable"
+ );
+
+ const supportLink = message.querySelector("a");
+ Assert.equal(
+ supportLink.getAttribute("support-page"),
+ "quarantined-domains",
+ "expected the correct support page ID"
+ );
+ Assert.equal(
+ supportLink.getAttribute("aria-labelledby"),
+ "unified-extensions-mb-quarantined-domain-title",
+ "expected the correct aria-labelledby value"
+ );
+ Assert.equal(
+ supportLink.getAttribute("aria-describedby"),
+ "unified-extensions-mb-quarantined-domain-message",
+ "expected the correct aria-describedby value"
+ );
+};
+
+add_task(async function test_quarantined_domain_message_disabled() {
+ const quarantinedDomain = "example.org";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.quarantinedDomains.enabled", false],
+ ["extensions.quarantinedDomains.list", quarantinedDomain],
+ ],
+ });
+
+ // Load an extension that will have access to all domains, including the
+ // quarantined domain.
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ });
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `https://${quarantinedDomain}/` },
+ async () => {
+ await openExtensionsPanel();
+ Assert.equal(getMessageBars().length, 0, "expected no message");
+ await closeExtensionsPanel();
+ }
+ );
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_quarantined_domain_message() {
+ const quarantinedDomain = "example.org";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.quarantinedDomains.enabled", true],
+ ["extensions.quarantinedDomains.list", quarantinedDomain],
+ ],
+ });
+
+ // Load an extension that will have access to all domains, including the
+ // quarantined domain.
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ });
+ await extension.startup();
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `https://${quarantinedDomain}/` },
+ async () => {
+ await openExtensionsPanel();
+
+ const messages = getMessageBars();
+ Assert.equal(messages.length, 1, "expected a message");
+
+ const [message] = messages;
+ verifyMessageBar(message);
+
+ await closeExtensionsPanel();
+ }
+ );
+
+ // Navigating to a different tab/domain shouldn't show any message.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `http://mochi.test:8888/` },
+ async () => {
+ await openExtensionsPanel();
+ Assert.equal(getMessageBars().length, 0, "expected no message");
+ await closeExtensionsPanel();
+ }
+ );
+
+ // Back to a quarantined domain, if we update the list, we expect the message
+ // to be gone when we re-open the panel (and not before because we don't
+ // listen to the pref currently).
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `https://${quarantinedDomain}/` },
+ async () => {
+ await openExtensionsPanel();
+
+ const messages = getMessageBars();
+ Assert.equal(messages.length, 1, "expected a message");
+
+ const [message] = messages;
+ verifyMessageBar(message);
+
+ await closeExtensionsPanel();
+
+ // Clear the list of quarantined domains.
+ Services.prefs.setStringPref("extensions.quarantinedDomains.list", "");
+
+ await openExtensionsPanel();
+ Assert.equal(getMessageBars().length, 0, "expected no message");
+ await closeExtensionsPanel();
+ }
+ );
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_quarantined_domain_message_learn_more_link() {
+ const quarantinedDomain = "example.org";
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["extensions.quarantinedDomains.enabled", true],
+ ["extensions.quarantinedDomains.list", quarantinedDomain],
+ ],
+ });
+
+ // Load an extension that will have access to all domains, including the
+ // quarantined domain.
+ const extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["activeTab"],
+ browser_action: {},
+ },
+ });
+ await extension.startup();
+
+ const expectedSupportURL =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "quarantined-domains";
+
+ // We expect the SUMO page to be open in a new tab and the panel to be closed
+ // when the user clicks on the "learn more" link.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `https://${quarantinedDomain}/` },
+ async () => {
+ await openExtensionsPanel();
+ const messages = getMessageBars();
+ Assert.equal(messages.length, 1, "expected a message");
+
+ const [message] = messages;
+ verifyMessageBar(message);
+
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ expectedSupportURL
+ );
+ const hidden = BrowserTestUtils.waitForEvent(
+ gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ message.querySelector("a").click();
+ const [tab] = await Promise.all([tabPromise, hidden]);
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+
+ // Same as above but with keyboard navigation.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: `https://${quarantinedDomain}/` },
+ async () => {
+ await openExtensionsPanel();
+ const messages = getMessageBars();
+ Assert.equal(messages.length, 1, "expected a message");
+
+ const [message] = messages;
+ verifyMessageBar(message);
+
+ const supportLink = message.querySelector("a");
+
+ // Focus the "learn more" (support) link.
+ const focused = BrowserTestUtils.waitForEvent(supportLink, "focus");
+ EventUtils.synthesizeKey("VK_TAB", {});
+ await focused;
+
+ const tabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ expectedSupportURL
+ );
+ const hidden = BrowserTestUtils.waitForEvent(
+ gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ EventUtils.synthesizeKey("KEY_Enter", {});
+ const [tab] = await Promise.all([tabPromise, hidden]);
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+
+ await extension.unload();
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/extensions/test/browser/browser_unified_extensions_overflowable_toolbar.js b/browser/components/extensions/test/browser/browser_unified_extensions_overflowable_toolbar.js
new file mode 100644
index 0000000000..187e1a111f
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_unified_extensions_overflowable_toolbar.js
@@ -0,0 +1,1389 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the behaviour of the overflowable nav-bar with Unified
+ * Extensions enabled and disabled.
+ */
+
+"use strict";
+
+loadTestSubscript("head_unified_extensions.js");
+
+requestLongerTimeout(2);
+
+const NUM_EXTENSIONS = 5;
+const OVERFLOW_WINDOW_WIDTH_PX = 450;
+const DEFAULT_WIDGET_IDS = [
+ "home-button",
+ "library-button",
+ "zoom-controls",
+ "search-container",
+ "sidebar-button",
+];
+const OVERFLOWED_EXTENSIONS_LIST_ID = "overflowed-extensions-list";
+
+add_setup(async function () {
+ // To make it easier to control things that will overflow, we'll start by
+ // removing that's removable out of the nav-bar and adding just a fixed
+ // set of items (DEFAULT_WIDGET_IDS) at the end of the nav-bar.
+ let existingWidgetIDs = CustomizableUI.getWidgetIdsInArea(
+ CustomizableUI.AREA_NAVBAR
+ );
+ for (let widgetID of existingWidgetIDs) {
+ if (CustomizableUI.isWidgetRemovable(widgetID)) {
+ CustomizableUI.removeWidgetFromArea(widgetID);
+ }
+ }
+ for (const widgetID of DEFAULT_WIDGET_IDS) {
+ CustomizableUI.addWidgetToArea(widgetID, CustomizableUI.AREA_NAVBAR);
+ }
+
+ registerCleanupFunction(async () => {
+ await CustomizableUI.reset();
+ });
+});
+
+/**
+ * Returns the IDs of the children of parent.
+ *
+ * @param {Element} parent
+ * @returns {string[]} the IDs of the children
+ */
+function getChildrenIDs(parent) {
+ return Array.from(parent.children).map(child => child.id);
+}
+
+/**
+ * Returns a NodeList of all non-hidden menu, menuitem and menuseparators
+ * that are direct descendants of popup.
+ *
+ * @param {Element} popup
+ * @returns {NodeList} the visible items.
+ */
+function getVisibleMenuItems(popup) {
+ return popup.querySelectorAll(
+ ":scope > :is(menu, menuitem, menuseparator):not([hidden])"
+ );
+}
+
+/**
+ * This helper function does most of the heavy lifting for these tests.
+ * It does the following in order:
+ *
+ * 1. Registers and enables NUM_EXTENSIONS test WebExtensions that add
+ * browser_action buttons to the nav-bar.
+ * 2. Resizes the window to force things after the URL bar to overflow.
+ * 3. Calls an async test function to analyze the overflow lists.
+ * 4. Restores the window's original width, ensuring that the IDs of the
+ * nav-bar match the original set.
+ * 5. Unloads all of the test WebExtensions
+ *
+ * @param {DOMWindow} win The browser window to perform the test on.
+ * @param {object} options Additional options when running this test.
+ * @param {Function} options.beforeOverflowed This optional async function will
+ * be run after the extensions are created and added to the toolbar, but
+ * before the toolbar overflows. The function is called with the following
+ * arguments:
+ *
+ * {string[]} extensionIDs: The IDs of the test WebExtensions.
+ *
+ * The return value of the function is ignored.
+ * @param {Function} options.whenOverflowed This optional async function will
+ * run once the window is in the overflow state. The function is called
+ * with the following arguments:
+ *
+ * {Element} defaultList: The DOM element that holds overflowed default
+ * items.
+ * {Element} unifiedExtensionList: The DOM element that holds overflowed
+ * WebExtension browser_actions when Unified Extensions is enabled.
+ * {string[]} extensionIDs: The IDs of the test WebExtensions.
+ *
+ * The return value of the function is ignored.
+ * @param {Function} options.afterUnderflowed This optional async function will
+ * be run after the window is expanded and the toolbar has underflowed, but
+ * before the extensions are removed. This function is not passed any
+ * arguments. The return value of the function is ignored.
+ *
+ */
+async function withWindowOverflowed(
+ win,
+ {
+ beforeOverflowed = async () => {},
+ whenOverflowed = async () => {},
+ afterUnderflowed = async () => {},
+ } = {}
+) {
+ const doc = win.document;
+ doc.documentElement.removeAttribute("persist");
+ const navbar = doc.getElementById(CustomizableUI.AREA_NAVBAR);
+
+ await ensureMaximizedWindow(win);
+
+ // The OverflowableToolbar operates asynchronously at times, so we will
+ // poll a widget's overflowedItem attribute to detect whether or not the
+ // widgets have finished being moved. We'll use the first widget that
+ // we added to the nav-bar, as this should be the left-most item in the
+ // set that we added.
+ const signpostWidgetID = "home-button";
+ // We'll also force the signpost widget to be extra-wide to ensure that it
+ // overflows after we shrink the window.
+ CustomizableUI.getWidget(signpostWidgetID).forWindow(win).node.style =
+ "width: 150px";
+
+ const extWithMenuBrowserAction = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Extension #0",
+ browser_specific_settings: {
+ gecko: { id: "unified-extensions-overflowable-toolbar@ext-0" },
+ },
+ browser_action: {
+ default_area: "navbar",
+ },
+ // We pass `activeTab` to have a different permission message when
+ // hovering the primary/action button.
+ permissions: ["activeTab", "contextMenus"],
+ },
+ background() {
+ browser.contextMenus.create(
+ {
+ id: "some-menu-id",
+ title: "Click me!",
+ contexts: ["all"],
+ },
+ () => browser.test.sendMessage("menu-created")
+ );
+ },
+ useAddonManager: "temporary",
+ });
+
+ const extWithSubMenuBrowserAction = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Extension #1",
+ browser_specific_settings: {
+ gecko: { id: "unified-extensions-overflowable-toolbar@ext-1" },
+ },
+ browser_action: {
+ default_area: "navbar",
+ },
+ permissions: ["contextMenus"],
+ },
+ background() {
+ browser.contextMenus.create({
+ id: "some-menu-id",
+ title: "Open sub-menu",
+ contexts: ["all"],
+ });
+ browser.contextMenus.create(
+ {
+ id: "some-sub-menu-id",
+ parentId: "some-menu-id",
+ title: "Click me!",
+ contexts: ["all"],
+ },
+ () => browser.test.sendMessage("menu-created")
+ );
+ },
+ useAddonManager: "temporary",
+ });
+
+ const manifests = [];
+ for (let i = 2; i < NUM_EXTENSIONS; ++i) {
+ manifests.push({
+ name: `Extension #${i}`,
+ browser_action: {
+ default_area: "navbar",
+ },
+ browser_specific_settings: {
+ gecko: { id: `unified-extensions-overflowable-toolbar@ext-${i}` },
+ },
+ });
+ }
+
+ const extensions = [
+ extWithMenuBrowserAction,
+ extWithSubMenuBrowserAction,
+ ...createExtensions(manifests),
+ ];
+
+ // Adding browser actions is asynchronous, so this CustomizableUI listener
+ // is used to make sure that the browser action widgets have finished getting
+ // added.
+ let listener = {
+ _remainingBrowserActions: NUM_EXTENSIONS,
+ _deferred: PromiseUtils.defer(),
+
+ get promise() {
+ return this._deferred.promise;
+ },
+
+ onWidgetAdded(widgetID, area) {
+ if (widgetID.endsWith("-browser-action")) {
+ this._remainingBrowserActions--;
+ }
+ if (!this._remainingBrowserActions) {
+ this._deferred.resolve();
+ }
+ },
+ };
+ CustomizableUI.addListener(listener);
+ // Start all the extensions sequentially.
+ for (const extension of extensions) {
+ await extension.startup();
+ }
+ await Promise.all([
+ extWithMenuBrowserAction.awaitMessage("menu-created"),
+ extWithSubMenuBrowserAction.awaitMessage("menu-created"),
+ ]);
+ await listener.promise;
+ CustomizableUI.removeListener(listener);
+
+ const extensionIDs = extensions.map(extension => extension.id);
+
+ try {
+ info("Running beforeOverflowed task");
+ await beforeOverflowed(extensionIDs);
+ } finally {
+ // The beforeOverflowed task may have moved some items out from the navbar,
+ // so only listen for overflows for items still in there.
+ const browserActionIDs = extensionIDs.map(id =>
+ AppUiTestInternals.getBrowserActionWidgetId(id)
+ );
+ const browserActionsInNavBar = browserActionIDs.filter(widgetID => {
+ let placement = CustomizableUI.getPlacementOfWidget(widgetID);
+ return placement.area == CustomizableUI.AREA_NAVBAR;
+ });
+
+ let widgetOverflowListener = {
+ _remainingOverflowables:
+ browserActionsInNavBar.length + DEFAULT_WIDGET_IDS.length,
+ _deferred: PromiseUtils.defer(),
+
+ get promise() {
+ return this._deferred.promise;
+ },
+
+ onWidgetOverflow(widgetNode, areaNode) {
+ this._remainingOverflowables--;
+ if (!this._remainingOverflowables) {
+ this._deferred.resolve();
+ }
+ },
+ };
+ CustomizableUI.addListener(widgetOverflowListener);
+
+ win.resizeTo(OVERFLOW_WINDOW_WIDTH_PX, win.outerHeight);
+ await widgetOverflowListener.promise;
+ CustomizableUI.removeListener(widgetOverflowListener);
+
+ Assert.ok(
+ navbar.hasAttribute("overflowing"),
+ "Should have an overflowing toolbar."
+ );
+
+ const defaultList = doc.getElementById(
+ navbar.getAttribute("default-overflowtarget")
+ );
+
+ const unifiedExtensionList = doc.getElementById(
+ navbar.getAttribute("addon-webext-overflowtarget")
+ );
+
+ try {
+ info("Running whenOverflowed task");
+ await whenOverflowed(defaultList, unifiedExtensionList, extensionIDs);
+ } finally {
+ await ensureMaximizedWindow(win);
+
+ // Notably, we don't wait for the nav-bar to not have the "overflowing"
+ // attribute. This is because we might be running in an environment
+ // where the nav-bar was overflowing to begin with. Let's just hope that
+ // our sign-post widget has stopped overflowing.
+ await TestUtils.waitForCondition(() => {
+ return !doc
+ .getElementById(signpostWidgetID)
+ .hasAttribute("overflowedItem");
+ });
+
+ try {
+ info("Running afterUnderflowed task");
+ await afterUnderflowed();
+ } finally {
+ await Promise.all(extensions.map(extension => extension.unload()));
+ }
+ }
+ }
+}
+
+async function verifyExtensionWidget(widget, win = window) {
+ Assert.ok(widget, "expected widget");
+
+ let actionButton = widget.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ Assert.ok(
+ actionButton.classList.contains("unified-extensions-item-action-button"),
+ "expected action class on the button"
+ );
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+
+ let menuButton = widget.lastElementChild;
+ Assert.ok(
+ menuButton.classList.contains("unified-extensions-item-menu-button"),
+ "expected class on the button"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+
+ let contents = actionButton.querySelector(
+ ".unified-extensions-item-contents"
+ );
+
+ Assert.ok(contents, "expected contents element");
+ // This is needed to correctly position the contents (vbox) element in the
+ // toolbarbutton.
+ Assert.equal(
+ contents.getAttribute("move-after-stack"),
+ "true",
+ "expected move-after-stack attribute to be set"
+ );
+ // Make sure the contents element is inserted after the stack one (which is
+ // automagically created by the toolbarbutton element).
+ Assert.deepEqual(
+ Array.from(actionButton.childNodes.values()).map(
+ child => child.classList[0]
+ ),
+ [
+ // The stack (which contains the extension icon) should be the first
+ // child.
+ "toolbarbutton-badge-stack",
+ // This is the widget label, which is hidden with CSS.
+ "toolbarbutton-text",
+ // This is the contents element, which displays the extension name and
+ // messages.
+ "unified-extensions-item-contents",
+ ],
+ "expected the correct order for the children of the action button"
+ );
+
+ let name = contents.querySelector(".unified-extensions-item-name");
+ Assert.ok(name, "expected name element");
+ Assert.ok(
+ name.textContent.startsWith("Extension "),
+ "expected name to not be empty"
+ );
+ Assert.ok(
+ contents.querySelector(".unified-extensions-item-message-default"),
+ "expected message default element"
+ );
+ Assert.ok(
+ contents.querySelector(".unified-extensions-item-message-hover"),
+ "expected message hover element"
+ );
+
+ Assert.equal(
+ win.document.l10n.getAttributes(menuButton).id,
+ "unified-extensions-item-open-menu",
+ "expected l10n id attribute for the extension"
+ );
+ Assert.deepEqual(
+ Object.keys(win.document.l10n.getAttributes(menuButton).args),
+ ["extensionName"],
+ "expected l10n args attribute for the extension"
+ );
+ Assert.ok(
+ win.document.l10n
+ .getAttributes(menuButton)
+ .args.extensionName.startsWith("Extension "),
+ "expected l10n args attribute to start with the correct name"
+ );
+ Assert.ok(
+ menuButton.getAttribute("aria-label") !== "",
+ "expected menu button to have non-empty localized content"
+ );
+}
+
+/**
+ * Tests that overflowed browser actions go to the Unified Extensions
+ * panel, and default toolbar items go into the default overflow
+ * panel.
+ */
+add_task(async function test_overflowable_toolbar() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let movedNode;
+
+ await withWindowOverflowed(win, {
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ // Ensure that there are 5 items in the Unified Extensions overflow
+ // list, and the default widgets should all be in the default overflow
+ // list (though there might be more items from the nav-bar in there that
+ // already existed in the nav-bar before we put the default widgets in
+ // there as well).
+ let defaultListIDs = getChildrenIDs(defaultList);
+ for (const widgetID of DEFAULT_WIDGET_IDS) {
+ Assert.ok(
+ defaultListIDs.includes(widgetID),
+ `Default overflow list should have ${widgetID}`
+ );
+ }
+
+ Assert.ok(
+ unifiedExtensionList.children.length,
+ "Should have items in the Unified Extension list."
+ );
+
+ for (const child of Array.from(unifiedExtensionList.children)) {
+ Assert.ok(
+ extensionIDs.includes(child.dataset.extensionid),
+ `Unified Extensions overflow list should have ${child.dataset.extensionid}`
+ );
+ await verifyExtensionWidget(child, win);
+ }
+
+ let extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extensionIDs.at(-1)
+ );
+ movedNode =
+ CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node;
+ Assert.equal(movedNode.getAttribute("cui-areatype"), "toolbar");
+
+ CustomizableUI.addWidgetToArea(
+ extensionWidgetID,
+ CustomizableUI.AREA_ADDONS
+ );
+
+ Assert.equal(
+ movedNode.getAttribute("cui-areatype"),
+ "panel",
+ "The moved browser action button should have the right cui-areatype set."
+ );
+ },
+ afterUnderflowed: async () => {
+ // Ensure that the moved node's parent is still the add-ons panel.
+ Assert.equal(
+ movedNode.parentElement.id,
+ CustomizableUI.AREA_ADDONS,
+ "The browser action should still be in the addons panel"
+ );
+ CustomizableUI.addWidgetToArea(movedNode.id, CustomizableUI.AREA_NAVBAR);
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_context_menu() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await withWindowOverflowed(win, {
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ Assert.ok(
+ unifiedExtensionList.children.length,
+ "Should have items in the Unified Extension list."
+ );
+
+ // Open the extension panel.
+ await openExtensionsPanel(win);
+
+ // Let's verify the context menus for the following extensions:
+ //
+ // - the first one defines a menu in the background script
+ // - the second one defines a menu with submenu
+ // - the third extension has no menu
+
+ info("extension with browser action and a menu");
+ const firstExtensionWidget = unifiedExtensionList.children[0];
+ Assert.ok(firstExtensionWidget, "expected extension widget");
+ let contextMenu = await openUnifiedExtensionsContextMenu(
+ firstExtensionWidget.dataset.extensionid,
+ win
+ );
+ Assert.ok(contextMenu, "expected a context menu");
+ let visibleItems = getVisibleMenuItems(contextMenu);
+
+ // The context menu for the extension that declares a browser action menu
+ // should have the menu item created by the extension, a menu separator, the control
+ // for pinning the browser action to the toolbar, a menu separator and the 3 default menu items.
+ is(
+ visibleItems.length,
+ 7,
+ "expected a custom context menu item, a menu separator, the pin to " +
+ "toolbar menu item, a menu separator, and the 3 default menu items"
+ );
+
+ const [item, separator] = visibleItems;
+ is(
+ item.getAttribute("label"),
+ "Click me!",
+ "expected menu item as first child"
+ );
+ is(
+ separator.tagName,
+ "menuseparator",
+ "expected separator after last menu item created by the extension"
+ );
+
+ await closeChromeContextMenu(contextMenu.id, null, win);
+
+ info("extension with browser action and a menu with submenu");
+ const secondExtensionWidget = unifiedExtensionList.children[1];
+ Assert.ok(secondExtensionWidget, "expected extension widget");
+ contextMenu = await openUnifiedExtensionsContextMenu(
+ secondExtensionWidget.dataset.extensionid,
+ win
+ );
+ visibleItems = getVisibleMenuItems(contextMenu);
+ is(visibleItems.length, 7, "expected 7 menu items");
+ const popup = await openSubmenu(visibleItems[0]);
+ is(popup.children.length, 1, "expected 1 submenu item");
+ is(
+ popup.children[0].getAttribute("label"),
+ "Click me!",
+ "expected menu item"
+ );
+ // The number of items in the (main) context menu should remain the same.
+ visibleItems = getVisibleMenuItems(contextMenu);
+ is(visibleItems.length, 7, "expected 7 menu items");
+ await closeChromeContextMenu(contextMenu.id, null, win);
+
+ info("extension with no browser action and no menu");
+ // There is no context menu created by this extension, so there should
+ // only be 3 menu items corresponding to the default manage/remove/report
+ // items.
+ const thirdExtensionWidget = unifiedExtensionList.children[2];
+ Assert.ok(thirdExtensionWidget, "expected extension widget");
+ contextMenu = await openUnifiedExtensionsContextMenu(
+ thirdExtensionWidget.dataset.extensionid,
+ win
+ );
+ Assert.ok(contextMenu, "expected a context menu");
+ visibleItems = getVisibleMenuItems(contextMenu);
+ is(visibleItems.length, 5, "expected 5 menu items");
+
+ await closeChromeContextMenu(contextMenu.id, null, win);
+
+ // We can close the unified extensions panel now.
+ await closeExtensionsPanel(win);
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_message_deck() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ await withWindowOverflowed(win, {
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ Assert.ok(
+ unifiedExtensionList.children.length,
+ "Should have items in the Unified Extension list."
+ );
+
+ const firstExtensionWidget = unifiedExtensionList.children[0];
+ Assert.ok(firstExtensionWidget, "expected extension widget");
+ Assert.ok(
+ firstExtensionWidget.dataset.extensionid,
+ "expected data attribute for extension ID"
+ );
+
+ // Navigate to a page where `activeTab` is useful.
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "https://example.com/" },
+ async () => {
+ // Open the extension panel.
+ await openExtensionsPanel(win);
+
+ info("verify message when focusing the action button");
+ const item = getUnifiedExtensionsItem(
+ firstExtensionWidget.dataset.extensionid,
+ win
+ );
+ Assert.ok(item, "expected an item for the extension");
+
+ const actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ Assert.ok(actionButton, "expected action button");
+
+ const menuButton = item.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ Assert.ok(menuButton, "expected menu button");
+
+ const messageDeck = item.querySelector(
+ ".unified-extensions-item-message-deck"
+ );
+ Assert.ok(messageDeck, "expected message deck");
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+
+ const defaultMessage = item.querySelector(
+ ".unified-extensions-item-message-default"
+ );
+ Assert.deepEqual(
+ win.document.l10n.getAttributes(defaultMessage),
+ { id: "origin-controls-state-when-clicked", args: null },
+ "expected correct l10n attributes for the default message"
+ );
+ Assert.ok(
+ defaultMessage.textContent !== "",
+ "expected default message to not be empty"
+ );
+
+ const hoverMessage = item.querySelector(
+ ".unified-extensions-item-message-hover"
+ );
+ Assert.deepEqual(
+ win.document.l10n.getAttributes(hoverMessage),
+ { id: "origin-controls-state-hover-run-visit-only", args: null },
+ "expected correct l10n attributes for the hover message"
+ );
+ Assert.ok(
+ hoverMessage.textContent !== "",
+ "expected hover message to not be empty"
+ );
+
+ const hoverMenuButtonMessage = item.querySelector(
+ ".unified-extensions-item-message-hover-menu-button"
+ );
+ Assert.deepEqual(
+ win.document.l10n.getAttributes(hoverMenuButtonMessage),
+ { id: "unified-extensions-item-message-manage", args: null },
+ "expected correct l10n attributes for the message when hovering the menu button"
+ );
+ Assert.ok(
+ hoverMenuButtonMessage.textContent !== "",
+ "expected message for when the menu button is hovered to not be empty"
+ );
+
+ // 1. Focus the action button of the first extension in the panel.
+ let focused = BrowserTestUtils.waitForEvent(actionButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ await focused;
+ is(
+ actionButton,
+ win.document.activeElement,
+ "expected action button of the first extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ // 2. Focus the menu button, causing the action button to lose focus.
+ focused = BrowserTestUtils.waitForEvent(menuButton, "focus");
+ EventUtils.synthesizeKey("VK_TAB", {}, win);
+ await focused;
+ is(
+ menuButton,
+ win.document.activeElement,
+ "expected menu button of the first extension item to be focused"
+ );
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER,
+ "expected selected message in the deck to be the message when focusing the menu button"
+ );
+
+ await closeExtensionsPanel(win);
+
+ info("verify message when hovering the action button");
+ await openExtensionsPanel(win);
+
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_DEFAULT,
+ "expected selected message in the deck to be the default message"
+ );
+
+ // 1. Hover the action button of the first extension in the panel.
+ let hovered = BrowserTestUtils.waitForEvent(
+ actionButton,
+ "mouseover"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ actionButton,
+ { type: "mouseover" },
+ win
+ );
+ await hovered;
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_HOVER,
+ "expected selected message in the deck to be the hover message"
+ );
+
+ // 2. Hover the menu button, causing the action button to no longer
+ // be hovered.
+ hovered = BrowserTestUtils.waitForEvent(menuButton, "mouseover");
+ EventUtils.synthesizeMouseAtCenter(
+ menuButton,
+ { type: "mouseover" },
+ win
+ );
+ await hovered;
+ is(
+ messageDeck.selectedIndex,
+ win.gUnifiedExtensions.MESSAGE_DECK_INDEX_MENU_HOVER,
+ "expected selected message in the deck to be the message when hovering the menu button"
+ );
+
+ await closeExtensionsPanel(win);
+ }
+ );
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Tests that if we pin a browser action button listed in the addons panel
+ * to the toolbar when that button would immediately overflow, that the
+ * button is put into the addons panel overflow list.
+ */
+add_task(async function test_pinning_to_toolbar_when_overflowed() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ let movedNode;
+ let extensionWidgetID;
+ let actionButton;
+ let menuButton;
+
+ await withWindowOverflowed(win, {
+ beforeOverflowed: async extensionIDs => {
+ // Before we overflow the toolbar, let's move the last item to the addons
+ // panel.
+ extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extensionIDs.at(-1)
+ );
+
+ movedNode =
+ CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node;
+
+ actionButton = movedNode.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button in the navbar"
+ );
+ ok(
+ !actionButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button in the navbar"
+ );
+
+ menuButton = movedNode.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button in the navbar"
+ );
+ ok(
+ !menuButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button in the navbar"
+ );
+
+ CustomizableUI.addWidgetToArea(
+ extensionWidgetID,
+ CustomizableUI.AREA_ADDONS
+ );
+
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+ },
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+
+ // Now that the window is overflowed, let's move the widget in the addons
+ // panel back to the navbar. This should cause the widget to overflow back
+ // into the addons panel.
+ CustomizableUI.addWidgetToArea(
+ extensionWidgetID,
+ CustomizableUI.AREA_NAVBAR
+ );
+ await TestUtils.waitForCondition(() => {
+ return movedNode.hasAttribute("overflowedItem");
+ });
+ Assert.equal(
+ movedNode.parentElement,
+ unifiedExtensionList,
+ "Should have overflowed the extension button to the right list."
+ );
+
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * This test verifies that, when an extension placed in the toolbar is
+ * overflowed into the addons panel and context-clicked, it shows the "Pin to
+ * Toolbar" item as checked, and that unchecking this menu item inserts the
+ * extension into the dedicated addons area of the panel, and that the item
+ * then does not underflow.
+ */
+add_task(async function test_unpin_overflowed_widget() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let extensionID;
+
+ await withWindowOverflowed(win, {
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ const firstExtensionWidget = unifiedExtensionList.children[0];
+ Assert.ok(firstExtensionWidget, "expected an extension widget");
+ extensionID = firstExtensionWidget.dataset.extensionid;
+
+ let movedNode = CustomizableUI.getWidget(
+ firstExtensionWidget.id
+ ).forWindow(win).node;
+ Assert.equal(
+ movedNode.getAttribute("cui-areatype"),
+ "toolbar",
+ "expected extension widget to be in the toolbar"
+ );
+ Assert.ok(
+ movedNode.hasAttribute("overflowedItem"),
+ "expected extension widget to be overflowed"
+ );
+ let actionButton = movedNode.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ let menuButton = movedNode.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+
+ // Open the panel, then the context menu of the extension widget, verify
+ // the 'Pin to Toolbar' menu item, then click on this menu item to
+ // uncheck it (i.e. unpin the extension).
+ await openExtensionsPanel(win);
+ const contextMenu = await openUnifiedExtensionsContextMenu(
+ extensionID,
+ win
+ );
+ Assert.ok(contextMenu, "expected a context menu");
+
+ const pinToToolbar = contextMenu.querySelector(
+ ".unified-extensions-context-menu-pin-to-toolbar"
+ );
+ Assert.ok(pinToToolbar, "expected a 'Pin to Toolbar' menu item");
+ Assert.ok(
+ !pinToToolbar.hidden,
+ "expected 'Pin to Toolbar' to be visible"
+ );
+ Assert.equal(
+ pinToToolbar.getAttribute("checked"),
+ "true",
+ "expected 'Pin to Toolbar' to be checked"
+ );
+
+ // Uncheck "Pin to Toolbar" menu item. Clicking a menu item in the
+ // context menu closes the unified extensions panel automatically.
+ const hidden = BrowserTestUtils.waitForEvent(
+ win.gUnifiedExtensions.panel,
+ "popuphidden",
+ true
+ );
+ contextMenu.activateItem(pinToToolbar);
+ await hidden;
+
+ // We expect the widget to no longer be overflowed.
+ await TestUtils.waitForCondition(() => {
+ return !movedNode.hasAttribute("overflowedItem");
+ });
+
+ Assert.equal(
+ movedNode.parentElement.id,
+ CustomizableUI.AREA_ADDONS,
+ "expected extension widget to have been unpinned and placed in the addons area"
+ );
+ Assert.equal(
+ movedNode.getAttribute("cui-areatype"),
+ "panel",
+ "expected extension widget to be in the unified extensions panel"
+ );
+ },
+ afterUnderflowed: async () => {
+ await openExtensionsPanel(win);
+
+ const item = getUnifiedExtensionsItem(extensionID, win);
+ Assert.ok(
+ item,
+ "expected extension widget to be listed in the unified extensions panel"
+ );
+ let actionButton = item.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ let menuButton = item.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+
+ await closeExtensionsPanel(win);
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_overflow_with_a_second_window() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ // Open a second window that will stay maximized. We want to be sure that
+ // overflowing a widget in one window isn't going to affect the other window
+ // since we have an instance (of a CUI widget) per window.
+ let secondWin = await BrowserTestUtils.openNewBrowserWindow();
+ await ensureMaximizedWindow(secondWin);
+ await BrowserTestUtils.openNewForegroundTab(
+ secondWin.gBrowser,
+ "https://example.com/"
+ );
+
+ // Make sure the first window is the active window.
+ let windowActivePromise = new Promise(resolve => {
+ if (Services.focus.activeWindow == win) {
+ resolve();
+ } else {
+ win.addEventListener(
+ "activate",
+ () => {
+ resolve();
+ },
+ { once: true }
+ );
+ }
+ });
+ win.focus();
+ await windowActivePromise;
+
+ let extensionWidgetID;
+ let aNode;
+ let aNodeInSecondWindow;
+
+ await withWindowOverflowed(win, {
+ beforeOverflowed: async extensionIDs => {
+ extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extensionIDs.at(-1)
+ );
+
+ // This is the DOM node for the current window that is overflowed.
+ aNode = CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node;
+ Assert.ok(
+ !aNode.hasAttribute("overflowedItem"),
+ "expected extension widget to NOT be overflowed"
+ );
+
+ let actionButton = aNode.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button"
+ );
+ ok(
+ !actionButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button"
+ );
+
+ let menuButton = aNode.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button"
+ );
+ ok(
+ !menuButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button"
+ );
+
+ // This is the DOM node of the same CUI widget but in the maximized
+ // window opened before.
+ aNodeInSecondWindow =
+ CustomizableUI.getWidget(extensionWidgetID).forWindow(secondWin).node;
+
+ let actionButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button in the second window"
+ );
+ ok(
+ !actionButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button in the second window"
+ );
+
+ let menuButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button in the second window"
+ );
+ ok(
+ !menuButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button in the second window"
+ );
+ },
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ // The DOM node should have been overflowed.
+ Assert.ok(
+ aNode.hasAttribute("overflowedItem"),
+ "expected extension widget to be overflowed"
+ );
+ Assert.equal(
+ aNode.getAttribute("widget-id"),
+ extensionWidgetID,
+ "expected the CUI widget ID to be set on the DOM node"
+ );
+
+ // When the node is overflowed, we swap the CSS class on the action
+ // and menu buttons since the node is now placed in the extensions panel.
+ let actionButton = aNode.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the action button"
+ );
+ ok(
+ !actionButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the action button"
+ );
+ let menuButton = aNode.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("subviewbutton"),
+ "expected the .subviewbutton CSS class on the menu button"
+ );
+ ok(
+ !menuButton.classList.contains("toolbarbutton-1"),
+ "expected no .toolbarbutton-1 CSS class on the menu button"
+ );
+
+ // The DOM node in the other window should not have been overflowed.
+ Assert.ok(
+ !aNodeInSecondWindow.hasAttribute("overflowedItem"),
+ "expected extension widget to NOT be overflowed in the other window"
+ );
+ Assert.equal(
+ aNodeInSecondWindow.getAttribute("widget-id"),
+ extensionWidgetID,
+ "expected the CUI widget ID to be set on the DOM node"
+ );
+
+ // We expect no CSS class changes for the node in the other window.
+ let actionButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button in the second window"
+ );
+ ok(
+ !actionButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button in the second window"
+ );
+ let menuButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button in the second window"
+ );
+ ok(
+ !menuButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button in the second window"
+ );
+ },
+ afterUnderflowed: async () => {
+ // After underflow, we expect the CSS class on the action and menu
+ // buttons of the DOM node of the current window to be updated.
+ let actionButton = aNode.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button in the panel"
+ );
+ ok(
+ !actionButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button in the panel"
+ );
+ let menuButton = aNode.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButton.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button in the panel"
+ );
+ ok(
+ !menuButton.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button in the panel"
+ );
+
+ // The DOM node of the other window should not be changed.
+ let actionButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-action-button"
+ );
+ ok(
+ actionButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the action button in the second window"
+ );
+ ok(
+ !actionButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the action button in the second window"
+ );
+ let menuButtonInSecondWindow = aNodeInSecondWindow.querySelector(
+ ".unified-extensions-item-menu-button"
+ );
+ ok(
+ menuButtonInSecondWindow.classList.contains("toolbarbutton-1"),
+ "expected .toolbarbutton-1 CSS class on the menu button in the second window"
+ );
+ ok(
+ !menuButtonInSecondWindow.classList.contains("subviewbutton"),
+ "expected no .subviewbutton CSS class on the menu button in the second window"
+ );
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+ await BrowserTestUtils.closeWindow(secondWin);
+});
+
+add_task(async function test_overflow_with_extension_in_collapsed_area() {
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ const bookmarksToolbar = win.document.getElementById(
+ CustomizableUI.AREA_BOOKMARKS
+ );
+
+ let movedNode;
+ let extensionWidgetID;
+ let extensionWidgetPosition;
+
+ await withWindowOverflowed(win, {
+ beforeOverflowed: async extensionIDs => {
+ // Before we overflow the toolbar, let's move the last item to the
+ // (visible) bookmarks toolbar.
+ extensionWidgetID = AppUiTestInternals.getBrowserActionWidgetId(
+ extensionIDs.at(-1)
+ );
+
+ movedNode =
+ CustomizableUI.getWidget(extensionWidgetID).forWindow(win).node;
+
+ // Ensure that the toolbar is currently visible.
+ await promiseSetToolbarVisibility(bookmarksToolbar, true);
+
+ // Move an extension to the bookmarks toolbar.
+ CustomizableUI.addWidgetToArea(
+ extensionWidgetID,
+ CustomizableUI.AREA_BOOKMARKS
+ );
+
+ Assert.equal(
+ movedNode.parentElement.id,
+ CustomizableUI.AREA_BOOKMARKS,
+ "expected extension widget to be in the bookmarks toolbar"
+ );
+ Assert.ok(
+ !movedNode.hasAttribute("artificallyOverflowed"),
+ "expected node to not have any artificallyOverflowed prop"
+ );
+
+ extensionWidgetPosition =
+ CustomizableUI.getPlacementOfWidget(extensionWidgetID).position;
+
+ // At this point we have an extension in the bookmarks toolbar, and this
+ // toolbar is visible. We are going to resize the window (width) AND
+ // collapse the toolbar to verify that the extension placed in the
+ // bookmarks toolbar is overflowed in the panel without any side effects.
+ },
+ whenOverflowed: async () => {
+ // Ensure that the toolbar is currently collapsed.
+ await promiseSetToolbarVisibility(bookmarksToolbar, false);
+
+ Assert.equal(
+ movedNode.parentElement.id,
+ OVERFLOWED_EXTENSIONS_LIST_ID,
+ "expected extension widget to be in the extensions panel"
+ );
+ Assert.ok(
+ movedNode.getAttribute("artificallyOverflowed"),
+ "expected node to be artifically overflowed"
+ );
+
+ // At this point the extension is in the panel because it was overflowed
+ // after the bookmarks toolbar has been collapsed. The window is also
+ // narrow, but we are going to restore the initial window size. Since the
+ // visibility of the bookmarks toolbar hasn't changed, the extension
+ // should still be in the panel.
+ },
+ afterUnderflowed: async () => {
+ Assert.equal(
+ movedNode.parentElement.id,
+ OVERFLOWED_EXTENSIONS_LIST_ID,
+ "expected extension widget to still be in the extensions panel"
+ );
+ Assert.ok(
+ movedNode.getAttribute("artificallyOverflowed"),
+ "expected node to still be artifically overflowed"
+ );
+
+ // Ensure that the toolbar is visible again, which should move the
+ // extension back to where it was initially.
+ await promiseSetToolbarVisibility(bookmarksToolbar, true);
+
+ Assert.equal(
+ movedNode.parentElement.id,
+ CustomizableUI.AREA_BOOKMARKS,
+ "expected extension widget to be in the bookmarks toolbar"
+ );
+ Assert.ok(
+ !movedNode.hasAttribute("artificallyOverflowed"),
+ "expected node to not have any artificallyOverflowed prop"
+ );
+ Assert.equal(
+ CustomizableUI.getPlacementOfWidget(extensionWidgetID).position,
+ extensionWidgetPosition,
+ "expected the extension to be back at the same position in the bookmarks toolbar"
+ );
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_overflowed_extension_cannot_be_moved() {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let extensionID;
+
+ await withWindowOverflowed(win, {
+ whenOverflowed: async (defaultList, unifiedExtensionList, extensionIDs) => {
+ const secondExtensionWidget = unifiedExtensionList.children[1];
+ Assert.ok(secondExtensionWidget, "expected an extension widget");
+ extensionID = secondExtensionWidget.dataset.extensionid;
+
+ await openExtensionsPanel(win);
+ const contextMenu = await openUnifiedExtensionsContextMenu(
+ extensionID,
+ win
+ );
+ Assert.ok(contextMenu, "expected a context menu");
+
+ const moveUp = contextMenu.querySelector(
+ ".unified-extensions-context-menu-move-widget-up"
+ );
+ Assert.ok(moveUp, "expected 'move up' item in the context menu");
+ Assert.ok(moveUp.hidden, "expected 'move up' item to be hidden");
+
+ const moveDown = contextMenu.querySelector(
+ ".unified-extensions-context-menu-move-widget-down"
+ );
+ Assert.ok(moveDown, "expected 'move down' item in the context menu");
+ Assert.ok(moveDown.hidden, "expected 'move down' item to be hidden");
+
+ await closeChromeContextMenu(contextMenu.id, null, win);
+ await closeExtensionsPanel(win);
+ },
+ });
+
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/extensions/test/browser/context.html b/browser/components/extensions/test/browser/context.html
new file mode 100644
index 0000000000..cd1a3db904
--- /dev/null
+++ b/browser/components/extensions/test/browser/context.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+ just some text 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
+
+
+
Sed ut perspiciatis unde omnis iste natus error sit
+ voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque
+ ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta
+ sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut
+ odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem
+ sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit
+ amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora
+ incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad
+ minima veniam, quis nostrum exercitationem ullam corporis suscipit
+ laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum
+ iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae
+ consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
+
+
+
+
diff --git a/browser/components/extensions/test/browser/context_frame.html b/browser/components/extensions/test/browser/context_frame.html
new file mode 100644
index 0000000000..39ed37674f
--- /dev/null
+++ b/browser/components/extensions/test/browser/context_frame.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+ Just some text
+
+
diff --git a/browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html b/browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html
new file mode 100644
index 0000000000..0e9b54b523
--- /dev/null
+++ b/browser/components/extensions/test/browser/context_tabs_onUpdated_iframe.html
@@ -0,0 +1,19 @@
+
+
+
+
+ `;
+ // We always want the iframe to have a different host from the top-level document.
+ const iframeHost =
+ request.host === "example.com" ? "example.org" : "example.com";
+ const iframeOrigin = `${request.scheme}://${iframeHost}`;
+ const iframeUrl = `${iframeOrigin}/document-builder.sjs?html=${encodeURI(
+ IFRAME_HTML
+ )}`;
+
+ const HTML = `
+
+
+
+
+ test
+
+
+
+
Top-level
+
${userAgentHeader ?? "no user-agent header"}
+
+
+ `;
+
+ response.write(HTML);
+}
+
+function handleInjectedScriptTestRequest(request, response, params) {
+ response.setHeader("Content-Type", "text/html; charset=UTF-8", false);
+
+ let content = "";
+ const frames = parseInt(params.get("frames"), 10);
+ if (frames > 0) {
+ // Output an iframe in seamless mode, so that there is an higher chance that in case
+ // of test failures we get a screenshot where the nested iframes are all visible.
+ content = ``;
+ }
+
+ response.write(`
+
+
+
+
+
+
+
IFRAME ${frames}
+
injected script NOT executed
+
+ ${content}
+
+
+ `);
+}
diff --git a/browser/components/extensions/test/browser/file_language_fr_en.html b/browser/components/extensions/test/browser/file_language_fr_en.html
new file mode 100644
index 0000000000..5e3c7b3b08
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_language_fr_en.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ France is the largest country in Western Europe and the third-largest in Europe as a whole.
+ A accès aux chiens et aux frontaux qui lui ont été il peut consulter et modifier ses collections et exporter
+ Cet article concerne le pays européen aujourd’hui appelé République française. Pour d’autres usages du nom France,
+ Pour une aide rapide et effective, veuiller trouver votre aide dans le menu ci-dessus.
+ Motoring events began soon after the construction of the first successful gasoline-fueled automobiles. The quick brown fox jumps over the lazy dog.
+
+
diff --git a/browser/components/extensions/test/browser/file_language_ja.html b/browser/components/extensions/test/browser/file_language_ja.html
new file mode 100644
index 0000000000..ed07ba70e5
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_language_ja.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+ このペ ジでは アカウントに指定された予算の履歴を一覧にしています それぞれの項目には 予算額と特定期間のステ タスが表示されます 現在または今後の予算を設定するには
+
+
diff --git a/browser/components/extensions/test/browser/file_language_tlh.html b/browser/components/extensions/test/browser/file_language_tlh.html
new file mode 100644
index 0000000000..dd7da7bdbf
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_language_tlh.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+ tlhIngan maH!
+ Hab SoSlI' Quch!
+ Heghlu'meH QaQ jajvam
+
+
diff --git a/browser/components/extensions/test/browser/file_popup_api_injection_a.html b/browser/components/extensions/test/browser/file_popup_api_injection_a.html
new file mode 100644
index 0000000000..750ff1db37
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_popup_api_injection_a.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/browser/components/extensions/test/browser/file_popup_api_injection_b.html b/browser/components/extensions/test/browser/file_popup_api_injection_b.html
new file mode 100644
index 0000000000..b8c287e55c
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_popup_api_injection_b.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/browser/components/extensions/test/browser/file_slowed_document.sjs b/browser/components/extensions/test/browser/file_slowed_document.sjs
new file mode 100644
index 0000000000..8c42fcc966
--- /dev/null
+++ b/browser/components/extensions/test/browser/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 two seconds before completing the request.
+
+let 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(`
+
+
+
+
+
+
+ `);
+
+ // 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(``);
+ }
+ response.write(``);
+ response.finish();
+ },
+ DELAY,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
diff --git a/browser/components/extensions/test/browser/file_title.html b/browser/components/extensions/test/browser/file_title.html
new file mode 100644
index 0000000000..2a5d0bca30
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_title.html
@@ -0,0 +1,9 @@
+
+
+Different title test page
+
+
+
+
A page with a different title
+
+
diff --git a/browser/components/extensions/test/browser/file_with_example_com_frame.html b/browser/components/extensions/test/browser/file_with_example_com_frame.html
new file mode 100644
index 0000000000..a4263b3315
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_with_example_com_frame.html
@@ -0,0 +1,5 @@
+
+
+
+Load an iframe from example.com
+
diff --git a/browser/components/extensions/test/browser/file_with_xorigin_frame.html b/browser/components/extensions/test/browser/file_with_xorigin_frame.html
new file mode 100644
index 0000000000..cee430a387
--- /dev/null
+++ b/browser/components/extensions/test/browser/file_with_xorigin_frame.html
@@ -0,0 +1,5 @@
+
+
+
+Load a cross-origin iframe from example.net
+
diff --git a/browser/components/extensions/test/browser/head.js b/browser/components/extensions/test/browser/head.js
new file mode 100644
index 0000000000..786f183011
--- /dev/null
+++ b/browser/components/extensions/test/browser/head.js
@@ -0,0 +1,1054 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* exported CustomizableUI makeWidgetId focusWindow forceGC
+ * getBrowserActionWidget assertPersistentListeners
+ * clickBrowserAction clickPageAction clickPageActionInPanel
+ * triggerPageActionWithKeyboard triggerPageActionWithKeyboardInPanel
+ * triggerBrowserActionWithKeyboard
+ * getBrowserActionPopup getPageActionPopup getPageActionButton
+ * openBrowserActionPanel
+ * closeBrowserAction closePageAction
+ * promisePopupShown promisePopupHidden promisePopupNotificationShown
+ * toggleBookmarksToolbar
+ * openContextMenu closeContextMenu promiseContextMenuClosed
+ * openContextMenuInSidebar openContextMenuInPopup
+ * openExtensionContextMenu closeExtensionContextMenu
+ * openActionContextMenu openSubmenu closeActionContextMenu
+ * openTabContextMenu closeTabContextMenu
+ * openToolsMenu closeToolsMenu
+ * imageBuffer imageBufferFromDataURI
+ * getInlineOptionsBrowser
+ * getListStyleImage getPanelForNode
+ * awaitExtensionPanel awaitPopupResize
+ * promiseContentDimensions alterContent
+ * promisePrefChangeObserved openContextMenuInFrame
+ * promiseAnimationFrame getCustomizableUIPanelID
+ * awaitEvent BrowserWindowIterator
+ * navigateTab historyPushState promiseWindowRestored
+ * getIncognitoWindow startIncognitoMonitorExtension
+ * loadTestSubscript awaitBrowserLoaded backgroundColorSetOnRoot
+ * getScreenAt roundCssPixcel getCssAvailRect isRectContained
+ */
+
+// There are shutdown issues for which multiple rejections are left uncaught.
+// This bug should be fixed, but for the moment all tests in this directory
+// allow various classes of promise rejections.
+//
+// NOTE: Allowing rejections on an entire directory should be avoided.
+// Normally you should use "expectUncaughtRejection" to flag individual
+// failures.
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Message manager disconnected/
+);
+PromiseTestUtils.allowMatchingRejectionsGlobally(/No matching message handler/);
+PromiseTestUtils.allowMatchingRejectionsGlobally(
+ /Receiving end does not exist/
+);
+
+const { AppUiTestDelegate, AppUiTestInternals } = ChromeUtils.importESModule(
+ "resource://testing-common/AppUiTestDelegate.sys.mjs"
+);
+
+const { Preferences } = ChromeUtils.importESModule(
+ "resource://gre/modules/Preferences.sys.mjs"
+);
+const { ClientEnvironmentBase } = ChromeUtils.importESModule(
+ "resource://gre/modules/components-utils/ClientEnvironment.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ Management: "resource://gre/modules/Extension.sys.mjs",
+});
+
+var { makeWidgetId, promisePopupShown, getPanelForNode, awaitBrowserLoaded } =
+ AppUiTestInternals;
+
+// The extension tests can run a lot slower under ASAN.
+if (AppConstants.ASAN) {
+ requestLongerTimeout(5);
+}
+
+function loadTestSubscript(filePath) {
+ Services.scriptloader.loadSubScript(new URL(filePath, gTestPath).href, this);
+}
+
+// Ensure when we turn off topsites in the next few lines,
+// we don't hit any remote endpoints.
+Services.prefs
+ .getDefaultBranch("browser.newtabpage.activity-stream.")
+ .setStringPref("discoverystream.endpointSpocsClear", "");
+// Leaving Top Sites enabled during these tests would create site screenshots
+// and update pinned Top Sites unnecessarily.
+Services.prefs
+ .getDefaultBranch("browser.newtabpage.activity-stream.")
+ .setBoolPref("feeds.topsites", false);
+Services.prefs
+ .getDefaultBranch("browser.newtabpage.activity-stream.")
+ .setBoolPref("feeds.system.topsites", false);
+
+{
+ // Touch the recipeParentPromise lazy getter so we don't get
+ // `this._recipeManager is undefined` errors during tests.
+ const { LoginManagerParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/LoginManagerParent.sys.mjs"
+ );
+ void LoginManagerParent.recipeParentPromise;
+}
+
+// Persistent Listener test functionality
+const { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
+
+// Bug 1239884: Our tests occasionally hit a long GC pause at unpredictable
+// times in debug builds, which results in intermittent timeouts. Until we have
+// a better solution, we force a GC after certain strategic tests, which tend to
+// accumulate a high number of unreaped windows.
+function forceGC() {
+ if (AppConstants.DEBUG) {
+ Cu.forceGC();
+ }
+}
+
+var focusWindow = async function focusWindow(win) {
+ if (Services.focus.activeWindow == win) {
+ return;
+ }
+
+ let promise = new Promise(resolve => {
+ win.addEventListener(
+ "focus",
+ function () {
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+ });
+
+ win.focus();
+ await promise;
+};
+
+function imageBufferFromDataURI(encodedImageData) {
+ let decodedImageData = atob(encodedImageData);
+ return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer;
+}
+
+let img =
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==";
+var imageBuffer = imageBufferFromDataURI(img);
+
+function getInlineOptionsBrowser(aboutAddonsBrowser) {
+ let { contentDocument } = aboutAddonsBrowser;
+ return contentDocument.getElementById("addon-inline-options");
+}
+
+function getListStyleImage(button) {
+ // Ensure popups are initialized so that the elements are rendered and
+ // getComputedStyle works.
+ for (
+ let popup = button.closest("panel,menupopup");
+ popup;
+ popup = popup.parentElement?.closest("panel,menupopup")
+ ) {
+ popup.ensureInitialized();
+ }
+
+ let style = button.ownerGlobal.getComputedStyle(button);
+
+ let match = /^url\("(.*)"\)$/.exec(style.listStyleImage);
+
+ return match && match[1];
+}
+
+function promiseAnimationFrame(win = window) {
+ return AppUiTestInternals.promiseAnimationFrame(win);
+}
+
+function promisePopupHidden(popup) {
+ return new Promise(resolve => {
+ let onPopupHidden = event => {
+ popup.removeEventListener("popuphidden", onPopupHidden);
+ resolve();
+ };
+ popup.addEventListener("popuphidden", onPopupHidden);
+ });
+}
+
+/**
+ * Wait for the given PopupNotification to display
+ *
+ * @param {string} name
+ * The name of the notification to wait for.
+ * @param {Window} [win]
+ * The chrome window in which to wait for the notification.
+ *
+ * @returns {Promise}
+ * Resolves with the notification window.
+ */
+function promisePopupNotificationShown(name, win = window) {
+ return new Promise(resolve => {
+ function popupshown() {
+ let notification = win.PopupNotifications.getNotification(name);
+ if (!notification) {
+ return;
+ }
+
+ ok(notification, `${name} notification shown`);
+ ok(win.PopupNotifications.isPanelOpen, "notification panel open");
+
+ win.PopupNotifications.panel.removeEventListener(
+ "popupshown",
+ popupshown
+ );
+ resolve(win.PopupNotifications.panel.firstElementChild);
+ }
+
+ win.PopupNotifications.panel.addEventListener("popupshown", popupshown);
+ });
+}
+
+function promisePossiblyInaccurateContentDimensions(browser) {
+ return SpecialPowers.spawn(browser, [], async function () {
+ function copyProps(obj, props) {
+ let res = {};
+ for (let prop of props) {
+ res[prop] = obj[prop];
+ }
+ return res;
+ }
+
+ return {
+ window: copyProps(content, [
+ "innerWidth",
+ "innerHeight",
+ "outerWidth",
+ "outerHeight",
+ "scrollX",
+ "scrollY",
+ "scrollMaxX",
+ "scrollMaxY",
+ ]),
+ body: copyProps(content.document.body, [
+ "clientWidth",
+ "clientHeight",
+ "scrollWidth",
+ "scrollHeight",
+ ]),
+ root: copyProps(content.document.documentElement, [
+ "clientWidth",
+ "clientHeight",
+ "scrollWidth",
+ "scrollHeight",
+ ]),
+ isStandards: content.document.compatMode !== "BackCompat",
+ };
+ });
+}
+
+function delay(ms = 0) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Retrieve the content dimensions (and wait until the content gets to the.
+ * size of the browser element they are loaded into, optionally tollerating
+ * size differences to prevent intermittent failures).
+ *
+ * @param {BrowserElement} browser
+ * The browser element where the content has been loaded.
+ * @param {number} [tolleratedWidthSizeDiff]
+ * width size difference to tollerate in pixels (defaults to 1).
+ *
+ * @returns {Promise