Bug 1580545 - Convert ResponsiveUI and ResponsiveUIManager to ES6 classes. r=mtigley
[gecko.git] / devtools / client / responsive / test / browser / head.js
blobc8c267e7c8e9664b3219335e50f03fffd96513de
1 /* Any copyright is dedicated to the Public Domain.
2    http://creativecommons.org/publicdomain/zero/1.0/ */
4 "use strict";
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",
13   this
15 Services.scriptloader.loadSubScript(
16   "chrome://mochitests/content/browser/devtools/client/shared/test/shared-redux-head.js",
17   this
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",
23   this
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",
29   this
32 const {
33   _loadPreferredDevices,
34 } = require("devtools/client/responsive/actions/devices");
35 const { getStr } = require("devtools/client/responsive/utils/l10n");
36 const {
37   getTopLevelWindow,
38 } = require("devtools/client/responsive/utils/window");
39 const {
40   addDevice,
41   removeDevice,
42   removeLocalDevices,
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(
48   this,
49   "ResponsiveUIManager",
50   "devtools/client/responsive/manager"
53 const E10S_MULTI_ENABLED =
54   Services.prefs.getIntPref("dom.ipc.processCount") > 1;
55 const TEST_URI_ROOT =
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
60 ).userAgent;
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
67 // should be enough.
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",
78   false
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"
88   );
89   Services.prefs.clearUserPref("devtools.responsive.html.displayedDeviceList");
90   Services.prefs.clearUserPref(
91     "devtools.responsive.reloadConditions.touchSimulation"
92   );
93   Services.prefs.clearUserPref(
94     "devtools.responsive.reloadConditions.userAgent"
95   );
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.
110  */
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, {
115     trigger: "test",
116   });
117   info("Responsive design mode opened");
118   return { ui, manager };
122  * Close responsive design mode for the given tab.
123  */
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
134  * removes the tab.
136  * Example usage:
138  *   addRDMTask(TEST_URL, async function ({ ui, manager }) {
139  *     // Your tests go here...
140  *   });
141  */
142 function addRDMTask(url, task) {
143   add_task(async function() {
144     const tab = await addTab(url);
145     const results = await openRDM(tab);
147     try {
148       await task(results);
149     } catch (err) {
150       ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err));
151     }
153     await closeRDM(tab);
154     await removeTab(tab);
155   });
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) {
164     if (
165       (content.document.readyState == "complete" ||
166         content.document.readyState == "interactive") &&
167       content.location.href == args.targetURL
168     ) {
169       return;
170     }
171     await ContentTaskUtils.waitForEvent(this, "DOMContentLoaded");
172   });
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}`);
183       resolve();
184       return;
185     }
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.
191     // See bug 1302879.
192     const browser = ui.getViewportBrowser();
194     const onResizeViewport = data => {
195       if (!isSizeMatching(data)) {
196         return;
197       }
198       ui.off("viewport-resize", onResizeViewport);
199       browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd);
200       info(`Got viewport-resize to ${width} x ${height}`);
201       resolve();
202     };
204     const onBrowserLoadEnd = async function() {
205       const data = ui.getViewportSize(ui);
206       onResizeViewport(data);
207     };
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
212     // desired size.
213     ui.on("viewport-resize", onResizeViewport);
214     browser.addEventListener("mozbrowserloadend", onBrowserLoadEnd, {
215       once: true,
216     });
217   });
220 var setViewportSize = async function(ui, manager, width, height) {
221   const size = ui.getViewportSize();
222   info(
223     `Current size: ${size.width} x ${size.height}, ` +
224       `set to: ${width} x ${height}`
225   );
226   if (size.width != width || size.height != height) {
227     const resized = waitForViewportResizeTo(ui, width, height);
228     ui.setViewportSize({ width, height });
229     await resized;
230   }
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(),
239     {},
240     async function() {
241       return new Promise(resolve => {
242         content.requestAnimationFrame(resolve);
243       });
244     }
245   );
246   await reflowed;
249 function getViewportDevicePixelRatio(ui) {
250   return ContentTask.spawn(ui.getViewportBrowser(), {}, async function() {
251     return content.devicePixelRatio;
252   });
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.
263  */
264 function dragElementBy(selector, x, y, win) {
265   const { Simulate } = win.require(
266     "devtools/client/shared/vendor/react-dom-test-utils"
267   );
268   const rect = getElRect(selector, win);
269   const startPoint = {
270     clientX: Math.floor(rect.left + rect.width / 2),
271     clientY: Math.floor(rect.top + rect.height / 2),
272   };
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);
284   return rect;
287 async function testViewportResize(
288   ui,
289   selector,
290   moveBy,
291   expectedViewportSize,
292   expectedHandleMove
293 ) {
294   const win = ui.toolWindow;
295   const resized = waitForViewportResizeTo(ui, ...expectedViewportSize);
296   const startRect = dragElementBy(selector, ...moveBy, win);
297   await resized;
299   const endRect = getElRect(selector, win);
300   is(
301     endRect.left - startRect.left,
302     expectedHandleMove[0],
303     `The x move of ${selector} is as expected`
304   );
305   is(
306     endRect.top - startRect.top,
307     expectedHandleMove[1],
308     `The y move of ${selector} is as expected`
309   );
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(
318     ui,
319     "#device-selector",
320     getStr("responsive.editDeviceList2")
321   );
322   await onModalOpen;
324   const modal = document.getElementById("device-modal-wrapper");
325   ok(
326     modal.classList.contains("opened") && !modal.classList.contains("closed"),
327     "The device modal is displayed."
328   );
331 async function selectMenuItem({ toolWindow }, selector, value) {
332   const { document } = toolWindow;
334   const button = document.querySelector(selector);
335   isnot(
336     button,
337     null,
338     `Selector "${selector}" should match an existing element.`
339   );
341   info(`Selecting ${value} in ${selector}.`);
343   await testMenuItems(toolWindow, button, items => {
344     const menuItem = items.find(item => item.getAttribute("label") === value);
345     isnot(
346       menuItem,
347       undefined,
348       `Value "${value}" should match an existing menu item.`
349     );
350     menuItem.click();
351   });
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
363  *         as an argument.
364  */
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(
372       "popupshown",
373       () => {
374         const popup = win.document.querySelector('menupopup[menu-api="true"]');
375         const menuItems = [...popup.children];
377         testFn(menuItems);
379         popup.hidePopup();
380         resolve();
381       },
382       { once: true }
383     );
385     button.click();
386   });
389 const selectDevice = (ui, value) =>
390   Promise.all([
391     once(ui, "device-changed"),
392     selectMenuItem(ui, "#device-selector", value),
393   ]);
395 const selectDevicePixelRatio = (ui, value) =>
396   selectMenuItem(ui, "#device-pixel-ratio-menu", `DPR: ${value}`);
398 const selectNetworkThrottling = (ui, value) =>
399   Promise.all([
400     once(ui, "network-throttling-changed"),
401     selectMenuItem(ui, "#network-throttling-menu", value),
402   ]);
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"
409     );
410     return SessionHistory.collect(docShell);
411     /* eslint-enable no-undef */
412   });
415 function getContentSize(ui) {
416   return spawnViewportTask(ui, {}, () => ({
417     width: content.screen.width,
418     height: content.screen.height,
419   }));
422 function getViewportScroll(ui) {
423   return spawnViewportTask(ui, {}, () => ({
424     x: content.scrollX,
425     y: content.scrollY,
426   }));
429 async function waitForPageShow(browser) {
430   const tab = gBrowser.getTabForBrowser(browser);
431   const ui = ResponsiveUIManager.getResponsiveUIForTab(tab);
432   if (ui) {
433     browser = ui.getViewportBrowser();
434   }
435   info(
436     "Waiting for pageshow from " + (ui ? "responsive" : "regular") + " browser"
437   );
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(),
447     "load",
448     true
449   );
452 function waitForViewportScroll(ui) {
453   return BrowserTestUtils.waitForContentEvent(
454     ui.getViewportBrowser(),
455     "scroll",
456     true
457   );
460 function load(browser, url) {
461   const loaded = BrowserTestUtils.browserLoaded(browser, false, url);
462   BrowserTestUtils.loadURI(browser, url);
463   return loaded;
466 function back(browser) {
467   const shown = waitForPageShow(browser);
468   browser.goBack();
469   return shown;
472 function forward(browser) {
473   const shown = waitForPageShow(browser);
474   browser.goForward();
475   return shown;
478 function addDeviceForTest(device) {
479   info(`Adding Test Device "${device.name}" to the list.`);
480   addDevice(device);
482   registerCleanupFunction(() => {
483     // Note that assertions in cleanup functions are not displayed unless they failed.
484     ok(
485       removeDevice(device),
486       `Removed Test Device "${device.name}" from the list.`
487     );
488   });
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();
507   is(
508     flag === Ci.nsIDocShell.TOUCHEVENTS_OVERRIDE_ENABLED,
509     expected,
510     `Touch events override should be ${expected ? "enabled" : "disabled"}`
511   );
512   is(
513     touchButton.classList.contains("checked"),
514     expected,
515     `Touch simulation button should be ${expected ? "" : "in"}active.`
516   );
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);
531   touchButton.click();
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");
541   } else {
542     is(userAgentInput.value, expected, `UA input should be set to ${expected}`);
543   }
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;
551   });
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");
558   is(
559     ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
560     `${w}px`,
561     `Viewport should have width of ${w}px`
562   );
563   is(
564     ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
565     `${h}px`,
566     `Viewport should have height of ${h}px`
567   );
570 async function changeUserAgentInput(ui, value) {
571   const { Simulate } = ui.toolWindow.require(
572     "devtools/client/shared/vendor/react-dom-test-utils"
573   );
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(
581     store,
582     state => state.ui.userAgent === value
583   );
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.
593  */
594 function addDeviceInModal(ui, device) {
595   const { Simulate } = ui.toolWindow.require(
596     "devtools/client/shared/vendor/react-dom-test-utils"
597   );
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"
603   );
604   const pixelRatioInput = document.querySelector(
605     "#device-form-pixel-ratio input"
606   );
607   const userAgentInput = document.querySelector(
608     "#device-form-user-agent input"
609   );
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(
630     store,
631     state => state.devices.custom.length == existingCustomDevices + 1
632   );
633   Simulate.click(adderSave);
634   return saved;
637 function editDeviceInModal(ui, device, newDevice) {
638   const { Simulate } = ui.toolWindow.require(
639     "devtools/client/shared/vendor/react-dom-test-utils"
640   );
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"
646   );
647   const pixelRatioInput = document.querySelector(
648     "#device-form-pixel-ratio input"
649   );
650   const userAgentInput = document.querySelector(
651     "#device-form-user-agent input"
652   );
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(
674     store,
675     state =>
676       state.devices.custom.length == existingCustomDevices &&
677       state.devices.custom.find(({ name }) => name == newDevice.name) &&
678       !state.devices.custom.find(({ name }) => name == device.name)
679   );
680   Simulate.click(formSave);
681   return saved;
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);
703   if (reloadNeeded) {
704     info("Reload is needed -- waiting for it.");
705     const reload = waitForViewportLoad(ui);
706     const browser = ui.getViewportBrowser();
707     browser.reload();
708     await reload;
709   }
710   return reloadNeeded;
713 // This function checks that zoom, layout viewport width and height
714 // are all as expected.
715 async function testViewportZoomWidthAndHeight(
716   message,
717   ui,
718   zoom,
719   width,
720   height
721 ) {
722   if (typeof zoom !== "undefined") {
723     const resolution = await spawnViewportTask(ui, {}, function() {
724       return content.windowUtils.getResolution();
725     });
726     is(resolution, zoom, message + " should have expected zoom.");
727   }
729   if (typeof width !== "undefined" || typeof height !== "undefined") {
730     const innerSize = await spawnViewportTask(ui, {}, function() {
731       return {
732         width: content.innerWidth,
733         height: content.innerHeight,
734       };
735     });
736     if (typeof width !== "undefined") {
737       is(
738         innerSize.width,
739         width,
740         message + " should have expected inner width."
741       );
742     }
743     if (typeof height !== "undefined") {
744       is(
745         innerSize.height,
746         height,
747         message + " should have expected inner height."
748       );
749     }
750   }