Commit 80c9c861 authored by Alexandre Poirot's avatar Alexandre Poirot
Browse files

Bug 1780912 - [devtools] Make the local web extension toolbox be always on top. r=nchevobbe,eemeli

This allows to keep the DevTools visible while interacting with the Firefox
window where the extension is running.

This behavior is enabled by default, but can be disable on-demand via a button
in the top toolbar.
Note that it requires to close and reopen the window/toolbox as platform APIs
disallow changing this behavior "live" on a given window.

Differential Revision: https://phabricator.services.mozilla.com/D155843
parent 8792e1fb
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -2297,6 +2297,11 @@ pref("devtools.toolbox.zoomValue", "1");
pref("devtools.toolbox.splitconsoleEnabled", false);
pref("devtools.toolbox.splitconsoleHeight", 100);
pref("devtools.toolbox.tabsOrder", "");
// This is only used for local Web Extension debugging,
// and allows to keep the window on top of all others,
// so that you can debug the Firefox window, while keeping the devtools
// always visible
pref("devtools.toolbox.alwaysOnTop", true);

// The fission pref for enabling the "Multiprocess Browser Toolbox", which will
// make it possible to debug anything in Firefox (See Bug 1570639 for more information).
+2 −0
Original line number Diff line number Diff line
@@ -46,6 +46,8 @@ tags = webextensions
tags = webextensions
[browser_aboutdebugging_addons_debug_storage.js]
tags = webextensions
[browser_aboutdebugging_addons_debug_toolbox.js]
tags = webextensions
[browser_aboutdebugging_addons_eventpage_actions_and_status.js]
tags = webextensions
[browser_aboutdebugging_addons_eventpage_terminate_on_idle.js]
+122 −0
Original line number Diff line number Diff line
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";

/* import-globals-from helper-addons.js */
Services.scriptloader.loadSubScript(CHROME_URL_ROOT + "helper-addons.js", this);

// There are shutdown issues for which multiple rejections are left uncaught.
// See bug 1018184 for resolving these issues.
const { PromiseTestUtils } = ChromeUtils.import(
  "resource://testing-common/PromiseTestUtils.jsm"
);
PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);

const ADDON_ID = "test-devtools-webextension@mozilla.org";
const ADDON_NAME = "test-devtools-webextension";

/**
 * This test file ensures that the webextension addon developer toolbox:
 * - always on top is enabled by default and can be toggled off
 */
add_task(async function testWebExtensionsToolbox() {
  await enableExtensionDebugging();
  const { document, tab, window } = await openAboutDebugging();
  await selectThisFirefoxPage(document, window.AboutDebugging.store);

  await installTemporaryExtensionFromXPI(
    {
      background() {
        document.body.innerText = "Background Page Body Test Content";
      },
      id: ADDON_ID,
      name: ADDON_NAME,
    },
    document
  );

  info("Open a toolbox to debug the addon");
  const { devtoolsWindow } = await openAboutDevtoolsToolbox(
    document,
    tab,
    window,
    ADDON_NAME
  );

  const toolbox = getToolbox(devtoolsWindow);

  ok(
    isWindowAlwaysOnTop(devtoolsWindow),
    "The toolbox window is always on top"
  );
  const toggleButton = toolbox.doc.querySelector(".toolbox-always-on-top");
  ok(!!toggleButton, "The always on top toggle button is visible");
  ok(
    toggleButton.classList.contains("checked"),
    "The button is highlighted to report that always on top is enabled"
  );

  // When running the test, the devtools window might not be focused, while it does IRL.
  // Force it to be focused to better reflect the default behavior.
  info("Force focusing the devtools window");
  devtoolsWindow.focus();

  // As we update the button with a debounce, we have to wait for it to updates
  await waitFor(
    () => toggleButton.classList.contains("toolbox-is-focused"),
    "Wait for the button to be highlighting that the toolbox is focused (the button isn't highlighted)"
  );
  ok(true, "Expected class is added when toolbox is focused");

  info("Focus the browser window");
  window.focus();

  await waitFor(
    () => !toggleButton.classList.contains("toolbox-is-focused"),
    "Wait for the button to be highlighting that the toolbox is no longer focused (the button is highlighted)"
  );
  ok(true, "Focused class is removed when browser window gets focused");

  info("Re-focus the DevTools window");
  devtoolsWindow.focus();

  await waitFor(
    () => toggleButton.classList.contains("toolbox-is-focused"),
    "Wait for the button to be re-highlighting that the toolbox is focused"
  );

  const onToolboxReady = gDevTools.once("toolbox-ready");
  info("Click on the button");
  toggleButton.click();

  info("Wait for a new toolbox to be created");
  const secondToolbox = await onToolboxReady;

  ok(
    !isWindowAlwaysOnTop(secondToolbox.win),
    "The toolbox window is no longer always on top"
  );
  const secondToggleButton = secondToolbox.doc.querySelector(
    ".toolbox-always-on-top"
  );
  ok(!!secondToggleButton, "The always on top toggle button is still visible");

  ok(
    !secondToggleButton.classList.contains("checked"),
    "The button is no longer highlighted to report that always on top is disabled"
  );

  await closeWebExtAboutDevtoolsToolbox(secondToolbox.win, window);
  await removeTemporaryExtension(ADDON_NAME, document);
  await removeTab(tab);
});

// Check if the window has the "alwaysontop" chrome flag
function isWindowAlwaysOnTop(window) {
  return (
    window.docShell.treeOwner
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIAppWindow).chromeFlags &
    Ci.nsIWebBrowserChrome.CHROME_ALWAYS_ON_TOP
  );
}
+15 −0
Original line number Diff line number Diff line
@@ -103,6 +103,21 @@ async function openAboutDevtoolsToolbox(
  // WebExtension open a toolbox in a dedicated window
  if (isWebExtension) {
    const toolbox = await onToolboxReady;
    // For some reason the test helpers prevents the toolbox from being automatically focused on opening,
    // whereas it is IRL.
    const focusedWin = Services.focus.focusedWindow;
    if (focusedWin?.top != toolbox.win) {
      info("Wait for the toolbox window to be focused");
      await new Promise(r => {
        // focus event only fired on the chrome event handler and in capture phase
        toolbox.win.docShell.chromeEventHandler.addEventListener("focus", r, {
          once: true,
          capture: true,
        });
        toolbox.win.focus();
      });
      info("The toolbox is focused");
    }
    return {
      devtoolsBrowser: null,
      devtoolsDocument: toolbox.doc,
+52 −3
Original line number Diff line number Diff line
@@ -4,13 +4,18 @@
"use strict";

const Services = require("Services");
const { PureComponent } = require("devtools/client/shared/vendor/react");
const {
  PureComponent,
  createFactory,
} = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const {
  CONNECTION_TYPES,
} = require("devtools/client/shared/remote-debugging/constants");
const DESCRIPTOR_TYPES = require("devtools/client/fronts/descriptors/descriptor-types");
const FluentReact = require("devtools/client/shared/vendor/fluent-react");
const Localized = createFactory(FluentReact.Localized);

/**
 * This is header that should be displayed on top of the toolbox when using
@@ -19,6 +24,9 @@ const DESCRIPTOR_TYPES = require("devtools/client/fronts/descriptors/descriptor-
class DebugTargetInfo extends PureComponent {
  static get propTypes() {
    return {
      alwaysOnTop: PropTypes.boolean.isRequired,
      focusedState: PropTypes.boolean,
      toggleAlwaysOnTop: PropTypes.func.isRequired,
      debugTargetData: PropTypes.shape({
        connectionType: PropTypes.oneOf(Object.values(CONNECTION_TYPES))
          .isRequired,
@@ -200,10 +208,18 @@ class DebugTargetInfo extends PureComponent {
  }

  renderRuntime() {
    if (!this.props.debugTargetData.runtimeInfo) {
    if (
      !this.props.debugTargetData.runtimeInfo ||
      (this.props.debugTargetData.connectionType ===
        CONNECTION_TYPES.THIS_FIREFOX &&
        this.props.debugTargetData.descriptorType ===
          DESCRIPTOR_TYPES.EXTENSION)
    ) {
      // Skip the runtime render if no runtimeInfo is available.
      // Runtime info is retrieved from the remote-client-manager, which might not be
      // setup if about:devtools-toolbox was not opened from about:debugging.
      //
      // Also skip the runtime if we are debugging firefox itself, mainly to save some space.
      return null;
    }

@@ -269,6 +285,38 @@ class DebugTargetInfo extends PureComponent {
    );
  }

  renderAlwaysOnTopButton() {
    // This is only displayed for local web extension debugging
    if (
      this.props.debugTargetData.descriptorType !==
        DESCRIPTOR_TYPES.EXTENSION &&
      this.props.debugTargetData.connectionType ===
        CONNECTION_TYPES.THIS_FIREFOX
    ) {
      return [];
    }
    const checked = this.props.alwaysOnTop;
    const toolboxFocused = this.props.focusedState;
    return [
      dom.div({ className: "toolbox-toolbar-spacer" }),
      Localized(
        {
          id: checked
            ? "toolbox-always-on-top-enabled"
            : "toolbox-always-on-top-disabled",
          attrs: { title: true },
        },
        dom.button({
          className:
            `toolbox-always-on-top` +
            (checked ? " checked" : "") +
            (toolboxFocused ? " toolbox-is-focused" : ""),
          onClick: this.props.toggleAlwaysOnTop,
        })
      ),
    ];
  }

  renderNavigationButton(detail) {
    const { L10N } = this.props;

@@ -346,7 +394,8 @@ class DebugTargetInfo extends PureComponent {
      this.renderRuntime(),
      this.renderTargetTitle(),
      this.renderNavigation(),
      this.renderTargetURI()
      this.renderTargetURI(),
      ...this.renderAlwaysOnTopButton()
    );
  }
}
Loading