diff options
Diffstat (limited to '')
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> |