From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- devtools/client/responsive/test/browser/head.js | 1008 +++++++++++++++++++++++ 1 file changed, 1008 insertions(+) create mode 100644 devtools/client/responsive/test/browser/head.js (limited to 'devtools/client/responsive/test/browser/head.js') 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} 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; +} -- cgit v1.2.3