Bug 1221539 - Add search engine discovery to the page action menu. Part 1: Page action changes. r?Gijs draft
authorDrew Willcoxon <adw@mozilla.com>
Wed, 28 Mar 2018 11:28:13 -0700
changeset 773961 27120170a5e5aaa6d876aca0ef09ad41c0041725
parent 773797 a456475502b80a1264642d9eaee9394a8fad8315
child 773962 f744531232e6b3e4a99cb959ce9b600445fed164
push id104360
push userbmo:adw@mozilla.com
push dateWed, 28 Mar 2018 18:30:23 +0000
reviewersGijs
bugs1221539
milestone61.0a1
Bug 1221539 - Add search engine discovery to the page action menu. Part 1: Page action changes. r?Gijs MozReview-Commit-ID: DGy3sBibpRW
browser/base/content/browser-pageActions.js
browser/base/content/browser.css
browser/base/content/test/urlbar/browser_page_action_menu.js
browser/components/uitour/UITour.jsm
browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
browser/extensions/webcompat-reporter/test/browser/browser_button_state.js
browser/extensions/webcompat-reporter/test/browser/browser_report_site_issue.js
browser/extensions/webcompat-reporter/test/browser/head.js
browser/modules/PageActions.jsm
browser/modules/test/browser/browser_PageActions.js
--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -43,137 +43,227 @@ var BrowserPageActions = {
     return this.mainViewBodyNode = this.mainViewNode.querySelector(".panel-subview-body");
   },
 
   /**
    * Inits.  Call to init.
    */
   init() {
     this.placeAllActions();
+    this._onPanelShowing = this._onPanelShowing.bind(this);
+    this.panelNode.addEventListener("popupshowing", this._onPanelShowing);
+    this.panelNode.addEventListener("popuphiding", () => {
+      this.mainButtonNode.removeAttribute("open");
+    });
   },
 
+  _onPanelShowing() {
+    this.placeLazyActionsInPanel();
+    for (let action of PageActions.actionsInPanel(window)) {
+      let buttonNode = this.panelButtonNodeForActionID(action.id);
+      action.onShowingInPanel(buttonNode);
+    }
+  },
+
+  placeLazyActionsInPanel() {
+    let actions = this._actionsToLazilyPlaceInPanel;
+    this._actionsToLazilyPlaceInPanel = [];
+    for (let action of actions) {
+      this._placeActionInPanelNow(action);
+    }
+  },
+
+  // Actions placed in the panel aren't actually placed until the panel is
+  // subsequently opened.
+  _actionsToLazilyPlaceInPanel: [],
+
   /**
    * Places all registered actions.
    */
   placeAllActions() {
-    // Place actions in the panel.  Notify of onBeforePlacedInWindow too.
-    for (let action of PageActions.actions) {
-      action.onBeforePlacedInWindow(window);
+    let panelActions = PageActions.actionsInPanel(window);
+    for (let action of panelActions) {
       this.placeActionInPanel(action);
     }
-
-    // Place actions in the urlbar.  Do this in reverse order.  The reason is
-    // subtle.  If there were no urlbar nodes already in markup (like the
-    // bookmark star button), then doing this in forward order would be fine.
-    // Forward order means that the insert-before relationship is always broken:
-    // there's never a next-sibling node before which to insert a new node, so
-    // node.insertBefore() is always passed null, and nodes are always appended.
-    // That will break the position of nodes that should be inserted before
-    // nodes that are in markup, which in turn can break other nodes.
-    let actionsInUrlbar = PageActions.actionsInUrlbar(window);
-    for (let i = actionsInUrlbar.length - 1; i >= 0; i--) {
-      let action = actionsInUrlbar[i];
+    let urlbarActions = PageActions.actionsInUrlbar(window);
+    for (let action of urlbarActions) {
       this.placeActionInUrlbar(action);
     }
   },
 
   /**
    * Adds or removes as necessary DOM nodes for the given action.
    *
    * @param  action (PageActions.Action, required)
    *         The action to place.
    */
   placeAction(action) {
-    action.onBeforePlacedInWindow(window);
     this.placeActionInPanel(action);
     this.placeActionInUrlbar(action);
   },
 
   /**
    * Adds or removes as necessary DOM nodes for the action in the panel.
    *
    * @param  action (PageActions.Action, required)
    *         The action to place.
    */
   placeActionInPanel(action) {
+    if (this.panelNode.state != "closed") {
+      this._placeActionInPanelNow(action);
+    } else {
+      // Lazily place the action in the panel the next time it opens.
+      this._actionsToLazilyPlaceInPanel.push(action);
+    }
+  },
+
+  _placeActionInPanelNow(action) {
+    if (action.shouldShowInPanel(window)) {
+      this._addActionToPanel(action);
+    } else {
+      this._removeActionFromPanel(action);
+    }
+  },
+
+  _addActionToPanel(action) {
     let id = this.panelButtonNodeIDForActionID(action.id);
     let node = document.getElementById(id);
+    if (node) {
+      return;
+    }
+    this._maybeNotifyBeforePlacedInWindow(action);
+    node = this._makePanelButtonNodeForAction(action);
+    node.id = id;
+    let insertBeforeNode = this._getNextNode(action, false);
+    this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
+    this.updateAction(action, null, {
+      panelNode: node,
+    });
+    this._updateActionDisabledInPanel(action, node);
+    action.onPlacedInPanel(node);
+    this._addOrRemoveSeparatorsInPanel();
+  },
+
+  _removeActionFromPanel(action) {
+    let lazyIndex =
+      this._actionsToLazilyPlaceInPanel.findIndex(a => a.id == action.id);
+    if (lazyIndex >= 0) {
+      this._actionsToLazilyPlaceInPanel.splice(lazyIndex, 1);
+    }
+    let node = this.panelButtonNodeForActionID(action.id);
     if (!node) {
-      let panelViewNode;
-      [node, panelViewNode] = this._makePanelButtonNodeForAction(action);
-      node.id = id;
-      let insertBeforeID = PageActions.nextActionIDInPanel(action);
-      let insertBeforeNode =
-        insertBeforeID ? this.panelButtonNodeForActionID(insertBeforeID) :
-        null;
-      this.mainViewBodyNode.insertBefore(node, insertBeforeNode);
-      this.updateAction(action);
-      this._updateActionDisabledInPanel(action);
-      action.onPlacedInPanel(node);
+      return;
+    }
+    node.remove();
+    if (action.getWantsSubview(window)) {
+      let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
+      let panelViewNode = document.getElementById(panelViewNodeID);
       if (panelViewNode) {
-        action.subview.onPlaced(panelViewNode);
+        panelViewNode.remove();
       }
     }
+    this._addOrRemoveSeparatorsInPanel();
+  },
+
+  _addOrRemoveSeparatorsInPanel() {
+    let actions = PageActions.actionsInPanel(window);
+    let ids = [
+      PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+      PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+    ];
+    for (let id of ids) {
+      let sep = actions.find(a => a.id == id);
+      if (sep) {
+        this._addActionToPanel(sep);
+      } else {
+        let node = this.panelButtonNodeForActionID(id);
+        if (node) {
+          node.remove();
+        }
+      }
+    }
+  },
+
+  /**
+   * Returns the node before which an action's node should be inserted.
+   *
+   * @param  action (PageActions.Action, required)
+   *         The action that will be inserted.
+   * @param  forUrlbar (bool, required)
+   *         True if you're inserting into the urlbar, false if you're inserting
+   *         into the panel.
+   * @return (DOM node, maybe null) The DOM node before which to insert the
+   *         given action.  Null if the action should be inserted at the end.
+   */
+  _getNextNode(action, forUrlbar) {
+    let actions =
+      forUrlbar ?
+      PageActions.actionsInUrlbar(window) :
+      PageActions.actionsInPanel(window);
+    let index = actions.findIndex(a => a.id == action.id);
+    if (index < 0) {
+      return null;
+    }
+    for (let i = index + 1; i < actions.length; i++) {
+      let node =
+        forUrlbar ?
+        this.urlbarButtonNodeForActionID(actions[i].id) :
+        this.panelButtonNodeForActionID(actions[i].id);
+      if (node) {
+        return node;
+      }
+    }
+    return null;
+  },
+
+  _maybeNotifyBeforePlacedInWindow(action) {
+    if (!this._isActionPlacedInWindow(action)) {
+      action.onBeforePlacedInWindow(window);
+    }
+  },
+
+  _isActionPlacedInWindow(action) {
+    if (this.panelButtonNodeForActionID(action.id)) {
+      return true;
+    }
+    let urlbarNode = this.urlbarButtonNodeForActionID(action.id);
+    return urlbarNode && !urlbarNode.hidden;
   },
 
   _makePanelButtonNodeForAction(action) {
     if (action.__isSeparator) {
       let node = document.createElement("toolbarseparator");
-      return [node, null];
+      return node;
     }
-
     let buttonNode = document.createElement("toolbarbutton");
     buttonNode.classList.add(
       "subviewbutton",
       "subviewbutton-iconic",
       "pageAction-panel-button"
     );
     buttonNode.setAttribute("actionid", action.id);
     if (action.nodeAttributes) {
       for (let name in action.nodeAttributes) {
         buttonNode.setAttribute(name, action.nodeAttributes[name]);
       }
     }
-    let panelViewNode = null;
-    if (action.subview) {
-      buttonNode.classList.add("subviewbutton-nav");
-      panelViewNode = this._makePanelViewNodeForAction(action, false);
-      this.multiViewNode.appendChild(panelViewNode);
-    }
     buttonNode.addEventListener("command", event => {
       this.doCommandForAction(action, event, buttonNode);
     });
-    return [buttonNode, panelViewNode];
+    return buttonNode;
   },
 
   _makePanelViewNodeForAction(action, forUrlbar) {
     let panelViewNode = document.createElement("panelview");
     panelViewNode.id = this._panelViewNodeIDForActionID(action.id, forUrlbar);
     panelViewNode.classList.add("PanelUI-subView");
     let bodyNode = document.createElement("vbox");
     bodyNode.id = panelViewNode.id + "-body";
     bodyNode.classList.add("panel-subview-body");
     panelViewNode.appendChild(bodyNode);
-    for (let button of action.subview.buttons) {
-      let buttonNode = document.createElement("toolbarbutton");
-      buttonNode.id =
-        this._panelViewButtonNodeIDForActionID(action.id, button.id, forUrlbar);
-      buttonNode.classList.add("subviewbutton", "subviewbutton-iconic");
-      buttonNode.setAttribute("label", button.title);
-      if (button.shortcut) {
-        buttonNode.setAttribute("shortcut", button.shortcut);
-      }
-      if (button.disabled) {
-        buttonNode.setAttribute("disabled", "true");
-      }
-      buttonNode.addEventListener("command", event => {
-        button.onCommand(event, buttonNode);
-      });
-      bodyNode.appendChild(buttonNode);
-    }
     return panelViewNode;
   },
 
   /**
    * Shows or hides a panel for an action.  You can supply your own panel;
    * otherwise one is created.
    *
    * @param  action (PageActions.Action, required)
@@ -224,24 +314,20 @@ var BrowserPageActions = {
     panelNode.setAttribute("actionID", action.id);
     panelNode.setAttribute("role", "group");
     panelNode.setAttribute("type", "arrow");
     panelNode.setAttribute("flip", "slide");
     panelNode.setAttribute("noautofocus", "true");
     panelNode.setAttribute("tabspecific", "true");
     panelNode.setAttribute("photon", "true");
 
-    if (this._disablePanelAnimations) {
-      panelNode.setAttribute("animate", "false");
-    }
-
     let panelViewNode = null;
     let iframeNode = null;
 
-    if (action.subview) {
+    if (action.getWantsSubview(window)) {
       let multiViewNode = document.createElement("panelmultiview");
       panelViewNode = this._makePanelViewNodeForAction(action, true);
       multiViewNode.setAttribute("mainViewId", panelViewNode.id);
       multiViewNode.appendChild(panelViewNode);
       panelNode.appendChild(multiViewNode);
     } else if (action.wantsIframe) {
       iframeNode = document.createElement("iframe");
       iframeNode.setAttribute("type", "content");
@@ -262,50 +348,36 @@ var BrowserPageActions = {
         action.onIframeHiding(iframeNode, panelNode);
       }, { once: true });
       panelNode.addEventListener("popuphidden", () => {
         action.onIframeHidden(iframeNode, panelNode);
       }, { once: true });
     }
 
     if (panelViewNode) {
-      action.subview.onPlaced(panelViewNode);
+      action.onSubviewPlaced(panelViewNode);
       panelNode.addEventListener("popupshowing", () => {
-        action.subview.onShowing(panelViewNode);
+        action.onSubviewShowing(panelViewNode);
       }, { once: true });
     }
 
     return panelNode;
   },
 
-  // For tests.
-  get _disablePanelAnimations() {
-    return this.__disablePanelAnimations || false;
-  },
-  set _disablePanelAnimations(val) {
-    this.__disablePanelAnimations = val;
-    if (val) {
-      this.panelNode.setAttribute("animate", "false");
-    } else {
-      this.panelNode.removeAttribute("animate");
-    }
-  },
-
   /**
    * Returns the node in the urlbar to which popups for the given action should
    * be anchored.  If the action is null, a sensible anchor is returned.
    *
    * @param  action (PageActions.Action, optional)
    *         The action you want to anchor.
    * @param  event (DOM event, optional)
    *         This is used to display the feedback panel on the right node when
    *         the command can be invoked from both the main panel and another
    *         location, such as an activated action panel or a button.
-   * @return (DOM node, nonnull) The node to which the action should be
-   *         anchored.
+   * @return (DOM node) The node to which the action should be anchored.
    */
   panelAnchorNodeForAction(action, event) {
     if (event && event.target.closest("panel") == this.panelNode) {
       return this.mainButtonNode;
     }
 
     // Try each of the following nodes in order, using the first that's visible.
     let potentialAnchorNodeIDs = [
@@ -357,42 +429,51 @@ var BrowserPageActions = {
           node.remove();
         }
       }
       return;
     }
 
     let newlyPlaced = false;
     if (action.__urlbarNodeInMarkup) {
-      newlyPlaced = node && node.hidden;
+      this._maybeNotifyBeforePlacedInWindow(action);
+      // Allow the consumer to add the node in response to the
+      // onBeforePlacedInWindow notification.
+      node = document.getElementById(id);
+      if (!node) {
+        return;
+      }
+      newlyPlaced = node.hidden;
       node.hidden = false;
     } else if (!node) {
       newlyPlaced = true;
+      this._maybeNotifyBeforePlacedInWindow(action);
       node = this._makeUrlbarButtonNode(action);
       node.id = id;
     }
 
-    if (newlyPlaced) {
-      let insertBeforeID = PageActions.nextActionIDInUrlbar(window, action);
-      let insertBeforeNode =
-        insertBeforeID ? this.urlbarButtonNodeForActionID(insertBeforeID) :
-        null;
-      this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode);
-      this.updateAction(action);
-      action.onPlacedInUrlbar(node);
+    if (!newlyPlaced) {
+      return;
+    }
 
-      // urlbar buttons should always have tooltips, so if the node doesn't have
-      // one, then as a last resort use the label of the corresponding panel
-      // button.  Why not set tooltiptext to action.title when the node is
-      // created?  Because the consumer may set a title dynamically.
-      if (!node.hasAttribute("tooltiptext")) {
-        let panelNode = this.panelButtonNodeForActionID(action.id);
-        if (panelNode) {
-          node.setAttribute("tooltiptext", panelNode.getAttribute("label"));
-        }
+    let insertBeforeNode = this._getNextNode(action, true);
+    this.mainButtonNode.parentNode.insertBefore(node, insertBeforeNode);
+    this.updateAction(action, null, {
+      urlbarNode: node,
+    });
+    action.onPlacedInUrlbar(node);
+
+    // urlbar buttons should always have tooltips, so if the node doesn't have
+    // one, then as a last resort use the label of the corresponding panel
+    // button.  Why not set tooltiptext to action.title when the node is
+    // created?  Because the consumer may set a title dynamically.
+    if (!node.hasAttribute("tooltiptext")) {
+      let panelNode = this.panelButtonNodeForActionID(action.id);
+      if (panelNode) {
+        node.setAttribute("tooltiptext", panelNode.getAttribute("label"));
       }
     }
   },
 
   _makeUrlbarButtonNode(action) {
     let buttonNode = document.createElement("image");
     buttonNode.classList.add("urlbar-icon", "urlbar-page-action");
     buttonNode.setAttribute("actionid", action.id);
@@ -415,170 +496,182 @@ var BrowserPageActions = {
    *         The action to remove.
    */
   removeAction(action) {
     this._removeActionFromPanel(action);
     this._removeActionFromUrlbar(action);
     action.onRemovedFromWindow(window);
   },
 
-  _removeActionFromPanel(action) {
-    let node = this.panelButtonNodeForActionID(action.id);
-    if (node) {
-      node.remove();
-    }
-    if (action.subview) {
-      let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
-      let panelViewNode = document.getElementById(panelViewNodeID);
-      if (panelViewNode) {
-        panelViewNode.remove();
-      }
-    }
-    // If there are now no more non-built-in actions, remove the separator
-    // between the built-ins and non-built-ins.
-    if (!PageActions.nonBuiltInActions.length) {
-      let separator = document.getElementById(
-        this.panelButtonNodeIDForActionID(
-          PageActions.ACTION_ID_BUILT_IN_SEPARATOR
-        )
-      );
-      if (separator) {
-        separator.remove();
-      }
-    }
-  },
-
   _removeActionFromUrlbar(action) {
     let node = this.urlbarButtonNodeForActionID(action.id);
     if (node) {
       node.remove();
     }
   },
 
   /**
    * Updates the DOM nodes of an action to reflect either a changed property or
    * all properties.
    *
    * @param  action (PageActions.Action, required)
    *         The action to update.
    * @param  propertyName (string, optional)
    *         The name of the property to update.  If not given, then DOM nodes
    *         will be updated to reflect the current values of all properties.
-   * @param  value (optional)
-   *         If a property name is passed, this argument may contain its
-   *         current value, in order to prevent a further look-up.
+   * @param  opts (object, optional)
+   *         - panelNode: The action's node in the panel to update.
+   *         - urlbarNode: The action's node in the urlbar to update.
+   *         - value: If a property name is passed, this argument may contain
+   *           its current value, in order to prevent a further look-up.
    */
-  updateAction(action, propertyName = null, value) {
+  updateAction(action, propertyName = null, opts = {}) {
+    let anyNodeGiven = "panelNode" in opts || "urlbarNode" in opts;
+    let panelNode =
+      anyNodeGiven ?
+      opts.panelNode || null :
+      this.panelButtonNodeForActionID(action.id);
+    let urlbarNode =
+      anyNodeGiven ?
+      opts.urlbarNode || null :
+      this.urlbarButtonNodeForActionID(action.id);
+    let value = opts.value || undefined;
     if (propertyName) {
-      this[this._updateMethods[propertyName]](action, value);
+      this[this._updateMethods[propertyName]](action, panelNode, urlbarNode,
+                                              value);
     } else {
-      for (let name of ["iconURL", "title", "tooltip"]) {
-        this[this._updateMethods[name]](action, value);
+      for (let name of ["iconURL", "title", "tooltip", "wantsSubview"]) {
+        this[this._updateMethods[name]](action, panelNode, urlbarNode, value);
       }
     }
   },
 
   _updateMethods: {
     disabled: "_updateActionDisabled",
     iconURL: "_updateActionIconURL",
     title: "_updateActionTitle",
     tooltip: "_updateActionTooltip",
+    wantsSubview: "_updateActionWantsSubview",
   },
 
-  _updateActionDisabled(action, disabled) {
-    this._updateActionDisabledInPanel(action, disabled);
+  _updateActionDisabled(action, panelNode, urlbarNode,
+                        disabled = action.getDisabled(window)) {
+    if (action.__transient) {
+      this.placeActionInPanel(action);
+    } else {
+      this._updateActionDisabledInPanel(action, panelNode, disabled);
+    }
     this.placeActionInUrlbar(action);
   },
 
-  _updateActionDisabledInPanel(action, disabled = action.getDisabled(window)) {
-    let panelButton = this.panelButtonNodeForActionID(action.id);
-    if (panelButton) {
+  _updateActionDisabledInPanel(action, panelNode,
+                               disabled = action.getDisabled(window)) {
+    if (panelNode) {
       if (disabled) {
-        panelButton.setAttribute("disabled", "true");
+        panelNode.setAttribute("disabled", "true");
       } else {
-        panelButton.removeAttribute("disabled");
+        panelNode.removeAttribute("disabled");
       }
     }
   },
 
-  _updateActionIconURL(action, properties = action.getIconProperties(window)) {
-    let panelButton = this.panelButtonNodeForActionID(action.id);
-    let urlbarButton = this.urlbarButtonNodeForActionID(action.id);
-
+  _updateActionIconURL(action, panelNode, urlbarNode,
+                       properties = action.getIconProperties(window)) {
     for (let [prop, value] of Object.entries(properties)) {
-      if (panelButton) {
-        panelButton.style.setProperty(prop, value);
+      if (panelNode) {
+        panelNode.style.setProperty(prop, value);
       }
-      if (urlbarButton) {
-        urlbarButton.style.setProperty(prop, value);
+      if (urlbarNode) {
+        urlbarNode.style.setProperty(prop, value);
       }
     }
   },
 
-  _updateActionTitle(action, title = action.getTitle(window)) {
+  _updateActionTitle(action, panelNode, urlbarNode,
+                     title = action.getTitle(window)) {
     if (!title) {
       // `title` is a required action property, but the bookmark action's is an
       // empty string since its actual title is set via
       // BookmarkingUI.updateBookmarkPageMenuItem().  The purpose of this early
       // return is to ignore that empty title.
       return;
     }
-    let panelButton = this.panelButtonNodeForActionID(action.id);
-    if (panelButton) {
-      panelButton.setAttribute("label", title);
+    if (panelNode) {
+      panelNode.setAttribute("label", title);
     }
-
-    let urlbarButton = this.urlbarButtonNodeForActionID(action.id);
-    if (urlbarButton) {
-      urlbarButton.setAttribute("aria-label", title);
-
-      // tooltiptext falls back to the title, so update it, too.
-      this._updateActionTooltip(action, undefined, title, urlbarButton);
+    if (urlbarNode) {
+      urlbarNode.setAttribute("aria-label", title);
+      // tooltiptext falls back to the title, so update it too if necessary.
+      let tooltip = action.getTooltip(window);
+      if (!tooltip && title) {
+        urlbarNode.setAttribute("tooltiptext", title);
+      }
     }
   },
 
-  _updateActionTooltip(action, tooltip = action.getTooltip(window),
-                       title,
-                       node = this.urlbarButtonNodeForActionID(action.id)) {
-    if (node) {
-      if (!tooltip && title === undefined) {
-        title = action.getTitle(window);
+  _updateActionTooltip(action, panelNode, urlbarNode,
+                       tooltip = action.getTooltip(window)) {
+    if (urlbarNode) {
+      if (!tooltip) {
+        tooltip = action.getTitle(window);
+      }
+      if (tooltip) {
+        urlbarNode.setAttribute("tooltiptext", tooltip);
       }
-      node.setAttribute("tooltiptext", tooltip || title);
+    }
+  },
+
+  _updateActionWantsSubview(action, panelNode, urlbarNode,
+                            wantsSubview = action.getWantsSubview(window)) {
+    if (!panelNode) {
+      return;
+    }
+    let panelViewID = this._panelViewNodeIDForActionID(action.id, false);
+    let panelViewNode = document.getElementById(panelViewID);
+    panelNode.classList.toggle("subviewbutton-nav", wantsSubview);
+    if (!wantsSubview) {
+      if (panelViewNode) {
+        panelViewNode.remove();
+      }
+      return;
+    }
+    if (!panelViewNode) {
+      panelViewNode = this._makePanelViewNodeForAction(action, false);
+      this.multiViewNode.appendChild(panelViewNode);
+      action.onSubviewPlaced(panelViewNode);
     }
   },
 
   doCommandForAction(action, event, buttonNode) {
     if (event && event.type == "click" && event.button != 0) {
       return;
     }
     PageActions.logTelemetry("used", action, buttonNode);
     // If we're in the panel, open a subview inside the panel:
     // Note that we can't use this.panelNode.contains(buttonNode) here
     // because of XBL boundaries breaking Element.contains.
-    if (action.subview &&
+    if (action.getWantsSubview(window) &&
         buttonNode &&
         buttonNode.closest("panel") == this.panelNode) {
       let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
       let panelViewNode = document.getElementById(panelViewNodeID);
-      action.subview.onShowing(panelViewNode);
+      action.onSubviewShowing(panelViewNode);
       this.multiViewNode.showSubView(panelViewNode, buttonNode);
       return;
     }
     // Otherwise, hide the main popup in case it was open:
     PanelMultiView.hidePopup(this.panelNode);
 
-    // Toggle the activated action's panel if necessary
-    if (action.subview || action.wantsIframe) {
+    let aaPanelNode = this.activatedActionPanelNode;
+    if (!aaPanelNode || aaPanelNode.getAttribute("actionID") != action.id) {
+      action.onCommand(event, buttonNode);
+    }
+    if (action.getWantsSubview(window) || action.wantsIframe) {
       this.togglePanelForAction(action);
-      return;
     }
-
-    // Otherwise, run the action.
-    action.onCommand(event, buttonNode);
   },
 
   /**
    * Returns the action for a node.
    *
    * @param  node (DOM node, required)
    *         A button DOM node, either one that's shown in the page action panel
    *         or the urlbar.
@@ -656,22 +749,16 @@ var BrowserPageActions = {
   },
 
   // The ID of the given action's panelview.
   _panelViewNodeIDForActionID(actionID, forUrlbar) {
     let placementID = forUrlbar ? "urlbar" : "panel";
     return `pageAction-${placementID}-${actionID}-subview`;
   },
 
-  // The ID of the given button in the given action's panelview.
-  _panelViewButtonNodeIDForActionID(actionID, buttonID, forUrlbar) {
-    let placementID = forUrlbar ? "urlbar" : "panel";
-    return `pageAction-${placementID}-${actionID}-${buttonID}`;
-  },
-
   // The ID of the action corresponding to the given top-level button in the
   // panel or button in the urlbar.
   _actionIDForNodeID(nodeID) {
     if (!nodeID) {
       return null;
     }
     let match = nodeID.match(/^pageAction-(?:panel|urlbar)-(.+)$/);
     if (match) {
@@ -718,25 +805,17 @@ var BrowserPageActions = {
   /**
    * Show the page action panel
    *
    * @param  event (DOM event, optional)
    *         The event that triggers showing the panel. (such as a mouse click,
    *         if the user clicked something to open the panel)
    */
   showPanel(event = null) {
-    for (let action of PageActions.actions) {
-      let buttonNode = this.panelButtonNodeForActionID(action.id);
-      action.onShowingInPanel(buttonNode);
-    }
-
     this.panelNode.hidden = false;
-    this.panelNode.addEventListener("popuphiding", () => {
-      this.mainButtonNode.removeAttribute("open");
-    }, {once: true});
     this.mainButtonNode.setAttribute("open", "true");
     PanelMultiView.openPopup(this.panelNode, this.mainButtonNode, {
       position: "bottomcenter topright",
       triggerEvent: event,
     }).catch(Cu.reportError);
   },
 
   /**
@@ -961,16 +1040,25 @@ BrowserPageActions.emailLink = {
 BrowserPageActions.sendToDevice = {
   onPlacedInPanel(buttonNode) {
     let action = PageActions.actionForID("sendToDevice");
     BrowserPageActions.takeActionTitleFromPanel(action);
   },
 
   onSubviewPlaced(panelViewNode) {
     let bodyNode = panelViewNode.querySelector(".panel-subview-body");
+    let notReady = document.createElement("toolbarbutton");
+    notReady.classList.add(
+      "subviewbutton",
+      "subviewbutton-iconic",
+      "pageAction-sendToDevice-notReady"
+    );
+    notReady.setAttribute("label", "sendToDevice-notReadyTitle");
+    notReady.setAttribute("disabled", "true");
+    bodyNode.appendChild(notReady);
     for (let node of bodyNode.childNodes) {
       BrowserPageActions.takeNodeAttributeFromPanel(node, "title");
       BrowserPageActions.takeNodeAttributeFromPanel(node, "shortcut");
     }
   },
 
   onLocationChange() {
     let action = PageActions.actionForID("sendToDevice");
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1381,18 +1381,18 @@ toolbarpaletteitem[place="palette"][hidd
 
 .dragfeedback-tab {
   -moz-appearance: none;
   opacity: 0.65;
   -moz-window-shadow: none;
 }
 
 /* Page action panel */
-#pageAction-panel-sendToDevice-subview-body:not([state="notready"]) > #pageAction-panel-sendToDevice-notReady,
-#pageAction-urlbar-sendToDevice-subview-body:not([state="notready"]) > #pageAction-urlbar-sendToDevice-notReady {
+#pageAction-panel-sendToDevice-subview-body:not([state="notready"]) > .pageAction-sendToDevice-notReady,
+#pageAction-urlbar-sendToDevice-subview-body:not([state="notready"]) > .pageAction-sendToDevice-notReady {
   display: none;
 }
 
 /* Page action buttons */
 .pageAction-panel-button > .toolbarbutton-icon {
   list-style-image: var(--pageAction-image-16px, inherit);
 }
 .urlbar-page-action {
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu.js
@@ -9,25 +9,16 @@ registerCleanupFunction(function() {
 
 const lastModifiedFixture = 1507655615.87; // Approx Oct 10th 2017
 const mockRemoteClients = [
   { id: "0", name: "foo", type: "mobile", serverLastModified: lastModifiedFixture },
   { id: "1", name: "bar", type: "desktop", serverLastModified: lastModifiedFixture },
   { id: "2", name: "baz", type: "mobile", serverLastModified: lastModifiedFixture },
 ];
 
-add_task(async function init() {
-  // Disable panel animations.  They cause intermittent timeouts on Linux when
-  // the test tries to synthesize clicks on items in newly opened panels.
-  BrowserPageActions._disablePanelAnimations = true;
-  registerCleanupFunction(() => {
-    BrowserPageActions._disablePanelAnimations = false;
-  });
-});
-
 add_task(async function bookmark() {
   // Open a unique page.
   let url = "http://example.com/browser_page_action_menu";
   await BrowserTestUtils.withNewTab(url, async () => {
     // Open the panel.
     await promisePageActionPanelOpen();
 
     // The bookmark button should read "Bookmark This Page" and not be starred.
@@ -231,17 +222,17 @@ add_task(async function sendToDevice_syn
     // Click Send to Device.
     let viewPromise = promisePageActionViewShown();
     EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
     let view = await viewPromise;
     Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
 
     let expectedItems = [
       {
-        id: "pageAction-panel-sendToDevice-notReady",
+        className: "pageAction-sendToDevice-notReady",
         display: "none",
         disabled: true,
       },
       {
         attrs: {
           label: "Account Not Verified",
         },
         disabled: true
@@ -304,25 +295,25 @@ add_task(async function sendToDevice_syn
     let view = await viewPromise;
     Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
 
     function testSendTabToDeviceMenu(numCall) {
       if (numCall == 1) {
         // "Syncing devices" should be shown.
         checkSendToDeviceItems([
           {
-            id: "pageAction-panel-sendToDevice-notReady",
+            className: "pageAction-sendToDevice-notReady",
             disabled: true,
           },
         ]);
       } else if (numCall == 2) {
         // The devices should be shown in the subview.
         let expectedItems = [
           {
-            id: "pageAction-panel-sendToDevice-notReady",
+            className: "pageAction-sendToDevice-notReady",
             display: "none",
             disabled: true,
           },
         ];
         for (let client of mockRemoteClients) {
           expectedItems.push({
             attrs: {
               clientId: client.id,
@@ -368,17 +359,17 @@ add_task(async function sendToDevice_not
     // Click Send to Device.
     let viewPromise = promisePageActionViewShown();
     EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
     let view = await viewPromise;
     Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
 
     let expectedItems = [
       {
-        id: "pageAction-panel-sendToDevice-notReady",
+        className: "pageAction-sendToDevice-notReady",
         display: "none",
         disabled: true,
       },
       {
         attrs: {
           label: "Not Connected to Sync",
         },
         disabled: true
@@ -429,17 +420,17 @@ add_task(async function sendToDevice_noD
     // Click Send to Device.
     let viewPromise = promisePageActionViewShown();
     EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
     let view = await viewPromise;
     Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
 
     let expectedItems = [
       {
-        id: "pageAction-panel-sendToDevice-notReady",
+        className: "pageAction-sendToDevice-notReady",
         display: "none",
         disabled: true,
       },
       {
         attrs: {
           label: "No Devices Connected",
         },
         disabled: true
@@ -495,17 +486,17 @@ add_task(async function sendToDevice_dev
     let viewPromise = promisePageActionViewShown();
     EventUtils.synthesizeMouseAtCenter(sendToDeviceButton, {});
     let view = await viewPromise;
     Assert.equal(view.id, "pageAction-panel-sendToDevice-subview");
 
     // The devices should be shown in the subview.
     let expectedItems = [
       {
-        id: "pageAction-panel-sendToDevice-notReady",
+        className: "pageAction-sendToDevice-notReady",
         display: "none",
         disabled: true,
       },
     ];
     for (let client of mockRemoteClients) {
       expectedItems.push({
         attrs: {
           clientId: client.id,
@@ -565,17 +556,17 @@ add_task(async function sendToDevice_inU
     // only after the associated button has been clicked.
     await promisePanelShown(BrowserPageActions._activatedActionPanelID);
     Assert.equal(urlbarButton.getAttribute("open"), "true",
       "Button has open attribute");
 
     // The devices should be shown in the subview.
     let expectedItems = [
       {
-        id: "pageAction-urlbar-sendToDevice-notReady",
+        className: "pageAction-sendToDevice-notReady",
         display: "none",
         disabled: true,
       },
     ];
     for (let client of mockRemoteClients) {
       expectedItems.push({
         attrs: {
           clientId: client.id,
@@ -767,16 +758,23 @@ function checkSendToDeviceItems(expected
     let actual = body.childNodes[i];
     if (!expected) {
       Assert.equal(actual.localName, "toolbarseparator");
       continue;
     }
     if ("id" in expected) {
       Assert.equal(actual.id, expected.id);
     }
+    if ("className" in expected) {
+      let expectedNames = expected.className.split(/\s+/);
+      for (let name of expectedNames) {
+        Assert.ok(actual.classList.contains(name),
+                  `classList contains: ${name}`);
+      }
+    }
     let display = "display" in expected ? expected.display : "-moz-box";
     Assert.equal(getComputedStyle(actual).display, display);
     let disabled = "disabled" in expected ? expected.disabled : false;
     Assert.equal(actual.disabled, disabled);
     if ("attrs" in expected) {
       for (let name in expected.attrs) {
         Assert.ok(actual.hasAttribute(name));
         let attrVal = actual.getAttribute(name);
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -153,16 +153,17 @@ var UITour = {
       allowAdd: true,
       query: (aDocument) => {
         // The pocket's urlbar page action button is pre-defined in the DOM.
         // It would be hidden if toggled off from the urlbar.
         let node = aDocument.getElementById("pocket-button-box");
         if (node && !node.hidden) {
           return node;
         }
+        aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
         return aDocument.getElementById("pageAction-panel-pocket");
       },
     }],
     ["privateWindow", {query: "#appMenu-private-window-button"}],
     ["quit",        {query: "#appMenu-quit-button"}],
     ["readerMode-urlBar", {query: "#reader-mode-button"}],
     ["search",      {
       infoPanelOffsetX: 18,
@@ -219,39 +220,44 @@ var UITour = {
     ["pageAction-bookmark", {
       query: (aDocument) => {
         // The bookmark's urlbar page action button is pre-defined in the DOM.
         // It would be hidden if toggled off from the urlbar.
         let node = aDocument.getElementById("star-button-box");
         if (node && !node.hidden) {
           return node;
         }
+        aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
         return aDocument.getElementById("pageAction-panel-bookmark");
       },
     }],
     ["pageAction-copyURL", {
       query: (aDocument) => {
+        aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
         return aDocument.getElementById("pageAction-urlbar-copyURL") ||
                aDocument.getElementById("pageAction-panel-copyURL");
       },
     }],
     ["pageAction-emailLink", {
       query: (aDocument) => {
+        aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
         return aDocument.getElementById("pageAction-urlbar-emailLink") ||
                aDocument.getElementById("pageAction-panel-emailLink");
       },
     }],
     ["pageAction-sendToDevice", {
       query: (aDocument) => {
+        aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
         return aDocument.getElementById("pageAction-urlbar-sendToDevice") ||
                aDocument.getElementById("pageAction-panel-sendToDevice");
       },
     }],
     ["screenshots", {
       query: (aDocument) => {
+        aDocument.ownerGlobal.BrowserPageActions.placeLazyActionsInPanel();
         return aDocument.getElementById("pageAction-urlbar-screenshots") ||
                aDocument.getElementById("pageAction-panel-screenshots");
       },
     }]
   ]),
 
   init() {
     log.debug("Initializing UITour");
--- a/browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
+++ b/browser/extensions/screenshots/test/browser/browser_screenshots_ui_check.js
@@ -3,22 +3,89 @@
 const BUTTON_ID = "pageAction-panel-screenshots";
 
 function checkElements(expectPresent, l) {
   for (const id of l) {
     is(!!document.getElementById(id), expectPresent, "element " + id + (expectPresent ? " is" : " is not") + " present");
   }
 }
 
+async function togglePageActionPanel() {
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelEvent("popuphidden");
+}
+
+function promiseOpenPageActionPanel() {
+  let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                  .getInterface(Ci.nsIDOMWindowUtils);
+  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 = promisePageActionPanelEvent("popupshown");
+    EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+    return shownPromise;
+  }).then(() => {
+    // Wait for items in the panel to become visible.
+    return promisePageActionViewChildrenVisible(BrowserPageActions.mainViewNode);
+  });
+}
+
+function promisePageActionPanelEvent(eventType) {
+  return new Promise(resolve => {
+    let panel = BrowserPageActions.panelNode;
+    if ((eventType == "popupshown" && panel.state == "open") ||
+        (eventType == "popuphidden" && panel.state == "closed")) {
+      executeSoon(resolve);
+      return;
+    }
+    panel.addEventListener(eventType, () => {
+      executeSoon(resolve);
+    }, { once: true });
+  });
+}
+
+function promisePageActionViewChildrenVisible(panelViewNode) {
+  info("promisePageActionViewChildrenVisible waiting for a child node to be visible");
+  let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                  .getInterface(Ci.nsIDOMWindowUtils);
+  return BrowserTestUtils.waitForCondition(() => {
+    let bodyNode = panelViewNode.firstChild;
+    for (let childNode of bodyNode.childNodes) {
+      let bounds = dwu.getBoundsWithoutFlushing(childNode);
+      if (bounds.width > 0 && bounds.height > 0) {
+        return true;
+      }
+    }
+    return false;
+  });
+}
+
 add_task(async function() {
   await promiseScreenshotsEnabled();
 
   registerCleanupFunction(async function() {
     await promiseScreenshotsReset();
   });
 
+  // Toggle the page action panel to get it to rebuild itself.  An actionable
+  // page must be opened first.
+  let url = "http://example.com/browser_screenshots_ui_check";
+  await BrowserTestUtils.withNewTab(url, async () => {
+    await togglePageActionPanel();
 
-  await BrowserTestUtils.waitForCondition(
-    () => document.getElementById(BUTTON_ID),
-    "Screenshots button should be present", 100, 100);
+    await BrowserTestUtils.waitForCondition(
+      () => document.getElementById(BUTTON_ID),
+      "Screenshots button should be present", 100, 100);
 
-  checkElements(true, [BUTTON_ID]);
+    checkElements(true, [BUTTON_ID]);
+  });
 });
--- a/browser/extensions/webcompat-reporter/test/browser/browser_button_state.js
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_button_state.js
@@ -1,28 +1,25 @@
 const REPORTABLE_PAGE = "http://example.com/";
 const REPORTABLE_PAGE2 = "https://example.com/";
 const NONREPORTABLE_PAGE = "about:mozilla";
 
 /* Test that the Report Site Issue button is enabled for http and https tabs,
    on page load, or TabSelect, and disabled for everything else. */
 add_task(async function test_button_state_disabled() {
   let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE);
-  openPageActions();
-  await BrowserTestUtils.waitForEvent(BrowserPageActions.panelNode, "popupshown");
+  await openPageActions();
   is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on tab load");
 
   let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, NONREPORTABLE_PAGE);
-  openPageActions();
-  await BrowserTestUtils.waitForEvent(BrowserPageActions.panelNode, "popupshown");
+  await openPageActions();
   is(isButtonDisabled(), true, "Check that button is disabled for non-reportable schemes on tab load");
 
   let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, REPORTABLE_PAGE2);
-  openPageActions();
-  await BrowserTestUtils.waitForEvent(BrowserPageActions.panelNode, "popupshown");
+  await openPageActions();
   is(isButtonDisabled(), false, "Check that button is enabled for reportable schemes on tab load");
 
   BrowserTestUtils.removeTab(tab1);
   BrowserTestUtils.removeTab(tab2);
   BrowserTestUtils.removeTab(tab3);
 });
 
 /* Test that the button is enabled or disabled when we expected it to be, when
--- a/browser/extensions/webcompat-reporter/test/browser/browser_report_site_issue.js
+++ b/browser/extensions/webcompat-reporter/test/browser/browser_report_site_issue.js
@@ -3,29 +3,29 @@
 add_task(async function test_opened_page() {
   requestLongerTimeout(2);
 
   // ./head.js sets the value for PREF_WC_REPORTER_ENDPOINT
   await SpecialPowers.pushPrefEnv({set: [[PREF_WC_REPORTER_ENDPOINT, NEW_ISSUE_PAGE]]});
 
   let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE);
 
+  await openPageActions();
   let webcompatButton = document.getElementById(WC_PAGE_ACTION_ID);
   ok(webcompatButton, "Report Site Issue button exists.");
 
   let screenshotPromise;
   let newTabPromise = new Promise(resolve => {
     gBrowser.tabContainer.addEventListener("TabOpen", event => {
       let tab = event.target;
       screenshotPromise = BrowserTestUtils.waitForContentEvent(
         tab.linkedBrowser, "ScreenshotReceived", false, null, true);
       resolve(tab);
     }, { once: true });
   });
-  openPageActions();
   webcompatButton.click();
   let tab2 = await newTabPromise;
   await screenshotPromise;
 
   await ContentTask.spawn(tab2.linkedBrowser, {TEST_PAGE}, function(args) {
     let doc = content.document;
     let urlParam = doc.getElementById("url").innerText;
     let preview = doc.getElementById("screenshot-preview");
--- a/browser/extensions/webcompat-reporter/test/browser/head.js
+++ b/browser/extensions/webcompat-reporter/test/browser/head.js
@@ -12,18 +12,66 @@ function isButtonDisabled() {
   return document.getElementById(WC_PAGE_ACTION_ID).disabled;
 }
 
 function isURLButtonEnabled() {
   return document.getElementById(WC_PAGE_ACTION_URLBAR_ID) !== null;
 }
 
 function openPageActions() {
-  var event = new MouseEvent("click");
-  BrowserPageActions.mainButtonClicked(event);
+  let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                  .getInterface(Ci.nsIDOMWindowUtils);
+  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 new Promise(resolve => {
+    if (BrowserPageActions.panelNode.state == "open") {
+      executeSoon(resolve);
+      return;
+    }
+    BrowserPageActions.panelNode.addEventListener("popupshown", () => {
+      executeSoon(resolve);
+    }, { once: true });
+  });
+}
+
+function promisePageActionViewChildrenVisible(panelViewNode) {
+  info("promisePageActionViewChildrenVisible waiting for a child node to be visible");
+  let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                  .getInterface(Ci.nsIDOMWindowUtils);
+  return BrowserTestUtils.waitForCondition(() => {
+    let bodyNode = panelViewNode.firstChild;
+    for (let childNode of bodyNode.childNodes) {
+      let bounds = dwu.getBoundsWithoutFlushing(childNode);
+      if (bounds.width > 0 && bounds.height > 0) {
+        return true;
+      }
+    }
+    return false;
+  });
 }
 
 function pinToURLBar() {
   PageActions.actionForID("webcompat-reporter-button").pinnedToUrlbar = true;
 }
 
 function unpinFromURLBar() {
   PageActions.actionForID("webcompat-reporter-button").pinnedToUrlbar = false;
--- a/browser/modules/PageActions.jsm
+++ b/browser/modules/PageActions.jsm
@@ -2,37 +2,37 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var EXPORTED_SYMBOLS = [
   "PageActions",
   // PageActions.Action
-  // PageActions.Button
-  // PageActions.Subview
   // PageActions.ACTION_ID_BOOKMARK
   // PageActions.ACTION_ID_BOOKMARK_SEPARATOR
   // PageActions.ACTION_ID_BUILT_IN_SEPARATOR
+  // PageActions.ACTION_ID_TRANSIENT_SEPARATOR
 ];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 ChromeUtils.defineModuleGetter(this, "AppConstants",
   "resource://gre/modules/AppConstants.jsm");
 ChromeUtils.defineModuleGetter(this, "AsyncShutdown",
   "resource://gre/modules/AsyncShutdown.jsm");
 ChromeUtils.defineModuleGetter(this, "BinarySearch",
   "resource://gre/modules/BinarySearch.jsm");
 
 
 const ACTION_ID_BOOKMARK = "bookmark";
 const ACTION_ID_BOOKMARK_SEPARATOR = "bookmarkSeparator";
 const ACTION_ID_BUILT_IN_SEPARATOR = "builtInSeparator";
+const ACTION_ID_TRANSIENT_SEPARATOR = "transientSeparator";
 
 const PREF_PERSISTED_ACTIONS = "browser.pageActions.persistedActions";
 const PERSISTED_ACTIONS_CURRENT_VERSION = 1;
 
 // Escapes the given raw URL string, and returns an equivalent CSS url()
 // value for it.
 function escapeCSSURL(url) {
   return `url("${url.replace(/[\\\s"]/g, encodeURIComponent)}")`;
@@ -79,54 +79,68 @@ var PageActions = {
       "PageActions: purging unregistered actions from cache",
       () => this._purgeUnregisteredPersistedActions(),
     );
   },
 
   _deferredAddActionCalls: [],
 
   /**
-   * The list of Action objects, sorted in the order in which they should be
-   * 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)
+   * A list of all Action objects, not in any particular order.  Not live.
+   * (array of Action objects)
    */
   get actions() {
-    let actions = this.builtInActions;
-    if (this.nonBuiltInActions.length) {
-      // There are non-built-in actions, so include them too.
+    let lists = [
+      this._builtInActions,
+      this._nonBuiltInActions,
+      this._transientActions,
+    ];
+    return lists.reduce((memo, list) => memo.concat(list), []);
+  },
+
+  /**
+   * The list of Action objects that should appear in the panel for a given
+   * window, sorted in the order in which they appear.  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)
+   *
+   * @param  browserWindow (DOM window, required)
+   *         This window's actions will be returned.
+   * @return (array of PageAction.Action objects) The actions currently in the
+   *         given window's panel.
+   */
+  actionsInPanel(browserWindow) {
+    function filter(action) {
+      return action.shouldShowInPanel(browserWindow);
+    }
+    let actions = this._builtInActions.filter(filter);
+    let nonBuiltInActions = this._nonBuiltInActions.filter(filter);
+    if (nonBuiltInActions.length) {
       if (actions.length) {
-        // There are both built-in and non-built-in actions.  Add a separator
-        // between the two groups 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(...nonBuiltInActions);
+    }
+    let transientActions = this._transientActions.filter(filter);
+    if (transientActions.length) {
+      if (actions.length) {
+        actions.push(new Action({
+          id: ACTION_ID_TRANSIENT_SEPARATOR,
+          _isSeparator: true,
+        }));
+      }
+      actions.push(...transientActions);
     }
     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();
-  },
-
-  /**
    * The list of actions currently in the urlbar, sorted in the order in which
    * they appear.  Not live.
    *
    * @param  browserWindow (DOM window, required)
    *         This window's actions will be returned.
    * @return (array of PageAction.Action objects) The actions currently in the
    *         given window's urlbar.
    */
@@ -170,35 +184,20 @@ var PageActions = {
    */
   addAction(action) {
     if (this._deferredAddActionCalls) {
       // init() hasn't been called yet.  Defer all additions until it's called,
       // at which time _deferredAddActionCalls will be deleted.
       this._deferredAddActionCalls.push(() => this.addAction(action));
       return action;
     }
-
-    let hadSep = this.actions.some(a => a.id == ACTION_ID_BUILT_IN_SEPARATOR);
-
     this._registerAction(action);
-
-    let sep = null;
-    if (!hadSep) {
-      sep = this.actions.find(a => a.id == ACTION_ID_BUILT_IN_SEPARATOR);
-    }
-
     for (let bpa of allBrowserPageActions()) {
-      if (sep) {
-        // There are now both built-in and non-built-in actions, so place the
-        // separator between the two groups.
-        bpa.placeAction(sep);
-      }
       bpa.placeAction(action);
     }
-
     return action;
   },
 
   _registerAction(action) {
     if (this.actionForID(action.id)) {
       throw new Error(`Action with ID '${action.id}' already added`);
     }
     this._actionsByID.set(action.id, action);
@@ -213,20 +212,23 @@ var PageActions = {
       // bundled with the browser.  Right now we simply assume that no other
       // consumers will use _insertBeforeActionID.
       let index =
         !action.__insertBeforeActionID ? -1 :
         this._builtInActions.findIndex(a => {
           return a.id == action.__insertBeforeActionID;
         });
       if (index < 0) {
-        // Append the action.
-        index = this._builtInActions.length;
+        // Append the action (excluding transient actions).
+        index = this._builtInActions.filter(a => !a.__transient).length;
       }
       this._builtInActions.splice(index, 0, action);
+    } else if (action.__transient) {
+      // A transient action.
+      this._transientActions.push(action);
     } else if (gBuiltInActions.find(a => a.id == action.id)) {
       // A built-in action.  These are always added on init before all other
       // actions, one after the other, so just push onto the array.
       this._builtInActions.push(action);
     } else {
       // A non-built-in action, like a non-bundled extension potentially.
       // Keep this list sorted by title.
       let index = BinarySearch.insertionIndexOf((a1, a2) => {
@@ -264,95 +266,38 @@ var PageActions = {
       this._persistedActions.idsInUrlbar.splice(index, 1);
     }
     this._storePersistedActions();
   },
 
   // These keep track of currently registered actions.
   _builtInActions: [],
   _nonBuiltInActions: [],
+  _transientActions: [],
   _actionsByID: new Map(),
 
   /**
-   * Returns the ID of the action before which the given action should be
-   * inserted in the urlbar.
-   *
-   * @param  action (Action object, required)
-   *         The action you're inserting.
-   * @return The ID of the reference action, or null if your action should be
-   *         appended.
-   */
-  nextActionIDInUrlbar(browserWindow, action) {
-    // Actions in the urlbar are always inserted before the bookmark action,
-    // which always comes last if it's present.
-    if (action.id == ACTION_ID_BOOKMARK) {
-      return null;
-    }
-    let id = this._nextActionID(action, this.actionsInUrlbar(browserWindow));
-    return id || ACTION_ID_BOOKMARK;
-  },
-
-  /**
-   * Returns the ID of the action before which the given action should be
-   * inserted in the panel.
-   *
-   * @param  action (Action object, required)
-   *         The action you're inserting.
-   * @return The ID of the reference action, or null if your action should be
-   *         appended.
-   */
-  nextActionIDInPanel(action) {
-    return this._nextActionID(action, this.actions);
-  },
-
-  /**
-   * The DOM nodes of actions should be ordered properly, both in the panel and
-   * the urlbar.  This method returns the ID of the action that comes after the
-   * given action in the given array.  You can use the returned ID to get a DOM
-   * node ID to pass to node.insertBefore().
-   *
-   * Pass PageActions.actions to get the ID of the next action in the panel.
-   * Pass PageActions.actionsInUrlbar to get the ID of the next action in the
-   * urlbar.
-   *
-   * @param  action
-   *         The action whose node you want to insert into your DOM.
-   * @param  actionArray
-   *         The relevant array of actions, either PageActions.actions or
-   *         actionsInUrlbar.
-   * @return The ID of the action before which the given action should be
-   *         inserted.  If the given action should be inserted last, returns
-   *         null.
-   */
-  _nextActionID(action, actionArray) {
-    let index = actionArray.findIndex(a => a.id == action.id);
-    if (index < 0) {
-      return null;
-    }
-    let nextAction = actionArray[index + 1];
-    if (!nextAction) {
-      return null;
-    }
-    return nextAction.id;
-  },
-
-  /**
    * Call this when an action is removed.
    *
    * @param  action (Action object, required)
    *         The action that was removed.
    */
   onActionRemoved(action) {
     if (!this.actionForID(action.id)) {
       // The action isn't registered (yet).  Not an error.
       return;
     }
 
     this._actionsByID.delete(action.id);
-    for (let list of [this._nonBuiltInActions, this._builtInActions]) {
+    let lists = [
+      this._builtInActions,
+      this._nonBuiltInActions,
+      this._transientActions,
+    ];
+    for (let list of lists) {
       let index = list.findIndex(a => a.id == action.id);
       if (index >= 0) {
         list.splice(index, 1);
         break;
       }
     }
 
     for (let bpa of allBrowserPageActions()) {
@@ -396,16 +341,17 @@ var PageActions = {
     }
   },
 
   // For tests.  See Bug 1413692.
   _reset() {
     PageActions._purgeUnregisteredPersistedActions();
     PageActions._builtInActions = [];
     PageActions._nonBuiltInActions = [];
+    PageActions._transientActions = [];
     PageActions._actionsByID = new Map();
   },
 
   _storePersistedActions() {
     let json = JSON.stringify(this._persistedActions);
     Services.prefs.setStringPref(PREF_PERSISTED_ACTIONS, json);
   },
 
@@ -467,16 +413,17 @@ var PageActions = {
     version: PERSISTED_ACTIONS_CURRENT_VERSION,
     // action IDs that have ever been seen and not removed, order not important
     ids: [],
     // action IDs ordered by position in urlbar
     idsInUrlbar: [],
   },
 };
 
+
 /**
  * A single page action.
  *
  * Each action can have both per-browser-window state and global state.
  * Per-window state takes precedence over global state.  This is reflected in
  * the title, tooltip, disabled, and icon properties.  Each of these properties
  * has a getter method and setter method that takes a browser window.  Pass null
  * to get the action's global state.  Pass a browser window to get the per-
@@ -551,32 +498,39 @@ var PageActions = {
  * @param onRemovedFromWindow (function, optional)
  *        Called after the action is removed from a browser window:
  *        onRemovedFromWindow(browserWindow)
  *        * browserWindow: The browser window that the action was removed from.
  * @param onShowingInPanel (function, optional)
  *        Called when a browser window's page action panel is showing:
  *        onShowingInPanel(buttonNode)
  *        * buttonNode: The action's node in the page action panel.
+ * @param onSubviewPlaced (function, optional)
+ *        Called when the action's subview is added to its parent panel in a
+ *        browser window:
+ *        onSubviewPlaced(panelViewNode)
+ *        * panelViewNode: The subview's panelview node.
+ * @param onSubviewShowing (function, optional)
+ *        Called when the action's subview is showing in a browser window:
+ *        onSubviewShowing(panelViewNode)
+ *        * panelViewNode: The subview's panelview node.
  * @param pinnedToUrlbar (bool, optional)
  *        Pass true to pin the action to the urlbar.  An action is shown in the
  *        urlbar if it's pinned and not disabled.  False by default.
- * @param subview (object, optional)
- *        An options object suitable for passing to the Subview constructor, if
- *        you'd like the action to have a subview.  See the subview constructor
- *        for info on this object's properties.
  * @param tooltip (string, optional)
  *        The action's button tooltip text.
  * @param urlbarIDOverride (string, optional)
  *        Usually the ID of the action's button in the urlbar will be generated
  *        automatically.  Pass a string for this property to override that with
  *        your own ID.
  * @param wantsIframe (bool, optional)
  *        Pass true to make an action that shows an iframe in a panel when
  *        clicked.
+ * @param wantsSubview (bool, optional)
+ *        Pass true to make an action that shows a panel subview when clicked.
  */
 function Action(options) {
   setProperties(this, options, {
     id: true,
     title: !options._isSeparator,
     anchorIDOverride: false,
     disabled: false,
     extensionID: false,
@@ -588,121 +542,127 @@ function Action(options) {
     onIframeHiding: false,
     onIframeHidden: false,
     onIframeShowing: false,
     onLocationChange: false,
     onPlacedInPanel: false,
     onPlacedInUrlbar: false,
     onRemovedFromWindow: false,
     onShowingInPanel: false,
+    onSubviewPlaced: false,
+    onSubviewShowing: false,
     pinnedToUrlbar: false,
-    subview: false,
     tooltip: false,
     urlbarIDOverride: false,
     wantsIframe: false,
+    wantsSubview: false,
 
     // private
 
     // (string, optional)
     // The ID of another action before which to insert this new action in the
     // panel.
     _insertBeforeActionID: false,
 
     // (bool, optional)
     // True if this isn't really an action but a separator to be shown in the
     // page action panel.
     _isSeparator: false,
 
     // (bool, optional)
+    // Transient actions have a couple of special properties: (1) They stick to
+    // the bottom of the panel, and (2) they're hidden in the panel when they're
+    // disabled.  Other than that they behave like other actions.
+    _transient: false,
+
+    // (bool, optional)
     // True if the action's urlbar button is defined in markup.  In that case, a
     // node with the action's urlbar node ID should already exist in the DOM
     // (either the auto-generated ID or urlbarIDOverride).  That node will be
     // shown when the action is added to the urlbar and hidden when the action
     // is removed from the urlbar.
     _urlbarNodeInMarkup: false,
   });
-  if (this._subview) {
-    this._subview = new Subview(options.subview);
-  }
 
   /**
    * A cache of the pre-computed CSS variable values for a given icon
    * URLs object, as passed to _createIconProperties.
    */
   this._iconProperties = new WeakMap();
 
   /**
    * The global values for the action properties.
    */
   this._globalProps = {
     disabled: this._disabled,
     iconURL: this._iconURL,
     iconProps: this._createIconProperties(this._iconURL),
     title: this._title,
     tooltip: this._tooltip,
+    wantsSubview: this._wantsSubview,
   };
 
   /**
    * A mapping of window-specific action property objects, each of which
    * derives from the _globalProps object.
    */
   this._windowProps = new WeakMap();
 }
 
 Action.prototype = {
   /**
-   * The ID of the action's parent extension (string, nullable)
+   * The ID of the action's parent extension (string)
    */
   get extensionID() {
     return this._extensionID;
   },
 
   /**
-   * The action's ID (string, nonnull)
+   * The action's ID (string)
    */
   get id() {
     return this._id;
   },
 
   /**
    * Attribute name => value mapping to set on nodes created for this action
-   * (object, nullable)
+   * (object)
    */
   get nodeAttributes() {
     return this._nodeAttributes;
   },
 
   /**
    * True if the action is pinned to the urlbar.  The action is shown in the
-   * urlbar if it's pinned and not disabled.  (bool, nonnull)
+   * urlbar if it's pinned and not disabled.  (bool)
    */
   get pinnedToUrlbar() {
     return this._pinnedToUrlbar || false;
   },
   set pinnedToUrlbar(shown) {
     if (this.pinnedToUrlbar != shown) {
       this._pinnedToUrlbar = shown;
       PageActions.onActionToggledPinnedToUrlbar(this);
     }
     return this.pinnedToUrlbar;
   },
 
   /**
-   * The action's disabled state (bool, nonnull)
+   * The action's disabled state (bool)
    */
   getDisabled(browserWindow = null) {
     return !!this._getProperties(browserWindow).disabled;
   },
   setDisabled(value, browserWindow = null) {
     return this._setProperty("disabled", !!value, browserWindow);
   },
 
   /**
    * The action's icon URL string, or an object mapping sizes to URL strings
-   * (string or object, nullable)
+   * (string or object)
    */
   getIconURL(browserWindow = null) {
     return this._getProperties(browserWindow).iconURL;
   },
   setIconURL(value, browserWindow = null) {
     let props = this._getProperties(browserWindow, !!browserWindow);
     props.iconURL = value;
     props.iconProps = this._createIconProperties(value);
@@ -734,36 +694,46 @@ Action.prototype = {
 
     return Object.freeze({
       "--pageAction-image-16px": null,
       "--pageAction-image-32px": urls ? escapeCSSURL(urls) : null,
     });
   },
 
   /**
-   * The action's title (string, nonnull)
+   * The action's title (string)
    */
   getTitle(browserWindow = null) {
     return this._getProperties(browserWindow).title;
   },
   setTitle(value, browserWindow = null) {
     return this._setProperty("title", value, browserWindow);
   },
 
   /**
-   * The action's tooltip (string, nullable)
+   * The action's tooltip (string)
    */
   getTooltip(browserWindow = null) {
     return this._getProperties(browserWindow).tooltip;
   },
   setTooltip(value, browserWindow = null) {
     return this._setProperty("tooltip", value, browserWindow);
   },
 
   /**
+   * Whether the action wants a subview (bool)
+   */
+  getWantsSubview(browserWindow = null) {
+    return !!this._getProperties(browserWindow).wantsSubview;
+  },
+  setWantsSubview(value, browserWindow = null) {
+    return this._setProperty("wantsSubview", !!value, browserWindow);
+  },
+
+  /**
    * Sets a property, optionally for a particular browser window.
    *
    * @param  name (string, required)
    *         The (non-underscored) name of the property.
    * @param  value
    *         The value.
    * @param  browserWindow (DOM window, optional)
    *         If given, then the property will be set in this window's state, not
@@ -776,17 +746,17 @@ Action.prototype = {
     this._updateProperty(name, value, browserWindow);
     return value;
   },
 
   _updateProperty(name, value, browserWindow) {
     // This may be called before the action has been added.
     if (PageActions.actionForID(this.id)) {
       for (let bpa of allBrowserPageActions(browserWindow)) {
-        bpa.updateAction(this, name, value);
+        bpa.updateAction(this, name, { value });
       }
     }
   },
 
   /**
    * Returns the properties object for the given window, if it exists,
    * or the global properties object if no window-specific properties
    * exist.
@@ -807,44 +777,36 @@ Action.prototype = {
       props = Object.create(this._globalProps);
       this._windowProps.set(window, props);
     }
 
     return props || this._globalProps;
   },
 
   /**
-   * Override for the ID of the action's activated-action panel anchor (string,
-   * nullable)
+   * Override for the ID of the action's activated-action panel anchor (string)
    */
   get anchorIDOverride() {
     return this._anchorIDOverride;
   },
 
   /**
-   * Override for the ID of the action's urlbar node (string, nullable)
+   * Override for the ID of the action's urlbar node (string)
    */
   get urlbarIDOverride() {
     return this._urlbarIDOverride;
   },
 
   /**
-   * True if the action is shown in an iframe (bool, nonnull)
+   * True if the action is shown in an iframe (bool)
    */
   get wantsIframe() {
     return this._wantsIframe || false;
   },
 
-  /**
-   * A Subview object if the action wants a subview (Subview, nullable)
-   */
-  get subview() {
-    return this._subview;
-  },
-
   get labelForHistogram() {
     return this._labelForHistogram || this._id;
   },
 
   /**
    * Returns the URL of the best icon to use given a preferred size.  The best
    * icon is the one with the smallest size that's equal to or bigger than the
    * preferred size.  Returns null if the action has no icon URL.
@@ -1026,28 +988,66 @@ Action.prototype = {
    */
   onShowingInPanel(buttonNode) {
     if (this._onShowingInPanel) {
       this._onShowingInPanel(buttonNode);
     }
   },
 
   /**
+   * Call this when a panelview node for the action's subview is added to the
+   * DOM.
+   *
+   * @param  panelViewNode (DOM node, required)
+   *         The subview's panelview node.
+   */
+  onSubviewPlaced(panelViewNode) {
+    if (this._onSubviewPlaced) {
+      this._onSubviewPlaced(panelViewNode);
+    }
+  },
+
+  /**
+   * Call this when a panelview node for the action's subview is showing.
+   *
+   * @param  panelViewNode (DOM node, required)
+   *         The subview's panelview node.
+   */
+  onSubviewShowing(panelViewNode) {
+    if (this._onSubviewShowing) {
+      this._onSubviewShowing(panelViewNode);
+    }
+  },
+
+  /**
    * Removes the action's DOM nodes from all browser windows.
    *
    * PageActions will remember the action's urlbar placement, if any, after this
    * method is called until app shutdown.  If the action is not added again
    * before shutdown, then PageActions will discard the placement, and the next
    * time the action is added, its placement will be reset.
    */
   remove() {
     PageActions.onActionRemoved(this);
   },
 
   /**
+   * Returns whether the action should be shown in a given window's panel.
+   *
+   * @param  browserWindow (DOM window, required)
+   *         The window.
+   * @return True if the action should be shown and false otherwise.  Actions
+   *         are always shown in the panel unless they're both transient and
+   *         disabled.
+   */
+  shouldShowInPanel(browserWindow) {
+    return !this.__transient || !this.getDisabled(browserWindow);
+  },
+
+  /**
    * Returns whether the action should be shown in a given window's urlbar.
    *
    * @param  browserWindow (DOM window, required)
    *         The window.
    * @return True if the action should be shown and false otherwise.  The action
    *         should be shown if it's both pinned and not disabled.
    */
   shouldShowInUrlbar(browserWindow) {
@@ -1061,168 +1061,24 @@ Action.prototype = {
       "webcompat-reporter-button",
     ].concat(gBuiltInActions.filter(a => !a.__isSeparator).map(a => a.id));
     return builtInIDs.includes(this.id);
   },
 };
 
 this.PageActions.Action = Action;
 
-
-/**
- * A Subview represents a PanelUI panelview that your actions can show.
- * `options` is a required object with the following properties.
- *
- * @param buttons (array, optional)
- *        An array of buttons to show in the subview.  Each item in the array
- *        must be an options object suitable for passing to the Button
- *        constructor.  See the Button constructor for information on these
- *        objects' properties.
- * @param onPlaced (function, optional)
- *        Called when the subview is added to its parent panel in a browser
- *        window:
- *        onPlaced(panelViewNode)
- *        * panelViewNode: The panelview node represented by this Subview.
- * @param onShowing (function, optional)
- *        Called when the subview is showing in a browser window:
- *        onShowing(panelViewNode)
- *        * panelViewNode: The panelview node represented by this Subview.
- */
-function Subview(options) {
-  setProperties(this, options, {
-    buttons: false,
-    onPlaced: false,
-    onShowing: false,
-  });
-  this._buttons = (this._buttons || []).map(buttonOptions => {
-    return new Button(buttonOptions);
-  });
-}
-
-Subview.prototype = {
-  /**
-   * The subview's buttons (array of Button objects, nonnull)
-   */
-  get buttons() {
-    return this._buttons;
-  },
-
-  /**
-   * Call this when a DOM node for the subview is added to the DOM.
-   *
-   * @param  panelViewNode (DOM node, required)
-   *         The subview's panelview node.
-   */
-  onPlaced(panelViewNode) {
-    if (this._onPlaced) {
-      this._onPlaced(panelViewNode);
-    }
-  },
-
-  /**
-   * Call this when a DOM node for the subview is showing.
-   *
-   * @param  panelViewNode (DOM node, required)
-   *         The subview's panelview node.
-   */
-  onShowing(panelViewNode) {
-    if (this._onShowing) {
-      this._onShowing(panelViewNode);
-    }
-  }
-};
-
-this.PageActions.Subview = Subview;
-
-
-/**
- * A button that can be shown in a subview.  `options` is a required object with
- * the following properties.
- *
- * @param id (string, required)
- *        The button's ID.  This will not become the ID of a DOM node by itself,
- *        but it will be used to generate DOM node IDs.  But in terms of spaces
- *        and weird characters and such, do treat this like a DOM node ID.
- * @param title (string, required)
- *        The button's title.
- * @param disabled (bool, required)
- *        Pass true to disable the button.
- * @param onCommand (function, optional)
- *        Called when the button is clicked:
- *        onCommand(event, buttonNode)
- *        * event: The triggering event.
- *        * buttonNode: The node that was clicked.
- * @param shortcut (string, optional)
- *        The button's shortcut text.
- */
-function Button(options) {
-  setProperties(this, options, {
-    id: true,
-    title: true,
-    disabled: false,
-    onCommand: false,
-    shortcut: false,
-  });
-}
-
-Button.prototype = {
-  /**
-   * True if the button is disabled (bool, nonnull)
-   */
-  get disabled() {
-    return this._disabled || false;
-  },
-
-  /**
-   * The button's ID (string, nonnull)
-   */
-  get id() {
-    return this._id;
-  },
-
-  /**
-   * The button's shortcut (string, nullable)
-   */
-  get shortcut() {
-    return this._shortcut;
-  },
-
-  /**
-   * The button's title (string, nonnull)
-   */
-  get title() {
-    return this._title;
-  },
-
-  /**
-   * Call this when the user clicks the button.
-   *
-   * @param  event (DOM event, required)
-   *         The triggering event.
-   * @param  buttonNode (DOM node, required)
-   *         The button's DOM node that was clicked.
-   */
-  onCommand(event, buttonNode) {
-    if (this._onCommand) {
-      this._onCommand(event, buttonNode);
-    }
-  }
-};
-
-this.PageActions.Button = Button;
-
+this.PageActions.ACTION_ID_BUILT_IN_SEPARATOR = ACTION_ID_BUILT_IN_SEPARATOR;
+this.PageActions.ACTION_ID_TRANSIENT_SEPARATOR = ACTION_ID_TRANSIENT_SEPARATOR;
 
 // These are only necessary so that Pocket and the test can use them.
 this.PageActions.ACTION_ID_BOOKMARK = ACTION_ID_BOOKMARK;
 this.PageActions.ACTION_ID_BOOKMARK_SEPARATOR = ACTION_ID_BOOKMARK_SEPARATOR;
 this.PageActions.PREF_PERSISTED_ACTIONS = PREF_PERSISTED_ACTIONS;
 
-// 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.
 // NOTE: If you add items to this list (or system add-on actions that we
 // want to keep track of), make sure to also update Histograms.json for the
 // new actions.
 var gBuiltInActions = [
@@ -1270,47 +1126,39 @@ var gBuiltInActions = [
     id: "emailLink",
     title: "emailLink-title",
     onPlacedInPanel(buttonNode) {
       browserPageActions(buttonNode).emailLink.onPlacedInPanel(buttonNode);
     },
     onCommand(event, buttonNode) {
       browserPageActions(buttonNode).emailLink.onCommand(event, buttonNode);
     },
-  }
+  },
 ];
 
 if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
   gBuiltInActions.push(
   // send to device
   {
     id: "sendToDevice",
     title: "sendToDevice-title",
     onPlacedInPanel(buttonNode) {
       browserPageActions(buttonNode).sendToDevice.onPlacedInPanel(buttonNode);
     },
     onLocationChange(browserWindow) {
       browserPageActions(browserWindow).sendToDevice.onLocationChange();
     },
-    subview: {
-      buttons: [
-        {
-          id: "notReady",
-          title: "sendToDevice-notReadyTitle",
-          disabled: true,
-        },
-      ],
-      onPlaced(panelViewNode) {
-        browserPageActions(panelViewNode).sendToDevice
-          .onSubviewPlaced(panelViewNode);
-      },
-      onShowing(panelViewNode) {
-        browserPageActions(panelViewNode).sendToDevice
-          .onShowingSubview(panelViewNode);
-      },
+    wantsSubview: true,
+    onSubviewPlaced(panelViewNode) {
+      browserPageActions(panelViewNode).sendToDevice
+        .onSubviewPlaced(panelViewNode);
+    },
+    onSubviewShowing(panelViewNode) {
+      browserPageActions(panelViewNode).sendToDevice
+        .onShowingSubview(panelViewNode);
     },
   });
 }
 
 
 /**
  * Gets a BrowserPageActions object in a browser window.
  *
--- a/browser/modules/test/browser/browser_PageActions.js
+++ b/browser/modules/test/browser/browser_PageActions.js
@@ -37,17 +37,25 @@ add_task(async function simple() {
   let onPlacedInPanelCallCount = 0;
   let onPlacedInUrlbarCallCount = 0;
   let onShowingInPanelCallCount = 0;
   let onCommandExpectedButtonID;
 
   let panelButtonID = BrowserPageActions.panelButtonNodeIDForActionID(id);
   let urlbarButtonID = BrowserPageActions.urlbarButtonNodeIDForActionID(id);
 
+
+  // Open the panel so that actions are added to it, and then close it.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+
   let initialActions = PageActions.actions;
+  let initialActionsInPanel = PageActions.actionsInPanel(window);
+  let initialActionsInUrlbar = PageActions.actionsInUrlbar(window);
 
   let action = PageActions.addAction(new PageActions.Action({
     iconURL,
     id,
     nodeAttributes,
     title,
     tooltip,
     onCommand(event, buttonNode) {
@@ -72,86 +80,139 @@ add_task(async function simple() {
       Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id");
     },
   }));
 
   Assert.equal(action.getIconURL(), iconURL, "iconURL");
   Assert.equal(action.id, id, "id");
   Assert.deepEqual(action.nodeAttributes, nodeAttributes, "nodeAttributes");
   Assert.equal(action.pinnedToUrlbar, false, "pinnedToUrlbar");
-  Assert.equal(action.subview, null, "subview");
   Assert.equal(action.getDisabled(), false, "disabled");
   Assert.equal(action.getDisabled(window), false, "disabled in window");
   Assert.equal(action.getTitle(), title, "title");
   Assert.equal(action.getTitle(window), title, "title in window");
   Assert.equal(action.getTooltip(), tooltip, "tooltip");
   Assert.equal(action.getTooltip(window), tooltip, "tooltip in window");
+  Assert.equal(action.getWantsSubview(), false, "subview");
+  Assert.equal(action.getWantsSubview(window), false, "subview in window");
   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.ok(!("__transient" in action), "__transient");
 
-  Assert.equal(onPlacedInPanelCallCount, 1,
-               "onPlacedInPanelCallCount should be inc'ed");
+  // The action shouldn't be placed in the panel until it opens for the first
+  // time.
+  Assert.equal(onPlacedInPanelCallCount, 0,
+               "onPlacedInPanelCallCount should remain 0");
   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");
+  // Open the panel so that actions are added to it, and then close it.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+
+  Assert.equal(onPlacedInPanelCallCount, 1,
+               "onPlacedInPanelCallCount should be inc'ed");
+  Assert.equal(onShowingInPanelCallCount, 1,
+               "onShowingInPanelCallCount should be inc'ed");
+
+  // Build an array of the expected actions in the panel and compare it to the
+  // actual actions.  Don't assume that there are or aren't already other non-
+  // built-in actions.
+  let sepIndex =
+    initialActionsInPanel
+    .findIndex(a => a.id == PageActions.ACTION_ID_BUILT_IN_SEPARATOR);
+  let initialSepIndex = sepIndex;
+  let indexInPanel;
+  if (sepIndex < 0) {
+    // No prior non-built-in actions.
+    indexInPanel = initialActionsInPanel.length;
+  } else {
+    // Prior non-built-in actions.  Find the index where the action goes.
+    for (indexInPanel = sepIndex + 1;
+         indexInPanel < initialActionsInPanel.length;
+         indexInPanel++) {
+      let a = initialActionsInPanel[indexInPanel];
+      if (a.getTitle().localeCompare(action.getTitle()) < 1) {
+        break;
+      }
+    }
+  }
+  let expectedActionsInPanel = initialActionsInPanel.slice();
+  expectedActionsInPanel.splice(indexInPanel, 0, action);
+  // The separator between the built-ins and non-built-ins should be present
+  // if it's not already.
+  if (sepIndex < 0) {
+    expectedActionsInPanel.splice(indexInPanel, 0, new PageActions.Action({
+      id: PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+      _isSeparator: true,
+    }));
+    sepIndex = indexInPanel;
+    indexInPanel++;
+  }
+  Assert.deepEqual(PageActions.actionsInPanel(window),
+                   expectedActionsInPanel,
+                   "Actions in panel after adding the action");
+
+  // The actions in the urlbar should be the same since the test action isn't
+  // shown there.
+  Assert.deepEqual(PageActions.actionsInUrlbar(window),
+                   initialActionsInUrlbar,
+                   "Actions in urlbar after adding the action");
+
+  // Check the list of all actions.
+  Assert.deepEqual(PageActions.actions,
+                   initialActions.concat([action]),
+                   "All actions after adding the action");
 
   Assert.deepEqual(PageActions.actionForID(action.id), action,
                    "actionForID should be action");
 
   Assert.ok(PageActions._persistedActions.ids.includes(action.id),
             "PageActions should record action in its list of seen actions");
 
   // The action's panel button should have been created.
-  let panelButtonNode = document.getElementById(panelButtonID);
+  let panelButtonNode =
+    BrowserPageActions.mainViewBodyNode.childNodes[indexInPanel];
   Assert.notEqual(panelButtonNode, null, "panelButtonNode");
+  Assert.equal(panelButtonNode.id, panelButtonID, "panelButtonID");
   Assert.equal(panelButtonNode.getAttribute("label"), action.getTitle(),
                "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");
+  // The separator between the built-ins and non-built-ins should exist.
+  let sepNode =
+    BrowserPageActions.mainViewBodyNode.childNodes[sepIndex];
+  Assert.notEqual(sepNode, null, "sepNode");
   Assert.equal(
-    panelButtonNode.previousSibling.id,
+    sepNode.id,
     BrowserPageActions.panelButtonNodeIDForActionID(
       PageActions.ACTION_ID_BUILT_IN_SEPARATOR
     ),
-    "previousSibling.id"
+    "sepNode.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,
+  await promiseOpenPageActionPanel();
+  Assert.equal(onShowingInPanelCallCount, 2,
                "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.pinnedToUrlbar = true;
@@ -228,28 +289,37 @@ add_task(async function simple() {
 
   // Remove the action.
   action.remove();
   panelButtonNode = document.getElementById(panelButtonID);
   Assert.equal(panelButtonNode, null, "panelButtonNode");
   urlbarButtonNode = document.getElementById(urlbarButtonID);
   Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
 
-  // 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");
+  if (initialSepIndex < 0) {
+    // The separator between the built-in actions and non-built-in actions
+    // should be gone now, too.
+    Assert.equal(separatorNode, null, "No separator");
+    Assert.ok(!BrowserPageActions.mainViewBodyNode
+              .lastChild.localName.includes("separator"),
+              "Last child should not be separator");
+  } else {
+    // The separator should still be present.
+    Assert.notEqual(separatorNode, null, "Separator should still exist");
+  }
 
+  Assert.deepEqual(PageActions.actionsInPanel(window), initialActionsInPanel,
+                   "Actions in panel should go back to initial");
+  Assert.deepEqual(PageActions.actionsInUrlbar(window), initialActionsInUrlbar,
+                   "Actions in urlbar should go back to initial");
   Assert.deepEqual(PageActions.actions, initialActions,
                    "Actions should go back to initial");
   Assert.equal(PageActions.actionForID(action.id), null,
                "actionForID should be null");
 
   Assert.ok(PageActions._persistedActions.ids.includes(action.id),
             "Action ID should remain in cache until purged");
   PageActions._purgeUnregisteredPersistedActions();
@@ -257,188 +327,132 @@ add_task(async function simple() {
             "Action ID should be removed from cache after being purged");
 });
 
 
 // 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/mail.svg",
     id,
     pinnedToUrlbar: true,
-    subview,
     title: "Test subview",
-    onCommand(event, buttonNode) {
-      onActionCommandCallCount++;
-    },
+    wantsSubview: true,
     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");
     },
+    onSubviewPlaced(panelViewNode) {
+      onSubviewPlacedCount++;
+      Assert.ok(panelViewNode,
+                "panelViewNode should be non-null: " + panelViewNode);
+      Assert.equal(panelViewNode.id, onSubviewPlacedExpectedPanelViewID,
+                   "panelViewNode.id");
+    },
+    onSubviewShowing(panelViewNode) {
+      onSubviewShowingCount++;
+      Assert.ok(panelViewNode,
+                "panelViewNode should be non-null: " + panelViewNode);
+      Assert.equal(panelViewNode.id, onSubviewShowingExpectedPanelViewID,
+                   "panelViewNode.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.equal(action.getWantsSubview(), true, "subview");
+  Assert.equal(action.getWantsSubview(window), true, "subview in window");
 
-  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);
-  }
+  // The action shouldn't be placed in the panel until it opens for the first
+  // time.
+  Assert.equal(onActionPlacedInPanelCallCount, 0,
+               "onActionPlacedInPanelCallCount should be 0");
+  Assert.equal(onSubviewPlacedCount, 0,
+               "onSubviewPlacedCount should be 0");
+
+  // But it should be placed in the urlbar.
+  Assert.equal(onActionPlacedInUrlbarCallCount, 1,
+               "onActionPlacedInUrlbarCallCount should be 0");
+
+  // Open the panel, which should place the action in it.
+  await promiseOpenPageActionPanel();
 
   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");
 
   // The button should have been inserted before the bookmark star.
   Assert.notEqual(urlbarButtonNode.nextSibling, null, "Should be a next node");
   Assert.equal(
     urlbarButtonNode.nextSibling.id,
     PageActions.actionForID(PageActions.ACTION_ID_BOOKMARK).urlbarIDOverride,
     "Next node should be the bookmark star"
   );
 
-  // Open the panel, click the action's button, click the subview's first
-  // button.
-  await promisePageActionPanelOpen();
+  // Click the action's button in the panel.  The subview should be shown.
   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();
+
+  // Click the main button to hide the main panel.
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
   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 activated-action
-  // panel showing the subview, and click the subview's first button.
+  // panel showing the subview.
   onSubviewPlacedExpectedPanelViewID = panelViewIDUrlbar;
   onSubviewShowingExpectedPanelViewID = panelViewIDUrlbar;
   EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
   await promisePanelShown(BrowserPageActions._activatedActionPanelID);
   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, {});
+
+  // Click the urlbar button again.  The activated-action panel should close.
+  EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
   assertActivatedPageActionPanelHidden();
-  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);
@@ -487,16 +501,20 @@ add_task(async function withIframe() {
       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");
 
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+
   Assert.equal(onPlacedInPanelCallCount, 1,
                "onPlacedInPanelCallCount should be inc'ed");
   Assert.equal(onPlacedInUrlbarCallCount, 1,
                "onPlacedInUrlbarCallCount should be inc'ed");
   Assert.equal(onIframeShowingCount, 0,
                "onIframeShowingCount should remain 0");
   Assert.equal(onCommandCallCount, 0,
                "onCommandCallCount should remain 0");
@@ -513,56 +531,56 @@ add_task(async function withIframe() {
   Assert.notEqual(urlbarButtonNode.nextSibling, null, "Should be a next node");
   Assert.equal(
     urlbarButtonNode.nextSibling.id,
     PageActions.actionForID(PageActions.ACTION_ID_BOOKMARK).urlbarIDOverride,
     "Next node should be the bookmark star"
   );
 
   // Open the panel, click the action's button.
-  await promisePageActionPanelOpen();
+  await promiseOpenPageActionPanel();
   Assert.equal(onIframeShowingCount, 0, "onIframeShowingCount should remain 0");
   EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
   await promisePanelShown(BrowserPageActions._activatedActionPanelID);
-  Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+  Assert.equal(onCommandCallCount, 1, "onCommandCallCount should be inc'ed");
   Assert.equal(onIframeShowingCount, 1, "onIframeShowingCount should be inc'ed");
 
   // The activated-action panel should have opened, anchored to the action's
   // urlbar button.
   let aaPanel =
     document.getElementById(BrowserPageActions._activatedActionPanelID);
   Assert.notEqual(aaPanel, null, "activated-action panel");
   Assert.equal(aaPanel.anchorNode.id, urlbarButtonID, "aaPanel.anchorNode.id");
   EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
   assertActivatedPageActionPanelHidden();
 
   // Click the action's urlbar button.
   EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
   await promisePanelShown(BrowserPageActions._activatedActionPanelID);
-  Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+  Assert.equal(onCommandCallCount, 2, "onCommandCallCount should be inc'ed");
   Assert.equal(onIframeShowingCount, 2, "onIframeShowingCount should be inc'ed");
 
   // The activated-action panel should have opened, again anchored to the
   // action's urlbar button.
   aaPanel = document.getElementById(BrowserPageActions._activatedActionPanelID);
   Assert.notEqual(aaPanel, null, "aaPanel");
   Assert.equal(aaPanel.anchorNode.id, urlbarButtonID, "aaPanel.anchorNode.id");
   EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {});
   assertActivatedPageActionPanelHidden();
 
   // Hide the action's button in the urlbar.
   action.pinnedToUrlbar = false;
   urlbarButtonNode = document.getElementById(urlbarButtonID);
   Assert.equal(urlbarButtonNode, null, "urlbarButtonNode");
 
   // Open the panel, click the action's button.
-  await promisePageActionPanelOpen();
+  await promiseOpenPageActionPanel();
   EventUtils.synthesizeMouseAtCenter(panelButtonNode, {});
   await promisePanelShown(BrowserPageActions._activatedActionPanelID);
-  Assert.equal(onCommandCallCount, 0, "onCommandCallCount should remain 0");
+  Assert.equal(onCommandCallCount, 3, "onCommandCallCount should be inc'ed");
   Assert.equal(onIframeShowingCount, 3, "onIframeShowingCount should be inc'ed");
 
   // The activated-action panel should have opened, this time anchored to the
   // main page action button in the urlbar.
   aaPanel = document.getElementById(BrowserPageActions._activatedActionPanelID);
   Assert.notEqual(aaPanel, null, "aaPanel");
   Assert.equal(aaPanel.anchorNode.id, BrowserPageActions.mainButtonNode.id,
                "aaPanel.anchorNode.id");
@@ -578,49 +596,54 @@ add_task(async function withIframe() {
 });
 
 
 // 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 => {
+  let initialActions = PageActions.actionsInPanel(window);
+  let initialBuiltInActions = PageActions._builtInActions.slice();
+  let initialNonBuiltInActions = PageActions._nonBuiltInActions.slice();
+  let initialBookmarkSeparatorIndex = initialActions.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,
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+
+  let newActions = PageActions.actionsInPanel(window);
+  Assert.equal(newActions.length,
                initialActions.length + 1,
                "PageActions.actions.length should be updated");
-  Assert.equal(PageActions.builtInActions.length,
+  Assert.equal(PageActions._builtInActions.length,
                initialBuiltInActions.length + 1,
-               "PageActions.builtInActions.length should be updated");
-  Assert.equal(PageActions.nonBuiltInActions.length,
+               "PageActions._builtInActions.length should be updated");
+  Assert.equal(PageActions._nonBuiltInActions.length,
                initialNonBuiltInActions.length,
-               "PageActions.nonBuiltInActions.length should be updated");
+               "PageActions._nonBuiltInActions.length should remain the same");
 
-  let actionIndex = PageActions.actions.findIndex(a => a.id == id);
+  let actionIndex = newActions.findIndex(a => a.id == id);
   Assert.equal(initialBookmarkSeparatorIndex, actionIndex,
                "initialBookmarkSeparatorIndex");
-  let newBookmarkSeparatorIndex = PageActions.actions.findIndex(a => {
+  let newBookmarkSeparatorIndex = newActions.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");
@@ -647,54 +670,59 @@ add_task(async function insertBeforeActi
     null,
     "Separator should be gone"
   );
 
   action.remove();
 });
 
 
-// Tests that the ordering of multiple non-built-in actions is alphabetical.
+// Tests that the ordering in the panel 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;
+  let initialActions = PageActions.actionsInPanel(window);
+  let initialBuiltInActions = PageActions._builtInActions.slice();
+  let initialNonBuiltInActions = PageActions._nonBuiltInActions.slice();
 
   // 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,
+  Assert.equal(PageActions.actionsInPanel(window).length,
                initialActions.length + actions.length + 1,
-               "PageActions.actions.length should be updated");
+               "PageActions.actionsInPanel().length should be updated");
 
-  Assert.equal(PageActions.builtInActions.length,
+  Assert.equal(PageActions._builtInActions.length,
                initialBuiltInActions.length,
-               "PageActions.builtInActions.length should be same");
-  Assert.equal(PageActions.nonBuiltInActions.length,
+               "PageActions._builtInActions.length should be same");
+  Assert.equal(PageActions._nonBuiltInActions.length,
                initialNonBuiltInActions.length + actions.length,
-               "PageActions.nonBuiltInActions.length should be updated");
+               "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];
+    let actualAction = PageActions._nonBuiltInActions[i];
     Assert.equal(actualAction.id, idPrefix + expectedIndex,
                  "actualAction.id for index: " + i);
   }
 
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+
   // 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");
@@ -734,114 +762,136 @@ add_task(async function multipleNonBuilt
 });
 
 
 // Makes sure the panel is correctly updated when a non-built-in action is
 // added before the built-in actions; and when all built-in actions are removed
 // and added back.
 add_task(async function nonBuiltFirst() {
   let initialActions = PageActions.actions;
+  let initialActionsInPanel = PageActions.actionsInPanel(window);
 
   // Remove all actions.
   for (let action of initialActions) {
     action.remove();
   }
 
   // Check the actions.
   Assert.deepEqual(PageActions.actions.map(a => a.id), [],
                    "PageActions.actions should be empty");
-  Assert.deepEqual(PageActions.builtInActions.map(a => a.id), [],
-                   "PageActions.builtInActions should be empty");
-  Assert.deepEqual(PageActions.nonBuiltInActions.map(a => a.id), [],
-                   "PageActions.nonBuiltInActions should be empty");
+  Assert.deepEqual(PageActions._builtInActions.map(a => a.id), [],
+                   "PageActions._builtInActions should be empty");
+  Assert.deepEqual(PageActions._nonBuiltInActions.map(a => a.id), [],
+                   "PageActions._nonBuiltInActions should be empty");
 
   // Check the panel.
   Assert.equal(BrowserPageActions.mainViewBodyNode.childNodes.length, 0,
                "All nodes should be gone");
 
   // Add a non-built-in action.
   let action = PageActions.addAction(new PageActions.Action({
     id: "test-nonBuiltFirst",
     title: "Test nonBuiltFirst",
   }));
 
   // Check the actions.
   Assert.deepEqual(PageActions.actions.map(a => a.id), [action.id],
                    "Action should be in PageActions.actions");
-  Assert.deepEqual(PageActions.builtInActions.map(a => a.id), [],
-                   "PageActions.builtInActions should be empty");
-  Assert.deepEqual(PageActions.nonBuiltInActions.map(a => a.id), [action.id],
-                   "Action should be in PageActions.nonBuiltInActions");
+  Assert.deepEqual(PageActions._builtInActions.map(a => a.id), [],
+                   "PageActions._builtInActions should be empty");
+  Assert.deepEqual(PageActions._nonBuiltInActions.map(a => a.id), [action.id],
+                   "Action should be in PageActions._nonBuiltInActions");
 
   // Check the panel.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
   Assert.deepEqual(
     Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
     [BrowserPageActions.panelButtonNodeIDForActionID(action.id)],
     "Action should be in panel"
   );
 
   // Now add back all the actions.
   for (let a of initialActions) {
     PageActions.addAction(a);
   }
 
   // Check the actions.
   Assert.deepEqual(
     PageActions.actions.map(a => a.id),
     initialActions.map(a => a.id).concat(
-      [PageActions.ACTION_ID_BUILT_IN_SEPARATOR],
       [action.id]
     ),
     "All actions should be in PageActions.actions"
   );
   Assert.deepEqual(
-    PageActions.builtInActions.map(a => a.id),
+    PageActions._builtInActions.map(a => a.id),
     initialActions.map(a => a.id),
-    "PageActions.builtInActions should be initial actions"
+    "PageActions._builtInActions should be initial actions"
   );
   Assert.deepEqual(
-    PageActions.nonBuiltInActions.map(a => a.id),
+    PageActions._nonBuiltInActions.map(a => a.id),
     [action.id],
-    "PageActions.nonBuiltInActions should contain action"
+    "PageActions._nonBuiltInActions should contain action"
   );
 
   // Check the panel.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+  Assert.deepEqual(
+    PageActions.actionsInPanel(window).map(a => a.id),
+    initialActionsInPanel.map(a => a.id).concat(
+      [PageActions.ACTION_ID_BUILT_IN_SEPARATOR],
+      [action.id]
+    ),
+    "All actions should be in PageActions.actionsInPanel()"
+  );
   Assert.deepEqual(
     Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
-    initialActions.map(a => a.id).concat(
+    initialActionsInPanel.map(a => a.id).concat(
       [PageActions.ACTION_ID_BUILT_IN_SEPARATOR],
       [action.id]
     ).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
     "Panel should contain all actions"
   );
 
   // Remove the test action.
   action.remove();
 
   // Check the actions.
   Assert.deepEqual(
     PageActions.actions.map(a => a.id),
     initialActions.map(a => a.id),
     "Action should no longer be in PageActions.actions"
   );
   Assert.deepEqual(
-    PageActions.builtInActions.map(a => a.id),
+    PageActions._builtInActions.map(a => a.id),
     initialActions.map(a => a.id),
-    "PageActions.builtInActions should be initial actions"
+    "PageActions._builtInActions should be initial actions"
   );
   Assert.deepEqual(
-    PageActions.nonBuiltInActions.map(a => a.id),
+    PageActions._nonBuiltInActions.map(a => a.id),
     [],
-    "Action should no longer be in PageActions.nonBuiltInActions"
+    "Action should no longer be in PageActions._nonBuiltInActions"
   );
 
   // Check the panel.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+  Assert.deepEqual(
+    PageActions.actionsInPanel(window).map(a => a.id),
+    initialActionsInPanel.map(a => a.id),
+    "Action should no longer be in PageActions.actionsInPanel()"
+  );
   Assert.deepEqual(
     Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
-    initialActions.map(a => BrowserPageActions.panelButtonNodeIDForActionID(a.id)),
+    initialActionsInPanel.map(a => BrowserPageActions.panelButtonNodeIDForActionID(a.id)),
     "Action should no longer be in panel"
   );
 });
 
 
 // Makes sure that urlbar nodes appear in the correct order in a new window.
 add_task(async function urlbarOrderNewWindow() {
   // Make some new actions.
@@ -1035,16 +1085,17 @@ add_task(async function perWindowState()
                "Title: old window");
   Assert.equal(action.getTitle(newWindow), newGlobalTitle,
                "Title: new window");
 
   // The action's panel button nodes should be updated in both windows.
   let panelButtonID =
     BrowserPageActions.panelButtonNodeIDForActionID(action.id);
   for (let win of [window, newWindow]) {
+    win.BrowserPageActions.placeLazyActionsInPanel();
     let panelButtonNode = win.document.getElementById(panelButtonID);
     Assert.equal(panelButtonNode.getAttribute("label"), newGlobalTitle,
                  "Panel button label should be global title");
   }
 
   // Set a new title in the new window.
   let newPerWinTitle = title + " new title in new window";
   action.setTitle(newPerWinTitle, newWindow);
@@ -1228,17 +1279,17 @@ add_task(async function contextMenu() {
   // 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 promisePageActionPanelOpen();
+  await promiseOpenPageActionPanel();
   let panelButton = BrowserPageActions.panelButtonNodeForActionID(action.id);
   let contextMenuPromise = promisePanelShown("pageActionContextMenu");
   EventUtils.synthesizeMouseAtCenter(panelButton, {
     type: "contextmenu",
     button: 2,
   });
   await contextMenuPromise;
 
@@ -1344,17 +1395,17 @@ add_task(async function contextMenu() {
   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 promisePageActionPanelOpen();
+  await promiseOpenPageActionPanel();
   contextMenuPromise = promisePanelShown("pageActionContextMenu");
   EventUtils.synthesizeMouseAtCenter(panelButton, {
     type: "contextmenu",
     button: 2,
   });
   await contextMenuPromise;
 
   // The context menu should show the "show" item and the "manage" item.  Click
@@ -1411,21 +1462,226 @@ add_task(async function contextMenu() {
   // 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, { type: "mousemove" });
   gURLBar.focus();
 });
 
 
+// Tests transient actions.
+add_task(async function transient() {
+  let initialActionsInPanel = PageActions.actionsInPanel(window);
+
+  let onPlacedInPanelCount = 0;
+  let onBeforePlacedInWindowCount = 0;
+
+  let action = PageActions.addAction(new PageActions.Action({
+    id: "test-transient",
+    title: "Test transient",
+    _transient: true,
+    onPlacedInPanel(buttonNode) {
+      onPlacedInPanelCount++;
+    },
+    onBeforePlacedInWindow(win) {
+      onBeforePlacedInWindowCount++;
+    },
+  }));
+
+  Assert.equal(action.__transient, true, "__transient");
+
+  Assert.equal(onPlacedInPanelCount, 0,
+               "onPlacedInPanelCount should remain 0");
+  Assert.equal(onBeforePlacedInWindowCount, 0,
+               "onBeforePlacedInWindowCount should remain 0");
+
+  Assert.deepEqual(
+    PageActions.actionsInPanel(window).map(a => a.id),
+    initialActionsInPanel.map(a => a.id).concat([
+      PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+      action.id,
+    ]),
+    "PageActions.actionsInPanel() should be updated"
+  );
+
+  // Check the panel.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+  Assert.deepEqual(
+    Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+    initialActionsInPanel.map(a => a.id).concat([
+      PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+      action.id,
+    ]).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
+    "Actions in panel should be correct"
+  );
+
+  Assert.equal(onPlacedInPanelCount, 1,
+               "onPlacedInPanelCount should be inc'ed");
+  Assert.equal(onBeforePlacedInWindowCount, 1,
+               "onBeforePlacedInWindowCount should be inc'ed");
+
+  // Disable the action.  It should be removed from the panel.
+  action.setDisabled(true, window);
+
+  Assert.deepEqual(
+    PageActions.actionsInPanel(window).map(a => a.id),
+    initialActionsInPanel.map(a => a.id),
+    "PageActions.actionsInPanel() should revert to initial"
+  );
+
+  // Check the panel.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+  Assert.deepEqual(
+    Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+    initialActionsInPanel
+      .map(a => BrowserPageActions.panelButtonNodeIDForActionID(a.id)),
+    "Actions in panel should be correct"
+  );
+
+  // Enable the action.  It should be added back to the panel.
+  action.setDisabled(false, window);
+
+  Assert.deepEqual(
+    PageActions.actionsInPanel(window).map(a => a.id),
+    initialActionsInPanel.map(a => a.id).concat([
+      PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+      action.id,
+    ]),
+    "PageActions.actionsInPanel() should be updated"
+  );
+
+  // Check the panel.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+  Assert.deepEqual(
+    Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+    initialActionsInPanel.map(a => a.id).concat([
+      PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+      action.id,
+    ]).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
+    "Actions in panel should be correct"
+  );
+
+  Assert.equal(onPlacedInPanelCount, 2,
+               "onPlacedInPanelCount should be inc'ed");
+  Assert.equal(onBeforePlacedInWindowCount, 2,
+               "onBeforePlacedInWindowCount should be inc'ed");
+
+  // Add another non-built in but non-transient action.
+  let otherAction = PageActions.addAction(new PageActions.Action({
+    id: "test-transient2",
+    title: "Test transient 2",
+  }));
+
+  Assert.deepEqual(
+    PageActions.actionsInPanel(window).map(a => a.id),
+    initialActionsInPanel.map(a => a.id).concat([
+      PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+      otherAction.id,
+      PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+      action.id,
+    ]),
+    "PageActions.actionsInPanel() should be updated"
+  );
+
+  // Check the panel.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+  Assert.deepEqual(
+    Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+    initialActionsInPanel.map(a => a.id).concat([
+      PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+      otherAction.id,
+      PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+      action.id,
+    ]).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
+    "Actions in panel should be correct"
+  );
+
+  Assert.equal(onPlacedInPanelCount, 2,
+               "onPlacedInPanelCount should remain the same");
+  Assert.equal(onBeforePlacedInWindowCount, 2,
+               "onBeforePlacedInWindowCount should remain the same");
+
+  // Disable the action again.  It should be removed from the panel.
+  action.setDisabled(true, window);
+
+  Assert.deepEqual(
+    PageActions.actionsInPanel(window).map(a => a.id),
+    initialActionsInPanel.map(a => a.id).concat([
+      PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+      otherAction.id,
+    ]),
+    "PageActions.actionsInPanel() should be updated"
+  );
+
+  // Check the panel.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+  Assert.deepEqual(
+    Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+    initialActionsInPanel.map(a => a.id).concat([
+      PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+      otherAction.id,
+    ]).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
+    "Actions in panel should be correct"
+  );
+
+  // Enable the action again.  It should be added back to the panel.
+  action.setDisabled(false, window);
+
+  Assert.deepEqual(
+    PageActions.actionsInPanel(window).map(a => a.id),
+    initialActionsInPanel.map(a => a.id).concat([
+      PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+      otherAction.id,
+      PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+      action.id,
+    ]),
+    "PageActions.actionsInPanel() should be updated"
+  );
+
+  // Check the panel.
+  await promiseOpenPageActionPanel();
+  EventUtils.synthesizeMouseAtCenter(BrowserPageActions.mainButtonNode, {});
+  await promisePageActionPanelHidden();
+  Assert.deepEqual(
+    Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id),
+    initialActionsInPanel.map(a => a.id).concat([
+      PageActions.ACTION_ID_BUILT_IN_SEPARATOR,
+      otherAction.id,
+      PageActions.ACTION_ID_TRANSIENT_SEPARATOR,
+      action.id,
+    ]).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)),
+    "Actions in panel should be correct"
+  );
+
+  Assert.equal(onPlacedInPanelCount, 3,
+               "onPlacedInPanelCount should be inc'ed");
+  Assert.equal(onBeforePlacedInWindowCount, 3,
+               "onBeforePlacedInWindowCount should be inc'ed");
+
+  // Done, clean up.
+  action.remove();
+  otherAction.remove();
+});
+
+
 function assertActivatedPageActionPanelHidden() {
   Assert.ok(!document.getElementById(BrowserPageActions._activatedActionPanelID));
 }
 
-function promisePageActionPanelOpen() {
+function promiseOpenPageActionPanel() {
   let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindowUtils);
   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);