Bug 1634042, Part 2: lazify page action panel r=Gijs

Differential Revision: https://phabricator.services.mozilla.com/D91187
This commit is contained in:
Emma Malysz 2020-10-01 15:00:55 +00:00
parent d9711aa658
commit 7fb79b4804
11 changed files with 597 additions and 505 deletions

View File

@ -14,6 +14,7 @@ ChromeUtils.defineModuleGetter(
);
var BrowserPageActions = {
_panelNode: null,
/**
* The main page action button in the urlbar (DOM node)
*/
@ -26,8 +27,12 @@ var BrowserPageActions = {
* The main page action panel DOM node (DOM node)
*/
get panelNode() {
// Lazy load the page action panel the first time we need to display it
if (!this._panelNode) {
this.initializePanel();
}
delete this.panelNode;
return (this.panelNode = document.getElementById("pageActionPanel"));
return (this.panelNode = this._panelNode);
},
/**
@ -64,16 +69,12 @@ var BrowserPageActions = {
* Inits. Call to init.
*/
init() {
this.placeAllActions();
this.placeAllActionsInUrlbar();
this._onPanelShowing = this._onPanelShowing.bind(this);
this.panelNode.addEventListener("popupshowing", this._onPanelShowing);
this.panelNode.addEventListener("popuphiding", () => {
this.mainButtonNode.removeAttribute("open");
});
},
_onPanelShowing() {
this.placeLazyActionsInPanel();
this.initializePanel();
for (let action of PageActions.actionsInPanel(window)) {
let buttonNode = this.panelButtonNodeForActionID(action.id);
action.onShowingInPanel(buttonNode);
@ -93,19 +94,36 @@ var BrowserPageActions = {
_actionsToLazilyPlaceInPanel: [],
/**
* Places all registered actions.
* Places all registered actions in the urlbar.
*/
placeAllActions() {
let panelActions = PageActions.actionsInPanel(window);
for (let action of panelActions) {
this.placeActionInPanel(action);
}
placeAllActionsInUrlbar() {
let urlbarActions = PageActions.actionsInUrlbar(window);
for (let action of urlbarActions) {
this.placeActionInUrlbar(action);
}
},
/**
* Initializes the panel if necessary.
*/
initializePanel() {
// Lazy load the page action panel the first time we need to display it
if (!this._panelNode) {
let template = document.getElementById("pageActionPanelTemplate");
template.replaceWith(template.content);
this._panelNode = document.getElementById("pageActionPanel");
this._panelNode.addEventListener("popupshowing", this._onPanelShowing);
this._panelNode.addEventListener("popuphiding", () => {
this.mainButtonNode.removeAttribute("open");
});
}
for (let action of PageActions.actionsInPanel(window)) {
this.placeActionInPanel(action);
}
this.placeLazyActionsInPanel();
},
/**
* Adds or removes as necessary DOM nodes for the given action.
*
@ -124,7 +142,7 @@ var BrowserPageActions = {
* The action to place.
*/
placeActionInPanel(action) {
if (this.panelNode.state != "closed") {
if (this._panelNode && this.panelNode.state != "closed") {
this._placeActionInPanelNow(action);
} else {
// Lazily place the action in the panel the next time it opens.

View File

@ -551,25 +551,27 @@
<hbox id="ctrlTab-showAll-container" pack="center"/>
</panel>
<panel id="pageActionPanel"
class="cui-widget-panel panel-no-padding"
role="group"
type="arrow"
hidden="true"
flip="slide"
position="bottomcenter topright"
tabspecific="true"
noautofocus="true">
<panelmultiview id="pageActionPanelMultiView"
mainViewId="pageActionPanelMainView"
viewCacheId="appMenu-viewCache">
<panelview id="pageActionPanelMainView"
context="pageActionContextMenu"
class="PanelUI-subView">
<vbox class="panel-subview-body"/>
</panelview>
</panelmultiview>
</panel>
<html:template id="pageActionPanelTemplate">
<panel id="pageActionPanel"
class="cui-widget-panel panel-no-padding"
role="group"
type="arrow"
hidden="true"
flip="slide"
position="bottomcenter topright"
tabspecific="true"
noautofocus="true">
<panelmultiview id="pageActionPanelMultiView"
mainViewId="pageActionPanelMainView"
viewCacheId="appMenu-viewCache">
<panelview id="pageActionPanelMainView"
context="pageActionContextMenu"
class="PanelUI-subView">
<vbox class="panel-subview-body"/>
</panelview>
</panelmultiview>
</panel>
</html:template>
<html:template id="confirmation-hint-wrapper">
<panel id="confirmation-hint"

View File

@ -115,9 +115,9 @@ add_task(async function testPageActionsButtonPress() {
await BrowserTestUtils.withNewTab("https://example.com", async function() {
let button = document.getElementById("pageActionButton");
forceFocus(button);
EventUtils.synthesizeKey(" ");
let view = document.getElementById("pageActionPanelMainView");
let focused = BrowserTestUtils.waitForEvent(view, "focus", true);
EventUtils.synthesizeKey(" ");
await focused;
ok(true, "Focus inside Page Actions menu after toolbar button pressed");
let hidden = BrowserTestUtils.waitForEvent(document, "popuphidden", true);

View File

@ -34,6 +34,45 @@ async function openAndCheckMenu(menu, target) {
menu.hidePopup();
}
async function openAndCheckLazyMenu(id, target) {
let menu = document.getElementById(id);
EventUtils.synthesizeNativeTapAtCenter(target);
let ev = await BrowserTestUtils.waitForEvent(
window,
"popupshown",
true,
e => e.target.id == id
);
menu = ev.target;
is(menu.state, "open", `Menu panel (${menu.id}) is open.`);
is(
menu.getAttribute("touchmode"),
"true",
`Menu panel (${menu.id}) is in touchmode.`
);
menu.hidePopup();
EventUtils.synthesizeNativeTapAtCenter(target);
ev = await BrowserTestUtils.waitForEvent(
window,
"popupshown",
true,
e => e.target.id == id
);
menu = ev.target;
is(menu.state, "open", `Menu panel (${menu.id}) is open.`);
ok(
!menu.hasAttribute("touchmode"),
`Menu panel (${menu.id}) is not in touchmode.`
);
menu.hidePopup();
}
// The customization UI menu is not attached to the document when it is
// closed and hence requires special attention.
async function openAndCheckCustomizationUIMenu(target) {
@ -96,9 +135,8 @@ add_task(async function test_main_menu_touch() {
add_task(async function test_page_action_panel_touch() {
// The page action menu only appears on a web page.
await BrowserTestUtils.withNewTab("https://example.com", async function() {
let pageActionPanel = document.getElementById("pageActionPanel");
let target = document.getElementById("pageActionButton");
await openAndCheckMenu(pageActionPanel, target);
await openAndCheckLazyMenu("pageActionPanel", target);
});
});

View File

@ -55,10 +55,10 @@ async function openSSB(uri) {
async function openSSBFromBrowserWindow(win = window) {
let doc = win.document;
let pageActionButton = doc.getElementById("pageActionButton");
EventUtils.synthesizeMouseAtCenter(pageActionButton, {}, win);
let panel = doc.getElementById("pageActionPanel");
let popupShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
EventUtils.synthesizeMouseAtCenter(pageActionButton, {}, win);
await popupShown;
let openItem = doc.getElementById("pageAction-panel-launchSSB");

View File

@ -372,6 +372,12 @@ var UITour = {
}
case "showHighlight": {
if (data.target.startsWith("pageAction-")) {
// The page action panel is lazily loaded, so we will need to initialize it
// and place actions in the panel before showing the highlight for a panel
// node.
window.BrowserPageActions.initializePanel();
}
let targetPromise = this.getTarget(window, data.target);
targetPromise
.then(target => {

View File

@ -76,11 +76,11 @@ var PageActions = {
// Now place them all in each window. Instead of splitting the register and
// place steps, we could simply call addAction, which does both, but doing
// it this way means that all windows initially place their actions the same
// way -- placeAllActions -- regardless of whether they're open when this
// method is called or opened later.
// it this way means that all windows initially place their actions in the
// urlbar the same way -- placeAllActions -- regardless of whether they're
// open when this method is called or opened later.
for (let bpa of allBrowserPageActions()) {
bpa.placeAllActions();
bpa.placeAllActionsInUrlbar();
}
// These callbacks are deferred until init happens and all built-in actions

View File

@ -19,6 +19,7 @@ skip-if = (debug && os == "linux" && bits == 64 && os_version == "18.04") # Bug
[browser_EveryWindow.js]
[browser_LiveBookmarkMigrator.js]
[browser_PageActions.js]
[browser_PageActions_contextMenus.js]
[browser_PartnerLinkAttribution.js]
support-files =
search-engines/basic/manifest.json

View File

@ -1270,6 +1270,10 @@ add_task(async function perWindowState() {
Assert.equal(action.getTitle(window), newGlobalTitle, "Title: old window");
Assert.equal(action.getTitle(newWindow), newGlobalTitle, "Title: new window");
// Initialize panel nodes in the new window
newWindow.document.getElementById("pageActionButton").click();
await BrowserTestUtils.waitForEvent(newWindow.document, "popupshowing", true);
// The action's panel button nodes should be updated in both windows.
let panelButtonID = BrowserPageActions.panelButtonNodeIDForActionID(
action.id
@ -1509,356 +1513,6 @@ add_task(async function removeRetainState() {
testAction.remove();
});
// Opens the context menu on a non-built-in action. (The context menu for
// built-in actions is tested in browser_page_action_menu.js.)
add_task(async function contextMenu() {
Services.telemetry.clearEvents();
// Add a test action.
let action = PageActions.addAction(
new PageActions.Action({
id: "test-contextMenu",
title: "Test contextMenu",
pinnedToUrlbar: true,
})
);
// Open the panel and then open the context menu on the action's item.
await promiseOpenPageActionPanel();
let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
let contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(panelButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "remove" item and the "manage" item. The
// 4th item is "remove extension" but it is hidden in this test case because
// the page action isn't bound to an addon. Click the "remove" item.
let menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Remove from Address Bar",
"Context menu is in the 'remove' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
await contextMenuPromise;
// The action should be removed from the urlbar.
await BrowserTestUtils.waitForCondition(() => {
return !BrowserPageActions.urlbarButtonNodeForActionID(action.id);
}, "Waiting for urlbar button to be removed");
// Open the context menu again on the action's button in the panel. (The
// panel should still be open.)
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(panelButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "add" item and the "manage" item. The 4th
// item is "remove extension" but it is hidden. Click the "add" item.
menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Add to Address Bar",
"Context menu is in the 'add' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
await contextMenuPromise;
// The action should be added back to the urlbar.
await BrowserTestUtils.waitForCondition(() => {
return BrowserPageActions.urlbarButtonNodeForActionID(action.id);
}, "Waiting for urlbar button to be added back");
// Open the context menu again on the action's button in the panel. (The
// panel should still be open.)
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(panelButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "remove" item and the "manage" item. The
// 4th item is "remove extension" but it is hidden. Click the "manage" item.
// about:addons should open.
menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Remove from Address Bar",
"Context menu is in the 'remove' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
// Click the "manage" item, about:addons should open.
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
let aboutAddonsPromise = BrowserTestUtils.waitForNewTab(
gBrowser,
"about:addons"
);
EventUtils.synthesizeMouseAtCenter(menuItems[2], {});
let values = await Promise.all([aboutAddonsPromise, contextMenuPromise]);
let aboutAddonsTab = values[0];
BrowserTestUtils.removeTab(aboutAddonsTab);
// Open the context menu on the action's urlbar button.
let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(action.id);
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(urlbarButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "remove" item and the "manage" item. The
// 4th item is "remove extension" but it is hidden. Click the "manage" item.
// about:addons should open.
menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Remove from Address Bar",
"Context menu is in the 'remove' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
await contextMenuPromise;
// The action should be removed from the urlbar.
await BrowserTestUtils.waitForCondition(() => {
return !BrowserPageActions.urlbarButtonNodeForActionID(action.id);
}, "Waiting for urlbar button to be removed");
// Open the panel and then open the context menu on the action's item.
await promiseOpenPageActionPanel();
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(panelButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "remove" item and the "manage" item. The
// 4th item is "remove extension" but it is hidden. Click the "remove" item.
menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Add to Address Bar",
"Context menu is in the 'add' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
await contextMenuPromise;
// The action should be added back to the urlbar.
await BrowserTestUtils.waitForCondition(() => {
return BrowserPageActions.urlbarButtonNodeForActionID(action.id);
}, "Waiting for urlbar button to be added back");
// Open the context menu on the action's urlbar button.
urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(action.id);
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(urlbarButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "add" item and the "manage" item. The 4th
// item is "remove extension" but it is hidden. Click the "add" item.
menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Remove from Address Bar",
"Context menu is in the 'remove' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
// Click the "manage" item, about:addons should open.
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
aboutAddonsPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
EventUtils.synthesizeMouseAtCenter(menuItems[2], {});
values = await Promise.all([aboutAddonsPromise, contextMenuPromise]);
aboutAddonsTab = values[0];
BrowserTestUtils.removeTab(aboutAddonsTab);
// Done, clean up.
action.remove();
// Check the telemetry was collected properly.
let snapshot = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
true
);
ok(
snapshot.parent && !!snapshot.parent.length,
"Got parent telemetry events in the snapshot"
);
let relatedEvents = snapshot.parent
.filter(
([timestamp, category, method]) =>
category == "addonsManager" && method == "action"
)
.map(relatedEvent => relatedEvent.slice(3, 6));
Assert.deepEqual(relatedEvents, [
["pageAction", null, { action: "manage" }],
["pageAction", null, { action: "manage" }],
]);
// 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();
});
// The context menu shouldn't open on separators in the panel.
add_task(async function contextMenuOnSeparator() {
// Open the panel and get the bookmark separator.
await promiseOpenPageActionPanel();
let separator = BrowserPageActions.panelButtonNodeForActionID(
PageActions.ACTION_ID_BOOKMARK_SEPARATOR
);
Assert.ok(separator, "The bookmark separator should be in the panel");
// Context-click it. popupshowing should be fired, but by the time the event
// reaches this listener, preventDefault should have been called on it.
let showingPromise = BrowserTestUtils.waitForEvent(
document.getElementById("pageActionContextMenu"),
"popupshowing",
false
);
EventUtils.synthesizeMouseAtCenter(separator, {
type: "contextmenu",
button: 2,
});
let event = await showingPromise;
Assert.ok(
event.defaultPrevented,
"defaultPrevented should be true on popupshowing event"
);
// Click the main button to hide the main panel.
EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
await promisePageActionPanelHidden();
// 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();
});
// Tests transient actions.
add_task(async function transient() {
let initialActionsInPanel = PageActions.actionsInPanel(window);
@ -2144,117 +1798,3 @@ add_task(async function action_disablePrivateBrowsing() {
privateWindow.close();
});
function assertActivatedPageActionPanelHidden() {
Assert.ok(
!document.getElementById(BrowserPageActions._activatedActionPanelID)
);
}
function promiseOpenPageActionPanel() {
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, {});
return shownPromise;
})
.then(() => {
// Wait for items in the panel to become visible.
return promisePageActionViewChildrenVisible(
BrowserPageActions.mainViewNode
);
});
}
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) {
info(
"promisePageActionViewChildrenVisible waiting for a child node to be visible"
);
let dwu = window.windowUtils;
return BrowserTestUtils.waitForCondition(() => {
let bodyNode = panelViewNode.firstElementChild;
for (let childNode of bodyNode.children) {
let bounds = dwu.getBoundsWithoutFlushing(childNode);
if (bounds.width > 0 && bounds.height > 0) {
return true;
}
}
return false;
});
}
function collectContextMenuItems() {
let contextMenu = document.getElementById("pageActionContextMenu");
return Array.prototype.filter.call(contextMenu.children, node => {
return window.getComputedStyle(node).visibility == "visible";
});
}

View File

@ -0,0 +1,380 @@
"use strict";
// This is a test for PageActions.jsm, specifically the context menus.
// 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/",
});
await disableNonReleaseActions();
registerCleanupFunction(async () => {
BrowserTestUtils.removeTab(tab);
});
// Ensure screenshots is really disabled (bug 1498738)
const addon = await AddonManager.getAddonByID("screenshots@mozilla.org");
await addon.disable({ allowSystemAddons: true });
});
// Opens the context menu on a non-built-in action. (The context menu for
// built-in actions is tested in browser_page_action_menu.js.)
add_task(async function contextMenu() {
Services.telemetry.clearEvents();
// Add a test action.
let action = PageActions.addAction(
new PageActions.Action({
id: "test-contextMenu",
title: "Test contextMenu",
pinnedToUrlbar: true,
})
);
// Open the panel and then open the context menu on the action's item.
await promiseOpenPageActionPanel();
let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
let contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(panelButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "remove" item and the "manage" item. The
// 4th item is "remove extension" but it is hidden in this test case because
// the page action isn't bound to an addon. Click the "remove" item.
let menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Remove from Address Bar",
"Context menu is in the 'remove' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
await contextMenuPromise;
// The action should be removed from the urlbar.
await BrowserTestUtils.waitForCondition(() => {
return !BrowserPageActions.urlbarButtonNodeForActionID(action.id);
}, "Waiting for urlbar button to be removed");
// Open the context menu again on the action's button in the panel. (The
// panel should still be open.)
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(panelButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "add" item and the "manage" item. The 4th
// item is "remove extension" but it is hidden. Click the "add" item.
menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Add to Address Bar",
"Context menu is in the 'add' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
await contextMenuPromise;
// The action should be added back to the urlbar.
await BrowserTestUtils.waitForCondition(() => {
return BrowserPageActions.urlbarButtonNodeForActionID(action.id);
}, "Waiting for urlbar button to be added back");
// Open the context menu again on the action's button in the panel. (The
// panel should still be open.)
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(panelButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "remove" item and the "manage" item. The
// 4th item is "remove extension" but it is hidden. Click the "manage" item.
// about:addons should open.
menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Remove from Address Bar",
"Context menu is in the 'remove' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
// Click the "manage" item, about:addons should open.
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
let aboutAddonsPromise = BrowserTestUtils.waitForNewTab(
gBrowser,
"about:addons"
);
EventUtils.synthesizeMouseAtCenter(menuItems[2], {});
let values = await Promise.all([aboutAddonsPromise, contextMenuPromise]);
let aboutAddonsTab = values[0];
BrowserTestUtils.removeTab(aboutAddonsTab);
// Open the context menu on the action's urlbar button.
let urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(action.id);
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(urlbarButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "remove" item and the "manage" item. The
// 4th item is "remove extension" but it is hidden. Click the "manage" item.
// about:addons should open.
menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Remove from Address Bar",
"Context menu is in the 'remove' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
await contextMenuPromise;
// The action should be removed from the urlbar.
await BrowserTestUtils.waitForCondition(() => {
return !BrowserPageActions.urlbarButtonNodeForActionID(action.id);
}, "Waiting for urlbar button to be removed");
// Open the panel and then open the context menu on the action's item.
await promiseOpenPageActionPanel();
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(panelButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "remove" item and the "manage" item. The
// 4th item is "remove extension" but it is hidden. Click the "remove" item.
menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Add to Address Bar",
"Context menu is in the 'add' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(menuItems[0], {});
await contextMenuPromise;
// The action should be added back to the urlbar.
await BrowserTestUtils.waitForCondition(() => {
return BrowserPageActions.urlbarButtonNodeForActionID(action.id);
}, "Waiting for urlbar button to be added back");
// Open the context menu on the action's urlbar button.
urlbarButton = BrowserPageActions.urlbarButtonNodeForActionID(action.id);
contextMenuPromise = promisePanelShown("pageActionContextMenu");
EventUtils.synthesizeMouseAtCenter(urlbarButton, {
type: "contextmenu",
button: 2,
});
await contextMenuPromise;
// The context menu should show the "add" item and the "manage" item. The 4th
// item is "remove extension" but it is hidden. Click the "add" item.
menuItems = collectContextMenuItems();
Assert.equal(menuItems.length, 4, "Context menu has 4 children");
Assert.equal(
menuItems[0].label,
"Remove from Address Bar",
"Context menu is in the 'remove' state"
);
Assert.equal(
menuItems[1].localName,
"menuseparator",
"menuseparator is present"
);
Assert.equal(
menuItems[2].label,
"Manage Extension\u2026",
"'Manage' item is present"
);
Assert.equal(
menuItems[3].label,
"Remove Extension",
"'Remove' item is present"
);
Assert.ok(menuItems[3].hidden, "'Remove' item is hidden");
// Click the "manage" item, about:addons should open.
contextMenuPromise = promisePanelHidden("pageActionContextMenu");
aboutAddonsPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:addons");
EventUtils.synthesizeMouseAtCenter(menuItems[2], {});
values = await Promise.all([aboutAddonsPromise, contextMenuPromise]);
aboutAddonsTab = values[0];
BrowserTestUtils.removeTab(aboutAddonsTab);
// Done, clean up.
action.remove();
// Check the telemetry was collected properly.
let snapshot = Services.telemetry.snapshotEvents(
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
true
);
ok(
snapshot.parent && !!snapshot.parent.length,
"Got parent telemetry events in the snapshot"
);
let relatedEvents = snapshot.parent
.filter(
([timestamp, category, method]) =>
category == "addonsManager" && method == "action"
)
.map(relatedEvent => relatedEvent.slice(3, 6));
Assert.deepEqual(relatedEvents, [
["pageAction", null, { action: "manage" }],
["pageAction", null, { action: "manage" }],
]);
// 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();
});
// The context menu shouldn't open on separators in the panel.
add_task(async function contextMenuOnSeparator() {
// Open the panel and get the bookmark separator.
await promiseOpenPageActionPanel();
let separator = BrowserPageActions.panelButtonNodeForActionID(
PageActions.ACTION_ID_BOOKMARK_SEPARATOR
);
Assert.ok(separator, "The bookmark separator should be in the panel");
// Context-click it. popupshowing should be fired, but by the time the event
// reaches this listener, preventDefault should have been called on it.
let showingPromise = BrowserTestUtils.waitForEvent(
document.getElementById("pageActionContextMenu"),
"popupshowing",
false
);
EventUtils.synthesizeMouseAtCenter(separator, {
type: "contextmenu",
button: 2,
});
let event = await showingPromise;
Assert.ok(
event.defaultPrevented,
"defaultPrevented should be true on popupshowing event"
);
// Click the main button to hide the main panel.
EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
await promisePageActionPanelHidden();
// 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 collectContextMenuItems() {
let contextMenu = document.getElementById("pageActionContextMenu");
return Array.prototype.filter.call(contextMenu.children, node => {
return window.getComputedStyle(node).visibility == "visible";
});
}

View File

@ -212,3 +212,110 @@ async function disableNonReleaseActions() {
);
}
}
function assertActivatedPageActionPanelHidden() {
Assert.ok(
!document.getElementById(BrowserPageActions._activatedActionPanelID)
);
}
function promiseOpenPageActionPanel() {
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, {});
return shownPromise;
})
.then(() => {
// Wait for items in the panel to become visible.
return promisePageActionViewChildrenVisible(
BrowserPageActions.mainViewNode
);
});
}
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) {
info(
"promisePageActionViewChildrenVisible waiting for a child node to be visible"
);
let dwu = window.windowUtils;
return BrowserTestUtils.waitForCondition(() => {
let bodyNode = panelViewNode.firstElementChild;
for (let childNode of bodyNode.children) {
let bounds = dwu.getBoundsWithoutFlushing(childNode);
if (bounds.width > 0 && bounds.height > 0) {
return true;
}
}
return false;
});
}