--- a/browser/modules/PageActions.jsm
+++ b/browser/modules/PageActions.jsm
@@ -5,89 +5,96 @@
"use strict";
this.EXPORTED_SYMBOLS = [
"PageActions",
// PageActions.Action
// PageActions.Button
// PageActions.Subview
// PageActions.ACTION_ID_BOOKMARK_SEPARATOR
+ // PageActions.ACTION_ID_BUILT_IN_SEPARATOR
];
const { utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch",
"resource://gre/modules/BinarySearch.jsm");
const ACTION_ID_BOOKMARK_SEPARATOR = "bookmarkSeparator";
const ACTION_ID_BUILT_IN_SEPARATOR = "builtInSeparator";
-const PREF_ACTION_IDS_IN_URLBAR = "browser.pageActions.actionIDsInUrlbar";
+const PREF_PERSISTED_ACTIONS = "browser.pageActions.persistedActions";
this.PageActions = {
/**
* Inits. Call to init.
*/
init() {
let callbacks = this._deferredAddActionCalls;
delete this._deferredAddActionCalls;
- let actionIDsInUrlbar = this._loadActionIDsInUrlbar();
- this._actionIDsInUrlbar = actionIDsInUrlbar || [];
+ this._loadPersistedActions();
// Add the built-in actions, which are defined below in this file.
for (let options of gBuiltInActions) {
if (options._isSeparator || !this.actionForID(options.id)) {
this.addAction(new Action(options));
}
}
// These callbacks are deferred until init happens and all built-in actions
// are added.
while (callbacks && callbacks.length) {
callbacks.shift()();
}
-
- if (!actionIDsInUrlbar) {
- // The action IDs in the urlbar haven't been stored yet. Compute them and
- // store them now. From now on, these stored IDs will determine the
- // actions that are shown in the urlbar and the order they're shown in.
- this._actionIDsInUrlbar =
- this.actions.filter(a => a.shownInUrlbar).map(a => a.id);
- this._storeActionIDsInUrlbar();
- }
},
_deferredAddActionCalls: [],
/**
* The list of Action objects, sorted in the order in which they should be
- * placed in the page action panel. Not live. (array of Action objects)
+ * placed in the page action panel. If there are both built-in and non-built-
+ * in actions, then the list will include the separator between the two. The
+ * list is not live. (array of Action objects)
*/
get actions() {
- let actions = this._builtInActions.slice();
- if (this._nonBuiltInActions.length) {
+ let actions = this.builtInActions;
+ if (this.nonBuiltInActions.length) {
// There are non-built-in actions, so include them too. Add a separator
// between the built-ins and non-built-ins so that the returned array
// looks like: [...built-ins, separator, ...non-built-ins]
actions.push(new Action({
id: ACTION_ID_BUILT_IN_SEPARATOR,
_isSeparator: true,
}));
- actions.push(...this._nonBuiltInActions);
+ actions.push(...this.nonBuiltInActions);
}
return actions;
},
/**
+ * The list of built-in actions. Not live. (array of Action objects)
+ */
+ get builtInActions() {
+ return this._builtInActions.slice();
+ },
+
+ /**
+ * The list of non-built-in actions. Not live. (array of Action objects)
+ */
+ get nonBuiltInActions() {
+ return this._nonBuiltInActions.slice();
+ },
+
+ /**
* Gets an action.
*
* @param id (string, required)
* The ID of the action to get.
* @return The Action object, or null if none.
*/
actionForID(id) {
return this._actionsByID.get(id);
@@ -174,21 +181,33 @@ this.PageActions = {
// If this is the first non-built-in, then the built-in separator must
// be placed between the built-ins and non-built-ins.
if (!this._nonBuiltInActions.length) {
placeBuiltInSeparator = true;
}
this._nonBuiltInActions.splice(index, 0, action);
}
- if (this._actionIDsInUrlbar.includes(action.id)) {
- // The action should be shown in the urlbar. Set its shownInUrlbar to
- // true, but set the private version so that onActionToggledShownInUrlbar
- // isn't called, which happens when the public version is set.
- action._shownInUrlbar = true;
+ if (this._persistedActions.ids[action.id]) {
+ // The action has been seen before. Override its shownInUrlbar value
+ // with the persisted value. Set the private version of that property
+ // so that onActionToggledShownInUrlbar isn't called, which happens when
+ // the public version is set.
+ action._shownInUrlbar =
+ this._persistedActions.idsInUrlbar.includes(action.id);
+ } else {
+ // The action is new. Store it in the persisted actions.
+ this._persistedActions.ids[action.id] = true;
+ if (action.shownInUrlbar) {
+ this._persistedActions.idsInUrlbar.push(action.id);
+ }
+ this._storePersistedActions();
+ }
+
+ if (action.shownInUrlbar) {
urlbarInsertBeforeID = this.insertBeforeActionIDInUrlbar(action);
}
}
for (let win of browserWindows()) {
if (placeBuiltInSeparator) {
let sep = new Action({
id: ACTION_ID_BUILT_IN_SEPARATOR,
@@ -213,25 +232,26 @@ this.PageActions = {
* the given action's shownInUrlbar is true or false.
*
* @return The ID of the action before which the given action should be
* inserted. If the given action should be inserted last or it should
* not be inserted at all, returns null.
*/
insertBeforeActionIDInUrlbar(action) {
// First, find the index of the given action.
- let index = this._actionIDsInUrlbar.findIndex(a => a.id == action.id);
+ let idsInUrlbar = this._persistedActions.idsInUrlbar;
+ let index = idsInUrlbar.indexOf(action.id);
if (index < 0) {
return null;
}
// Now start at the next index and find the ID of the first action that's
- // currently registered. Remember that IDs in _actionIDsInUrlbar may belong
- // to actions that aren't currently registered.
- for (let i = index + 1; i < this._actionIDsInUrlbar.length; i++) {
- let id = this._actionIDsInUrlbar[i];
+ // currently registered. Remember that IDs in idsInUrlbar may belong to
+ // actions that aren't currently registered.
+ for (let i = index + 1; i < idsInUrlbar.length; i++) {
+ let id = idsInUrlbar[i];
if (this.actionForID(id)) {
return id;
}
}
return null;
},
/**
@@ -240,25 +260,34 @@ this.PageActions = {
* @param action (Action object, required)
* The action that was removed.
*/
onActionRemoved(action) {
if (!this.actionForID(action.id)) {
// The action isn't present. Not an error.
return;
}
+
this._actionsByID.delete(action.id);
for (let list of [this._nonBuiltInActions, this._builtInActions]) {
let index = list.findIndex(a => a.id == action.id);
if (index >= 0) {
list.splice(index, 1);
break;
}
}
- this._updateActionIDsInUrlbar(action.id, false);
+
+ // Remove the action from persisted storage.
+ delete this._persistedActions.ids[action.id];
+ let index = this._persistedActions.idsInUrlbar.indexOf(action.id);
+ if (index >= 0) {
+ this._persistedActions.idsInUrlbar.splice(index, 1);
+ }
+ this._storePersistedActions();
+
for (let win of browserWindows()) {
browserPageActions(win).removeAction(action);
}
},
/**
* Call this when an action's iconURL changes.
*
@@ -297,61 +326,52 @@ this.PageActions = {
* @param action (Action object, required)
* The action whose shownInUrlbar property changed.
*/
onActionToggledShownInUrlbar(action) {
if (!this.actionForID(action.id)) {
// This may be called before the action has been added.
return;
}
- this._updateActionIDsInUrlbar(action.id, action.shownInUrlbar);
+
+ // Update persisted storage.
+ let index = this._persistedActions.idsInUrlbar.indexOf(action.id);
+ if (action.shownInUrlbar) {
+ if (index < 0) {
+ this._persistedActions.idsInUrlbar.push(action.id);
+ }
+ } else if (index >= 0) {
+ this._persistedActions.idsInUrlbar.splice(index, 1);
+ }
+ this._storePersistedActions();
+
let insertBeforeID = this.insertBeforeActionIDInUrlbar(action);
for (let win of browserWindows()) {
browserPageActions(win).placeActionInUrlbar(action, insertBeforeID);
}
},
- /**
- * Adds or removes the given action ID to or from _actionIDsInUrlbar.
- *
- * @param actionID (string, required)
- * The action ID to add or remove.
- * @param shownInUrlbar (bool, required)
- * If true, the ID is added if it's not already present. If false,
- * the ID is removed if it's not already absent.
- */
- _updateActionIDsInUrlbar(actionID, shownInUrlbar) {
- let index = this._actionIDsInUrlbar.indexOf(actionID);
- if (shownInUrlbar) {
- if (index < 0) {
- this._actionIDsInUrlbar.push(actionID);
- }
- } else if (index >= 0) {
- this._actionIDsInUrlbar.splice(index, 1);
- }
- this._storeActionIDsInUrlbar();
+ _storePersistedActions() {
+ let json = JSON.stringify(this._persistedActions);
+ Services.prefs.setStringPref(PREF_PERSISTED_ACTIONS, json);
},
- _storeActionIDsInUrlbar() {
- let json = JSON.stringify(this._actionIDsInUrlbar);
- Services.prefs.setStringPref(PREF_ACTION_IDS_IN_URLBAR, json);
+ _loadPersistedActions() {
+ try {
+ let json = Services.prefs.getStringPref(PREF_PERSISTED_ACTIONS);
+ this._persistedActions = JSON.parse(json);
+ } catch (ex) {}
},
- _loadActionIDsInUrlbar() {
- try {
- let json = Services.prefs.getStringPref(PREF_ACTION_IDS_IN_URLBAR);
- let obj = JSON.parse(json);
- if (Array.isArray(obj)) {
- return obj;
- }
- } catch (ex) {}
- return null;
+ _persistedActions: {
+ // action ID => true, for actions that have ever been seen and not removed
+ ids: {},
+ // action IDs ordered by position in urlbar
+ idsInUrlbar: [],
},
-
- _actionIDsInUrlbar: []
};
/**
* A single page action.
*
* @param options (object, required)
* An object with the following properties:
* @param id (string, required)
@@ -590,19 +610,22 @@ Action.prototype = {
*/
onShowingInPanel(buttonNode) {
if (this._onShowingInPanel) {
this._onShowingInPanel(buttonNode);
}
},
/**
- * Unregisters the action and removes its DOM nodes from all browser windows.
- * Call this when your action lives in an extension and your extension is
- * unloaded, for example.
+ * Makes PageActions forget about this action and removes its DOM nodes from
+ * all browser windows. Call this when the user removes your action, like
+ * when your extension is uninstalled. You probably don't want to call it
+ * simply when your extension is disabled or the app quits, because then
+ * PageActions won't remember it the next time your extension is enabled or
+ * the app starts.
*/
remove() {
PageActions.onActionRemoved(this);
}
};
this.PageActions.Action = Action;
@@ -749,20 +772,23 @@ Button.prototype = {
this._onCommand(event, buttonNode);
}
}
};
this.PageActions.Button = Button;
-// This is only necessary so that Pocket can specify it for
+// This is only necessary so that Pocket and the test can specify it for
// action._insertBeforeActionID.
this.PageActions.ACTION_ID_BOOKMARK_SEPARATOR = ACTION_ID_BOOKMARK_SEPARATOR;
+// This is only necessary so that the test can access it.
+this.PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR;
+
// Sorted in the order in which they should appear in the page action panel.
// Does not include the page actions of extensions bundled with the browser.
// They're added by the relevant extension code.
var gBuiltInActions = [
// bookmark
{
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser/browser_PageActions.js
@@ -0,0 +1,703 @@
+"use strict";
+
+// This is a test for PageActions.jsm, specifically the generalized parts that
+// add and remove page actions and toggle them in the urlbar. This does not
+// test the built-in page actions; browser_page_action_menu.js does that.
+
+// 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/",
+ });
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.removeTab(tab);
+ });
+});
+
+
+// Tests a simple non-built-in action without an iframe or subview. Also
+// thoroughly checks most of the action's properties, methods, and DOM nodes, so
+// it's not necessary to do that in general in other test tasks.
+add_task(async function simple() {
+ let iconURL = "chrome://browser/skin/email-link.svg";
+ let id = "test-simple";
+ let nodeAttributes = {
+ "test-attr": "test attr value",
+ };
+ let title = "Test simple";
+ let tooltip = "Test simple tooltip";
+
+ let onCommandCallCount = 0;
+ let onPlacedInPanelCallCount = 0;
+ let onPlacedInUrlbarCallCount = 0;
+ let onShowingInPanelCallCount = 0;
+ let onCommandExpectedButtonID;
+
+ let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id);
+ let urlbarButtonID = BrowserPageActions._urlbarButtonNodeIDForActionID(id);
+
+ let initialActions = PageActions.actions;
+
+ let action = PageActions.addAction(new PageActions.Action({
+ iconURL,
+ id,
+ nodeAttributes,
+ title,
+ tooltip,
+ onCommand(event, buttonNode) {
+ onCommandCallCount++;
+ Assert.ok(event, "event should be non-null: " + event);
+ Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+ Assert.equal(buttonNode.id, onCommandExpectedButtonID, "buttonNode.id");
+ },
+ onPlacedInPanel(buttonNode) {
+ onPlacedInPanelCallCount++;
+ Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+ Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
+ },
+ onPlacedInUrlbar(buttonNode) {
+ onPlacedInUrlbarCallCount++;
+ Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+ Assert.equal(buttonNode.id, urlbarButtonID, "buttonNode.id");
+ },
+ onShowingInPanel(buttonNode) {
+ onShowingInPanelCallCount++;
+ Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+ Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
+ },
+ }));
+
+ Assert.equal(action.iconURL, iconURL, "iconURL");
+ Assert.equal(action.id, id, "id");
+ Assert.deepEqual(action.nodeAttributes, nodeAttributes, "nodeAttributes");
+ Assert.equal(action.shownInUrlbar, false, "shownInUrlbar");
+ Assert.equal(action.subview, null, "subview");
+ Assert.equal(action.title, title, "title");
+ Assert.equal(action.tooltip, tooltip, "tooltip");
+ Assert.equal(action.urlbarIDOverride, null, "urlbarIDOverride");
+ Assert.equal(action.wantsIframe, false, "wantsIframe");
+
+ Assert.ok(!("__insertBeforeActionID" in action), "__insertBeforeActionID");
+ Assert.ok(!("__isSeparator" in action), "__isSeparator");
+ Assert.ok(!("__urlbarNodeInMarkup" in action), "__urlbarNodeInMarkup");
+
+ Assert.equal(onPlacedInPanelCallCount, 1,
+ "onPlacedInPanelCallCount should be inc'ed");
+ Assert.equal(onPlacedInUrlbarCallCount, 0,
+ "onPlacedInUrlbarCallCount should remain 0");
+ Assert.equal(onShowingInPanelCallCount, 0,
+ "onShowingInPanelCallCount should remain 0");
+
+ // The separator between the built-in and non-built-in actions should have
+ // been created and included in PageActions.actions, which is why the new
+ // count should be the initial count + 2, not + 1.
+ Assert.equal(PageActions.actions.length, initialActions.length + 2,
+ "PageActions.actions.length should be updated");
+ Assert.deepEqual(PageActions.actions[PageActions.actions.length - 1], action,
+ "Last page action should be action");
+ Assert.equal(PageActions.actions[PageActions.actions.length - 2].id,
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+ "2nd-to-last page action should be separator");
+
+ Assert.deepEqual(PageActions.actionForID(action.id), action,
+ "actionForID should be action");
+
+ // The action's panel button should have been created.
+ let panelButtonNode = document.getElementById(panelButtonID);
+ Assert.notEqual(panelButtonNode, null, "panelButtonNode");
+ Assert.equal(panelButtonNode.getAttribute("label"), action.title, "label");
+ for (let name in action.nodeAttributes) {
+ Assert.ok(panelButtonNode.hasAttribute(name), "Has attribute: " + name);
+ Assert.equal(panelButtonNode.getAttribute(name),
+ action.nodeAttributes[name],
+ "Equal attribute: " + name);
+ }
+
+ // The panel button should be the last node in the panel, and its previous
+ // sibling should be the separator between the built-in actions and non-built-
+ // in actions.
+ Assert.equal(panelButtonNode.nextSibling, null, "nextSibling");
+ Assert.notEqual(panelButtonNode.previousSibling, null, "previousSibling");
+ Assert.equal(
+ panelButtonNode.previousSibling.id,
+ BrowserPageActions._panelButtonNodeIDForActionID(
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+ ),
+ "previousSibling.id"
+ );
+
+ // The action's urlbar button should not have been created.
+ let urlbarButtonNode = document.getElementById(urlbarButtonID);
+ Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
+
+ // Open the panel, click the action's button.
+ await promisePageActionPanelOpen();
+ Assert.equal(onShowingInPanelCallCount, 1,
+ "onShowingInPanelCallCount should be inc'ed");
+ onCommandExpectedButtonID = panelButtonID;
+ EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
+ await promisePageActionPanelHidden();
+ Assert.equal(onCommandCallCount, 1, "onCommandCallCount should be inc'ed");
+
+ // Show the action's button in the urlbar.
+ action.shownInUrlbar = true;
+ Assert.equal(onPlacedInUrlbarCallCount, 1,
+ "onPlacedInUrlbarCallCount should be inc'ed");
+ urlbarButtonNode = document.getElementById(urlbarButtonID);
+ Assert.notEqual(urlbarButtonNode, null, "urlbarButtonNode");
+ for (let name in action.nodeAttributes) {
+ Assert.ok(urlbarButtonNode.hasAttribute(name), name,
+ "Has attribute: " + name);
+ Assert.equal(urlbarButtonNode.getAttribute(name),
+ action.nodeAttributes[name],
+ "Equal attribute: " + name);
+ }
+ onCommandExpectedButtonID = urlbarButtonID;
+ EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
+ Assert.equal(onCommandCallCount, 2, "onCommandCallCount should be inc'ed");
+
+ // Set a new title.
+ let newTitle = title + " new title";
+ action.title = newTitle;
+ Assert.equal(action.title, newTitle, "New title");
+ Assert.equal(panelButtonNode.getAttribute("label"), action.title, "New label");
+
+ // Remove the action.
+ action.remove();
+ panelButtonNode = document.getElementById(panelButtonID);
+ Assert.equal(panelButtonNode, null, "panelButtonNode");
+ urlbarButtonNode = document.getElementById(urlbarButtonID);
+ Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
+
+ Assert.deepEqual(PageActions.actions, initialActions,
+ "Actions should go back to initial");
+ Assert.equal(PageActions.actionForID(action.id), null,
+ "actionForID should be null");
+
+ // The separator between the built-in actions and non-built-in actions should
+ // be gone now, too.
+ let separatorNode = document.getElementById(
+ BrowserPageActions._panelButtonNodeIDForActionID(
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+ )
+ );
+ Assert.equal(separatorNode, null, "No separator");
+ Assert.ok(!BrowserPageActions.mainViewBodyNode
+ .lastChild.localName.includes("separator"),
+ "Last child should not be separator");
+});
+
+
+// Tests a non-built-in action with a subview.
+add_task(async function withSubview() {
+ let id = "test-subview";
+
+ let onActionCommandCallCount = 0;
+ let onActionPlacedInPanelCallCount = 0;
+ let onActionPlacedInUrlbarCallCount = 0;
+ let onSubviewPlacedCount = 0;
+ let onSubviewShowingCount = 0;
+ let onButtonCommandCallCount = 0;
+
+ let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id);
+ let urlbarButtonID = BrowserPageActions._urlbarButtonNodeIDForActionID(id);
+
+ let panelViewIDPanel =
+ BrowserPageActions._panelViewNodeIDForActionID(id, false);
+ let panelViewIDUrlbar =
+ BrowserPageActions._panelViewNodeIDForActionID(id, true);
+
+ let onSubviewPlacedExpectedPanelViewID = panelViewIDPanel;
+ let onSubviewShowingExpectedPanelViewID;
+ let onButtonCommandExpectedButtonID;
+
+ let subview = {
+ buttons: [0, 1, 2].map(index => {
+ return {
+ id: "test-subview-button-" + index,
+ title: "Test subview Button " + index,
+ };
+ }),
+ onPlaced(panelViewNode) {
+ onSubviewPlacedCount++;
+ Assert.ok(panelViewNode,
+ "panelViewNode should be non-null: " + panelViewNode);
+ Assert.equal(panelViewNode.id, onSubviewPlacedExpectedPanelViewID,
+ "panelViewNode.id");
+ },
+ onShowing(panelViewNode) {
+ onSubviewShowingCount++;
+ Assert.ok(panelViewNode,
+ "panelViewNode should be non-null: " + panelViewNode);
+ Assert.equal(panelViewNode.id, onSubviewShowingExpectedPanelViewID,
+ "panelViewNode.id");
+ },
+ };
+ subview.buttons[0].onCommand = (event, buttonNode) => {
+ onButtonCommandCallCount++;
+ Assert.ok(event, "event should be non-null: " + event);
+ Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+ Assert.equal(buttonNode.id, onButtonCommandExpectedButtonID,
+ "buttonNode.id");
+ for (let node = buttonNode.parentNode; node; node = node.parentNode) {
+ if (node.localName == "panel") {
+ node.hidePopup();
+ break;
+ }
+ }
+ };
+
+ let action = PageActions.addAction(new PageActions.Action({
+ iconURL: "chrome://browser/skin/email-link.svg",
+ id,
+ shownInUrlbar: true,
+ subview,
+ title: "Test subview",
+ onCommand(event, buttonNode) {
+ onActionCommandCallCount++;
+ },
+ onPlacedInPanel(buttonNode) {
+ onActionPlacedInPanelCallCount++;
+ Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+ Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
+ },
+ onPlacedInUrlbar(buttonNode) {
+ onActionPlacedInUrlbarCallCount++;
+ Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+ Assert.equal(buttonNode.id, urlbarButtonID, "buttonNode.id");
+ },
+ }));
+
+ let panelViewButtonIDPanel =
+ BrowserPageActions._panelViewButtonNodeIDForActionID(
+ id, subview.buttons[0].id, false
+ );
+ let panelViewButtonIDUrlbar =
+ BrowserPageActions._panelViewButtonNodeIDForActionID(
+ id, subview.buttons[0].id, true
+ );
+
+ Assert.equal(action.id, id, "id");
+ Assert.notEqual(action.subview, null, "subview");
+ Assert.notEqual(action.subview.buttons, null, "subview.buttons");
+ Assert.equal(action.subview.buttons.length, subview.buttons.length,
+ "subview.buttons.length");
+ for (let i = 0; i < subview.buttons.length; i++) {
+ Assert.equal(action.subview.buttons[i].id, subview.buttons[i].id,
+ "subview button id for index: " + i);
+ Assert.equal(action.subview.buttons[i].title, subview.buttons[i].title,
+ "subview button title for index: " + i);
+ }
+
+ Assert.equal(onActionPlacedInPanelCallCount, 1,
+ "onActionPlacedInPanelCallCount should be inc'ed");
+ Assert.equal(onActionPlacedInUrlbarCallCount, 1,
+ "onActionPlacedInUrlbarCallCount should be inc'ed");
+ Assert.equal(onSubviewPlacedCount, 1,
+ "onSubviewPlacedCount should be inc'ed");
+ Assert.equal(onSubviewShowingCount, 0,
+ "onSubviewShowingCount should remain 0");
+
+ // The action's panel button and view (in the main page action panel) should
+ // have been created.
+ let panelButtonNode = document.getElementById(panelButtonID);
+ Assert.notEqual(panelButtonNode, null, "panelButtonNode");
+ let panelViewButtonNodePanel =
+ document.getElementById(panelViewButtonIDPanel);
+ Assert.notEqual(panelViewButtonNodePanel, null, "panelViewButtonNodePanel");
+
+ // The action's urlbar button should have been created.
+ let urlbarButtonNode = document.getElementById(urlbarButtonID);
+ Assert.notEqual(urlbarButtonNode, null, "urlbarButtonNode");
+
+ // Open the panel, click the action's button, click the subview's first
+ // button.
+ await promisePageActionPanelOpen();
+ Assert.equal(onSubviewShowingCount, 0,
+ "onSubviewShowingCount should remain 0");
+ let subviewShownPromise = promisePageActionViewShown();
+ onSubviewShowingExpectedPanelViewID = panelViewIDPanel;
+
+ // synthesizeMouse often cannot seem to click the right node when used on
+ // buttons that show subviews and buttons inside subviews. That's why we're
+ // using node.click() twice here: the first time to show the subview, the
+ // second time to click a button in the subview.
+// EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
+ panelButtonNode.click();
+ await subviewShownPromise;
+ Assert.equal(onActionCommandCallCount, 0,
+ "onActionCommandCallCount should remain 0");
+ Assert.equal(onSubviewShowingCount, 1,
+ "onSubviewShowingCount should be inc'ed");
+ onButtonCommandExpectedButtonID = panelViewButtonIDPanel;
+// EventUtils.synthesizeMouseAtCenter(panelViewButtonNodePanel, {});
+ panelViewButtonNodePanel.click();
+ await promisePageActionPanelHidden();
+ Assert.equal(onActionCommandCallCount, 0,
+ "onActionCommandCallCount should remain 0");
+ Assert.equal(onButtonCommandCallCount, 1,
+ "onButtonCommandCallCount should be inc'ed");
+
+ // Click the action's urlbar button, which should open the temp panel showing
+ // the subview, and click the subview's first button.
+ onSubviewPlacedExpectedPanelViewID = panelViewIDUrlbar;
+ onSubviewShowingExpectedPanelViewID = panelViewIDUrlbar;
+ EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
+ await promisePanelShown(BrowserPageActions._tempPanelID);
+ Assert.equal(onSubviewPlacedCount, 2,
+ "onSubviewPlacedCount should be inc'ed");
+ Assert.equal(onSubviewShowingCount, 2,
+ "onSubviewShowingCount should be inc'ed");
+ let panelViewButtonNodeUrlbar =
+ document.getElementById(panelViewButtonIDUrlbar);
+ Assert.notEqual(panelViewButtonNodeUrlbar, null, "panelViewButtonNodeUrlbar");
+ onButtonCommandExpectedButtonID = panelViewButtonIDUrlbar;
+ EventUtils.synthesizeMouseAtCenter(panelViewButtonNodeUrlbar, {});
+ await promisePanelHidden(BrowserPageActions._tempPanelID);
+ Assert.equal(onButtonCommandCallCount, 2,
+ "onButtonCommandCallCount should be inc'ed");
+
+ // Remove the action.
+ action.remove();
+ panelButtonNode = document.getElementById(panelButtonID);
+ Assert.equal(panelButtonNode, null, "panelButtonNode");
+ urlbarButtonNode = document.getElementById(urlbarButtonID);
+ Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
+ let panelViewNodePanel = document.getElementById(panelViewIDPanel);
+ Assert.equal(panelViewNodePanel, null, "panelViewNodePanel");
+ let panelViewNodeUrlbar = document.getElementById(panelViewIDUrlbar);
+ Assert.equal(panelViewNodeUrlbar, null, "panelViewNodeUrlbar");
+});
+
+
+// Tests a non-built-in action with an iframe.
+add_task(async function withIframe() {
+ let id = "test-iframe";
+
+ let onCommandCallCount = 0;
+ let onPlacedInPanelCallCount = 0;
+ let onPlacedInUrlbarCallCount = 0;
+ let onIframeShownCount = 0;
+
+ let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id);
+ let urlbarButtonID = BrowserPageActions._urlbarButtonNodeIDForActionID(id);
+
+ let action = PageActions.addAction(new PageActions.Action({
+ iconURL: "chrome://browser/skin/email-link.svg",
+ id,
+ shownInUrlbar: true,
+ title: "Test iframe",
+ wantsIframe: true,
+ onCommand(event, buttonNode) {
+ onCommandCallCount++;
+ },
+ onIframeShown(iframeNode, panelNode) {
+ onIframeShownCount++;
+ Assert.ok(iframeNode, "iframeNode should be non-null: " + iframeNode);
+ Assert.equal(iframeNode.localName, "iframe", "iframe localName");
+ Assert.ok(panelNode, "panelNode should be non-null: " + panelNode);
+ Assert.equal(panelNode.id, BrowserPageActions._tempPanelID,
+ "panelNode.id");
+ },
+ onPlacedInPanel(buttonNode) {
+ onPlacedInPanelCallCount++;
+ Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+ Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
+ },
+ onPlacedInUrlbar(buttonNode) {
+ onPlacedInUrlbarCallCount++;
+ Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode);
+ Assert.equal(buttonNode.id, urlbarButtonID, "buttonNode.id");
+ },
+ }));
+
+ Assert.equal(action.id, id, "id");
+ Assert.equal(action.wantsIframe, true, "wantsIframe");
+
+ Assert.equal(onPlacedInPanelCallCount, 1,
+ "onPlacedInPanelCallCount should be inc'ed");
+ Assert.equal(onPlacedInUrlbarCallCount, 1,
+ "onPlacedInUrlbarCallCount should be inc'ed");
+ Assert.equal(onIframeShownCount, 0,
+ "onIframeShownCount should remain 0");
+ Assert.equal(onCommandCallCount, 0,
+ "onCommandCallCount should remain 0");
+
+ // The action's panel button should have been created.
+ let panelButtonNode = document.getElementById(panelButtonID);
+ Assert.notEqual(panelButtonNode, null, "panelButtonNode");
+
+ // The action's urlbar button should have been created.
+ let urlbarButtonNode = document.getElementById(urlbarButtonID);
+ Assert.notEqual(urlbarButtonNode, null, "urlbarButtonNode");
+
+ // Open the panel, click the action's button.
+ await promisePageActionPanelOpen();
+ Assert.equal(onIframeShownCount, 0, "onIframeShownCount should remain 0");
+ EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
+ await promisePanelShown(BrowserPageActions._tempPanelID);
+ Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+ Assert.equal(onIframeShownCount, 1, "onIframeShownCount should be inc'ed");
+
+ // The temp panel should have opened, anchored to the action's urlbar button.
+ let tempPanel = document.getElementById(BrowserPageActions._tempPanelID);
+ Assert.notEqual(tempPanel, null, "tempPanel");
+ Assert.equal(tempPanel.anchorNode.id, urlbarButtonID,
+ "tempPanel.anchorNode.id");
+ EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
+ await promisePanelHidden(BrowserPageActions._tempPanelID);
+
+ // Click the action's urlbar button.
+ EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
+ await promisePanelShown(BrowserPageActions._tempPanelID);
+ Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+ Assert.equal(onIframeShownCount, 2, "onIframeShownCount should be inc'ed");
+
+ // The temp panel should have opened, again anchored to the action's urlbar
+ // button.
+ tempPanel = document.getElementById(BrowserPageActions._tempPanelID);
+ Assert.notEqual(tempPanel, null, "tempPanel");
+ Assert.equal(tempPanel.anchorNode.id, urlbarButtonID,
+ "tempPanel.anchorNode.id");
+ EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
+ await promisePanelHidden(BrowserPageActions._tempPanelID);
+
+ // Hide the action's button in the urlbar.
+ action.shownInUrlbar = false;
+ urlbarButtonNode = document.getElementById(urlbarButtonID);
+ Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
+
+ // Open the panel, click the action's button.
+ await promisePageActionPanelOpen();
+ EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
+ await promisePanelShown(BrowserPageActions._tempPanelID);
+ Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+ Assert.equal(onIframeShownCount, 3, "onIframeShownCount should be inc'ed");
+
+ // The temp panel should have opened, this time anchored to the main page
+ // action button in the urlbar.
+ tempPanel = document.getElementById(BrowserPageActions._tempPanelID);
+ Assert.notEqual(tempPanel, null, "tempPanel");
+ Assert.equal(tempPanel.anchorNode.id, BrowserPageActions.mainButtonNode.id,
+ "tempPanel.anchorNode.id");
+ EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+ await promisePanelHidden(BrowserPageActions._tempPanelID);
+
+ // Remove the action.
+ action.remove();
+ panelButtonNode = document.getElementById(panelButtonID);
+ Assert.equal(panelButtonNode, null, "panelButtonNode");
+ urlbarButtonNode = document.getElementById(urlbarButtonID);
+ Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
+});
+
+
+// Tests an action with the _insertBeforeActionID option set.
+add_task(async function insertBeforeActionID() {
+ let id = "test-insertBeforeActionID";
+ let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id);
+
+ let initialActions = PageActions.actions;
+ let initialBuiltInActions = PageActions.builtInActions;
+ let initialNonBuiltInActions = PageActions.nonBuiltInActions;
+ let initialBookmarkSeparatorIndex = PageActions.actions.findIndex(a => {
+ return a.id == PageActions.ACTION_ID_BOOKMARK_SEPARATOR;
+ });
+
+ let action = PageActions.addAction(new PageActions.Action({
+ id,
+ title: "Test insertBeforeActionID",
+ _insertBeforeActionID: PageActions.ACTION_ID_BOOKMARK_SEPARATOR,
+ }));
+
+ Assert.equal(action.id, id, "id");
+ Assert.ok("__insertBeforeActionID" in action, "__insertBeforeActionID");
+ Assert.equal(action.__insertBeforeActionID,
+ PageActions.ACTION_ID_BOOKMARK_SEPARATOR,
+ "action.__insertBeforeActionID");
+
+ Assert.equal(PageActions.actions.length,
+ initialActions.length + 1,
+ "PageActions.actions.length should be updated");
+ Assert.equal(PageActions.builtInActions.length,
+ initialBuiltInActions.length + 1,
+ "PageActions.builtInActions.length should be updated");
+ Assert.equal(PageActions.nonBuiltInActions.length,
+ initialNonBuiltInActions.length,
+ "PageActions.nonBuiltInActions.length should be updated");
+
+ let actionIndex = PageActions.actions.findIndex(a => a.id == id);
+ Assert.equal(initialBookmarkSeparatorIndex, actionIndex,
+ "initialBookmarkSeparatorIndex");
+ let newBookmarkSeparatorIndex = PageActions.actions.findIndex(a => {
+ return a.id == PageActions.ACTION_ID_BOOKMARK_SEPARATOR;
+ });
+ Assert.equal(newBookmarkSeparatorIndex, initialBookmarkSeparatorIndex + 1,
+ "newBookmarkSeparatorIndex");
+
+ // The action's panel button should have been created.
+ let panelButtonNode = document.getElementById(panelButtonID);
+ Assert.notEqual(panelButtonNode, null, "panelButtonNode");
+
+ // The button's next sibling should be the bookmark separator.
+ Assert.notEqual(panelButtonNode.nextSibling, null,
+ "panelButtonNode.nextSibling");
+ Assert.equal(
+ panelButtonNode.nextSibling.id,
+ BrowserPageActions._panelButtonNodeIDForActionID(
+ PageActions.ACTION_ID_BOOKMARK_SEPARATOR
+ ),
+ "panelButtonNode.nextSibling.id"
+ );
+
+ // The separator between the built-in and non-built-in actions should not have
+ // been created.
+ Assert.equal(
+ document.getElementById(
+ BrowserPageActions._panelButtonNodeIDForActionID(
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+ )
+ ),
+ null,
+ "Separator should be gone"
+ );
+
+ action.remove();
+});
+
+
+// Tests that the ordering of multiple non-built-in actions is alphabetical.
+add_task(async function multipleNonBuiltInOrdering() {
+ let idPrefix = "test-multipleNonBuiltInOrdering-";
+ let titlePrefix = "Test multipleNonBuiltInOrdering ";
+
+ let initialActions = PageActions.actions;
+ let initialBuiltInActions = PageActions.builtInActions;
+ let initialNonBuiltInActions = PageActions.nonBuiltInActions;
+
+ // Create some actions in an out-of-order order.
+ let actions = [2, 1, 4, 3].map(index => {
+ return PageActions.addAction(new PageActions.Action({
+ id: idPrefix + index,
+ title: titlePrefix + index,
+ }));
+ });
+
+ // + 1 for the separator between built-in and non-built-in actions.
+ Assert.equal(PageActions.actions.length,
+ initialActions.length + actions.length + 1,
+ "PageActions.actions.length should be updated");
+
+ Assert.equal(PageActions.builtInActions.length,
+ initialBuiltInActions.length,
+ "PageActions.builtInActions.length should be same");
+ Assert.equal(PageActions.nonBuiltInActions.length,
+ initialNonBuiltInActions.length + actions.length,
+ "PageActions.nonBuiltInActions.length should be updated");
+
+ // Look at the final actions.length actions in PageActions.actions, from first
+ // to last.
+ for (let i = 0; i < actions.length; i++) {
+ let expectedIndex = i + 1;
+ let actualAction = PageActions.nonBuiltInActions[i];
+ Assert.equal(actualAction.id, idPrefix + expectedIndex,
+ "actualAction.id for index: " + i);
+ }
+
+ // Check the button nodes in the panel.
+ let expectedIndex = 1;
+ let buttonNode = document.getElementById(
+ BrowserPageActions._panelButtonNodeIDForActionID(idPrefix + expectedIndex)
+ );
+ Assert.notEqual(buttonNode, null, "buttonNode");
+ Assert.notEqual(buttonNode.previousSibling, null,
+ "buttonNode.previousSibling");
+ Assert.equal(
+ buttonNode.previousSibling.id,
+ BrowserPageActions._panelButtonNodeIDForActionID(
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+ ),
+ "buttonNode.previousSibling.id"
+ );
+ for (let i = 0; i < actions.length; i++) {
+ Assert.notEqual(buttonNode, null, "buttonNode at index: " + i);
+ Assert.equal(
+ buttonNode.id,
+ BrowserPageActions._panelButtonNodeIDForActionID(idPrefix + expectedIndex),
+ "buttonNode.id at index: " + i
+ );
+ buttonNode = buttonNode.nextSibling;
+ expectedIndex++;
+ }
+ Assert.equal(buttonNode, null, "Nothing should come after the last button");
+
+ for (let action of actions) {
+ action.remove();
+ }
+
+ // The separator between the built-in and non-built-in actions should be gone.
+ Assert.equal(
+ document.getElementById(
+ BrowserPageActions._panelButtonNodeIDForActionID(
+ PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+ )
+ ),
+ null,
+ "Separator should be gone"
+ );
+});
+
+
+function promisePageActionPanelOpen() {
+ let button = document.getElementById("pageActionButton");
+ let shownPromise = promisePageActionPanelShown();
+ EventUtils.synthesizeMouseAtCenter(button, {});
+ return shownPromise;
+}
+
+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 = typeof(panelIDOrNode) != "string" ? panelIDOrNode :
+ document.getElementById(panelIDOrNode);
+ if (!panel ||
+ (eventType == "popupshowing" && panel.state == "open") ||
+ (eventType == "popuphidden" && panel.state == "closed")) {
+ executeSoon(resolve);
+ return;
+ }
+ panel.addEventListener(eventType, () => {
+ executeSoon(resolve);
+ }, { once: true });
+ });
+}
+
+function promisePageActionViewShown() {
+ return new Promise(resolve => {
+ BrowserPageActions.panelNode.addEventListener("ViewShown", (event) => {
+ let target = event.originalTarget;
+ window.setTimeout(() => {
+ resolve(target);
+ }, 5000);
+ }, { once: true });
+ });
+}