1 /* Any copyright is dedicated to the Public Domain.
2 http://creativecommons.org/publicdomain/zero/1.0/ */
6 /* eslint no-unused-vars: [2, {"vars": "local"}] */
7 /* import-globals-from ../../../shared/test/shared-head.js */
8 /* import-globals-from ../../../shared/test/shared-redux-head.js */
9 /* import-globals-from ../../../inspector/test/shared-head.js */
11 Services.scriptloader.loadSubScript(
12 "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
15 Services.scriptloader.loadSubScript(
16 "chrome://mochitests/content/browser/devtools/client/shared/test/shared-redux-head.js",
20 // Import helpers registering the test-actor in remote targets
21 Services.scriptloader.loadSubScript(
22 "chrome://mochitests/content/browser/devtools/client/shared/test/test-actor-registry.js",
26 // Import helpers for the inspector that are also shared with others
27 Services.scriptloader.loadSubScript(
28 "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
33 _loadPreferredDevices,
34 } = require("devtools/client/responsive/actions/devices");
35 const { getStr } = require("devtools/client/responsive/utils/l10n");
38 } = require("devtools/client/responsive/utils/window");
43 } = require("devtools/client/shared/devices");
44 const { KeyCodes } = require("devtools/client/shared/keycodes");
45 const asyncStorage = require("devtools/shared/async-storage");
47 loader.lazyRequireGetter(
49 "ResponsiveUIManager",
50 "devtools/client/responsive/manager"
53 const E10S_MULTI_ENABLED =
54 Services.prefs.getIntPref("dom.ipc.processCount") > 1;
56 "http://example.com/browser/devtools/client/responsive/test/browser/";
57 const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions.";
58 const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"].getService(
59 Ci.nsIHttpProtocolHandler
62 SimpleTest.requestCompleteLog();
63 SimpleTest.waitForExplicitFinish();
65 // Toggling the RDM UI involves several docShell swap operations, which are somewhat slow
66 // on debug builds. Usually we are just barely over the limit, so a blanket factor of 2
68 requestLongerTimeout(2);
70 Services.prefs.setCharPref(
71 "devtools.devices.url",
72 TEST_URI_ROOT + "devices.json"
74 // The appearance of this notification causes intermittent behavior in some tests that
75 // send mouse events, since it causes the content to shift when it appears.
76 Services.prefs.setBoolPref(
77 "devtools.responsive.reloadNotification.enabled",
80 // Don't show the setting onboarding tooltip in the test suites.
81 Services.prefs.setBoolPref("devtools.responsive.show-setting-tooltip", false);
82 Services.prefs.setBoolPref("devtools.responsive.showUserAgentInput", true);
84 registerCleanupFunction(async () => {
85 Services.prefs.clearUserPref("devtools.devices.url");
86 Services.prefs.clearUserPref(
87 "devtools.responsive.reloadNotification.enabled"
89 Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
90 Services.prefs.clearUserPref(
91 "devtools.responsive.reloadConditions.touchSimulation"
93 Services.prefs.clearUserPref(
94 "devtools.responsive.reloadConditions.userAgent"
96 Services.prefs.clearUserPref("devtools.responsive.show-setting-tooltip");
97 Services.prefs.clearUserPref("devtools.responsive.showUserAgentInput");
98 Services.prefs.clearUserPref("devtools.responsive.touchSimulation.enabled");
99 Services.prefs.clearUserPref("devtools.responsive.userAgent");
100 Services.prefs.clearUserPref("devtools.responsive.viewport.height");
101 Services.prefs.clearUserPref("devtools.responsive.viewport.pixelRatio");
102 Services.prefs.clearUserPref("devtools.responsive.viewport.width");
103 await asyncStorage.removeItem("devtools.devices.url_cache");
104 await asyncStorage.removeItem("devtools.responsive.deviceState");
105 await removeLocalDevices();
109 * Open responsive design mode for the given tab.
111 var openRDM = async function(tab) {
112 info("Opening responsive design mode");
113 const manager = ResponsiveUIManager;
114 const ui = await manager.openIfNeeded(tab.ownerGlobal, tab, {
117 info("Responsive design mode opened");
118 return { ui, manager };
122 * Close responsive design mode for the given tab.
124 var closeRDM = async function(tab, options) {
125 info("Closing responsive design mode");
126 const manager = ResponsiveUIManager;
127 await manager.closeIfNeeded(tab.ownerGlobal, tab, options);
128 info("Responsive design mode closed");
132 * Adds a new test task that adds a tab with the given URL, opens responsive
133 * design mode, runs the given generator, closes responsive design mode, and
138 * addRDMTask(TEST_URL, async function ({ ui, manager }) {
139 * // Your tests go here...
142 function addRDMTask(url, task) {
143 add_task(async function() {
144 const tab = await addTab(url);
145 const results = await openRDM(tab);
150 ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
154 await removeTab(tab);
158 function spawnViewportTask(ui, args, task) {
159 return ContentTask.spawn(ui.getViewportBrowser(), args, task);
162 function waitForFrameLoad(ui, targetURL) {
163 return spawnViewportTask(ui, { targetURL }, async function(args) {
165 (content.document.readyState == "complete" ||
166 content.document.readyState == "interactive") &&
167 content.location.href == args.targetURL
171 await ContentTaskUtils.waitForEvent(this, "DOMContentLoaded");
175 function waitForViewportResizeTo(ui, width, height) {
176 return new Promise(function(resolve) {
177 const isSizeMatching = data => data.width == width && data.height == height;
179 // If the viewport has already the expected size, we resolve the promise immediately.
180 const size = ui.getViewportSize();
181 if (isSizeMatching(size)) {
182 info(`Viewport already resized to ${width} x ${height}`);
187 // Otherwise, we'll listen to the viewport's resize event, and the
188 // browser's load end; since a racing condition can happen, where the
189 // viewport's listener is added after the resize, because the viewport's
190 // document was reloaded; therefore the test would hang forever.
192 const browser = ui.getViewportBrowser();
194 const onResizeViewport = data => {
195 if (!isSizeMatching(data)) {
198 ui.off("viewport-resize", onResizeViewport);
199 browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd);
200 info(`Got viewport-resize to ${width} x ${height}`);
204 const onBrowserLoadEnd = async function() {
205 const data = ui.getViewportSize(ui);
206 onResizeViewport(data);
209 info(`Waiting for viewport-resize to ${width} x ${height}`);
210 // We're changing the viewport size, which may also change the content
211 // size. We wait on the viewport resize event, and check for the
213 ui.on("viewport-resize", onResizeViewport);
214 browser.addEventListener("mozbrowserloadend", onBrowserLoadEnd, {
220 var setViewportSize = async function(ui, manager, width, height) {
221 const size = ui.getViewportSize();
223 `Current size: ${size.width} x ${size.height}, ` +
224 `set to: ${width} x ${height}`
226 if (size.width != width || size.height != height) {
227 const resized = waitForViewportResizeTo(ui, width, height);
228 ui.setViewportSize({ width, height });
233 // This performs the same function as setViewportSize, but additionally
234 // ensures that reflow of the viewport has completed.
235 var setViewportSizeAndAwaitReflow = async function(ui, manager, width, height) {
236 await setViewportSize(ui, manager, width, height);
237 const reflowed = ContentTask.spawn(
238 ui.getViewportBrowser(),
241 return new Promise(resolve => {
242 content.requestAnimationFrame(resolve);
249 function getViewportDevicePixelRatio(ui) {
250 return ContentTask.spawn(ui.getViewportBrowser(), {}, async function() {
251 return content.devicePixelRatio;
255 function getElRect(selector, win) {
256 const el = win.document.querySelector(selector);
257 return el.getBoundingClientRect();
261 * Drag an element identified by 'selector' by [x,y] amount. Returns
262 * the rect of the dragged element as it was before drag.
264 function dragElementBy(selector, x, y, win) {
265 const { Simulate } = win.require(
266 "devtools/client/shared/vendor/react-dom-test-utils"
268 const rect = getElRect(selector, win);
270 clientX: Math.floor(rect.left + rect.width / 2),
271 clientY: Math.floor(rect.top + rect.height / 2),
273 const endPoint = [startPoint.clientX + x, startPoint.clientY + y];
275 const elem = win.document.querySelector(selector);
277 // mousedown is a React listener, need to use its testing tools to avoid races
278 Simulate.mouseDown(elem, startPoint);
280 // mousemove and mouseup are regular DOM listeners
281 EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mousemove" }, win);
282 EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mouseup" }, win);
287 async function testViewportResize(
291 expectedViewportSize,
294 const win = ui.toolWindow;
295 const resized = waitForViewportResizeTo(ui, ...expectedViewportSize);
296 const startRect = dragElementBy(selector, ...moveBy, win);
299 const endRect = getElRect(selector, win);
301 endRect.left - startRect.left,
302 expectedHandleMove[0],
303 `The x move of ${selector} is as expected`
306 endRect.top - startRect.top,
307 expectedHandleMove[1],
308 `The y move of ${selector} is as expected`
312 async function openDeviceModal(ui) {
313 const { document, store } = ui.toolWindow;
315 info("Opening device modal through device selector.");
316 const onModalOpen = waitUntilState(store, state => state.devices.isModalOpen);
317 await selectMenuItem(
320 getStr("responsive.editDeviceList2")
324 const modal = document.getElementById("device-modal-wrapper");
326 modal.classList.contains("opened") && !modal.classList.contains("closed"),
327 "The device modal is displayed."
331 async function selectMenuItem({ toolWindow }, selector, value) {
332 const { document } = toolWindow;
334 const button = document.querySelector(selector);
338 `Selector "${selector}" should match an existing element.`
341 info(`Selecting ${value} in ${selector}.`);
343 await testMenuItems(toolWindow, button, items => {
344 const menuItem = items.find(item => item.getAttribute("label") === value);
348 `Value "${value}" should match an existing menu item.`
355 * Runs the menu items from the button's context menu against a test function.
357 * @param {Window} toolWindow
358 * A window reference.
359 * @param {Element} button
360 * The button that will show a context menu when clicked.
361 * @param {Function} testFn
362 * A test function that will be ran with the found menu item in the context menu
365 function testMenuItems(toolWindow, button, testFn) {
366 // The context menu appears only in the top level window, which is different from
367 // the inner toolWindow.
368 const win = getTopLevelWindow(toolWindow);
370 return new Promise(resolve => {
371 win.document.addEventListener(
374 const popup = win.document.querySelector('menupopup[menu-api="true"]');
375 const menuItems = [...popup.children];
389 const selectDevice = (ui, value) =>
391 once(ui, "device-changed"),
392 selectMenuItem(ui, "#device-selector", value),
395 const selectDevicePixelRatio = (ui, value) =>
396 selectMenuItem(ui, "#device-pixel-ratio-menu", `DPR: ${value}`);
398 const selectNetworkThrottling = (ui, value) =>
400 once(ui, "network-throttling-changed"),
401 selectMenuItem(ui, "#network-throttling-menu", value),
404 function getSessionHistory(browser) {
405 return ContentTask.spawn(browser, {}, async function() {
406 /* eslint-disable no-undef */
407 const { SessionHistory } = ChromeUtils.import(
408 "resource://gre/modules/sessionstore/SessionHistory.jsm"
410 return SessionHistory.collect(docShell);
411 /* eslint-enable no-undef */
415 function getContentSize(ui) {
416 return spawnViewportTask(ui, {}, () => ({
417 width: content.screen.width,
418 height: content.screen.height,
422 function getViewportScroll(ui) {
423 return spawnViewportTask(ui, {}, () => ({
429 async function waitForPageShow(browser) {
430 const tab = gBrowser.getTabForBrowser(browser);
431 const ui = ResponsiveUIManager.getResponsiveUIForTab(tab);
433 browser = ui.getViewportBrowser();
436 "Waiting for pageshow from " + (ui ? "responsive" : "regular") + " browser"
438 // Need to wait an extra tick after pageshow to ensure everyone is up-to-date,
439 // hence the waitForTick.
440 await BrowserTestUtils.waitForContentEvent(browser, "pageshow");
441 return waitForTick();
444 function waitForViewportLoad(ui) {
445 return BrowserTestUtils.waitForContentEvent(
446 ui.getViewportBrowser(),
452 function waitForViewportScroll(ui) {
453 return BrowserTestUtils.waitForContentEvent(
454 ui.getViewportBrowser(),
460 function load(browser, url) {
461 const loaded = BrowserTestUtils.browserLoaded(browser, false, url);
462 BrowserTestUtils.loadURI(browser, url);
466 function back(browser) {
467 const shown = waitForPageShow(browser);
472 function forward(browser) {
473 const shown = waitForPageShow(browser);
478 function addDeviceForTest(device) {
479 info(`Adding Test Device "${device.name}" to the list.`);
482 registerCleanupFunction(() => {
483 // Note that assertions in cleanup functions are not displayed unless they failed.
485 removeDevice(device),
486 `Removed Test Device "${device.name}" from the list.`
491 async function waitForClientClose(ui) {
492 info("Waiting for RDM debugger client to close");
493 await ui.client.once("closed");
494 info("RDM's debugger client is now closed");
497 async function testDevicePixelRatio(ui, expected) {
498 const dppx = await getViewportDevicePixelRatio(ui);
499 is(dppx, expected, `devicePixelRatio should be set to ${expected}`);
502 async function testTouchEventsOverride(ui, expected) {
503 const { document } = ui.toolWindow;
504 const touchButton = document.getElementById("touch-simulation-button");
506 const flag = await ui.emulationFront.getTouchEventsOverride();
508 flag === Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED,
510 `Touch events override should be ${expected ? "enabled" : "disabled"}`
513 touchButton.classList.contains("checked"),
515 `Touch simulation button should be ${expected ? "" : "in"}active.`
519 function testViewportDeviceMenuLabel(ui, expected) {
520 info("Test viewport's device select label");
522 const label = ui.toolWindow.document.querySelector("#device-selector .title");
523 is(label.textContent, expected, `Device Select value should be: ${expected}`);
526 async function toggleTouchSimulation(ui) {
527 const { document } = ui.toolWindow;
528 const touchButton = document.getElementById("touch-simulation-button");
529 const changed = once(ui, "touch-simulation-changed");
530 const loaded = waitForViewportLoad(ui);
532 await Promise.all([changed, loaded]);
535 async function testUserAgent(ui, expected) {
536 const { document } = ui.toolWindow;
537 const userAgentInput = document.getElementById("user-agent-input");
539 if (expected === DEFAULT_UA) {
540 is(userAgentInput.value, "", "UA input should be empty");
542 is(userAgentInput.value, expected, `UA input should be set to ${expected}`);
545 await testUserAgentFromBrowser(ui.getViewportBrowser(), expected);
548 async function testUserAgentFromBrowser(browser, expected) {
549 const ua = await ContentTask.spawn(browser, {}, async function() {
550 return content.navigator.userAgent;
552 is(ua, expected, `UA should be set to ${expected}`);
555 function testViewportDimensions(ui, w, h) {
556 const viewport = ui.toolWindow.document.querySelector(".viewport-content");
559 ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
561 `Viewport should have width of ${w}px`
564 ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
566 `Viewport should have height of ${h}px`
570 async function changeUserAgentInput(ui, value) {
571 const { Simulate } = ui.toolWindow.require(
572 "devtools/client/shared/vendor/react-dom-test-utils"
574 const { document, store } = ui.toolWindow;
576 const userAgentInput = document.getElementById("user-agent-input");
577 userAgentInput.value = value;
578 Simulate.change(userAgentInput);
580 const userAgentChanged = waitUntilState(
582 state => state.ui.userAgent === value
584 const changed = once(ui, "user-agent-changed");
585 const loaded = waitForViewportLoad(ui);
586 Simulate.keyUp(userAgentInput, { keyCode: KeyCodes.DOM_VK_RETURN });
587 await Promise.all([changed, loaded, userAgentChanged]);
591 * Assuming the device modal is open and the device adder form is shown, this helper
592 * function adds `device` via the form, saves it, and waits for it to appear in the store.
594 function addDeviceInModal(ui, device) {
595 const { Simulate } = ui.toolWindow.require(
596 "devtools/client/shared/vendor/react-dom-test-utils"
598 const { document, store } = ui.toolWindow;
600 const nameInput = document.querySelector("#device-form-name input");
601 const [widthInput, heightInput] = document.querySelectorAll(
602 "#device-form-size input"
604 const pixelRatioInput = document.querySelector(
605 "#device-form-pixel-ratio input"
607 const userAgentInput = document.querySelector(
608 "#device-form-user-agent input"
610 const touchInput = document.querySelector("#device-form-touch input");
612 nameInput.value = device.name;
613 Simulate.change(nameInput);
614 widthInput.value = device.width;
615 Simulate.change(widthInput);
616 Simulate.blur(widthInput);
617 heightInput.value = device.height;
618 Simulate.change(heightInput);
619 Simulate.blur(heightInput);
620 pixelRatioInput.value = device.pixelRatio;
621 Simulate.change(pixelRatioInput);
622 userAgentInput.value = device.userAgent;
623 Simulate.change(userAgentInput);
624 touchInput.checked = device.touch;
625 Simulate.change(touchInput);
627 const existingCustomDevices = store.getState().devices.custom.length;
628 const adderSave = document.querySelector("#device-form-save");
629 const saved = waitUntilState(
631 state => state.devices.custom.length == existingCustomDevices + 1
633 Simulate.click(adderSave);
637 function editDeviceInModal(ui, device, newDevice) {
638 const { Simulate } = ui.toolWindow.require(
639 "devtools/client/shared/vendor/react-dom-test-utils"
641 const { document, store } = ui.toolWindow;
643 const nameInput = document.querySelector("#device-form-name input");
644 const [widthInput, heightInput] = document.querySelectorAll(
645 "#device-form-size input"
647 const pixelRatioInput = document.querySelector(
648 "#device-form-pixel-ratio input"
650 const userAgentInput = document.querySelector(
651 "#device-form-user-agent input"
653 const touchInput = document.querySelector("#device-form-touch input");
655 nameInput.value = newDevice.name;
656 Simulate.change(nameInput);
657 widthInput.value = newDevice.width;
658 Simulate.change(widthInput);
659 Simulate.blur(widthInput);
660 heightInput.value = newDevice.height;
661 Simulate.change(heightInput);
662 Simulate.blur(heightInput);
663 pixelRatioInput.value = newDevice.pixelRatio;
664 Simulate.change(pixelRatioInput);
665 userAgentInput.value = newDevice.userAgent;
666 Simulate.change(userAgentInput);
667 touchInput.checked = newDevice.touch;
668 Simulate.change(touchInput);
670 const existingCustomDevices = store.getState().devices.custom.length;
671 const formSave = document.querySelector("#device-form-save");
673 const saved = waitUntilState(
676 state.devices.custom.length == existingCustomDevices &&
677 state.devices.custom.find(({ name }) => name == newDevice.name) &&
678 !state.devices.custom.find(({ name }) => name == device.name)
680 Simulate.click(formSave);
684 function reloadOnUAChange(enabled) {
685 const pref = RELOAD_CONDITION_PREF_PREFIX + "userAgent";
686 Services.prefs.setBoolPref(pref, enabled);
689 function reloadOnTouchChange(enabled) {
690 const pref = RELOAD_CONDITION_PREF_PREFIX + "touchSimulation";
691 Services.prefs.setBoolPref(pref, enabled);
694 function rotateViewport(ui) {
695 const { document } = ui.toolWindow;
696 const rotateButton = document.getElementById("rotate-button");
697 rotateButton.click();
700 // Call this to switch between on/off support for meta viewports.
701 async function setTouchAndMetaViewportSupport(ui, value) {
702 const reloadNeeded = await ui.updateTouchSimulation(value);
704 info("Reload is needed -- waiting for it.");
705 const reload = waitForViewportLoad(ui);
706 const browser = ui.getViewportBrowser();
713 // This function checks that zoom, layout viewport width and height
714 // are all as expected.
715 async function testViewportZoomWidthAndHeight(
722 if (typeof zoom !== "undefined") {
723 const resolution = await spawnViewportTask(ui, {}, function() {
724 return content.windowUtils.getResolution();
726 is(resolution, zoom, message + " should have expected zoom.");
729 if (typeof width !== "undefined" || typeof height !== "undefined") {
730 const innerSize = await spawnViewportTask(ui, {}, function() {
732 width: content.innerWidth,
733 height: content.innerHeight,
736 if (typeof width !== "undefined") {
740 message + " should have expected inner width."
743 if (typeof height !== "undefined") {
747 message + " should have expected inner height."