summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsive/test/browser/head.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/responsive/test/browser/head.js
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/responsive/test/browser/head.js')
-rw-r--r--devtools/client/responsive/test/browser/head.js1008
1 files changed, 1008 insertions, 0 deletions
diff --git a/devtools/client/responsive/test/browser/head.js b/devtools/client/responsive/test/browser/head.js
new file mode 100644
index 0000000000..d2b42316a9
--- /dev/null
+++ b/devtools/client/responsive/test/browser/head.js
@@ -0,0 +1,1008 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+// Import helpers for the inspector that are also shared with others
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+// Load APZ test utils so we properly wait after resize
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://mochikit/content/tests/SimpleTest/paint_listener.js",
+ this
+);
+
+const {
+ _loadPreferredDevices,
+} = require("resource://devtools/client/responsive/actions/devices.js");
+const {
+ getStr,
+} = require("resource://devtools/client/responsive/utils/l10n.js");
+const {
+ getTopLevelWindow,
+} = require("resource://devtools/client/responsive/utils/window.js");
+const {
+ addDevice,
+ removeDevice,
+ removeLocalDevices,
+} = require("resource://devtools/client/shared/devices.js");
+const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
+const asyncStorage = require("resource://devtools/shared/async-storage.js");
+const localTypes = require("resource://devtools/client/responsive/types.js");
+
+loader.lazyRequireGetter(
+ this,
+ "ResponsiveUIManager",
+ "resource://devtools/client/responsive/manager.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "message",
+ "resource://devtools/client/responsive/utils/message.js"
+);
+
+const E10S_MULTI_ENABLED =
+ Services.prefs.getIntPref("dom.ipc.processCount") > 1;
+const TEST_URI_ROOT =
+ "http://example.com/browser/devtools/client/responsive/test/browser/";
+const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions.";
+const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
+ Ci.nsIHttpProtocolHandler
+).userAgent;
+
+SimpleTest.requestCompleteLog();
+SimpleTest.waitForExplicitFinish();
+
+// Toggling the RDM UI involves several docShell swap operations, which are somewhat slow
+// on debug builds. Usually we are just barely over the limit, so a blanket factor of 2
+// should be enough.
+requestLongerTimeout(2);
+
+// The appearance of this notification causes intermittent behavior in some tests that
+// send mouse events, since it causes the content to shift when it appears.
+Services.prefs.setBoolPref(
+ "devtools.responsive.reloadNotification.enabled",
+ false
+);
+// Don't show the setting onboarding tooltip in the test suites.
+Services.prefs.setBoolPref("devtools.responsive.show-setting-tooltip", false);
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(
+ "devtools.responsive.reloadNotification.enabled"
+ );
+ Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
+ Services.prefs.clearUserPref(
+ "devtools.responsive.reloadConditions.touchSimulation"
+ );
+ Services.prefs.clearUserPref(
+ "devtools.responsive.reloadConditions.userAgent"
+ );
+ Services.prefs.clearUserPref("devtools.responsive.show-setting-tooltip");
+ Services.prefs.clearUserPref("devtools.responsive.showUserAgentInput");
+ Services.prefs.clearUserPref("devtools.responsive.touchSimulation.enabled");
+ Services.prefs.clearUserPref("devtools.responsive.userAgent");
+ Services.prefs.clearUserPref("devtools.responsive.viewport.height");
+ Services.prefs.clearUserPref("devtools.responsive.viewport.pixelRatio");
+ Services.prefs.clearUserPref("devtools.responsive.viewport.width");
+ await asyncStorage.removeItem("devtools.responsive.deviceState");
+ await removeLocalDevices();
+
+ delete window.waitForAllPaintsFlushed;
+ delete window.waitForAllPaints;
+ delete window.promiseAllPaintsDone;
+});
+
+/**
+ * Adds a new test task that adds a tab with the given URL, awaits the
+ * preTask (if provided), opens responsive design mode, awaits the task,
+ * closes responsive design mode, awaits the postTask (if provided), and
+ * removes the tab. The final argument is an options object, with these
+ * optional properties:
+ *
+ * onlyPrefAndTask: if truthy, only the pref will be set and the task
+ * will be called, with none of the tab creation/teardown or open/close
+ * of RDM (default false).
+ * waitForDeviceList: if truthy, the function will wait until the device
+ * list is loaded before calling the task (default false).
+ *
+ * Example usage:
+ *
+ * addRDMTaskWithPreAndPost(
+ * TEST_URL,
+ * async function preTask({ message, browser }) {
+ * // Your pre-task goes here...
+ * },
+ * async function task({ ui, manager, message, browser, preTaskValue, tab }) {
+ * // Your task goes here...
+ * },
+ * async function postTask({ message, browser, preTaskValue, taskValue }) {
+ * // Your post-task goes here...
+ * },
+ * { waitForDeviceList: true }
+ * );
+ */
+function addRDMTaskWithPreAndPost(url, preTask, task, postTask, options) {
+ let onlyPrefAndTask = false;
+ let waitForDeviceList = false;
+ if (typeof options == "object") {
+ onlyPrefAndTask = !!options.onlyPrefAndTask;
+ waitForDeviceList = !!options.waitForDeviceList;
+ }
+
+ add_task(async function () {
+ let tab;
+ let browser;
+ let preTaskValue = null;
+ let taskValue = null;
+ let ui;
+ let manager;
+
+ if (!onlyPrefAndTask) {
+ tab = await addTab(url);
+ browser = tab.linkedBrowser;
+
+ if (preTask) {
+ preTaskValue = await preTask({ message, browser });
+ }
+
+ const rdmValues = await openRDM(tab, { waitForDeviceList });
+ ui = rdmValues.ui;
+ manager = rdmValues.manager;
+ }
+
+ try {
+ taskValue = await task({
+ ui,
+ manager,
+ message,
+ browser,
+ preTaskValue,
+ tab,
+ });
+ } catch (err) {
+ ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
+ }
+
+ if (!onlyPrefAndTask) {
+ await closeRDM(tab);
+ if (postTask) {
+ await postTask({
+ message,
+ browser,
+ preTaskValue,
+ taskValue,
+ });
+ }
+ await removeTab(tab);
+ }
+
+ // Flush prefs to not only undo our earlier change, but also undo
+ // any changes made by the tasks.
+ await SpecialPowers.flushPrefEnv();
+ });
+}
+
+/**
+ * This is a simplified version of addRDMTaskWithPreAndPost. Adds a new test
+ * task that adds a tab with the given URL, opens responsive design mode,
+ * closes responsive design mode, and removes the tab.
+ *
+ * Example usage:
+ *
+ * addRDMTask(
+ * TEST_URL,
+ * async function task({ ui, manager, message, browser }) {
+ * // Your task goes here...
+ * },
+ * { waitForDeviceList: true }
+ * );
+ */
+function addRDMTask(rdmURL, rdmTask, options) {
+ addRDMTaskWithPreAndPost(rdmURL, undefined, rdmTask, undefined, options);
+}
+
+async function spawnViewportTask(ui, args, task) {
+ // Await a reflow after the task.
+ const result = await ContentTask.spawn(ui.getViewportBrowser(), args, task);
+ await promiseContentReflow(ui);
+ return result;
+}
+
+function waitForFrameLoad(ui, targetURL) {
+ return spawnViewportTask(ui, { targetURL }, async function (args) {
+ if (
+ (content.document.readyState == "complete" ||
+ content.document.readyState == "interactive") &&
+ content.location.href == args.targetURL
+ ) {
+ return;
+ }
+ await ContentTaskUtils.waitForEvent(this, "DOMContentLoaded");
+ });
+}
+
+function waitForViewportResizeTo(ui, width, height) {
+ return new Promise(function (resolve) {
+ const isSizeMatching = data => data.width == width && data.height == height;
+
+ // If the viewport has already the expected size, we resolve the promise immediately.
+ const size = ui.getViewportSize();
+ if (isSizeMatching(size)) {
+ info(`Viewport already resized to ${width} x ${height}`);
+ resolve();
+ return;
+ }
+
+ // Otherwise, we'll listen to the viewport's resize event, and the
+ // browser's load end; since a racing condition can happen, where the
+ // viewport's listener is added after the resize, because the viewport's
+ // document was reloaded; therefore the test would hang forever.
+ // See bug 1302879.
+ const browser = ui.getViewportBrowser();
+
+ const onContentResize = data => {
+ if (!isSizeMatching(data)) {
+ return;
+ }
+ ui.off("content-resize", onContentResize);
+ browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd);
+ info(`Got content-resize to ${width} x ${height}`);
+ resolve();
+ };
+
+ const onBrowserLoadEnd = async function () {
+ const data = ui.getViewportSize(ui);
+ onContentResize(data);
+ };
+
+ info(`Waiting for viewport-resize to ${width} x ${height}`);
+ // We're changing the viewport size, which may also change the content
+ // size. We wait on the viewport resize event, and check for the
+ // desired size.
+ ui.on("content-resize", onContentResize);
+ browser.addEventListener("mozbrowserloadend", onBrowserLoadEnd, {
+ once: true,
+ });
+ });
+}
+
+var setViewportSize = async function (ui, manager, width, height) {
+ const size = ui.getViewportSize();
+ info(
+ `Current size: ${size.width} x ${size.height}, ` +
+ `set to: ${width} x ${height}`
+ );
+ if (size.width != width || size.height != height) {
+ const resized = waitForViewportResizeTo(ui, width, height);
+ ui.setViewportSize({ width, height });
+ await resized;
+ }
+};
+
+// This performs the same function as setViewportSize, but additionally
+// ensures that reflow of the viewport has completed.
+var setViewportSizeAndAwaitReflow = async function (
+ ui,
+ manager,
+ width,
+ height
+) {
+ await setViewportSize(ui, manager, width, height);
+ await promiseContentReflow(ui);
+ await promiseApzFlushedRepaints();
+};
+
+function getViewportDevicePixelRatio(ui) {
+ return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ // Note that devicePixelRatio doesn't return the override to privileged
+ // code, see bug 1759962.
+ return content.browsingContext.overrideDPPX || content.devicePixelRatio;
+ });
+}
+
+function getElRect(selector, win) {
+ const el = win.document.querySelector(selector);
+ return el.getBoundingClientRect();
+}
+
+/**
+ * Drag an element identified by 'selector' by [x,y] amount. Returns
+ * the rect of the dragged element as it was before drag.
+ */
+function dragElementBy(selector, x, y, ui) {
+ const browserWindow = ui.getBrowserWindow();
+ const rect = getElRect(selector, browserWindow);
+ const startPoint = {
+ clientX: Math.floor(rect.left + rect.width / 2),
+ clientY: Math.floor(rect.top + rect.height / 2),
+ };
+ const endPoint = [startPoint.clientX + x, startPoint.clientY + y];
+
+ EventUtils.synthesizeMouseAtPoint(
+ startPoint.clientX,
+ startPoint.clientY,
+ { type: "mousedown" },
+ browserWindow
+ );
+
+ // mousemove and mouseup are regular DOM listeners
+ EventUtils.synthesizeMouseAtPoint(
+ ...endPoint,
+ { type: "mousemove" },
+ browserWindow
+ );
+ EventUtils.synthesizeMouseAtPoint(
+ ...endPoint,
+ { type: "mouseup" },
+ browserWindow
+ );
+
+ return rect;
+}
+
+/**
+ * Resize the viewport and check that the resize happened as expected.
+ *
+ * @param {ResponsiveUI} ui
+ * The ResponsiveUI instance.
+ * @param {String} selector
+ * The css selector of the resize handler, eg .viewport-horizontal-resize-handle.
+ * @param {Array<number>} moveBy
+ * Array of 2 integers representing the x,y distance of the resize action.
+ * @param {Array<number>} moveBy
+ * Array of 2 integers representing the actual resize performed.
+ * @param {Object} options
+ * @param {Boolean} options.hasDevice
+ * Whether a device is currently set and will be overridden by the resize
+ */
+async function testViewportResize(
+ ui,
+ selector,
+ moveBy,
+ expectedHandleMove,
+ { hasDevice } = {}
+) {
+ let deviceRemoved;
+ let waitForDevToolsReload;
+ if (hasDevice) {
+ // If a device was defined, a reload will be triggered by the resize,
+ // wait for devtools to reload completely.
+ waitForDevToolsReload = await watchForDevToolsReload(
+ ui.getViewportBrowser()
+ );
+ // and wait for the device-associaton-removed event.
+ deviceRemoved = once(ui, "device-association-removed");
+ }
+
+ const resized = ui.once("viewport-resize-dragend");
+ const startRect = dragElementBy(selector, ...moveBy, ui);
+ await resized;
+
+ const endRect = getElRect(selector, ui.getBrowserWindow());
+ is(
+ endRect.left - startRect.left,
+ expectedHandleMove[0],
+ `The x move of ${selector} is as expected`
+ );
+ is(
+ endRect.top - startRect.top,
+ expectedHandleMove[1],
+ `The y move of ${selector} is as expected`
+ );
+
+ if (hasDevice) {
+ const { reloadTriggered } = await deviceRemoved;
+ if (reloadTriggered) {
+ await waitForDevToolsReload();
+ }
+ }
+}
+
+async function openDeviceModal(ui) {
+ const { document, store } = ui.toolWindow;
+
+ info("Opening device modal through device selector.");
+ const onModalOpen = waitUntilState(store, state => state.devices.isModalOpen);
+ await selectMenuItem(
+ ui,
+ "#device-selector",
+ getStr("responsive.editDeviceList2")
+ );
+ await onModalOpen;
+
+ const modal = document.getElementById("device-modal-wrapper");
+ ok(
+ modal.classList.contains("opened") && !modal.classList.contains("closed"),
+ "The device modal is displayed."
+ );
+}
+
+async function selectMenuItem({ toolWindow }, selector, value) {
+ const { document } = toolWindow;
+
+ const button = document.querySelector(selector);
+ isnot(
+ button,
+ null,
+ `Selector "${selector}" should match an existing element.`
+ );
+
+ info(`Selecting ${value} in ${selector}.`);
+
+ await testMenuItems(toolWindow, button, items => {
+ const menuItem = findMenuItem(items, value);
+ isnot(
+ menuItem,
+ undefined,
+ `Value "${value}" should match an existing menu item.`
+ );
+ menuItem.click();
+ });
+}
+
+/**
+ * Runs the menu items from the button's context menu against a test function.
+ *
+ * @param {Window} toolWindow
+ * A window reference.
+ * @param {Element} button
+ * The button that will show a context menu when clicked.
+ * @param {Function} testFn
+ * A test function that will be ran with the found menu item in the context menu
+ * as an argument.
+ */
+async function testMenuItems(toolWindow, button, testFn) {
+ // The context menu appears only in the top level window, which is different from
+ // the inner toolWindow.
+ const win = getTopLevelWindow(toolWindow);
+
+ await new Promise(resolve => {
+ win.document.addEventListener(
+ "popupshown",
+ async () => {
+ if (button.id === "device-selector") {
+ const popup = toolWindow.document.querySelector(
+ "#device-selector-menu"
+ );
+ const menuItems = [...popup.querySelectorAll(".menuitem > .command")];
+
+ testFn(menuItems);
+
+ if (popup.classList.contains("tooltip-visible")) {
+ // Close the tooltip explicitly.
+ button.click();
+ await waitUntil(() => !popup.classList.contains("tooltip-visible"));
+ }
+ } else {
+ const popup = win.document.querySelector(
+ 'menupopup[menu-api="true"]'
+ );
+ const menuItems = [...popup.children];
+
+ testFn(menuItems);
+
+ popup.hidePopup();
+ }
+
+ resolve();
+ },
+ { once: true }
+ );
+
+ button.click();
+ });
+}
+
+const selectDevice = async (ui, value) => {
+ const browser = ui.getViewportBrowser();
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+
+ const onDeviceChanged = once(ui, "device-changed");
+ await selectMenuItem(ui, "#device-selector", value);
+ const { reloadTriggered } = await onDeviceChanged;
+ if (reloadTriggered) {
+ await waitForDevToolsReload();
+ }
+};
+
+const selectDevicePixelRatio = (ui, value) =>
+ selectMenuItem(ui, "#device-pixel-ratio-menu", `DPR: ${value}`);
+
+const selectNetworkThrottling = (ui, value) =>
+ Promise.all([
+ once(ui, "network-throttling-changed"),
+ selectMenuItem(ui, "#network-throttling-menu", value),
+ ]);
+
+function getSessionHistory(browser) {
+ if (Services.appinfo.sessionHistoryInParent) {
+ const browsingContext = browser.browsingContext;
+ const uri = browsingContext.currentWindowGlobal.documentURI.displaySpec;
+ const history = browsingContext.sessionHistory;
+ const body = ContentTask.spawn(
+ browser,
+ browsingContext,
+ function (
+ // eslint-disable-next-line no-shadow
+ browsingContext
+ ) {
+ const docShell = browsingContext.docShell.QueryInterface(
+ Ci.nsIWebNavigation
+ );
+ return docShell.document.body;
+ }
+ );
+ const { SessionHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/sessionstore/SessionHistory.sys.mjs"
+ );
+ return SessionHistory.collectFromParent(uri, body, history);
+ }
+ return ContentTask.spawn(browser, null, function () {
+ const { SessionHistory } = ChromeUtils.importESModule(
+ "resource://gre/modules/sessionstore/SessionHistory.sys.mjs"
+ );
+ return SessionHistory.collect(docShell);
+ });
+}
+
+function getContentSize(ui) {
+ return spawnViewportTask(ui, {}, () => ({
+ width: content.screen.width,
+ height: content.screen.height,
+ }));
+}
+
+function getViewportScroll(ui) {
+ return spawnViewportTask(ui, {}, () => ({
+ x: content.scrollX,
+ y: content.scrollY,
+ }));
+}
+
+async function waitForPageShow(browser) {
+ const tab = gBrowser.getTabForBrowser(browser);
+ const ui = ResponsiveUIManager.getResponsiveUIForTab(tab);
+ if (ui) {
+ browser = ui.getViewportBrowser();
+ }
+ info(
+ "Waiting for pageshow from " + (ui ? "responsive" : "regular") + " browser"
+ );
+ // Need to wait an extra tick after pageshow to ensure everyone is up-to-date,
+ // hence the waitForTick.
+ await BrowserTestUtils.waitForContentEvent(browser, "pageshow");
+ return waitForTick();
+}
+
+function waitForViewportScroll(ui) {
+ return BrowserTestUtils.waitForContentEvent(
+ ui.getViewportBrowser(),
+ "scroll",
+ true
+ );
+}
+
+async function back(browser) {
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+ const onPageShow = waitForPageShow(browser);
+
+ browser.goBack();
+
+ await onPageShow;
+ await waitForDevToolsReload();
+}
+
+async function forward(browser) {
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+ const onPageShow = waitForPageShow(browser);
+
+ browser.goForward();
+
+ await onPageShow;
+ await waitForDevToolsReload();
+}
+
+function addDeviceForTest(device) {
+ info(`Adding Test Device "${device.name}" to the list.`);
+ addDevice(device);
+
+ registerCleanupFunction(() => {
+ // Note that assertions in cleanup functions are not displayed unless they failed.
+ ok(
+ removeDevice(device),
+ `Removed Test Device "${device.name}" from the list.`
+ );
+ });
+}
+
+async function waitForClientClose(ui) {
+ info("Waiting for RDM devtools client to close");
+ await ui.commands.client.once("closed");
+ info("RDM's devtools client is now closed");
+}
+
+async function testDevicePixelRatio(ui, expected) {
+ const dppx = await getViewportDevicePixelRatio(ui);
+ is(dppx, expected, `devicePixelRatio should be set to ${expected}`);
+}
+
+async function testTouchEventsOverride(ui, expected) {
+ const { document } = ui.toolWindow;
+ const touchButton = document.getElementById("touch-simulation-button");
+
+ const flag = gBrowser.selectedBrowser.browsingContext.touchEventsOverride;
+
+ is(
+ flag === "enabled",
+ expected,
+ `Touch events override should be ${expected ? "enabled" : "disabled"}`
+ );
+ is(
+ touchButton.classList.contains("checked"),
+ expected,
+ `Touch simulation button should be ${expected ? "" : "in"}active.`
+ );
+}
+
+function testViewportDeviceMenuLabel(ui, expectedDeviceName) {
+ info("Test viewport's device select label");
+
+ const button = ui.toolWindow.document.querySelector("#device-selector");
+ ok(
+ button.textContent.includes(expectedDeviceName),
+ `Device Select value ${button.textContent} should be: ${expectedDeviceName}`
+ );
+}
+
+async function toggleTouchSimulation(ui) {
+ const { document } = ui.toolWindow;
+ const browser = ui.getViewportBrowser();
+
+ const touchButton = document.getElementById("touch-simulation-button");
+ const wasChecked = touchButton.classList.contains("checked");
+ const onTouchSimulationChanged = once(ui, "touch-simulation-changed");
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+ const onTouchButtonStateChanged = waitFor(
+ () => touchButton.classList.contains("checked") !== wasChecked
+ );
+
+ touchButton.click();
+ await Promise.all([
+ onTouchSimulationChanged,
+ onTouchButtonStateChanged,
+ waitForDevToolsReload(),
+ ]);
+}
+
+async function testUserAgent(ui, expected) {
+ const { document } = ui.toolWindow;
+ const userAgentInput = document.getElementById("user-agent-input");
+
+ if (expected === DEFAULT_UA) {
+ is(userAgentInput.value, "", "UA input should be empty");
+ } else {
+ is(userAgentInput.value, expected, `UA input should be set to ${expected}`);
+ }
+
+ await testUserAgentFromBrowser(ui.getViewportBrowser(), expected);
+}
+
+async function testUserAgentFromBrowser(browser, expected) {
+ const ua = await SpecialPowers.spawn(browser, [], async function () {
+ return content.navigator.userAgent;
+ });
+ is(ua, expected, `UA should be set to ${expected}`);
+}
+
+function testViewportDimensions(ui, w, h) {
+ const viewport = ui.viewportElement;
+
+ is(
+ ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
+ `${w}px`,
+ `Viewport should have width of ${w}px`
+ );
+ is(
+ ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
+ `${h}px`,
+ `Viewport should have height of ${h}px`
+ );
+}
+
+async function changeUserAgentInput(ui, value) {
+ const { Simulate } = ui.toolWindow.require(
+ "resource://devtools/client/shared/vendor/react-dom-test-utils.js"
+ );
+ const { document, store } = ui.toolWindow;
+ const browser = ui.getViewportBrowser();
+
+ const userAgentInput = document.getElementById("user-agent-input");
+ userAgentInput.value = value;
+ Simulate.change(userAgentInput);
+
+ const userAgentChanged = waitUntilState(
+ store,
+ state => state.ui.userAgent === value
+ );
+ const changed = once(ui, "user-agent-changed");
+
+ const waitForDevToolsReload = await watchForDevToolsReload(browser);
+ Simulate.keyUp(userAgentInput, { keyCode: KeyCodes.DOM_VK_RETURN });
+ await Promise.all([changed, waitForDevToolsReload(), userAgentChanged]);
+}
+
+/**
+ * Assuming the device modal is open and the device adder form is shown, this helper
+ * function adds `device` via the form, saves it, and waits for it to appear in the store.
+ */
+function addDeviceInModal(ui, device) {
+ const { Simulate } = ui.toolWindow.require(
+ "resource://devtools/client/shared/vendor/react-dom-test-utils.js"
+ );
+ const { document, store } = ui.toolWindow;
+
+ const nameInput = document.querySelector("#device-form-name input");
+ const [widthInput, heightInput] = document.querySelectorAll(
+ "#device-form-size input"
+ );
+ const pixelRatioInput = document.querySelector(
+ "#device-form-pixel-ratio input"
+ );
+ const userAgentInput = document.querySelector(
+ "#device-form-user-agent input"
+ );
+ const touchInput = document.querySelector("#device-form-touch input");
+
+ nameInput.value = device.name;
+ Simulate.change(nameInput);
+ widthInput.value = device.width;
+ Simulate.change(widthInput);
+ Simulate.blur(widthInput);
+ heightInput.value = device.height;
+ Simulate.change(heightInput);
+ Simulate.blur(heightInput);
+ pixelRatioInput.value = device.pixelRatio;
+ Simulate.change(pixelRatioInput);
+ userAgentInput.value = device.userAgent;
+ Simulate.change(userAgentInput);
+ touchInput.checked = device.touch;
+ Simulate.change(touchInput);
+
+ const existingCustomDevices = store.getState().devices.custom.length;
+ const adderSave = document.querySelector("#device-form-save");
+ const saved = waitUntilState(
+ store,
+ state => state.devices.custom.length == existingCustomDevices + 1
+ );
+ Simulate.click(adderSave);
+ return saved;
+}
+
+async function editDeviceInModal(ui, device, newDevice) {
+ const { Simulate } = ui.toolWindow.require(
+ "resource://devtools/client/shared/vendor/react-dom-test-utils.js"
+ );
+ const { document, store } = ui.toolWindow;
+
+ const nameInput = document.querySelector("#device-form-name input");
+ const [widthInput, heightInput] = document.querySelectorAll(
+ "#device-form-size input"
+ );
+ const pixelRatioInput = document.querySelector(
+ "#device-form-pixel-ratio input"
+ );
+ const userAgentInput = document.querySelector(
+ "#device-form-user-agent input"
+ );
+ const touchInput = document.querySelector("#device-form-touch input");
+
+ nameInput.value = newDevice.name;
+ Simulate.change(nameInput);
+ widthInput.value = newDevice.width;
+ Simulate.change(widthInput);
+ Simulate.blur(widthInput);
+ heightInput.value = newDevice.height;
+ Simulate.change(heightInput);
+ Simulate.blur(heightInput);
+ pixelRatioInput.value = newDevice.pixelRatio;
+ Simulate.change(pixelRatioInput);
+ userAgentInput.value = newDevice.userAgent;
+ Simulate.change(userAgentInput);
+ touchInput.checked = newDevice.touch;
+ Simulate.change(touchInput);
+
+ const existingCustomDevices = store.getState().devices.custom.length;
+ const formSave = document.querySelector("#device-form-save");
+
+ const saved = waitUntilState(
+ store,
+ state =>
+ state.devices.custom.length == existingCustomDevices &&
+ state.devices.custom.find(({ name }) => name == newDevice.name) &&
+ !state.devices.custom.find(({ name }) => name == device.name)
+ );
+
+ // Editing a custom device triggers a "device-change" message.
+ // Wait for the `device-changed` event to avoid unfinished requests during the
+ // tests.
+ const onDeviceChanged = ui.once("device-changed");
+
+ Simulate.click(formSave);
+
+ await onDeviceChanged;
+ return saved;
+}
+
+function findMenuItem(menuItems, name) {
+ return menuItems.find(menuItem => menuItem.textContent.includes(name));
+}
+
+function reloadOnUAChange(enabled) {
+ const pref = RELOAD_CONDITION_PREF_PREFIX + "userAgent";
+ Services.prefs.setBoolPref(pref, enabled);
+}
+
+function reloadOnTouchChange(enabled) {
+ const pref = RELOAD_CONDITION_PREF_PREFIX + "touchSimulation";
+ Services.prefs.setBoolPref(pref, enabled);
+}
+
+function rotateViewport(ui) {
+ const { document } = ui.toolWindow;
+ const rotateButton = document.getElementById("rotate-button");
+ rotateButton.click();
+}
+
+// Call this to switch between on/off support for meta viewports.
+async function setTouchAndMetaViewportSupport(ui, value) {
+ await ui.updateTouchSimulation(value);
+ info("Reload so the new configuration applies cleanly to the page");
+ await reloadBrowser();
+
+ await promiseContentReflow(ui);
+}
+
+// This function checks that zoom, layout viewport width and height
+// are all as expected.
+async function testViewportZoomWidthAndHeight(msg, ui, zoom, width, height) {
+ if (typeof zoom !== "undefined") {
+ const resolution = await spawnViewportTask(ui, {}, function () {
+ return content.windowUtils.getResolution();
+ });
+ is(resolution, zoom, msg + " should have expected zoom.");
+ }
+
+ if (typeof width !== "undefined" || typeof height !== "undefined") {
+ const innerSize = await spawnViewportTask(ui, {}, function () {
+ return {
+ width: content.innerWidth,
+ height: content.innerHeight,
+ };
+ });
+ if (typeof width !== "undefined") {
+ is(innerSize.width, width, msg + " should have expected inner width.");
+ }
+ if (typeof height !== "undefined") {
+ is(innerSize.height, height, msg + " should have expected inner height.");
+ }
+ }
+}
+
+function promiseContentReflow(ui) {
+ return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ return new Promise(resolve => {
+ content.window.requestAnimationFrame(() => {
+ content.window.requestAnimationFrame(resolve);
+ });
+ });
+ });
+}
+
+// This function returns a promise that will be resolved when the
+// RDM zoom has been set and the content has finished rescaling
+// to the new size.
+async function promiseRDMZoom(ui, browser, zoom) {
+ const currentZoom = ZoomManager.getZoomForBrowser(browser);
+ if (currentZoom.toFixed(2) == zoom.toFixed(2)) {
+ return;
+ }
+
+ const width = browser.getBoundingClientRect().width;
+
+ ZoomManager.setZoomForBrowser(browser, zoom);
+
+ // RDM resizes the browser as a result of a zoom change, so we wait for that.
+ //
+ // This also has the side effect of updating layout which ensures that any
+ // remote frame dimension update message gets there in time.
+ await BrowserTestUtils.waitForCondition(function () {
+ return browser.getBoundingClientRect().width != width;
+ });
+}
+
+async function waitForDeviceAndViewportState(ui) {
+ const { store } = ui.toolWindow;
+
+ // Wait until the viewport has been added and the device list has been loaded
+ await waitUntilState(
+ store,
+ state =>
+ state.viewports.length == 1 &&
+ state.devices.listState == localTypes.loadableState.LOADED
+ );
+}
+
+/**
+ * Wait for the content page to be rendered with the expected pixel ratio.
+ *
+ * @param {ResponsiveUI} ui
+ * The ResponsiveUI instance.
+ * @param {Integer} expected
+ * The expected dpr for the content page.
+ * @param {Object} options
+ * @param {Boolean} options.waitForTargetConfiguration
+ * If set to true, the function will wait for the targetConfigurationCommand configuration
+ * to reflect the ratio that was set. This can be used to prevent pending requests
+ * to the actor.
+ */
+async function waitForDevicePixelRatio(
+ ui,
+ expected,
+ { waitForTargetConfiguration } = {}
+) {
+ const dpx = await SpecialPowers.spawn(
+ ui.getViewportBrowser(),
+ [{ expected }],
+ function (args) {
+ const getDpr = function () {
+ return content.browsingContext.overrideDPPX || content.devicePixelRatio;
+ };
+ const initial = getDpr();
+ info(
+ `Listening for pixel ratio change ` +
+ `(current: ${initial}, expected: ${args.expected})`
+ );
+ return new Promise(resolve => {
+ const mql = content.matchMedia(`(resolution: ${args.expected}dppx)`);
+ if (mql.matches) {
+ info(`Ratio already changed to ${args.expected}dppx`);
+ resolve(getDpr());
+ return;
+ }
+ mql.addListener(function listener() {
+ info(`Ratio changed to ${args.expected}dppx`);
+ mql.removeListener(listener);
+ resolve(getDpr());
+ });
+ });
+ }
+ );
+
+ if (waitForTargetConfiguration) {
+ // Ensure the configuration was updated so we limit the risk of the client closing before
+ // the server sent back the result of the updateConfiguration call.
+ await waitFor(() => {
+ return (
+ ui.commands.targetConfigurationCommand.configuration.overrideDPPX ===
+ expected
+ );
+ });
+ }
+
+ return dpx;
+}