summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/test/browser_toolbox_zoom_popup.js
blob: 7b764bf703206d2db8fb4afaba3e2e383c81992e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

// Test the popup menu position when zooming in the devtools panel.

const { Toolbox } = require("resource://devtools/client/framework/toolbox.js");

// Use a simple URL in order to prevent displacing the left position of the
// frames menu.
const TEST_URL = "data:text/html;charset=utf-8,<iframe/>";

add_task(async function () {
  registerCleanupFunction(async function () {
    Services.prefs.clearUserPref("devtools.toolbox.zoomValue");
  });
  const zoom = 1.4;
  Services.prefs.setCharPref("devtools.toolbox.zoomValue", zoom.toString(10));

  info("Load iframe page for checking the frame menu with x1.4 zoom.");
  await addTab(TEST_URL);
  const tab = gBrowser.selectedTab;
  const toolbox = await gDevTools.showToolboxForTab(tab, {
    toolId: "inspector",
    hostType: Toolbox.HostType.WINDOW,
  });
  const inspector = toolbox.getCurrentPanel();
  const hostWindow = toolbox.win.parent;
  const originWidth = hostWindow.outerWidth;
  const originHeight = hostWindow.outerHeight;

  info(`Waiting for the toolbox window will to be rendered with zoom x${zoom}`);
  await waitUntil(() => {
    return parseFloat(toolbox.win.browsingContext.fullZoom.toFixed(1)) === zoom;
  });

  info(
    "Resizing and moving the toolbox window in order to display the chevron menu."
  );
  // If the window is displayed bottom of screen, the menu might be displayed
  // above the button so move it to the top of the screen first.
  await moveWindowTo(hostWindow, 10, 10);

  // Shrink the width of the window such that the inspector's tab menu button
  // and chevron button are visible.
  const prevTabs = toolbox.doc.querySelectorAll(".devtools-tab").length;
  info("Shrinking window");

  hostWindow.resizeTo(400, hostWindow.outerHeight);
  await waitUntil(() => {
    info(`Waiting for chevron(${hostWindow.outerWidth})`);
    return (
      hostWindow.outerWidth === 400 &&
      toolbox.doc.getElementById("tools-chevron-menu-button") &&
      inspector.panelDoc.querySelector(".all-tabs-menu") &&
      prevTabs != toolbox.doc.querySelectorAll(".devtools-tab").length
    );
  });

  const menuList = [
    toolbox.win.document.getElementById("toolbox-meatball-menu-button"),
    toolbox.win.document.getElementById("command-button-frames"),
    toolbox.win.document.getElementById("tools-chevron-menu-button"),
    inspector.panelDoc.querySelector(".all-tabs-menu"),
  ];

  for (const menu of menuList) {
    const { buttonBounds, menuType, menuBounds, arrowBounds } =
      await getButtonAndMenuInfo(toolbox, menu);

    switch (menuType) {
      case "native":
        {
          // Allow rounded error and platform offset value.
          // horizontal : IntID::ContextMenuOffsetHorizontal of GTK and Windows
          //              uses 2.
          // vertical: IntID::ContextMenuOffsetVertical of macOS uses -6.
          const xDelta = Math.abs(menuBounds.left - buttonBounds.left);
          const yDelta = Math.abs(menuBounds.top - buttonBounds.bottom);
          Assert.less(
            xDelta,
            2,
            "xDelta is lower than 2: " + xDelta + ". #" + menu.id
          );
          Assert.less(
            yDelta,
            6,
            "yDelta is lower than 6: " + yDelta + ". #" + menu.id
          );
        }
        break;

      case "doorhanger":
        {
          // Calculate the center of the button and center of the arrow and
          // check they align.
          const buttonCenter = buttonBounds.left + buttonBounds.width / 2;
          const arrowCenter = arrowBounds.left + arrowBounds.width / 2;
          const delta = Math.abs(arrowCenter - buttonCenter);
          Assert.lessOrEqual(
            Math.round(delta),
            1,
            "Center of arrow is within 1px of button center" +
              ` (delta: ${delta})`
          );
        }
        break;
    }
  }

  const onResize = once(hostWindow, "resize");
  hostWindow.resizeTo(originWidth, originHeight);
  await onResize;

  await toolbox.destroy();
  gBrowser.removeCurrentTab();
});

function convertScreenToDoc(popup, doc) {
  const rect = popup.getOuterScreenRect();
  const screenX = doc.defaultView.mozInnerScreenX;
  const screenY = doc.defaultView.mozInnerScreenY;
  const scale =
    popup.ownerGlobal.devicePixelRatio / doc.ownerGlobal.devicePixelRatio;
  return new DOMRect(
    rect.x * scale - screenX,
    rect.y * scale - screenY,
    rect.width * scale,
    rect.height * scale
  );
}

/**
 * Get the bounds of a menu button and its popup panel. The popup panel is
 * measured by clicking the menu button and looking for its panel (and then
 * hiding it again).
 *
 * @param {Object} doc
 *        The toolbox document to query.
 * @param {Object} menuButton
 *        The button whose size and popup size we should measure.
 * @return {Object}
 *         An object with the following properties:
 *         - buttonBounds {DOMRect} Bounds of the button.
 *         - menuType {string} Type of the menu, "native" or "doorhanger".
 *         - menuBounds {DOMRect} Bounds of the menu panel.
 *         - arrowBounds {DOMRect|null} Bounds of the arrow. Only set when
 *                       menuType is "doorhanger", null otherwise.
 */
async function getButtonAndMenuInfo(toolbox, menuButton) {
  const { doc, topDoc } = toolbox;
  info("Show popup menu with click event.");
  AccessibilityUtils.setEnv({
    // Keyboard accessibility is handled on the toolbox toolbar container level.
    // Users can use arrow keys to navigate between and select tabs.
    nonNegativeTabIndexRule: false,
  });
  EventUtils.sendMouseEvent(
    {
      type: "click",
      screenX: 1,
    },
    menuButton,
    doc.defaultView
  );
  AccessibilityUtils.resetEnv();

  let menuPopup;
  let menuType;
  let menuBounds = null;
  let arrowBounds = null;
  if (menuButton.hasAttribute("aria-controls")) {
    menuType = "doorhanger";
    menuPopup = doc.getElementById(menuButton.getAttribute("aria-controls"));
    await waitUntil(() => menuPopup.classList.contains("tooltip-visible"));
    // menuPopup can be a non-menupopup element, e.g. div. Call getBoxQuads to
    // get its bounds.
    menuBounds = menuPopup.getBoxQuads({ relativeTo: doc })[0].getBounds();
  } else {
    menuType = "native";
    await waitUntil(() => {
      const popupset = topDoc.querySelector("popupset");
      menuPopup = popupset?.querySelector('menupopup[menu-api="true"]');
      return menuPopup?.state === "open";
    });
    // menuPopup is a XUL menupopup element. Call getOuterScreenRect(), which is
    // suported on both native and non-native menupopup implementations.
    menuBounds = convertScreenToDoc(menuPopup, doc);
  }
  ok(menuPopup, "Menu popup is displayed.");

  const buttonBounds = menuButton
    .getBoxQuads({ relativeTo: doc })[0]
    .getBounds();

  if (menuType === "doorhanger") {
    const arrow = menuPopup.querySelector(".tooltip-arrow");
    arrowBounds = arrow.getBoxQuads({ relativeTo: doc })[0].getBounds();
  }

  info("Hide popup menu.");
  if (menuType === "doorhanger") {
    EventUtils.sendKey("Escape", doc.defaultView);
    await waitUntil(() => !menuPopup.classList.contains("tooltip-visible"));
  } else {
    const popupHidden = once(menuPopup, "popuphidden");
    menuPopup.hidePopup();
    await popupHidden;
  }

  return { buttonBounds, menuType, menuBounds, arrowBounds };
}