summaryrefslogtreecommitdiffstats
path: root/browser/base/content/test/pageActions
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/base/content/test/pageActions/.eslintrc.js5
-rw-r--r--browser/base/content/test/pageActions/browser.ini22
-rw-r--r--browser/base/content/test/pageActions/browser_PageActions_removeExtension.js320
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu.js1241
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu_add_search_engine.js672
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu_clipboard.js40
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu_share_mac.js172
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu_share_win.html2
-rw-r--r--browser/base/content/test/pageActions/browser_page_action_menu_share_win.js53
-rw-r--r--browser/base/content/test/pageActions/head.js147
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml7
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_1.xml7
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_2.xml7
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_invalid.html8
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_many.html10
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_one.html8
-rw-r--r--browser/base/content/test/pageActions/page_action_menu_add_search_engine_same_names.html9
17 files changed, 2730 insertions, 0 deletions
diff --git a/browser/base/content/test/pageActions/.eslintrc.js b/browser/base/content/test/pageActions/.eslintrc.js
new file mode 100644
index 0000000000..1779fd7f1c
--- /dev/null
+++ b/browser/base/content/test/pageActions/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/browser-test"],
+};
diff --git a/browser/base/content/test/pageActions/browser.ini b/browser/base/content/test/pageActions/browser.ini
new file mode 100644
index 0000000000..627757c0ed
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+support-files =
+ head.js
+
+[browser_PageActions_removeExtension.js]
+[browser_page_action_menu_add_search_engine.js]
+support-files =
+ page_action_menu_add_search_engine_invalid.html
+ page_action_menu_add_search_engine_one.html
+ page_action_menu_add_search_engine_many.html
+ page_action_menu_add_search_engine_same_names.html
+ page_action_menu_add_search_engine_0.xml
+ page_action_menu_add_search_engine_1.xml
+ page_action_menu_add_search_engine_2.xml
+[browser_page_action_menu_clipboard.js]
+[browser_page_action_menu_share_mac.js]
+skip-if = os != "mac" # Mac only feature
+[browser_page_action_menu_share_win.js]
+support-files =
+ browser_page_action_menu_share_win.html
+skip-if = os != "win" # Windows only feature
+[browser_page_action_menu.js]
diff --git a/browser/base/content/test/pageActions/browser_PageActions_removeExtension.js b/browser/base/content/test/pageActions/browser_PageActions_removeExtension.js
new file mode 100644
index 0000000000..8efc9b077c
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_PageActions_removeExtension.js
@@ -0,0 +1,320 @@
+"use strict";
+
+const { EnterprisePolicyTesting } = ChromeUtils.import(
+ "resource://testing-common/EnterprisePolicyTesting.jsm"
+);
+
+const { ExtensionCommon } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionCommon.jsm"
+);
+
+const { TelemetryTestUtils } = ChromeUtils.import(
+ "resource://testing-common/TelemetryTestUtils.jsm"
+);
+
+const TELEMETRY_EVENTS_FILTERS = {
+ category: "addonsManager",
+ method: "action",
+};
+
+// Initialization. Must run first.
+add_task(async function init() {
+ // The page action urlbar button, and therefore the panel, is only shown when
+ // the current tab is actionable -- i.e., a normal web page. about:blank is
+ // not, so open a new tab first thing, and close it when this test is done.
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "http://example.com/",
+ });
+
+ // The prompt service is mocked later, so set it up to be restored.
+ let { prompt } = Services;
+
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ Services.prompt = prompt;
+ });
+});
+
+add_task(async function contextMenu_removeExtension_panel() {
+ Services.telemetry.clearEvents();
+
+ // We use an extension that shows a page action so that we can test the
+ // "remove extension" item in the context menu.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test contextMenu",
+ page_action: { show_matches: ["<all_urls>"] },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ let actionId = ExtensionCommon.makeWidgetId(extension.id);
+
+ // Open the panel and then open the context menu on the action's item.
+ await promisePageActionPanelOpen();
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(actionId);
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(panelButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ let removeExtensionItem = getRemoveExtensionItem();
+ Assert.ok(removeExtensionItem, "'Remove' item exists");
+ Assert.ok(!removeExtensionItem.hidden, "'Remove' item is visible");
+ Assert.ok(!removeExtensionItem.disabled, "'Remove' item is not disabled");
+
+ // Click the "remove extension" item, a prompt should be displayed and then
+ // the add-on should be uninstalled. We mock the prompt service to confirm
+ // the removal of the add-on.
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ let addonUninstalledPromise = promiseAddonUninstalled(extension.id);
+ mockPromptService();
+ EventUtils.synthesizeMouseAtCenter(removeExtensionItem, {});
+ await Promise.all([contextMenuPromise, addonUninstalledPromise]);
+
+ // Done, clean up.
+ await extension.unload();
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "pageAction",
+ value: "accepted",
+ extra: { addonId: extension.id, action: "uninstall" },
+ },
+ ],
+ TELEMETRY_EVENTS_FILTERS
+ );
+
+ // urlbar tests that run after this one can break if the mouse is left over
+ // the area where the urlbar popup appears, which seems to happen due to the
+ // above synthesized mouse events. Move it over the urlbar.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { type: "mousemove" });
+ gURLBar.focus();
+});
+
+add_task(async function contextMenu_removeExtension_urlbar() {
+ Services.telemetry.clearEvents();
+
+ // We use an extension that shows a page action so that we can test the
+ // "remove extension" item in the context menu.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test contextMenu",
+ page_action: { show_matches: ["<all_urls>"] },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ // The pageAction implementation enables the button at the next animation
+ // frame, so before we look for the button we should wait one animation frame
+ // as well.
+ await promiseAnimationFrame();
+
+ let actionId = ExtensionCommon.makeWidgetId(extension.id);
+
+ // Open the context menu on the action's urlbar button.
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(actionId);
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(urlbarButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ let removeExtensionItem = getRemoveExtensionItem();
+ Assert.ok(removeExtensionItem, "'Remove' item exists");
+ Assert.ok(!removeExtensionItem.hidden, "'Remove' item is visible");
+ Assert.ok(!removeExtensionItem.disabled, "'Remove' item is not disabled");
+
+ // Click the "remove extension" item, a prompt should be displayed and then
+ // the add-on should be uninstalled. We mock the prompt service to cancel the
+ // removal of the add-on.
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ let promptService = mockPromptService();
+ let promptCancelledPromise = new Promise(resolve => {
+ promptService.confirmEx = () => resolve();
+ });
+ EventUtils.synthesizeMouseAtCenter(removeExtensionItem, {});
+ await Promise.all([contextMenuPromise, promptCancelledPromise]);
+
+ // Done, clean up.
+ await extension.unload();
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "pageAction",
+ value: "cancelled",
+ extra: { addonId: extension.id, action: "uninstall" },
+ },
+ ],
+ TELEMETRY_EVENTS_FILTERS
+ );
+
+ // urlbar tests that run after this one can break if the mouse is left over
+ // the area where the urlbar popup appears, which seems to happen due to the
+ // above synthesized mouse events. Move it over the urlbar.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { type: "mousemove" });
+ gURLBar.focus();
+});
+
+add_task(async function contextMenu_removeExtension_disabled_in_panel() {
+ // We use an extension that shows a page action so that we can test the
+ // "remove extension" item in the context menu.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test contextMenu",
+ page_action: { show_matches: ["<all_urls>"] },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ // Add a policy to prevent the add-on from being uninstalled.
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ Extensions: {
+ Locked: [extension.id],
+ },
+ },
+ });
+
+ let actionId = ExtensionCommon.makeWidgetId(extension.id);
+
+ // Open the panel and then open the context menu on the action's item.
+ await promisePageActionPanelOpen();
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(actionId);
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(panelButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ let removeExtensionItem = getRemoveExtensionItem();
+ Assert.ok(removeExtensionItem, "'Remove' item exists");
+ Assert.ok(!removeExtensionItem.hidden, "'Remove' item is visible");
+ Assert.ok(removeExtensionItem.disabled, "'Remove' item is disabled");
+
+ // Press escape to hide the context menu.
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await contextMenuPromise;
+
+ // Done, clean up.
+ await extension.unload();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+
+ // urlbar tests that run after this one can break if the mouse is left over
+ // the area where the urlbar popup appears, which seems to happen due to the
+ // above synthesized mouse events. Move it over the urlbar.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { type: "mousemove" });
+ gURLBar.focus();
+});
+
+add_task(async function contextMenu_removeExtension_disabled_in_urlbar() {
+ // We use an extension that shows a page action so that we can test the
+ // "remove extension" item in the context menu.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ name: "Test contextMenu",
+ page_action: { show_matches: ["<all_urls>"] },
+ },
+
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+ // The pageAction implementation enables the button at the next animation
+ // frame, so before we look for the button we should wait one animation frame
+ // as well.
+ await promiseAnimationFrame();
+ // Add a policy to prevent the add-on from being uninstalled.
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ Extensions: {
+ Locked: [extension.id],
+ },
+ },
+ });
+
+ let actionId = ExtensionCommon.makeWidgetId(extension.id);
+
+ // Open the context menu on the action's urlbar button.
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(actionId);
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(urlbarButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ let removeExtensionItem = getRemoveExtensionItem();
+ Assert.ok(removeExtensionItem, "'Remove' item exists");
+ Assert.ok(!removeExtensionItem.hidden, "'Remove' item is visible");
+ Assert.ok(removeExtensionItem.disabled, "'Remove' item is disabled");
+
+ // Press escape to hide the context menu.
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await contextMenuPromise;
+
+ // Done, clean up.
+ await extension.unload();
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+
+ // urlbar tests that run after this one can break if the mouse is left over
+ // the area where the urlbar popup appears, which seems to happen due to the
+ // above synthesized mouse events. Move it over the urlbar.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { type: "mousemove" });
+ gURLBar.focus();
+});
+
+function promiseAddonUninstalled(addonId) {
+ return new Promise(resolve => {
+ let listener = {};
+ listener.onUninstalled = addon => {
+ if (addon.id == addonId) {
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ }
+ };
+ AddonManager.addAddonListener(listener);
+ });
+}
+
+function mockPromptService() {
+ let promptService = {
+ // The prompt returns 1 for cancelled and 0 for accepted.
+ _response: 0,
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+ confirmEx: () => promptService._response,
+ };
+
+ Services.prompt = promptService;
+
+ return promptService;
+}
+
+function getRemoveExtensionItem() {
+ return document.querySelector(
+ "#pageActionContextMenu > menuitem[label='Remove Extension']"
+ );
+}
+
+async function promiseAnimationFrame(win = window) {
+ await new Promise(resolve => win.requestAnimationFrame(resolve));
+
+ let { tm } = Services;
+ return new Promise(resolve => tm.dispatchToMainThread(resolve));
+}
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu.js b/browser/base/content/test/pageActions/browser_page_action_menu.js
new file mode 100644
index 0000000000..deb5dacee8
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu.js
@@ -0,0 +1,1241 @@
+"use strict";
+
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+/* global UIState */
+
+const lastModifiedFixture = 1507655615.87; // Approx Oct 10th 2017
+const mockTargets = [
+ {
+ id: "0",
+ name: "foo",
+ type: "phone",
+ clientRecord: {
+ id: "cli0",
+ serverLastModified: lastModifiedFixture,
+ type: "phone",
+ },
+ },
+ {
+ id: "1",
+ name: "bar",
+ type: "desktop",
+ clientRecord: {
+ id: "cli1",
+ serverLastModified: lastModifiedFixture,
+ type: "desktop",
+ },
+ },
+ {
+ id: "2",
+ name: "baz",
+ type: "phone",
+ clientRecord: {
+ id: "cli2",
+ serverLastModified: lastModifiedFixture,
+ type: "phone",
+ },
+ },
+ { id: "3", name: "no client record device", type: "phone" },
+];
+
+add_task(async function openPanel() {
+ if (AppConstants.platform == "macosx") {
+ // Ignore this test on Mac.
+ return;
+ }
+
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Should still open the panel when Ctrl key is pressed.
+ await promisePageActionPanelOpen({ ctrlKey: true });
+
+ // Done.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+ });
+});
+
+add_task(async function starButtonCtrlClick() {
+ // On macOS, ctrl-click shouldn't open the panel because this normally opens
+ // the context menu. This happens via the `contextmenu` event which is created
+ // by widget code, so our simulated clicks do not do so, so we can't test
+ // anything on macOS.
+ if (AppConstants.platform == "macosx") {
+ return;
+ }
+
+ // Open a unique page.
+ let url = "http://example.com/browser_page_action_star_button";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ StarUI._createPanelIfNeeded();
+ // The button ignores activation while the bookmarked status is being
+ // updated. So, wait for it to finish updating.
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status != BookmarkingUI.STATUS_UPDATING
+ );
+
+ const popup = document.getElementById("editBookmarkPanel");
+ const starButtonBox = document.getElementById("star-button-box");
+
+ let shownPromise = promisePanelShown(popup);
+ EventUtils.synthesizeMouseAtCenter(starButtonBox, { ctrlKey: true });
+ await shownPromise;
+ ok(true, "Panel shown after button pressed");
+
+ let hiddenPromise = promisePanelHidden(popup);
+ document.getElementById("editBookmarkPanelRemoveButton").click();
+ await hiddenPromise;
+ });
+});
+
+add_task(async function bookmark() {
+ // Open a unique page.
+ let url = "http://example.com/browser_page_action_menu";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // The bookmark button should read "Bookmark This Page" and not be starred.
+ let bookmarkButton = document.getElementById("pageAction-panel-bookmark");
+ Assert.equal(bookmarkButton.label, "Bookmark This Page");
+ Assert.ok(!bookmarkButton.hasAttribute("starred"));
+
+ // Click the button.
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {});
+ await hiddenPromise;
+
+ // Make sure the edit-bookmark panel opens, then hide it.
+ await new Promise(resolve => {
+ if (StarUI.panel.state == "open") {
+ resolve();
+ return;
+ }
+ StarUI.panel.addEventListener("popupshown", resolve, { once: true });
+ });
+ Assert.equal(
+ BookmarkingUI.starBox.getAttribute("open"),
+ "true",
+ "Star has open attribute"
+ );
+ StarUI.panel.hidePopup();
+ Assert.ok(
+ !BookmarkingUI.starBox.hasAttribute("open"),
+ "Star no longer has open attribute"
+ );
+
+ // Open the panel again.
+ await promisePageActionPanelOpen();
+
+ // The bookmark button should now read "Edit This Bookmark" and be starred.
+ Assert.equal(bookmarkButton.label, "Edit This Bookmark");
+ Assert.ok(bookmarkButton.hasAttribute("starred"));
+ Assert.equal(bookmarkButton.getAttribute("starred"), "true");
+
+ // Click it again.
+ hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {});
+ await hiddenPromise;
+
+ // The edit-bookmark panel should open again.
+ await new Promise(resolve => {
+ if (StarUI.panel.state == "open") {
+ resolve();
+ return;
+ }
+ StarUI.panel.addEventListener("popupshown", resolve, { once: true });
+ });
+
+ let onItemRemovedPromise = PlacesTestUtils.waitForNotification(
+ "bookmark-removed",
+ events => events.some(event => event.url == url),
+ "places"
+ );
+
+ // Click the remove-bookmark button in the panel.
+ StarUI._element("editBookmarkPanelRemoveButton").click();
+
+ // Wait for the bookmark to be removed before continuing.
+ await onItemRemovedPromise;
+
+ // Open the panel again.
+ await promisePageActionPanelOpen();
+
+ // The bookmark button should read "Bookmark This Page" and not be starred.
+ Assert.equal(bookmarkButton.label, "Bookmark This Page");
+ Assert.ok(!bookmarkButton.hasAttribute("starred"));
+
+ // Done.
+ hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+ });
+});
+
+add_task(async function pinTabFromPanel() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel and click Pin Tab.
+ await promisePageActionPanelOpen();
+
+ let pinTabButton = document.getElementById("pageAction-panel-pinTab");
+ Assert.equal(pinTabButton.label, "Pin Tab");
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
+ await hiddenPromise;
+
+ Assert.ok(gBrowser.selectedTab.pinned, "Tab was pinned");
+
+ // Open the panel and click Unpin Tab.
+ await promisePageActionPanelOpen();
+ Assert.equal(pinTabButton.label, "Unpin Tab");
+
+ hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
+ await hiddenPromise;
+
+ Assert.ok(!gBrowser.selectedTab.pinned, "Tab was unpinned");
+ });
+});
+
+add_task(async function pinTabFromURLBar() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Add action to URL bar.
+ let action = PageActions._builtInActions.find(a => a.id == "pinTab");
+ action.pinnedToUrlbar = true;
+ registerCleanupFunction(() => (action.pinnedToUrlbar = false));
+
+ // Click the Pin Tab button.
+ let pinTabButton = document.getElementById("pageAction-urlbar-pinTab");
+ EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
+ await TestUtils.waitForCondition(
+ () => gBrowser.selectedTab.pinned,
+ "Tab was pinned"
+ );
+
+ // Click the Unpin Tab button
+ EventUtils.synthesizeMouseAtCenter(pinTabButton, {});
+ await TestUtils.waitForCondition(
+ () => !gBrowser.selectedTab.pinned,
+ "Tab was unpinned"
+ );
+ });
+});
+
+add_task(async function emailLink() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Replace the email-link entry point to check whether it's called.
+ let originalFn = MailIntegration.sendLinkForBrowser;
+ let fnCalled = false;
+ MailIntegration.sendLinkForBrowser = () => {
+ fnCalled = true;
+ };
+ registerCleanupFunction(() => {
+ MailIntegration.sendLinkForBrowser = originalFn;
+ });
+
+ // Open the panel and click Email Link.
+ await promisePageActionPanelOpen();
+ let emailLinkButton = document.getElementById("pageAction-panel-emailLink");
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(emailLinkButton, {});
+ await hiddenPromise;
+
+ Assert.ok(fnCalled);
+ });
+});
+
+add_task(async function copyURLFromPanel() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Add action to URL bar.
+ let action = PageActions._builtInActions.find(a => a.id == "copyURL");
+ action.pinnedToUrlbar = true;
+ registerCleanupFunction(() => (action.pinnedToUrlbar = false));
+
+ // Open the panel and click Copy URL.
+ await promisePageActionPanelOpen();
+ Assert.ok(true, "page action panel opened");
+
+ let copyURLButton = document.getElementById("pageAction-panel-copyURL");
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
+ await hiddenPromise;
+
+ let feedbackPanel = ConfirmationHint._panel;
+ let feedbackShownPromise = BrowserTestUtils.waitForEvent(
+ feedbackPanel,
+ "popupshown"
+ );
+ await feedbackShownPromise;
+ Assert.equal(
+ feedbackPanel.anchorNode.id,
+ "pageActionButton",
+ "Feedback menu should be anchored on the main Page Action button"
+ );
+ let feedbackHiddenPromise = promisePanelHidden(feedbackPanel);
+ await feedbackHiddenPromise;
+
+ action.pinnedToUrlbar = false;
+ });
+});
+
+add_task(async function copyURLFromURLBar() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Add action to URL bar.
+ let action = PageActions._builtInActions.find(a => a.id == "copyURL");
+ action.pinnedToUrlbar = true;
+ registerCleanupFunction(() => (action.pinnedToUrlbar = false));
+
+ let copyURLButton = document.getElementById("pageAction-urlbar-copyURL");
+ let panel = ConfirmationHint._panel;
+ let feedbackShownPromise = promisePanelShown(panel);
+ EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
+
+ await feedbackShownPromise;
+ Assert.equal(
+ panel.anchorNode.id,
+ "pageAction-urlbar-copyURL",
+ "Feedback menu should be anchored on the main URL bar button"
+ );
+ let feedbackHiddenPromise = promisePanelHidden(panel);
+ await feedbackHiddenPromise;
+
+ action.pinnedToUrlbar = false;
+ });
+});
+
+add_task(async function sendToDevice_nonSendable() {
+ // Open a tab that's not sendable but where the page action buttons still
+ // appear. about:about is convenient.
+ await BrowserTestUtils.withNewTab("about:about", async () => {
+ await promiseSyncReady();
+ // Open the panel. Send to Device should be disabled.
+ await promisePageActionPanelOpen();
+ Assert.equal(
+ BrowserPageActions.mainButtonNode.getAttribute("open"),
+ "true",
+ "Main button has 'open' attribute"
+ );
+ let panelButton = BrowserPageActions.panelButtonNodeForActionID(
+ "sendToDevice"
+ );
+ Assert.equal(
+ panelButton.disabled,
+ true,
+ "The panel button should be disabled"
+ );
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+ Assert.ok(
+ !BrowserPageActions.mainButtonNode.hasAttribute("open"),
+ "Main button no longer has 'open' attribute"
+ );
+ // The urlbar button shouldn't exist.
+ let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(
+ "sendToDevice"
+ );
+ Assert.equal(urlbarButton, null, "The urlbar button shouldn't exist");
+ });
+});
+
+add_task(async function sendToDevice_syncNotReady_other_states() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => null);
+ sandbox
+ .stub(UIState, "get")
+ .returns({ status: UIState.STATUS_NOT_VERIFIED });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ label: "Account Not Verified",
+ },
+ disabled: true,
+ },
+ null,
+ {
+ attrs: {
+ label: "Verify Your Account...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems);
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ cleanUp();
+ });
+});
+
+add_task(async function sendToDevice_syncNotReady_configured() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ const recentDeviceList = sandbox
+ .stub(fxAccounts.device, "recentDeviceList")
+ .get(() => null);
+ sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").callsFake(() => {
+ recentDeviceList.get(() =>
+ mockTargets.map(({ id, name, type }) => ({ id, name, type }))
+ );
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+ });
+
+ let onShowingSubview = BrowserPageActions.sendToDevice.onShowingSubview;
+ sandbox
+ .stub(BrowserPageActions.sendToDevice, "onShowingSubview")
+ .callsFake((...args) => {
+ this.numCall++ || (this.numCall = 1);
+ onShowingSubview.call(BrowserPageActions.sendToDevice, ...args);
+ testSendTabToDeviceMenu(this.numCall);
+ });
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ function testSendTabToDeviceMenu(numCall) {
+ if (numCall == 1) {
+ // "Syncing devices" should be shown.
+ checkSendToDeviceItems([
+ {
+ className: "pageAction-sendToDevice-notReady",
+ disabled: true,
+ },
+ ]);
+ } else if (numCall == 2) {
+ // The devices should be shown in the subview.
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ ];
+ for (let target of mockTargets) {
+ const attrs = {
+ clientId: target.id,
+ label: target.name,
+ clientType: target.type,
+ };
+ if (target.clientRecord && target.clientRecord.serverLastModified) {
+ attrs.tooltiptext = gSync.formatLastSyncDate(
+ new Date(target.clientRecord.serverLastModified * 1000)
+ );
+ }
+ expectedItems.push({
+ attrs,
+ });
+ }
+ expectedItems.push(null, {
+ attrs: {
+ label: "Send to All Devices",
+ },
+ });
+ expectedItems.push(null, {
+ attrs: {
+ label: "Manage Devices...",
+ },
+ });
+ checkSendToDeviceItems(expectedItems);
+ } else {
+ ok(false, "This should never happen");
+ }
+ }
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+ cleanUp();
+ });
+});
+
+add_task(async function sendToDevice_notSignedIn() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ label: "Not Signed In",
+ },
+ disabled: true,
+ },
+ null,
+ {
+ attrs: {
+ label: "Sign in to Firefox...",
+ },
+ },
+ {
+ attrs: {
+ label: "Learn About Sending Tabs...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems);
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+ });
+});
+
+add_task(async function sendToDevice_noDevices() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
+ sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ label: "No Devices Connected",
+ },
+ disabled: true,
+ },
+ null,
+ {
+ attrs: {
+ label: "Connect Another Device...",
+ },
+ },
+ {
+ attrs: {
+ label: "Learn About Sending Tabs...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems);
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ cleanUp();
+
+ await UIState.reset();
+ });
+});
+
+add_task(async function sendToDevice_devices() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(fxAccounts.device, "recentDeviceList")
+ .get(() => mockTargets.map(({ id, name, type }) => ({ id, name, type })));
+ sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+ sandbox
+ .stub(fxAccounts.commands.sendTab, "isDeviceCompatible")
+ .returns(true);
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
+ sandbox.spy(Weave.Service, "sync");
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ // The devices should be shown in the subview.
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ clientId: "1",
+ label: "bar",
+ clientType: "desktop",
+ },
+ },
+ {
+ attrs: {
+ clientId: "2",
+ label: "baz",
+ clientType: "phone",
+ },
+ },
+ {
+ attrs: {
+ clientId: "0",
+ label: "foo",
+ clientType: "phone",
+ },
+ },
+ {
+ attrs: {
+ clientId: "3",
+ label: "no client record device",
+ clientType: "phone",
+ },
+ },
+ null,
+ {
+ attrs: {
+ label: "Send to All Devices",
+ },
+ },
+ {
+ attrs: {
+ label: "Manage Devices...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems);
+
+ Assert.ok(Weave.Service.sync.notCalled);
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ cleanUp();
+ });
+});
+
+add_task(async function sendTabToDevice_syncEnabled() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
+ sandbox
+ .stub(UIState, "get")
+ .returns({ status: UIState.STATUS_SIGNED_IN, syncEnabled: true });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+ sandbox.spy(fxAccounts.device, "refreshDeviceList");
+ sandbox.spy(Weave.Service, "sync");
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ // Click Send to Device.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
+ let view = await viewPromise;
+ Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
+
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ label: "No Devices Connected",
+ },
+ disabled: true,
+ },
+ null,
+ {
+ attrs: {
+ label: "Connect Another Device...",
+ },
+ },
+ {
+ attrs: {
+ label: "Learn About Sending Tabs...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems);
+
+ Assert.ok(Weave.Service.sync.notCalled);
+ Assert.equal(fxAccounts.device.refreshDeviceList.callCount, 1);
+
+ // Done, hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ cleanUp();
+ });
+});
+
+add_task(async function sendToDevice_title() {
+ // Open two tabs that are sendable.
+ await BrowserTestUtils.withNewTab(
+ "http://example.com/a",
+ async otherBrowser => {
+ await BrowserTestUtils.withNewTab("http://example.com/b", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
+ sandbox
+ .stub(UIState, "get")
+ .returns({ status: UIState.STATUS_SIGNED_IN });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Open the panel. Only one tab is selected, so the action's title should
+ // be "Send Tab to Device".
+ await promisePageActionPanelOpen();
+ let sendToDeviceButton = document.getElementById(
+ "pageAction-panel-sendToDevice"
+ );
+ Assert.ok(!sendToDeviceButton.disabled);
+
+ Assert.equal(sendToDeviceButton.label, "Send Tab to Device");
+
+ // Hide the panel.
+ let hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ // Add the other tab to the selection.
+ gBrowser.addToMultiSelectedTabs(
+ gBrowser.getTabForBrowser(otherBrowser),
+ { isLastMultiSelectChange: true }
+ );
+
+ // Open the panel again. Now the action's title should be "Send 2 Tabs to
+ // Device".
+ await promisePageActionPanelOpen();
+ Assert.ok(!sendToDeviceButton.disabled);
+ Assert.equal(sendToDeviceButton.label, "Send 2 Tabs to Device");
+
+ // Hide the panel.
+ hiddenPromise = promisePageActionPanelHidden();
+ BrowserPageActions.panelNode.hidePopup();
+ await hiddenPromise;
+
+ cleanUp();
+
+ await UIState.reset();
+ });
+ }
+ );
+});
+
+add_task(async function sendToDevice_inUrlbar() {
+ // Open a tab that's sendable.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ await promiseSyncReady();
+ const sandbox = sinon.createSandbox();
+ sandbox
+ .stub(fxAccounts.device, "recentDeviceList")
+ .get(() => mockTargets.map(({ id, name, type }) => ({ id, name, type })));
+ sandbox.stub(UIState, "get").returns({ status: UIState.STATUS_SIGNED_IN });
+ sandbox.stub(gSync, "isSendableURI").returns(true);
+ sandbox
+ .stub(fxAccounts.commands.sendTab, "isDeviceCompatible")
+ .returns(true);
+ sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves(true);
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientByFxaDeviceId")
+ .callsFake(fxaDeviceId => {
+ let target = mockTargets.find(c => c.id == fxaDeviceId);
+ return target ? target.clientRecord : null;
+ });
+ sandbox
+ .stub(Weave.Service.clientsEngine, "getClientType")
+ .callsFake(
+ id =>
+ mockTargets.find(c => c.clientRecord && c.clientRecord.id == id)
+ .clientRecord.type
+ );
+ sandbox.stub(gSync, "sendTabToDevice").resolves(true);
+
+ let cleanUp = () => {
+ sandbox.restore();
+ };
+ registerCleanupFunction(cleanUp);
+
+ // Add Send to Device to the urlbar.
+ let action = PageActions.actionForID("sendToDevice");
+ action.pinnedToUrlbar = true;
+
+ // Click it to open its panel.
+ let urlbarButton = document.getElementById(
+ BrowserPageActions.urlbarButtonNodeIDForActionID(action.id)
+ );
+ Assert.notEqual(urlbarButton, null, "The urlbar button should exist");
+ Assert.ok(
+ !urlbarButton.disabled,
+ "The urlbar button should not be disabled"
+ );
+ EventUtils.synthesizeMouseAtCenter(urlbarButton, {});
+ // The panel element for _activatedActionPanelID is created synchronously
+ // only after the associated button has been clicked.
+ await promisePanelShown(BrowserPageActions._activatedActionPanelID);
+ Assert.equal(
+ urlbarButton.getAttribute("open"),
+ "true",
+ "Button has open attribute"
+ );
+
+ // The devices should be shown in the subview.
+ let expectedItems = [
+ {
+ className: "pageAction-sendToDevice-notReady",
+ display: "none",
+ disabled: true,
+ },
+ {
+ attrs: {
+ clientId: "1",
+ label: "bar",
+ clientType: "desktop",
+ },
+ },
+ {
+ attrs: {
+ clientId: "2",
+ label: "baz",
+ clientType: "phone",
+ },
+ },
+ {
+ attrs: {
+ clientId: "0",
+ label: "foo",
+ clientType: "phone",
+ },
+ },
+ {
+ attrs: {
+ clientId: "3",
+ label: "no client record device",
+ clientType: "phone",
+ },
+ },
+ null,
+ {
+ attrs: {
+ label: "Send to All Devices",
+ },
+ },
+ {
+ attrs: {
+ label: "Manage Devices...",
+ },
+ },
+ ];
+ checkSendToDeviceItems(expectedItems, true);
+
+ // Get the first device menu item in the panel.
+ let bodyID =
+ BrowserPageActions._panelViewNodeIDForActionID("sendToDevice", true) +
+ "-body";
+ let body = document.getElementById(bodyID);
+ let deviceMenuItem = body.querySelector(".sendtab-target");
+ Assert.notEqual(deviceMenuItem, null);
+
+ // For good measure, wait until it's visible.
+ let dwu = window.windowUtils;
+ await TestUtils.waitForCondition(() => {
+ let bounds = dwu.getBoundsWithoutFlushing(deviceMenuItem);
+ return bounds.height > 0 && bounds.width > 0;
+ }, "Waiting for first device menu item to appear");
+
+ // Click it, which should cause the panel to close.
+ let hiddenPromise = promisePanelHidden(
+ BrowserPageActions._activatedActionPanelID
+ );
+ EventUtils.synthesizeMouseAtCenter(deviceMenuItem, {});
+ info("Waiting for Send to Device panel to close after clicking a device");
+ await hiddenPromise;
+ Assert.ok(
+ !urlbarButton.hasAttribute("open"),
+ "URL bar button no longer has open attribute"
+ );
+
+ // And then the "Sent!" notification panel should open and close by itself
+ // after a moment.
+ info("Waiting for the Sent! notification panel to open");
+ await promisePanelShown(ConfirmationHint._panel.id);
+ Assert.equal(ConfirmationHint._panel.anchorNode.id, urlbarButton.id);
+ info("Waiting for the Sent! notification panel to close");
+ await promisePanelHidden(ConfirmationHint._panel.id);
+
+ // Remove Send to Device from the urlbar.
+ action.pinnedToUrlbar = false;
+
+ cleanUp();
+ });
+});
+
+add_task(async function contextMenu() {
+ // Open an actionable page so that the main page action button appears.
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel and then open the context menu on the bookmark button.
+ await promisePageActionPanelOpen();
+ let bookmarkButton = document.getElementById("pageAction-panel-bookmark");
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ // The context menu should show the "remove" item. Click it.
+ let menuItems = collectContextMenuItems();
+ Assert.equal(menuItems.length, 1, "Context menu has one child");
+ Assert.equal(
+ menuItems[0].label,
+ "Remove from Address Bar",
+ "Context menu is in the 'remove' state"
+ );
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+ await contextMenuPromise;
+
+ // The action should be removed from the urlbar. In this case, the bookmark
+ // star, the node in the urlbar should be hidden.
+ let starButtonBox = document.getElementById("star-button-box");
+ await TestUtils.waitForCondition(() => {
+ return starButtonBox.hidden;
+ }, "Waiting for star button to become hidden");
+
+ // Open the context menu again on the bookmark button. (The page action
+ // panel remains open.)
+ contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ // The context menu should show the "add" item. Click it.
+ menuItems = collectContextMenuItems();
+ Assert.equal(menuItems.length, 1, "Context menu has one child");
+ Assert.equal(
+ menuItems[0].label,
+ "Add to Address Bar",
+ "Context menu is in the 'add' state"
+ );
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+ await contextMenuPromise;
+
+ // The action should be added to the urlbar.
+ await TestUtils.waitForCondition(() => {
+ return !starButtonBox.hidden;
+ }, "Waiting for star button to become unhidden");
+
+ // Open the context menu on the bookmark star in the urlbar.
+ contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(starButtonBox, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ // The context menu should show the "remove" item. Click it.
+ menuItems = collectContextMenuItems();
+ Assert.equal(menuItems.length, 1, "Context menu has one child");
+ Assert.equal(
+ menuItems[0].label,
+ "Remove from Address Bar",
+ "Context menu is in the 'remove' state"
+ );
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+ await contextMenuPromise;
+
+ // The action should be removed from the urlbar.
+ await TestUtils.waitForCondition(() => {
+ return starButtonBox.hidden;
+ }, "Waiting for star button to become hidden");
+
+ // Finally, add the bookmark star back to the urlbar so that other tests
+ // that rely on it are OK.
+ await promisePageActionPanelOpen();
+ contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(bookmarkButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ menuItems = collectContextMenuItems();
+ Assert.equal(menuItems.length, 1, "Context menu has one child");
+ Assert.equal(
+ menuItems[0].label,
+ "Add to Address Bar",
+ "Context menu is in the 'add' state"
+ );
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
+ await contextMenuPromise;
+ await TestUtils.waitForCondition(() => {
+ return !starButtonBox.hidden;
+ }, "Waiting for star button to become unhidden");
+ });
+
+ // urlbar tests that run after this one can break if the mouse is left over
+ // the area where the urlbar popup appears, which seems to happen due to the
+ // above synthesized mouse events. Move it over the urlbar.
+ EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, { type: "mousemove" });
+ gURLBar.focus();
+});
+
+function promiseSyncReady() {
+ let service = Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
+ .wrappedJSObject;
+ return service.whenLoaded().then(() => {
+ UIState.isReady();
+ return UIState.refresh();
+ });
+}
+
+function checkSendToDeviceItems(expectedItems, forUrlbar = false) {
+ let bodyID =
+ BrowserPageActions._panelViewNodeIDForActionID("sendToDevice", forUrlbar) +
+ "-body";
+ let body = document.getElementById(bodyID);
+ Assert.equal(body.children.length, expectedItems.length);
+ for (let i = 0; i < expectedItems.length; i++) {
+ let expected = expectedItems[i];
+ let actual = body.children[i];
+ if (!expected) {
+ Assert.equal(actual.localName, "toolbarseparator");
+ continue;
+ }
+ if ("id" in expected) {
+ Assert.equal(actual.id, expected.id);
+ }
+ if ("className" in expected) {
+ let expectedNames = expected.className.split(/\s+/);
+ for (let name of expectedNames) {
+ Assert.ok(
+ actual.classList.contains(name),
+ `classList contains: ${name}`
+ );
+ }
+ }
+ let display = "display" in expected ? expected.display : "-moz-box";
+ Assert.equal(getComputedStyle(actual).display, display);
+ let disabled = "disabled" in expected ? expected.disabled : false;
+ Assert.equal(actual.disabled, disabled);
+ if ("attrs" in expected) {
+ for (let name in expected.attrs) {
+ Assert.ok(actual.hasAttribute(name));
+ let attrVal = actual.getAttribute(name);
+ if (name == "label") {
+ attrVal = attrVal.normalize("NFKC"); // There's a bug with …
+ }
+ Assert.equal(attrVal, expected.attrs[name]);
+ }
+ }
+ }
+}
+
+function collectContextMenuItems() {
+ let contextMenu = document.getElementById("pageActionContextMenu");
+ return Array.prototype.filter.call(contextMenu.children, node => {
+ return window.getComputedStyle(node).visibility == "visible";
+ });
+}
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu_add_search_engine.js b/browser/base/content/test/pageActions/browser_page_action_menu_add_search_engine.js
new file mode 100644
index 0000000000..9dd88947ba
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu_add_search_engine.js
@@ -0,0 +1,672 @@
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromptTestUtils.jsm"
+);
+
+// Checks the panel button with a page that doesn't offer any engines.
+add_task(async function none() {
+ let url = "http://mochi.test:8888/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
+ // The action should not be present.
+ let actions = PageActions.actionsInPanel(window);
+ Assert.ok(
+ !actions.some(a => a.id == "addSearchEngine"),
+ "Action should not be present in panel"
+ );
+ let button = BrowserPageActions.panelButtonNodeForActionID(
+ "addSearchEngine"
+ );
+ Assert.ok(!button, "Action button should not be in panel");
+ });
+});
+
+// Checks the panel button with a page that offers one engine.
+add_task(async function one() {
+ let url =
+ getRootDirectory(gTestPath) + "page_action_menu_add_search_engine_one.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // The action should be present.
+ let actions = PageActions.actionsInPanel(window);
+ let action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ let expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ let button = BrowserPageActions.panelButtonNodeForActionID(
+ "addSearchEngine"
+ );
+ Assert.ok(button, "Button should be in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ false,
+ "Button should not expand into a subview"
+ );
+
+ // Click the action's button.
+ let enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_0"
+ );
+ let hiddenPromise = promisePageActionPanelHidden();
+ let feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await hiddenPromise;
+ let engine = await enginePromise;
+ let feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!");
+
+ // Open the panel again.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
+ // The action should be gone.
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(!action, "Action should not be present in panel");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(!button, "Action button should not be in panel");
+
+ // Remove the engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_0"
+ );
+ await Services.search.removeEngine(engine);
+ await enginePromise;
+
+ // Open the panel again.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
+ // The action should be present again.
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(button, "Action button should be in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ false,
+ "Button should not expand into a subview"
+ );
+ });
+});
+
+// Checks the panel button with a page that offers an invalid engine.
+add_task(async function invalid() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", false]],
+ });
+
+ let url =
+ getRootDirectory(gTestPath) +
+ "page_action_menu_add_search_engine_invalid.html";
+ await BrowserTestUtils.withNewTab(url, async tab => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // The action should be present.
+ let actions = PageActions.actionsInPanel(window);
+ let action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ let expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ let button = BrowserPageActions.panelButtonNodeForActionID(
+ "addSearchEngine"
+ );
+ Assert.ok(button, "Button should be in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ false,
+ "Button should not expand into a subview"
+ );
+
+ // Click the action's button.
+ let hiddenPromise = promisePageActionPanelHidden();
+ let promptPromise = PromptTestUtils.waitForPrompt(tab.linkedBrowser, {
+ modalType: Ci.nsIPromptService.MODAL_TYPE_CONTENT,
+ promptType: "alert",
+ });
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await hiddenPromise;
+ let prompt = await promptPromise;
+
+ Assert.ok(
+ prompt.ui.infoBody.textContent.includes(
+ "http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_404.xml"
+ ),
+ "Should have included the url in the prompt body"
+ );
+
+ await PromptTestUtils.handlePrompt(prompt);
+ });
+});
+
+// Checks the panel button with a page that offers many engines.
+add_task(async function many() {
+ let url =
+ getRootDirectory(gTestPath) +
+ "page_action_menu_add_search_engine_many.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // The action should be present.
+ let actions = PageActions.actionsInPanel(window);
+ let action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ let expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ let button = BrowserPageActions.panelButtonNodeForActionID(
+ "addSearchEngine"
+ );
+ Assert.ok(button, "Action button should be in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ true,
+ "Button should expand into a subview"
+ );
+
+ // Click the action's button. The subview should be shown.
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ let view = await viewPromise;
+ let viewID = BrowserPageActions._panelViewNodeIDForActionID(
+ "addSearchEngine",
+ false
+ );
+ Assert.equal(view.id, viewID, "View ID");
+ let bodyID = viewID + "-body";
+ let body = document.getElementById(bodyID);
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+
+ // Click the first engine to install it.
+ let enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_0"
+ );
+ let hiddenPromise = promisePageActionPanelHidden();
+ let feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(body.children[0], {});
+ await hiddenPromise;
+ let engines = [];
+ let engine = await enginePromise;
+ engines.push(engine);
+ let feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // Open the panel and show the subview again. The installed engine should
+ // be gone.
+ await promisePageActionPanelOpen();
+ viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await viewPromise;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+
+ // Click the next engine to install it.
+ enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_1"
+ );
+ hiddenPromise = promisePageActionPanelHidden();
+ feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(body.children[0], {});
+ await hiddenPromise;
+ engine = await enginePromise;
+ engines.push(engine);
+ feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // Open the panel again. This time the action button should show the one
+ // remaining engine.
+ await promisePageActionPanelOpen();
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(button, "Button should be present in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ false,
+ "Button should not expand into a subview"
+ );
+
+ // Click the button.
+ enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_2"
+ );
+ hiddenPromise = promisePageActionPanelHidden();
+ feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await hiddenPromise;
+ engine = await enginePromise;
+ engines.push(engine);
+ feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // All engines are installed at this point. Open the panel and make sure
+ // the action is gone.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(!action, "Action should be gone");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(!button, "Button should not be in panel");
+
+ // Remove the first engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_0"
+ );
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // Open the panel again. The action should be present and showing the first
+ // engine.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(button, "Button should be present in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ false,
+ "Button should not expand into a subview"
+ );
+
+ // Remove the second engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_1"
+ );
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // Open the panel again and check the subview. The subview should be
+ // present now that there are two offered engines again.
+ await promisePageActionPanelOpen();
+ actions = PageActions.actionsInPanel(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in panel");
+ expectedTitle = "Add Search Engine";
+ Assert.equal(action.getTitle(window), expectedTitle, "Action title");
+ button = BrowserPageActions.panelButtonNodeForActionID("addSearchEngine");
+ Assert.ok(button, "Button should be in panel");
+ Assert.equal(button.label, expectedTitle, "Button label");
+ Assert.equal(
+ button.classList.contains("subviewbutton-nav"),
+ true,
+ "Button should expand into a subview"
+ );
+ viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await viewPromise;
+ body = document.getElementById(bodyID);
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ ],
+ "Subview children"
+ );
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+
+ // Remove the third engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_2"
+ );
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // Open the panel again and check the subview.
+ await promisePageActionPanelOpen();
+ viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await viewPromise;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePageActionPanelHidden();
+ });
+});
+
+// Checks the urlbar button with a page that offers one engine.
+add_task(async function urlbarOne() {
+ let url =
+ getRootDirectory(gTestPath) + "page_action_menu_add_search_engine_one.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await promiseNodeVisible(BrowserPageActions.mainButtonNode);
+
+ // Pin the action to the urlbar.
+ let placedPromise = promisePlacedInUrlbar();
+ PageActions.actionForID("addSearchEngine").pinnedToUrlbar = true;
+
+ // It should be placed.
+ let button = await placedPromise;
+ let actions = PageActions.actionsInUrlbar(window);
+ let action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in urlbar");
+ Assert.ok(button, "Action button should be in urlbar");
+
+ // Click the action's button.
+ let enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_0"
+ );
+ let feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ let engine = await enginePromise;
+ let feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!");
+
+ // The action should be gone.
+ actions = PageActions.actionsInUrlbar(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(!action, "Action should not be present in urlbar");
+ button = BrowserPageActions.urlbarButtonNodeForActionID("addSearchEngine");
+ Assert.ok(!button, "Action button should not be in urlbar");
+
+ // Remove the engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_0"
+ );
+ placedPromise = promisePlacedInUrlbar();
+ await Services.search.removeEngine(engine);
+ await enginePromise;
+
+ // The action should be present again.
+ button = await placedPromise;
+ actions = PageActions.actionsInUrlbar(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in urlbar");
+ Assert.ok(button, "Action button should be in urlbar");
+
+ // Clean up.
+ PageActions.actionForID("addSearchEngine").pinnedToUrlbar = false;
+ await TestUtils.waitForCondition(() => {
+ return !BrowserPageActions.urlbarButtonNodeForActionID("addSearchEngine");
+ });
+ });
+});
+
+// Checks the urlbar button with a page that offers many engines.
+add_task(async function urlbarMany() {
+ let url =
+ getRootDirectory(gTestPath) +
+ "page_action_menu_add_search_engine_many.html";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ await promiseNodeVisible(BrowserPageActions.mainButtonNode);
+
+ // Pin the action to the urlbar.
+ let placedPromise = promisePlacedInUrlbar();
+ PageActions.actionForID("addSearchEngine").pinnedToUrlbar = true;
+
+ // It should be placed.
+ let button = await placedPromise;
+ let actions = PageActions.actionsInUrlbar(window);
+ let action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in urlbar");
+ Assert.ok(button, "Action button should be in urlbar");
+
+ // Click the action's button. The activated-action panel should open, and
+ // it should contain the addSearchEngine subview.
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ let view = await waitForActivatedActionPanel();
+ let viewID = BrowserPageActions._panelViewNodeIDForActionID(
+ "addSearchEngine",
+ true
+ );
+ Assert.equal(view.id, viewID, "View ID");
+ let body = view.firstElementChild;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+
+ // Click the first engine to install it.
+ let enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_0"
+ );
+ let hiddenPromise = promisePanelHidden(
+ BrowserPageActions.activatedActionPanelNode
+ );
+ let feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(body.children[0], {});
+ await hiddenPromise;
+ let engines = [];
+ let engine = await enginePromise;
+ engines.push(engine);
+ let feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // Open the panel again. The installed engine should be gone.
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ view = await waitForActivatedActionPanel();
+ body = view.firstElementChild;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+
+ // Click the next engine to install it.
+ enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_1"
+ );
+ hiddenPromise = promisePanelHidden(
+ BrowserPageActions.activatedActionPanelNode
+ );
+ feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(body.children[0], {});
+ await hiddenPromise;
+ engine = await enginePromise;
+ engines.push(engine);
+ feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // Now there's only one engine left, so clicking the button should simply
+ // install it instead of opening the activated-action panel.
+ enginePromise = promiseEngine(
+ "engine-added",
+ "page_action_menu_add_search_engine_2"
+ );
+ feedbackPromise = promiseFeedbackPanelShownAndHidden();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ engine = await enginePromise;
+ engines.push(engine);
+ feedbackText = await feedbackPromise;
+ Assert.equal(feedbackText, "Search engine added!", "Feedback text");
+
+ // All engines are installed at this point. The action should be gone.
+ actions = PageActions.actionsInUrlbar(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(!action, "Action should be gone");
+ button = BrowserPageActions.urlbarButtonNodeForActionID("addSearchEngine");
+ Assert.ok(!button, "Button should not be in urlbar");
+
+ // Remove the first engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_0"
+ );
+ placedPromise = promisePlacedInUrlbar();
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // The action should be placed again.
+ button = await placedPromise;
+ actions = PageActions.actionsInUrlbar(window);
+ action = actions.find(a => a.id == "addSearchEngine");
+ Assert.ok(action, "Action should be present in urlbar");
+ Assert.ok(button, "Button should be in urlbar");
+
+ // Remove the second engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_1"
+ );
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // Open the panel again and check the subview. The subview should be
+ // present now that there are two offered engines again.
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ view = await waitForActivatedActionPanel();
+ body = view.firstElementChild;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ ],
+ "Subview children"
+ );
+
+ // Hide the panel.
+ hiddenPromise = promisePanelHidden(
+ BrowserPageActions.activatedActionPanelNode
+ );
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await hiddenPromise;
+
+ // Remove the third engine.
+ enginePromise = promiseEngine(
+ "engine-removed",
+ "page_action_menu_add_search_engine_2"
+ );
+ await Services.search.removeEngine(engines.shift());
+ await enginePromise;
+
+ // Open the panel again and check the subview.
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ view = await waitForActivatedActionPanel();
+ body = view.firstElementChild;
+ Assert.deepEqual(
+ Array.from(body.children, n => n.label),
+ [
+ "page_action_menu_add_search_engine_0",
+ "page_action_menu_add_search_engine_1",
+ "page_action_menu_add_search_engine_2",
+ ],
+ "Subview children"
+ );
+
+ // Hide the panel.
+ hiddenPromise = promisePanelHidden(
+ BrowserPageActions.activatedActionPanelNode
+ );
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ await hiddenPromise;
+
+ // Clean up.
+ PageActions.actionForID("addSearchEngine").pinnedToUrlbar = false;
+ await TestUtils.waitForCondition(() => {
+ return !BrowserPageActions.urlbarButtonNodeForActionID("addSearchEngine");
+ });
+ });
+});
+
+function promiseEngine(expectedData, expectedEngineName) {
+ info(`Waiting for engine ${expectedData}`);
+ return TestUtils.topicObserved(
+ "browser-search-engine-modified",
+ (engine, data) => {
+ info(`Got engine ${engine.wrappedJSObject.name} ${data}`);
+ return (
+ expectedData == data &&
+ expectedEngineName == engine.wrappedJSObject.name
+ );
+ }
+ ).then(([engine, data]) => engine);
+}
+
+function promiseFeedbackPanelShownAndHidden() {
+ info("Waiting for feedback panel popupshown");
+ return BrowserTestUtils.waitForEvent(
+ ConfirmationHint._panel,
+ "popupshown"
+ ).then(() => {
+ info("Got feedback panel popupshown. Now waiting for popuphidden");
+ return BrowserTestUtils.waitForEvent(
+ ConfirmationHint._panel,
+ "popuphidden"
+ ).then(() => ConfirmationHint._message.textContent);
+ });
+}
+
+function promisePlacedInUrlbar() {
+ let action = PageActions.actionForID("addSearchEngine");
+ return new Promise(resolve => {
+ let onPlaced = action._onPlacedInUrlbar;
+ action._onPlacedInUrlbar = button => {
+ action._onPlacedInUrlbar = onPlaced;
+ if (action._onPlacedInUrlbar) {
+ action._onPlacedInUrlbar(button);
+ }
+ promiseNodeVisible(button).then(() => resolve(button));
+ };
+ });
+}
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu_clipboard.js b/browser/base/content/test/pageActions/browser_page_action_menu_clipboard.js
new file mode 100644
index 0000000000..12d9ef8468
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu_clipboard.js
@@ -0,0 +1,40 @@
+"use strict";
+
+const mockRemoteClients = [
+ { id: "0", name: "foo", type: "mobile" },
+ { id: "1", name: "bar", type: "desktop" },
+ { id: "2", name: "baz", type: "mobile" },
+];
+
+add_task(async function copyURL() {
+ // Open an actionable page so that the main page action button appears. (It
+ // does not appear on about:blank for example.)
+ let url = "http://example.com/";
+ await BrowserTestUtils.withNewTab(url, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // Click Copy URL.
+ let copyURLButton = document.getElementById("pageAction-panel-copyURL");
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(copyURLButton, {});
+ await hiddenPromise;
+
+ // Check the clipboard.
+ let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ transferable.init(null);
+ let flavor = "text/unicode";
+ transferable.addDataFlavor(flavor);
+ Services.clipboard.getData(
+ transferable,
+ Services.clipboard.kGlobalClipboard
+ );
+ let strObj = {};
+ transferable.getTransferData(flavor, strObj);
+ Assert.ok(!!strObj.value);
+ strObj.value.QueryInterface(Ci.nsISupportsString);
+ Assert.equal(strObj.value.data, gBrowser.selectedBrowser.currentURI.spec);
+ });
+});
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu_share_mac.js b/browser/base/content/test/pageActions/browser_page_action_menu_share_mac.js
new file mode 100644
index 0000000000..e15e7619a8
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu_share_mac.js
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+
+const URL = "http://example.org/";
+
+// Keep track of title of service we chose to share with
+let serviceName, sharedUrl, sharedTitle;
+let sharingPreferencesCalled = false;
+
+let mockShareData = [
+ {
+ name: "NSA",
+ menuItemTitle: "National Security Agency",
+ image:
+ "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEA" +
+ "LAAAAAABAAEAAAICTAEAOw==",
+ },
+];
+
+let stub = sinon
+ .stub(BrowserPageActions.shareURL, "_sharingService")
+ .get(() => {
+ return {
+ getSharingProviders(url) {
+ return mockShareData;
+ },
+ shareUrl(name, url, title) {
+ serviceName = name;
+ sharedUrl = url;
+ sharedTitle = title;
+ },
+ openSharingPreferences() {
+ sharingPreferencesCalled = true;
+ },
+ };
+ });
+
+registerCleanupFunction(async function() {
+ stub.restore();
+ await EventUtils.synthesizeNativeMouseMove(document.documentElement, 0, 0);
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function shareURL() {
+ await BrowserTestUtils.withNewTab(URL, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // Click Share URL.
+ let shareURLButton = document.getElementById("pageAction-panel-shareURL");
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(shareURLButton, {});
+
+ let view = await viewPromise;
+ let body = document.getElementById(view.id + "-body");
+
+ // We should see 1 receiver and one extra node for the "More..." button
+ Assert.equal(body.children.length, 2, "Has correct share receivers");
+ let shareButton = body.children[0];
+ Assert.equal(shareButton.label, mockShareData[0].menuItemTitle);
+ let hiddenPromise = promisePageActionPanelHidden();
+ // Click on share, panel should hide and sharingService should be
+ // given the title of service to share with
+ EventUtils.synthesizeMouseAtCenter(shareButton, {});
+ await hiddenPromise;
+
+ Assert.equal(
+ serviceName,
+ mockShareData[0].name,
+ "Shared the correct service name"
+ );
+ Assert.equal(sharedUrl, "http://example.org/", "Shared correct URL");
+ Assert.equal(
+ sharedTitle,
+ "mochitest index /",
+ "Shared with the correct title"
+ );
+ });
+});
+
+add_task(async function shareURLAddressBar() {
+ await BrowserTestUtils.withNewTab(URL, async () => {
+ // Open pageAction panel
+ await promisePageActionPanelOpen();
+
+ // Right click the Share button
+ let contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ let shareURLButton = document.getElementById("pageAction-panel-shareURL");
+ EventUtils.synthesizeMouseAtCenter(shareURLButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ // Click "Add to Address Bar"
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ let ctxMenuButton = document.querySelector(
+ "#pageActionContextMenu .pageActionContextMenuItem"
+ );
+ EventUtils.synthesizeMouseAtCenter(ctxMenuButton, {});
+ await contextMenuPromise;
+
+ // Wait for the Share button to be added
+ await BrowserTestUtils.waitForCondition(() => {
+ return document.getElementById("pageAction-urlbar-shareURL");
+ }, "Waiting for the share url button to be added to url bar");
+
+ // Press the Share button
+ let shareButton = document.getElementById("pageAction-urlbar-shareURL");
+ let viewPromise = promisePageActionPanelShown();
+ EventUtils.synthesizeMouseAtCenter(shareButton, {});
+ await viewPromise;
+
+ // Ensure we have share providers
+ let panel = document.getElementById(
+ "pageAction-urlbar-shareURL-subview-body"
+ );
+ // We should see 1 receiver and one extra node for the "More..." button
+ Assert.equal(panel.children.length, 2, "Has correct share receivers");
+
+ // Remove the Share URL button from the Address bar so we dont interfere
+ // with future tests
+ contextMenuPromise = promisePanelShown("pageActionContextMenu");
+ EventUtils.synthesizeMouseAtCenter(shareButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await contextMenuPromise;
+
+ contextMenuPromise = promisePanelHidden("pageActionContextMenu");
+ ctxMenuButton = document.querySelector(
+ "#pageActionContextMenu .pageActionContextMenuItem"
+ );
+ EventUtils.synthesizeMouseAtCenter(ctxMenuButton, {});
+ await contextMenuPromise;
+ });
+});
+
+add_task(async function openSharingPreferences() {
+ await BrowserTestUtils.withNewTab(URL, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // Click Share URL.
+ let shareURLButton = document.getElementById("pageAction-panel-shareURL");
+ let viewPromise = promisePageActionViewShown();
+ EventUtils.synthesizeMouseAtCenter(shareURLButton, {});
+
+ let view = await viewPromise;
+ let body = document.getElementById(view.id + "-body");
+
+ // We should see 1 receiver and one extra node for the "More..." button
+ Assert.equal(body.children.length, 2, "Has correct share receivers");
+ let moreButton = body.children[1];
+ let hiddenPromise = promisePageActionPanelHidden();
+ // Click on the "more" button, panel should hide and we should call
+ // the sharingService function to open preferences
+ EventUtils.synthesizeMouseAtCenter(moreButton, {});
+ await hiddenPromise;
+
+ Assert.equal(
+ sharingPreferencesCalled,
+ true,
+ "We called openSharingPreferences"
+ );
+ });
+});
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu_share_win.html b/browser/base/content/test/pageActions/browser_page_action_menu_share_win.html
new file mode 100644
index 0000000000..6c47f98c7e
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu_share_win.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<title>Windows Sharing</title>
diff --git a/browser/base/content/test/pageActions/browser_page_action_menu_share_win.js b/browser/base/content/test/pageActions/browser_page_action_menu_share_win.js
new file mode 100644
index 0000000000..973365086f
--- /dev/null
+++ b/browser/base/content/test/pageActions/browser_page_action_menu_share_win.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+
+const TEST_URL =
+ getRootDirectory(gTestPath) + "browser_page_action_menu_share_win.html";
+
+// Keep track of site details we are sharing
+let sharedUrl, sharedTitle;
+
+let stub = sinon
+ .stub(BrowserPageActions.shareURL, "_windowsUIUtils")
+ .get(() => {
+ return {
+ shareUrl(url, title) {
+ sharedUrl = url;
+ sharedTitle = title;
+ },
+ };
+ });
+
+registerCleanupFunction(async function() {
+ stub.restore();
+});
+
+add_task(async function shareURL() {
+ if (!AppConstants.isPlatformAndVersionAtLeast("win", "6.4")) {
+ Assert.ok(true, "We only expose share on windows 10 and above");
+ return;
+ }
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async () => {
+ // Open the panel.
+ await promisePageActionPanelOpen();
+
+ // Click Share URL.
+ let shareURLButton = document.getElementById("pageAction-panel-shareURL");
+ let hiddenPromise = promisePageActionPanelHidden();
+ EventUtils.synthesizeMouseAtCenter(shareURLButton, {});
+
+ await hiddenPromise;
+
+ Assert.equal(sharedUrl, TEST_URL, "Shared correct URL");
+ Assert.equal(
+ sharedTitle,
+ "Windows Sharing",
+ "Shared with the correct title"
+ );
+ });
+});
diff --git a/browser/base/content/test/pageActions/head.js b/browser/base/content/test/pageActions/head.js
new file mode 100644
index 0000000000..297ca00f65
--- /dev/null
+++ b/browser/base/content/test/pageActions/head.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { PlacesTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+function promisePageActionPanelOpen(eventDict = {}) {
+ let dwu = window.windowUtils;
+ return BrowserTestUtils.waitForCondition(() => {
+ // Wait for the main page action button to become visible. It's hidden for
+ // some URIs, so depending on when this is called, it may not yet be quite
+ // visible. It's up to the caller to make sure it will be visible.
+ info("Waiting for main page action button to have non-0 size");
+ let bounds = dwu.getBoundsWithoutFlushing(
+ BrowserPageActions.mainButtonNode
+ );
+ return bounds.width > 0 && bounds.height > 0;
+ })
+ .then(() => {
+ // Wait for the panel to become open, by clicking the button if necessary.
+ info("Waiting for main page action panel to be open");
+ if (BrowserPageActions.panelNode.state == "open") {
+ return Promise.resolve();
+ }
+ let shownPromise = promisePageActionPanelShown();
+ EventUtils.synthesizeMouseAtCenter(
+ BrowserPageActions.mainButtonNode,
+ eventDict
+ );
+ return shownPromise;
+ })
+ .then(() => {
+ // Wait for items in the panel to become visible.
+ return promisePageActionViewChildrenVisible(
+ BrowserPageActions.mainViewNode
+ );
+ });
+}
+
+async function waitForActivatedActionPanel() {
+ if (!BrowserPageActions.activatedActionPanelNode) {
+ info("Waiting for activated-action panel to be added to mainPopupSet");
+ await new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ if (BrowserPageActions.activatedActionPanelNode) {
+ observer.disconnect();
+ resolve();
+ }
+ });
+ let popupSet = document.getElementById("mainPopupSet");
+ observer.observe(popupSet, { childList: true });
+ });
+ info("Activated-action panel added to mainPopupSet");
+ }
+ if (!BrowserPageActions.activatedActionPanelNode.state == "open") {
+ info("Waiting for activated-action panel popupshown");
+ await promisePanelShown(BrowserPageActions.activatedActionPanelNode);
+ info("Got activated-action panel popupshown");
+ }
+ let panelView = BrowserPageActions.activatedActionPanelNode.querySelector(
+ "panelview"
+ );
+ if (panelView) {
+ await BrowserTestUtils.waitForEvent(
+ BrowserPageActions.activatedActionPanelNode,
+ "ViewShown"
+ );
+ await promisePageActionViewChildrenVisible(panelView);
+ }
+ return panelView;
+}
+
+function promisePageActionPanelShown() {
+ return promisePanelShown(BrowserPageActions.panelNode);
+}
+
+function promisePageActionPanelHidden() {
+ return promisePanelHidden(BrowserPageActions.panelNode);
+}
+
+function promisePanelShown(panelIDOrNode) {
+ return promisePanelEvent(panelIDOrNode, "popupshown");
+}
+
+function promisePanelHidden(panelIDOrNode) {
+ return promisePanelEvent(panelIDOrNode, "popuphidden");
+}
+
+function promisePanelEvent(panelIDOrNode, eventType) {
+ return new Promise(resolve => {
+ let panel = panelIDOrNode;
+ if (typeof panel == "string") {
+ panel = document.getElementById(panelIDOrNode);
+ if (!panel) {
+ throw new Error(`Panel with ID "${panelIDOrNode}" does not exist.`);
+ }
+ }
+ if (
+ (eventType == "popupshown" && panel.state == "open") ||
+ (eventType == "popuphidden" && panel.state == "closed")
+ ) {
+ executeSoon(resolve);
+ return;
+ }
+ panel.addEventListener(
+ eventType,
+ () => {
+ executeSoon(resolve);
+ },
+ { once: true }
+ );
+ });
+}
+
+function promisePageActionViewShown() {
+ info("promisePageActionViewShown waiting for ViewShown");
+ return BrowserTestUtils.waitForEvent(
+ BrowserPageActions.panelNode,
+ "ViewShown"
+ ).then(async event => {
+ let panelViewNode = event.originalTarget;
+ await promisePageActionViewChildrenVisible(panelViewNode);
+ return panelViewNode;
+ });
+}
+
+function promisePageActionViewChildrenVisible(panelViewNode) {
+ return promiseNodeVisible(panelViewNode.firstElementChild.firstElementChild);
+}
+
+function promiseNodeVisible(node) {
+ info(
+ `promiseNodeVisible waiting, node.id=${node.id} node.localeName=${node.localName}\n`
+ );
+ let dwu = window.windowUtils;
+ return BrowserTestUtils.waitForCondition(() => {
+ let bounds = dwu.getBoundsWithoutFlushing(node);
+ if (bounds.width > 0 && bounds.height > 0) {
+ info(
+ `promiseNodeVisible OK, node.id=${node.id} node.localeName=${node.localName}\n`
+ );
+ return true;
+ }
+ return false;
+ });
+}
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml
new file mode 100644
index 0000000000..7e3e732ec6
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>page_action_menu_add_search_engine_0</ShortName>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_1.xml b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_1.xml
new file mode 100644
index 0000000000..d7306b3f91
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_1.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>page_action_menu_add_search_engine_1</ShortName>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_2.xml b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_2.xml
new file mode 100644
index 0000000000..eacd28334e
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_2.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>page_action_menu_add_search_engine_2</ShortName>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_invalid.html b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_invalid.html
new file mode 100644
index 0000000000..97efc667bd
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_invalid.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_404.xml">
+</head>
+<body></body>
+</html>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_many.html b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_many.html
new file mode 100644
index 0000000000..a2e1c6bfa8
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_many.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_1" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_1.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_2" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_2.xml">
+</head>
+<body></body>
+</html>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_one.html b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_one.html
new file mode 100644
index 0000000000..1ef425d523
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_one.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml">
+</head>
+<body></body>
+</html>
diff --git a/browser/base/content/test/pageActions/page_action_menu_add_search_engine_same_names.html b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_same_names.html
new file mode 100644
index 0000000000..281f4a610a
--- /dev/null
+++ b/browser/base/content/test/pageActions/page_action_menu_add_search_engine_same_names.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_0" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="page_action_menu_add_search_engine_1" href="http://mochi.test:8888/browser/browser/base/content/test/pageActions/page_action_menu_add_search_engine_0.xml">
+</head>
+<body></body>
+</html>