diff --git a/browser/components/extensions/parent/.eslintrc.js b/browser/components/extensions/parent/.eslintrc.js index 4704e0615010..69c83daadb29 100644 --- a/browser/components/extensions/parent/.eslintrc.js +++ b/browser/components/extensions/parent/.eslintrc.js @@ -9,6 +9,7 @@ module.exports = { "Window": true, "actionContextMenu": true, "browserActionFor": true, + "clickModifiersFromEvent": true, "getContainerForCookieStoreId": true, "getDevToolsTargetForContext": true, "getInspectedWindowFront": true, diff --git a/browser/components/extensions/parent/ext-browser.js b/browser/components/extensions/parent/ext-browser.js index 9f319b83c7a2..405487b31af7 100644 --- a/browser/components/extensions/parent/ext-browser.js +++ b/browser/components/extensions/parent/ext-browser.js @@ -125,6 +125,24 @@ global.makeWidgetId = id => { return id.replace(/[^a-z0-9_-]/g, "_"); }; +global.clickModifiersFromEvent = event => { + const map = { + shiftKey: "Shift", + altKey: "Alt", + metaKey: "Command", + ctrlKey: "Ctrl", + }; + let modifiers = Object.keys(map) + .filter(key => event[key]) + .map(key => map[key]); + + if (event.ctrlKey && AppConstants.platform === "macosx") { + modifiers.push("MacCtrl"); + } + + return modifiers; +}; + global.waitForTabLoaded = (tab, url) => { return new Promise(resolve => { windowTracker.addListener("progress", { diff --git a/browser/components/extensions/parent/ext-browserAction.js b/browser/components/extensions/parent/ext-browserAction.js index f86426e17bbe..bd8acf176816 100644 --- a/browser/components/extensions/parent/ext-browserAction.js +++ b/browser/components/extensions/parent/ext-browserAction.js @@ -200,6 +200,8 @@ this.browserAction = class extends ExtensionAPI { node.onmousedown = event => this.handleEvent(event); node.onmouseover = event => this.handleEvent(event); node.onmouseout = event => this.handleEvent(event); + node.onkeypress = event => this.handleEvent(event); + node.onmouseup = event => this.handleMouseUp(event); this.updateButton(node, this.globals, true); }, @@ -280,7 +282,7 @@ this.browserAction = class extends ExtensionAPI { */ async triggerAction(window) { let popup = ViewPopup.for(this.extension, window); - if (popup) { + if (!this.pendingPopup && popup) { popup.closePopup(); return; } @@ -307,10 +309,27 @@ this.browserAction = class extends ExtensionAPI { widget.node.dispatchEvent(event); } else { this.tabManager.addActiveTabPermission(tab); + this.lastClickInfo = { button: 0, modifiers: [] }; this.emit("click"); } } + handleMouseUp(event) { + let window = event.target.ownerGlobal; + + this.lastClickInfo = { + button: event.button, + modifiers: clickModifiersFromEvent(event), + }; + + if (event.button === 1) { + let { gBrowser } = window; + if (this.getProperty(gBrowser.selectedTab, "enabled")) { + this.emit("click", gBrowser.selectedBrowser); + } + } + } + handleEvent(event) { let button = event.target; let window = button.ownerGlobal; @@ -412,6 +431,15 @@ this.browserAction = class extends ExtensionAPI { }); } break; + + case "keypress": + if (event.key === " " || event.key === "Enter") { + this.lastClickInfo = { + button: 0, + modifiers: clickModifiersFromEvent(event), + }; + } + break; } } @@ -760,7 +788,10 @@ this.browserAction = class extends ExtensionAPI { register: fire => { let listener = (event, browser) => { context.withPendingBrowser(browser, () => - fire.sync(tabManager.convert(tabTracker.activeTab)) + fire.sync( + tabManager.convert(tabTracker.activeTab), + browserAction.lastClickInfo + ) ); }; browserAction.on("click", listener); diff --git a/browser/components/extensions/parent/ext-menus.js b/browser/components/extensions/parent/ext-menus.js index 983fa7a1b086..9383893eee4e 100644 --- a/browser/components/extensions/parent/ext-menus.js +++ b/browser/components/extensions/parent/ext-menus.js @@ -445,19 +445,7 @@ var gMenuBuilder = { } let info = item.getClickInfo(contextData, wasChecked); - - const map = { - shiftKey: "Shift", - altKey: "Alt", - metaKey: "Command", - ctrlKey: "Ctrl", - }; - info.modifiers = Object.keys(map) - .filter(key => event[key]) - .map(key => map[key]); - if (event.ctrlKey && AppConstants.platform === "macosx") { - info.modifiers.push("MacCtrl"); - } + info.modifiers = clickModifiersFromEvent(event); info.button = button; diff --git a/browser/components/extensions/parent/ext-pageAction.js b/browser/components/extensions/parent/ext-pageAction.js index 348c6483fba4..9d5a96f32a0c 100644 --- a/browser/components/extensions/parent/ext-pageAction.js +++ b/browser/components/extensions/parent/ext-pageAction.js @@ -96,6 +96,28 @@ this.pageAction = class extends ExtensionAPI { this.lastValues = new DefaultWeakMap(() => ({})); if (!this.browserPageAction) { + let onPlacedHandler = (buttonNode, isPanel) => { + // eslint-disable-next-line mozilla/balanced-listeners + buttonNode.addEventListener("auxclick", event => { + if (event.button !== 1 || event.target.disabled) { + return; + } + + this.lastClickInfo = { + button: event.button, + modifiers: clickModifiersFromEvent(event), + }; + + // The panel is not automatically closed when middle-clicked. + if (isPanel) { + buttonNode.closest("#pageActionPanel").hidePopup(); + } + let window = event.target.ownerGlobal; + let tab = window.gBrowser.selectedTab; + this.emit("click", tab); + }); + }; + this.browserPageAction = PageActions.addAction( new PageActions.Action({ id: widgetId, @@ -105,6 +127,10 @@ this.pageAction = class extends ExtensionAPI { pinnedToUrlbar: this.defaults.pinned, disabled: !this.defaults.show, onCommand: (event, buttonNode) => { + this.lastClickInfo = { + button: event.button || 0, + modifiers: clickModifiersFromEvent(event), + }; this.handleClick(event.target.ownerGlobal); }, onBeforePlacedInWindow: browserWindow => { @@ -115,6 +141,8 @@ this.pageAction = class extends ExtensionAPI { browserWindow.document.addEventListener("popupshowing", this); } }, + onPlacedInPanel: buttonNode => onPlacedHandler(buttonNode, true), + onPlacedInUrlbar: buttonNode => onPlacedHandler(buttonNode, false), onRemovedFromWindow: browserWindow => { browserWindow.document.removeEventListener("popupshowing", this); }, @@ -250,6 +278,7 @@ this.pageAction = class extends ExtensionAPI { */ triggerAction(window) { if (this.isShown(window.gBrowser.selectedTab)) { + this.lastClickInfo = { button: 0, modifiers: [] }; this.handleClick(window); } } @@ -380,7 +409,7 @@ this.pageAction = class extends ExtensionAPI { register: fire => { let listener = (evt, tab) => { context.withPendingBrowser(tab.linkedBrowser, () => - fire.sync(tabManager.convert(tab)) + fire.sync(tabManager.convert(tab), this.lastClickInfo) ); }; diff --git a/browser/components/extensions/schemas/browser_action.json b/browser/components/extensions/schemas/browser_action.json index e759260c7f13..d9b93d47758f 100644 --- a/browser/components/extensions/schemas/browser_action.json +++ b/browser/components/extensions/schemas/browser_action.json @@ -104,6 +104,26 @@ {"$ref": "ColorArray"}, {"type": "null"} ] + }, + { + "id": "OnClickData", + "type": "object", + "description": "Information sent when a browser action is clicked.", + "properties": { + "modifiers": { + "type": "array", + "items": { + "type": "string", + "enum": ["Shift", "Alt", "Command", "Ctrl", "MacCtrl"] + }, + "description": "An array of keyboard modifiers that were held while the menu item was clicked." + }, + "button": { + "type": "integer", + "optional": true, + "description": "An integer value of button by which menu item was clicked." + } + } } ], "functions": [ @@ -448,6 +468,11 @@ { "name": "tab", "$ref": "tabs.Tab" + }, + { + "name": "info", + "$ref": "OnClickData", + "optional": true } ] } diff --git a/browser/components/extensions/schemas/page_action.json b/browser/components/extensions/schemas/page_action.json index 5aaf5eb4b9b7..3c7d4f1a829a 100644 --- a/browser/components/extensions/schemas/page_action.json +++ b/browser/components/extensions/schemas/page_action.json @@ -54,6 +54,26 @@ "optional": true } } + }, + { + "id": "OnClickData", + "type": "object", + "description": "Information sent when a page action is clicked.", + "properties": { + "modifiers": { + "type": "array", + "items": { + "type": "string", + "enum": ["Shift", "Alt", "Command", "Ctrl", "MacCtrl"] + }, + "description": "An array of keyboard modifiers that were held while the menu item was clicked." + }, + "button": { + "type": "integer", + "optional": true, + "description": "An integer value of button by which menu item was clicked." + } + } } ] }, @@ -284,6 +304,11 @@ { "name": "tab", "$ref": "tabs.Tab" + }, + { + "name": "info", + "$ref": "OnClickData", + "optional": true } ] } diff --git a/browser/components/extensions/test/browser/browser.ini b/browser/components/extensions/test/browser/browser.ini index 47152bacf120..28ee1522f4a3 100644 --- a/browser/components/extensions/test/browser/browser.ini +++ b/browser/components/extensions/test/browser/browser.ini @@ -53,6 +53,7 @@ disabled = bug 1438663 # same focus issue as Bug 1438663 [browser_ext_autoplayInBackground.js] [browser_ext_browserAction_area.js] [browser_ext_browserAction_experiment.js] +[browser_ext_browserAction_click_types.js] [browser_ext_browserAction_context.js] skip-if = os == 'linux' && debug # Bug 1504096 [browser_ext_browserAction_contextMenu.js] @@ -155,6 +156,7 @@ skip-if = (verify && !debug && (os == 'linux' || os == 'mac')) [browser_ext_optionsPage_modals.js] [browser_ext_optionsPage_popups.js] [browser_ext_optionsPage_privileges.js] +[browser_ext_pageAction_click_types.js] [browser_ext_pageAction_context.js] skip-if = (verify && !debug && (os == 'linux')) [browser_ext_pageAction_contextMenu.js] diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_click_types.js b/browser/components/extensions/test/browser/browser_ext_browserAction_click_types.js new file mode 100644 index 000000000000..73fbbe4d003e --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_click_types.js @@ -0,0 +1,149 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(async function test_clickData() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_action: {}, + }, + + background() { + function onClicked(tab, info) { + let button = info.button; + let modifiers = info.modifiers; + browser.test.sendMessage("onClick", { button, modifiers }); + } + + browser.browserAction.onClicked.addListener(onClicked); + browser.test.sendMessage("ready"); + }, + }); + + const map = { + shiftKey: "Shift", + altKey: "Alt", + metaKey: "Command", + ctrlKey: "Ctrl", + }; + + function assertSingleModifier(info, modifier, area) { + if (modifier === "ctrlKey" && AppConstants.platform === "macosx") { + is( + info.modifiers.length, + 2, + `MacCtrl modifier with control click on Mac` + ); + is( + info.modifiers[1], + "MacCtrl", + `MacCtrl modifier with control click on Mac` + ); + } else { + is( + info.modifiers.length, + 1, + `No unnecessary modifiers for exactly one key on event` + ); + } + + is(info.modifiers[0], map[modifier], `Correct modifier on ${area} click`); + } + + await extension.startup(); + await extension.awaitMessage("ready"); + + for (let area of [CustomizableUI.AREA_NAVBAR, getCustomizableUIPanelID()]) { + let widget = getBrowserActionWidget(extension); + CustomizableUI.addWidgetToArea(widget.id, area); + + for (let modifier of Object.keys(map)) { + for (let i = 0; i < 2; i++) { + let clickEventData = { button: i }; + clickEventData[modifier] = true; + await clickBrowserAction(extension, window, clickEventData); + let info = await extension.awaitMessage("onClick"); + + is(info.button, i, `Correct button in ${area} click`); + assertSingleModifier(info, modifier, area); + } + + let keypressEventData = {}; + keypressEventData[modifier] = true; + await triggerBrowserActionWithKeyboard( + extension, + "KEY_Enter", + keypressEventData + ); + let info = await extension.awaitMessage("onClick"); + + is(info.button, 0, `Key command emulates left click`); + assertSingleModifier(info, modifier, area); + } + } + + await extension.unload(); +}); + +add_task(async function test_clickData_reset() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_action: {}, + page_action: {}, + }, + + async background() { + function onBrowserActionClicked(tab, info) { + browser.test.sendMessage("onClick", info); + } + + function onPageActionClicked(tab, info) { + // openPopup requires user interaction, such as a page action click. + browser.browserAction.openPopup(); + } + + browser.browserAction.onClicked.addListener(onBrowserActionClicked); + browser.pageAction.onClicked.addListener(onPageActionClicked); + + let [tab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + await browser.pageAction.show(tab.id); + browser.test.sendMessage("ready"); + }, + }); + + // Pollute the state of the browserAction's lastClickInfo + async function clickBrowserActionWithModifiers() { + await clickBrowserAction(extension, window, { button: 1, shiftKey: true }); + let info = await extension.awaitMessage("onClick"); + is(info.button, 1); + is(info.modifiers[0], "Shift"); + } + + function assertInfoReset(info) { + is(info.button, 0, `ClickData button reset properly`); + is(info.modifiers.length, 0, `ClickData modifiers reset properly`); + } + + await extension.startup(); + await extension.awaitMessage("ready"); + + await clickBrowserActionWithModifiers(); + + await clickPageAction(extension); + assertInfoReset(await extension.awaitMessage("onClick")); + + await clickBrowserActionWithModifiers(); + + await triggerBrowserActionWithKeyboard(extension, "KEY_Enter"); + assertInfoReset(await extension.awaitMessage("onClick")); + + await clickBrowserActionWithModifiers(); + + await triggerBrowserActionWithKeyboard(extension, " "); + assertInfoReset(await extension.awaitMessage("onClick")); + + await extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js b/browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js index c64067165cac..5887021be5ce 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_disabled.js @@ -55,6 +55,23 @@ add_task(async function testDisabled() { extension.sendMessage("check-clicked", false); await extension.awaitMessage("next-test"); + await clickBrowserAction(extension, window, { button: 1 }); + await new Promise(resolve => setTimeout(resolve, 0)); + + extension.sendMessage("check-clicked", false); + await extension.awaitMessage("next-test"); + + let widget = getBrowserActionWidget(extension); + CustomizableUI.addWidgetToArea(widget.id, getCustomizableUIPanelID()); + + await clickBrowserAction(extension, window, { button: 1 }); + await new Promise(resolve => setTimeout(resolve, 0)); + + extension.sendMessage("check-clicked", false); + await extension.awaitMessage("next-test"); + + CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_NAVBAR); + extension.sendMessage("enable"); await extension.awaitMessage("next-test"); diff --git a/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js b/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js index bd86d06f89ac..3ef03f4c19d4 100644 --- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js +++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup.js @@ -53,8 +53,12 @@ let scriptPage = url => async function testInArea(area) { let extension = ExtensionTestUtils.loadExtension({ background() { - browser.browserAction.onClicked.addListener(() => { + let middleClickShowPopup = false; + browser.browserAction.onClicked.addListener((tabs, info) => { browser.test.sendMessage("browserAction-onClicked"); + if (info.button === 1 && middleClickShowPopup) { + browser.browserAction.openPopup(); + } }); browser.test.onMessage.addListener(async msg => { @@ -72,6 +76,9 @@ async function testInArea(area) { } await browser.browserAction.setPopup(opts); browser.test.sendMessage("setBrowserActionPopup:done"); + } else if (msg.type === "setMiddleClickShowPopup") { + middleClickShowPopup = msg.show; + browser.test.sendMessage("setMiddleClickShowPopup:done"); } }); @@ -132,6 +139,11 @@ async function testInArea(area) { await extension.awaitMessage("setBrowserActionPopup:done"); } + async function setShowPopupOnMiddleClick(show) { + extension.sendMessage({ type: "setMiddleClickShowPopup", show }); + await extension.awaitMessage("setMiddleClickShowPopup:done"); + } + async function runTest({ actionType, waitForPopupLoaded, @@ -148,6 +160,12 @@ async function testInArea(area) { clickBrowserAction(extension); } else if (actionType === "trigger") { getBrowserAction(extension).triggerAction(window); + } else if (actionType === "middleClick") { + clickBrowserAction(extension, window, { button: 1 }); + } + + if (expectOnClicked) { + await extension.awaitMessage("browserAction-onClicked"); } if (expectPopup) { @@ -157,8 +175,6 @@ async function testInArea(area) { expectPopup, "expected popup opened" ); - } else if (expectOnClicked) { - await extension.awaitMessage("browserAction-onClicked"); } await oncePopupLoaded; @@ -250,6 +266,30 @@ async function testInArea(area) { closePopup: true, }); }, + async () => { + info(`Middle-click browser action, expect an event only.`); + + await setShowPopupOnMiddleClick(false); + + await runTest({ + actionType: "middleClick", + expectOnClicked: true, + }); + }, + async () => { + info( + `Middle-click browser action again, expect a click event then a popup.` + ); + + await setShowPopupOnMiddleClick(true); + + await runTest({ + actionType: "middleClick", + expectOnClicked: true, + expectPopup: "popup-b", + closePopup: true, + }); + }, async () => { info(`Clear popup URL. Click browser action. Expect click event.`); diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_click_types.js b/browser/components/extensions/test/browser/browser_ext_pageAction_click_types.js new file mode 100644 index 000000000000..e76db6513090 --- /dev/null +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_click_types.js @@ -0,0 +1,211 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +add_task(async function test_clickData() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + page_action: {}, + }, + + async background() { + function onClicked(tab, info) { + let button = info.button; + let modifiers = info.modifiers; + browser.test.sendMessage("onClick", { button, modifiers }); + } + + browser.pageAction.onClicked.addListener(onClicked); + + let [tab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + await browser.pageAction.show(tab.id); + browser.test.sendMessage("ready"); + }, + }); + + const map = { + shiftKey: "Shift", + altKey: "Alt", + metaKey: "Command", + ctrlKey: "Ctrl", + }; + + function assertSingleModifier(info, modifier) { + if (modifier === "ctrlKey" && AppConstants.platform === "macosx") { + is( + info.modifiers.length, + 2, + `MacCtrl modifier with control click on Mac` + ); + is( + info.modifiers[1], + "MacCtrl", + `MacCtrl modifier with control click on Mac` + ); + } else { + is( + info.modifiers.length, + 1, + `No unnecessary modifiers for exactly one key on event` + ); + } + + is(info.modifiers[0], map[modifier], `Correct modifier on click event`); + } + + async function testClickPageAction(doClick, doEnterKey) { + for (let modifier of Object.keys(map)) { + for (let i = 0; i < 2; i++) { + let clickEventData = { button: i }; + clickEventData[modifier] = true; + await doClick(extension, window, clickEventData); + let info = await extension.awaitMessage("onClick"); + + is(info.button, i, `Correct button on click event`); + assertSingleModifier(info, modifier); + } + + let keypressEventData = {}; + keypressEventData[modifier] = true; + await doEnterKey(extension, keypressEventData); + let info = await extension.awaitMessage("onClick"); + + is(info.button, 0, `Key command emulates left click`); + assertSingleModifier(info, modifier); + } + } + + await extension.startup(); + await extension.awaitMessage("ready"); + + await testClickPageAction(clickPageAction, triggerPageActionWithKeyboard); + await testClickPageAction( + clickPageActionInPanel, + triggerPageActionWithKeyboardInPanel + ); + + await extension.unload(); +}); + +add_task(async function test_clickData_reset() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + browser_action: {}, + page_action: {}, + }, + + async background() { + function onBrowserActionClicked(tab, info) { + // openPopup requires user interaction, such as a browser action click. + browser.pageAction.openPopup(); + } + + function onPageActionClicked(tab, info) { + browser.test.sendMessage("onClick", info); + } + + browser.browserAction.onClicked.addListener(onBrowserActionClicked); + browser.pageAction.onClicked.addListener(onPageActionClicked); + + let [tab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + await browser.pageAction.show(tab.id); + browser.test.sendMessage("ready"); + }, + }); + + async function clickPageActionWithModifiers() { + await clickPageAction(extension, window, { button: 1, shiftKey: true }); + let info = await extension.awaitMessage("onClick"); + is(info.button, 1); + is(info.modifiers[0], "Shift"); + } + + function assertInfoReset(info) { + is(info.button, 0, `ClickData button reset properly`); + is(info.modifiers.length, 0, `ClickData modifiers reset properly`); + } + + await extension.startup(); + await extension.awaitMessage("ready"); + + await clickPageActionWithModifiers(); + + await clickBrowserAction(extension); + assertInfoReset(await extension.awaitMessage("onClick")); + + await clickPageActionWithModifiers(); + + await triggerPageActionWithKeyboard(extension); + assertInfoReset(await extension.awaitMessage("onClick")); + + await extension.unload(); +}); + +add_task(async function test_click_disabled() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + page_action: {}, + }, + + background() { + let expectClick = false; + function onClicked(tab, info) { + if (expectClick) { + browser.test.sendMessage("onClick"); + } else { + browser.test.fail( + `Unexpected click on disabled page action, button=${info.button}` + ); + } + } + + async function onMessage(msg, toggle) { + if (msg == "hide" || msg == "show") { + expectClick = msg == "show"; + + let [tab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + if (expectClick) { + await browser.pageAction.show(tab.id); + } else { + await browser.pageAction.hide(tab.id); + } + browser.test.sendMessage("visibilitySet"); + } else { + browser.test.fail("Unexpected message"); + } + } + + browser.pageAction.onClicked.addListener(onClicked); + browser.test.onMessage.addListener(onMessage); + browser.test.sendMessage("ready"); + }, + }); + + await extension.startup(); + await extension.awaitMessage("ready"); + + extension.sendMessage("hide"); + await extension.awaitMessage("visibilitySet"); + + await clickPageActionInPanel(extension, window, { button: 0 }); + await clickPageActionInPanel(extension, window, { button: 1 }); + + extension.sendMessage("show"); + await extension.awaitMessage("visibilitySet"); + + await clickPageActionInPanel(extension, window, { button: 0 }); + await extension.awaitMessage("onClick"); + await clickPageActionInPanel(extension, window, { button: 1 }); + await extension.awaitMessage("onClick"); + + await extension.unload(); +}); diff --git a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js index 208eb24d10c5..0561ae044923 100644 --- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js +++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js @@ -78,6 +78,13 @@ add_task(async function testPageActionPopup() { () => { sendClick({ expectEvent: false, expectPopup: "b" }); }, + () => { + sendClick({ + expectEvent: true, + expectPopup: "b", + middleClick: true, + }); + }, () => { browser.pageAction.setPopup({ tabId, popup: "" }); sendClick({ expectEvent: true, expectPopup: null }); @@ -109,9 +116,15 @@ add_task(async function testPageActionPopup() { ]; let expect = {}; - sendClick = ({ expectEvent, expectPopup, runNextTest }) => { + sendClick = ({ + expectEvent, + expectPopup, + runNextTest, + middleClick, + }) => { expect = { event: expectEvent, popup: expectPopup, runNextTest }; - browser.test.sendMessage("send-click"); + + browser.test.sendMessage("send-click", middleClick ? 1 : 0); }; browser.runtime.onMessage.addListener(msg => { @@ -136,14 +149,19 @@ add_task(async function testPageActionPopup() { } }); - browser.pageAction.onClicked.addListener(() => { + browser.pageAction.onClicked.addListener((tab, info) => { if (expect.event) { browser.test.succeed("expected click event received"); } else { browser.test.fail("unexpected click event"); } - expect.event = false; + + if (info.button == 1) { + browser.pageAction.openPopup(); + return; + } + browser.test.sendMessage("next-test"); }); @@ -157,6 +175,18 @@ add_task(async function testPageActionPopup() { browser.test.fail("Expecting 'next-test' message"); } + if (expect.event) { + browser.test.fail( + "Expecting click event before next test but none occurred" + ); + } + + if (expect.popup) { + browser.test.fail( + "Expecting popup before next test but none were shown" + ); + } + if (tests.length) { let test = tests.shift(); test(); @@ -177,8 +207,8 @@ add_task(async function testPageActionPopup() { }, }); - extension.onMessage("send-click", () => { - clickPageAction(extension); + extension.onMessage("send-click", button => { + clickPageAction(extension, window, { button }); }); let pageActionId, panelId; diff --git a/browser/components/extensions/test/browser/head.js b/browser/components/extensions/test/browser/head.js index b1b4f6bc49bb..d48942da75c7 100644 --- a/browser/components/extensions/test/browser/head.js +++ b/browser/components/extensions/test/browser/head.js @@ -4,7 +4,9 @@ /* exported CustomizableUI makeWidgetId focusWindow forceGC * getBrowserActionWidget - * clickBrowserAction clickPageAction + * clickBrowserAction clickPageAction clickPageActionInPanel + * triggerPageActionWithKeyboard triggerPageActionWithKeyboardInPanel + * triggerBrowserActionWithKeyboard * getBrowserActionPopup getPageActionPopup getPageActionButton * openBrowserActionPanel * closeBrowserAction closePageAction @@ -285,10 +287,19 @@ function alterContent(browser, task, arg = null) { } function getPanelForNode(node) { - while (node.localName != "panel") { - node = node.parentNode; - } - return node; + return node.closest("panel"); +} + +async function focusButtonAndPressKey(key, elem, modifiers) { + let focused = BrowserTestUtils.waitForEvent(elem, "focus", true); + + elem.setAttribute("tabindex", "-1"); + elem.focus(); + elem.removeAttribute("tabindex"); + await focused; + + EventUtils.synthesizeKey(key, modifiers); + elem.blur(); } var awaitBrowserLoaded = browser => @@ -357,13 +368,31 @@ var showBrowserAction = async function(extension, win = window) { } }; -var clickBrowserAction = async function(extension, win = window) { +async function clickBrowserAction(extension, win = window, modifiers) { await promiseAnimationFrame(win); await showBrowserAction(extension, win); let widget = getBrowserActionWidget(extension).forWindow(win); - widget.node.click(); -}; + + if (modifiers) { + EventUtils.synthesizeMouseAtCenter(widget.node, modifiers, win); + } else { + widget.node.click(); + } +} + +async function triggerBrowserActionWithKeyboard( + extension, + key = "KEY_Enter", + modifiers = {}, + win = window +) { + await promiseAnimationFrame(win); + await showBrowserAction(extension, win); + + let node = getBrowserActionWidget(extension).forWindow(win).node; + await focusButtonAndPressKey(key, node, modifiers); +} function closeBrowserAction(extension, win = window) { let group = getBrowserActionWidget(extension); @@ -660,9 +689,79 @@ async function getPageActionButton(extension, win = window) { return win.document.getElementById(pageActionId); } -async function clickPageAction(extension, win = window) { +async function clickPageAction(extension, win = window, modifiers = {}) { let elem = await getPageActionButton(extension, win); - EventUtils.synthesizeMouseAtCenter(elem, {}, win); + EventUtils.synthesizeMouseAtCenter(elem, modifiers, win); + return new Promise(SimpleTest.executeSoon); +} + +// Shows the popup for the page action which for lists +// all available page actions +async function showPageActionsPanel(win = window) { + // See the comment at getPageActionButton + SetPageProxyState("valid"); + await promiseAnimationFrame(win); + + let pageActionsPopup = win.document.getElementById("pageActionPanel"); + + let popupShownPromise = promisePopupShown(pageActionsPopup); + EventUtils.synthesizeMouseAtCenter( + win.document.getElementById("pageActionButton"), + {}, + win + ); + await popupShownPromise; + + return pageActionsPopup; +} + +async function clickPageActionInPanel(extension, win = window, modifiers = {}) { + let pageActionsPopup = await showPageActionsPanel(win); + + let pageActionId = BrowserPageActions.panelButtonNodeIDForActionID( + makeWidgetId(extension.id) + ); + + let popupHiddenPromise = promisePopupHidden(pageActionsPopup); + let widgetButton = win.document.getElementById(pageActionId); + EventUtils.synthesizeMouseAtCenter(widgetButton, modifiers, win); + if (widgetButton.disabled) { + pageActionsPopup.hidePopup(); + } + await popupHiddenPromise; + + return new Promise(SimpleTest.executeSoon); +} + +async function triggerPageActionWithKeyboard( + extension, + modifiers = {}, + win = window +) { + let elem = await getPageActionButton(extension, win); + await focusButtonAndPressKey("KEY_Enter", elem, modifiers); + return new Promise(SimpleTest.executeSoon); +} + +async function triggerPageActionWithKeyboardInPanel( + extension, + modifiers = {}, + win = window +) { + let pageActionsPopup = await showPageActionsPanel(win); + + let pageActionId = BrowserPageActions.panelButtonNodeIDForActionID( + makeWidgetId(extension.id) + ); + + let popupHiddenPromise = promisePopupHidden(pageActionsPopup); + let widgetButton = win.document.getElementById(pageActionId); + await focusButtonAndPressKey("KEY_Enter", widgetButton, modifiers); + if (widgetButton.disabled) { + pageActionsPopup.hidePopup(); + } + await popupHiddenPromise; + return new Promise(SimpleTest.executeSoon); }