/* 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} moveBy * Array of 2 integers representing the x,y distance of the resize action. * @param {Array} 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; }