head.js (30518B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 /* eslint no-unused-vars: [2, {"vars": "local"}] */ 7 8 Services.scriptloader.loadSubScript( 9 "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", 10 this 11 ); 12 13 // Import helpers for the inspector that are also shared with others 14 Services.scriptloader.loadSubScript( 15 "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js", 16 this 17 ); 18 19 // Load APZ test utils so we properly wait after resize 20 Services.scriptloader.loadSubScript( 21 "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js", 22 this 23 ); 24 Services.scriptloader.loadSubScript( 25 "chrome://mochikit/content/tests/SimpleTest/paint_listener.js", 26 this 27 ); 28 29 const { 30 _loadPreferredDevices, 31 } = require("resource://devtools/client/responsive/actions/devices.js"); 32 const { 33 getStr, 34 } = require("resource://devtools/client/responsive/utils/l10n.js"); 35 const { 36 getTopLevelWindow, 37 } = require("resource://devtools/client/responsive/utils/window.js"); 38 const { 39 addDevice, 40 removeDevice, 41 removeLocalDevices, 42 } = require("resource://devtools/client/shared/devices.js"); 43 const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js"); 44 const asyncStorage = require("resource://devtools/shared/async-storage.js"); 45 const localTypes = require("resource://devtools/client/responsive/types.js"); 46 47 loader.lazyRequireGetter( 48 this, 49 "ResponsiveUIManager", 50 "resource://devtools/client/responsive/manager.js" 51 ); 52 loader.lazyRequireGetter( 53 this, 54 "message", 55 "resource://devtools/client/responsive/utils/message.js" 56 ); 57 58 const E10S_MULTI_ENABLED = 59 Services.prefs.getIntPref("dom.ipc.processCount") > 1; 60 const TEST_URI_ROOT = 61 "http://example.com/browser/devtools/client/responsive/test/browser/"; 62 const RELOAD_CONDITION_PREF_PREFIX = "devtools.responsive.reloadConditions."; 63 const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"].getService( 64 Ci.nsIHttpProtocolHandler 65 ).userAgent; 66 67 SimpleTest.requestCompleteLog(); 68 SimpleTest.waitForExplicitFinish(); 69 70 // Toggling the RDM UI involves several docShell swap operations, which are somewhat slow 71 // on debug builds. Usually we are just barely over the limit, so a blanket factor of 2 72 // should be enough. 73 requestLongerTimeout(2); 74 75 registerCleanupFunction(async () => { 76 await asyncStorage.removeItem("devtools.responsive.deviceState"); 77 await removeLocalDevices(); 78 79 delete window.waitForAllPaintsFlushed; 80 delete window.waitForAllPaints; 81 delete window.promiseAllPaintsDone; 82 }); 83 84 /** 85 * Adds a new test task that adds a tab with the given URL, awaits the 86 * preTask (if provided), opens responsive design mode, awaits the task, 87 * closes responsive design mode, awaits the postTask (if provided), and 88 * removes the tab. The final argument is an options object, with these 89 * optional properties: 90 * 91 * onlyPrefAndTask: if truthy, only the pref will be set and the task 92 * will be called, with none of the tab creation/teardown or open/close 93 * of RDM (default false). 94 * waitForDeviceList: if truthy, the function will wait until the device 95 * list is loaded before calling the task (default false). 96 * 97 * Example usage: 98 * 99 * addRDMTaskWithPreAndPost( 100 * TEST_URL, 101 * async function preTask({ message, browser }) { 102 * // Your pre-task goes here... 103 * }, 104 * async function task({ ui, manager, message, browser, preTaskValue, tab }) { 105 * // Your task goes here... 106 * }, 107 * async function postTask({ message, browser, preTaskValue, taskValue }) { 108 * // Your post-task goes here... 109 * }, 110 * { waitForDeviceList: true } 111 * ); 112 */ 113 function addRDMTaskWithPreAndPost(url, preTask, task, postTask, options) { 114 let onlyPrefAndTask = false; 115 let waitForDeviceList = false; 116 if (typeof options == "object") { 117 onlyPrefAndTask = !!options.onlyPrefAndTask; 118 waitForDeviceList = !!options.waitForDeviceList; 119 } 120 121 add_task(async function () { 122 let tab; 123 let browser; 124 let preTaskValue = null; 125 let taskValue = null; 126 let ui; 127 let manager; 128 129 if (!onlyPrefAndTask) { 130 tab = await addTab(url); 131 browser = tab.linkedBrowser; 132 133 if (preTask) { 134 preTaskValue = await preTask({ message, browser }); 135 } 136 137 const rdmValues = await openRDM(tab, { waitForDeviceList }); 138 ui = rdmValues.ui; 139 manager = rdmValues.manager; 140 } 141 142 try { 143 taskValue = await task({ 144 ui, 145 manager, 146 message, 147 browser, 148 preTaskValue, 149 tab, 150 }); 151 } catch (err) { 152 ok(false, "Got an error: " + DevToolsUtils.safeErrorString(err)); 153 } 154 155 if (!onlyPrefAndTask) { 156 // Close the toolbox first, as closing RDM might trigger a reload if 157 // touch simulation was enabled, which will trigger RDP requests. 158 await closeToolboxIfOpen(); 159 160 await closeRDM(tab); 161 162 if (postTask) { 163 await postTask({ 164 message, 165 browser, 166 preTaskValue, 167 taskValue, 168 }); 169 } 170 await removeTab(tab); 171 } 172 173 // Flush prefs to not only undo our earlier change, but also undo 174 // any changes made by the tasks. 175 await SpecialPowers.flushPrefEnv(); 176 }); 177 } 178 179 /** 180 * This is a simplified version of addRDMTaskWithPreAndPost. Adds a new test 181 * task that adds a tab with the given URL, opens responsive design mode, 182 * closes responsive design mode, and removes the tab. 183 * 184 * Example usage: 185 * 186 * addRDMTask( 187 * TEST_URL, 188 * async function task({ ui, manager, message, browser }) { 189 * // Your task goes here... 190 * }, 191 * { waitForDeviceList: true } 192 * ); 193 */ 194 function addRDMTask(rdmURL, rdmTask, options) { 195 addRDMTaskWithPreAndPost(rdmURL, undefined, rdmTask, undefined, options); 196 } 197 198 async function spawnViewportTask(ui, args, task) { 199 // Await a reflow after the task. 200 const result = await SpecialPowers.spawn( 201 ui.getViewportBrowser(), 202 [args], 203 task 204 ); 205 await promiseContentReflow(ui); 206 return result; 207 } 208 209 function waitForFrameLoad(ui, targetURL) { 210 return spawnViewportTask(ui, { targetURL }, async function (args) { 211 if ( 212 (content.document.readyState == "complete" || 213 content.document.readyState == "interactive") && 214 content.location.href == args.targetURL 215 ) { 216 return; 217 } 218 await ContentTaskUtils.waitForEvent(this, "DOMContentLoaded"); 219 }); 220 } 221 222 function waitForViewportResizeTo(ui, width, height) { 223 return new Promise(function (resolve) { 224 const isSizeMatching = data => data.width == width && data.height == height; 225 226 // If the viewport has already the expected size, we resolve the promise immediately. 227 const size = ui.getViewportSize(); 228 if (isSizeMatching(size)) { 229 info(`Viewport already resized to ${width} x ${height}`); 230 resolve(); 231 return; 232 } 233 234 // Otherwise, we'll listen to the viewport's resize event, and the 235 // browser's load end; since a racing condition can happen, where the 236 // viewport's listener is added after the resize, because the viewport's 237 // document was reloaded; therefore the test would hang forever. 238 // See bug 1302879. 239 const browser = ui.getViewportBrowser(); 240 241 const onContentResize = data => { 242 if (!isSizeMatching(data)) { 243 return; 244 } 245 ui.off("content-resize", onContentResize); 246 browser.removeEventListener("mozbrowserloadend", onBrowserLoadEnd); 247 info(`Got content-resize to ${width} x ${height}`); 248 resolve(); 249 }; 250 251 const onBrowserLoadEnd = async function () { 252 const data = ui.getViewportSize(ui); 253 onContentResize(data); 254 }; 255 256 info(`Waiting for viewport-resize to ${width} x ${height}`); 257 // We're changing the viewport size, which may also change the content 258 // size. We wait on the viewport resize event, and check for the 259 // desired size. 260 ui.on("content-resize", onContentResize); 261 browser.addEventListener("mozbrowserloadend", onBrowserLoadEnd, { 262 once: true, 263 }); 264 }); 265 } 266 267 var setViewportSize = async function (ui, manager, width, height) { 268 const size = ui.getViewportSize(); 269 info( 270 `Current size: ${size.width} x ${size.height}, ` + 271 `set to: ${width} x ${height}` 272 ); 273 if (size.width != width || size.height != height) { 274 const resized = waitForViewportResizeTo(ui, width, height); 275 ui.setViewportSize({ width, height }); 276 await resized; 277 } 278 }; 279 280 // This performs the same function as setViewportSize, but additionally 281 // ensures that reflow of the viewport has completed. 282 var setViewportSizeAndAwaitReflow = async function ( 283 ui, 284 manager, 285 width, 286 height 287 ) { 288 await setViewportSize(ui, manager, width, height); 289 await promiseContentReflow(ui); 290 await promiseApzFlushedRepaints(); 291 }; 292 293 function getViewportDevicePixelRatio(ui) { 294 return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () { 295 // Note that devicePixelRatio doesn't return the override to privileged 296 // code, see bug 1759962. 297 return content.browsingContext.overrideDPPX || content.devicePixelRatio; 298 }); 299 } 300 301 function getElRect(selector, win) { 302 const el = win.document.querySelector(selector); 303 return el.getBoundingClientRect(); 304 } 305 306 /** 307 * Drag an element identified by 'selector' by [x,y] amount. Returns 308 * the rect of the dragged element as it was before drag. 309 */ 310 function dragElementBy(selector, x, y, ui) { 311 const browserWindow = ui.getBrowserWindow(); 312 const rect = getElRect(selector, browserWindow); 313 const startPoint = { 314 clientX: Math.floor(rect.left + rect.width / 2), 315 clientY: Math.floor(rect.top + rect.height / 2), 316 }; 317 const endPoint = [startPoint.clientX + x, startPoint.clientY + y]; 318 319 EventUtils.synthesizeMouseAtPoint( 320 startPoint.clientX, 321 startPoint.clientY, 322 { type: "mousedown" }, 323 browserWindow 324 ); 325 326 // mousemove and mouseup are regular DOM listeners 327 EventUtils.synthesizeMouseAtPoint( 328 ...endPoint, 329 { type: "mousemove" }, 330 browserWindow 331 ); 332 EventUtils.synthesizeMouseAtPoint( 333 ...endPoint, 334 { type: "mouseup" }, 335 browserWindow 336 ); 337 338 return rect; 339 } 340 341 /** 342 * Resize the viewport and check that the resize happened as expected. 343 * 344 * @param {ResponsiveUI} ui 345 * The ResponsiveUI instance. 346 * @param {string} selector 347 * The css selector of the resize handler, eg .viewport-horizontal-resize-handle. 348 * @param {Array<number>} moveBy 349 * Array of 2 integers representing the x,y distance of the resize action. 350 * @param {Array<number>} moveBy 351 * Array of 2 integers representing the actual resize performed. 352 * @param {object} options 353 * @param {boolean} options.hasDevice 354 * Whether a device is currently set and will be overridden by the resize 355 */ 356 async function testViewportResize( 357 ui, 358 selector, 359 moveBy, 360 expectedHandleMove, 361 { hasDevice } = {} 362 ) { 363 // If a device was defined, wait for the device-associaton-removed event. 364 const deviceRemoved = hasDevice 365 ? once(ui, "device-association-removed") 366 : null; 367 368 const resized = ui.once("viewport-resize-dragend"); 369 const startRect = dragElementBy(selector, ...moveBy, ui); 370 await resized; 371 372 const endRect = getElRect(selector, ui.getBrowserWindow()); 373 is( 374 endRect.left - startRect.left, 375 expectedHandleMove[0], 376 `The x move of ${selector} is as expected` 377 ); 378 is( 379 endRect.top - startRect.top, 380 expectedHandleMove[1], 381 `The y move of ${selector} is as expected` 382 ); 383 384 await deviceRemoved; 385 } 386 387 async function openDeviceModal(ui) { 388 const { document, store } = ui.toolWindow; 389 390 info("Opening device modal through device selector."); 391 const onModalOpen = waitUntilState(store, state => state.devices.isModalOpen); 392 await selectMenuItem( 393 ui, 394 "#device-selector", 395 getStr("responsive.editDeviceList2") 396 ); 397 await onModalOpen; 398 399 const modal = document.getElementById("device-modal-wrapper"); 400 ok( 401 modal.classList.contains("opened") && !modal.classList.contains("closed"), 402 "The device modal is displayed." 403 ); 404 } 405 406 async function selectMenuItem({ toolWindow }, selector, value) { 407 const { document } = toolWindow; 408 409 const button = document.querySelector(selector); 410 isnot( 411 button, 412 null, 413 `Selector "${selector}" should match an existing element.` 414 ); 415 416 info(`Selecting ${value} in ${selector}.`); 417 418 await testMenuItems(toolWindow, button, items => { 419 const menuItem = findMenuItem(items, value); 420 isnot( 421 menuItem, 422 undefined, 423 `Value "${value}" should match an existing menu item.` 424 ); 425 menuItem.click(); 426 }); 427 } 428 429 /** 430 * Runs the menu items from the button's context menu against a test function. 431 * 432 * @param {Window} toolWindow 433 * A window reference. 434 * @param {Element} button 435 * The button that will show a context menu when clicked. 436 * @param {Function} testFn 437 * A test function that will be ran with the found menu item in the context menu 438 * as an argument. 439 */ 440 async function testMenuItems(toolWindow, button, testFn) { 441 // The context menu appears only in the top level window, which is different from 442 // the inner toolWindow. 443 const win = getTopLevelWindow(toolWindow); 444 445 await new Promise(resolve => { 446 win.document.addEventListener( 447 "popupshown", 448 async () => { 449 // Handle MenuButton popups (device selector and network throttling). 450 if ( 451 button.id === "device-selector" || 452 button.id == "user-agent-selector" || 453 button.id === "network-throttling" 454 ) { 455 let popupId; 456 if (button.id === "device-selector") { 457 popupId = "#device-selector-menu"; 458 } else if (button.id === "user-agent-selector") { 459 popupId = "#user-agent-selector-menu"; 460 } else { 461 popupId = "#network-throttling-menu"; 462 } 463 464 const popup = toolWindow.document.querySelector(popupId); 465 const menuItems = [...popup.querySelectorAll(".menuitem > .command")]; 466 467 testFn(menuItems); 468 469 if (popup.classList.contains("tooltip-visible")) { 470 // Close the tooltip explicitly. 471 button.click(); 472 await waitUntil(() => !popup.classList.contains("tooltip-visible")); 473 } 474 } else { 475 const popup = win.document.querySelector( 476 'menupopup[menu-api="true"]' 477 ); 478 const menuItems = [...popup.children]; 479 480 testFn(menuItems); 481 482 popup.hidePopup(); 483 } 484 485 resolve(); 486 }, 487 { once: true } 488 ); 489 490 button.click(); 491 }); 492 } 493 494 const selectDevice = async (ui, value) => { 495 const browser = ui.getViewportBrowser(); 496 const waitForDevToolsReload = await watchForDevToolsReload(browser); 497 498 const onDeviceChanged = once(ui, "device-changed"); 499 await selectMenuItem(ui, "#device-selector", value); 500 const { reloadTriggered } = await onDeviceChanged; 501 if (reloadTriggered) { 502 await waitForDevToolsReload(); 503 } 504 }; 505 506 const selectDevicePixelRatio = (ui, value) => 507 selectMenuItem(ui, "#device-pixel-ratio-menu", `DPR: ${value}`); 508 509 const selectNetworkThrottling = (ui, value) => 510 Promise.all([ 511 once(ui, "network-throttling-changed"), 512 selectMenuItem(ui, "#network-throttling", value), 513 ]); 514 515 function getSessionHistory(browser) { 516 if (Services.appinfo.sessionHistoryInParent) { 517 const browsingContext = browser.browsingContext; 518 const uri = browsingContext.currentWindowGlobal.documentURI.displaySpec; 519 const history = browsingContext.sessionHistory; 520 const body = ContentTask.spawn( 521 browser, 522 browsingContext, 523 function ( 524 // eslint-disable-next-line no-shadow 525 browsingContext 526 ) { 527 const docShell = browsingContext.docShell.QueryInterface( 528 Ci.nsIWebNavigation 529 ); 530 return docShell.document.body; 531 } 532 ); 533 const { SessionHistory } = ChromeUtils.importESModule( 534 "resource://gre/modules/sessionstore/SessionHistory.sys.mjs" 535 ); 536 return SessionHistory.collectFromParent(uri, body, history); 537 } 538 return ContentTask.spawn(browser, null, function () { 539 const { SessionHistory } = ChromeUtils.importESModule( 540 "resource://gre/modules/sessionstore/SessionHistory.sys.mjs" 541 ); 542 return SessionHistory.collect(docShell); 543 }); 544 } 545 546 function getContentSize(ui) { 547 return spawnViewportTask(ui, {}, () => ({ 548 width: content.screen.width, 549 height: content.screen.height, 550 })); 551 } 552 553 function getViewportScroll(ui) { 554 return spawnViewportTask(ui, {}, () => ({ 555 x: content.scrollX, 556 y: content.scrollY, 557 })); 558 } 559 560 async function waitForPageShow(browser) { 561 const tab = gBrowser.getTabForBrowser(browser); 562 const ui = ResponsiveUIManager.getResponsiveUIForTab(tab); 563 if (ui) { 564 browser = ui.getViewportBrowser(); 565 } 566 info( 567 "Waiting for pageshow from " + (ui ? "responsive" : "regular") + " browser" 568 ); 569 // Need to wait an extra tick after pageshow to ensure everyone is up-to-date, 570 // hence the waitForTick. 571 await BrowserTestUtils.waitForContentEvent(browser, "pageshow"); 572 return waitForTick(); 573 } 574 575 function waitForViewportScroll(ui) { 576 return BrowserTestUtils.waitForContentEvent( 577 ui.getViewportBrowser(), 578 "scroll", 579 true 580 ); 581 } 582 583 async function back(browser) { 584 const waitForDevToolsReload = await watchForDevToolsReload(browser); 585 const onPageShow = waitForPageShow(browser); 586 587 browser.goBack(); 588 589 await onPageShow; 590 await waitForDevToolsReload(); 591 } 592 593 async function forward(browser) { 594 const waitForDevToolsReload = await watchForDevToolsReload(browser); 595 const onPageShow = waitForPageShow(browser); 596 597 browser.goForward(); 598 599 await onPageShow; 600 await waitForDevToolsReload(); 601 } 602 603 function addDeviceForTest(device) { 604 info(`Adding Test Device "${device.name}" to the list.`); 605 addDevice(device); 606 607 registerCleanupFunction(() => { 608 // Note that assertions in cleanup functions are not displayed unless they failed. 609 ok( 610 removeDevice(device), 611 `Removed Test Device "${device.name}" from the list.` 612 ); 613 }); 614 } 615 616 async function waitForClientClose(ui) { 617 info("Waiting for RDM devtools client to close"); 618 await ui.commands.client.once("closed"); 619 info("RDM's devtools client is now closed"); 620 } 621 622 async function testDevicePixelRatio(ui, expected) { 623 const dppx = await getViewportDevicePixelRatio(ui); 624 is(dppx, expected, `devicePixelRatio should be set to ${expected}`); 625 } 626 627 function testTouchEventsOverride(ui, expected) { 628 const { document } = ui.toolWindow; 629 const touchButton = document.getElementById("touch-simulation-button"); 630 631 const flag = gBrowser.selectedBrowser.browsingContext.touchEventsOverride; 632 633 is( 634 flag === "enabled", 635 expected, 636 `Touch events override should be ${expected ? "enabled" : "disabled"}` 637 ); 638 is( 639 touchButton.classList.contains("checked"), 640 expected, 641 `Touch simulation button should be ${expected ? "" : "in"}active.` 642 ); 643 } 644 645 function testViewportDeviceMenuLabel(ui, expectedDeviceName) { 646 info("Test viewport's device select label"); 647 648 const button = ui.toolWindow.document.querySelector("#device-selector"); 649 ok( 650 button.textContent.includes(expectedDeviceName), 651 `Device Select value ${button.textContent} should be: ${expectedDeviceName}` 652 ); 653 } 654 655 async function toggleTouchSimulation(ui) { 656 const { document } = ui.toolWindow; 657 const browser = ui.getViewportBrowser(); 658 659 const touchButton = document.getElementById("touch-simulation-button"); 660 const wasChecked = touchButton.classList.contains("checked"); 661 const onTouchSimulationChanged = once(ui, "touch-simulation-changed"); 662 const waitForDevToolsReload = await watchForDevToolsReload(browser); 663 const onTouchButtonStateChanged = waitFor( 664 () => touchButton.classList.contains("checked") !== wasChecked 665 ); 666 667 touchButton.click(); 668 await Promise.all([ 669 onTouchSimulationChanged, 670 onTouchButtonStateChanged, 671 waitForDevToolsReload(), 672 ]); 673 } 674 675 async function testUserAgent(ui, expected) { 676 const { document } = ui.toolWindow; 677 const userAgentInput = document.getElementById("user-agent-input"); 678 679 if (expected === DEFAULT_UA) { 680 is(userAgentInput.value, "", "UA input should be empty"); 681 } else { 682 is(userAgentInput.value, expected, `UA input should be set to ${expected}`); 683 } 684 685 await testUserAgentFromBrowser(ui.getViewportBrowser(), expected); 686 } 687 688 async function testUserAgentFromBrowser(browser, expected) { 689 const ua = await SpecialPowers.spawn(browser, [], async function () { 690 return content.navigator.userAgent; 691 }); 692 is(ua, expected, `UA should be set to ${expected}`); 693 } 694 695 function testViewportDimensions(ui, w, h) { 696 const viewport = ui.viewportElement; 697 698 is( 699 ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"), 700 `${w}px`, 701 `Viewport should have width of ${w}px` 702 ); 703 is( 704 ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"), 705 `${h}px`, 706 `Viewport should have height of ${h}px` 707 ); 708 } 709 710 async function changeUserAgentInput( 711 ui, 712 value, 713 keyPressedAfterChange = "VK_RETURN" 714 ) { 715 const { Simulate } = ui.toolWindow.require( 716 "resource://devtools/client/shared/vendor/react-dom-test-utils.js" 717 ); 718 const { document, store } = ui.toolWindow; 719 const browser = ui.getViewportBrowser(); 720 721 const userAgentInput = document.getElementById("user-agent-input"); 722 userAgentInput.value = value; 723 userAgentInput.focus(); 724 Simulate.change(userAgentInput); 725 726 function pressKey() { 727 EventUtils.synthesizeKey(keyPressedAfterChange, {}, ui.toolWindow); 728 } 729 730 if (keyPressedAfterChange === "VK_ESCAPE") { 731 pressKey(); 732 } else { 733 const userAgentChanged = waitUntilState( 734 store, 735 state => state.ui.userAgent === value 736 ); 737 const changed = once(ui, "user-agent-changed"); 738 const waitForDevToolsReload = await watchForDevToolsReload(browser); 739 740 pressKey(); 741 742 await Promise.all([changed, waitForDevToolsReload(), userAgentChanged]); 743 } 744 } 745 746 /** 747 * Assuming the device modal is open and the device adder form is shown, this helper 748 * function adds `device` via the form, saves it, and waits for it to appear in the store. 749 */ 750 function addDeviceInModal(ui, device) { 751 const { Simulate } = ui.toolWindow.require( 752 "resource://devtools/client/shared/vendor/react-dom-test-utils.js" 753 ); 754 const { document, store } = ui.toolWindow; 755 756 const nameInput = document.querySelector("#device-form-name input"); 757 const [widthInput, heightInput] = document.querySelectorAll( 758 "#device-form-size input" 759 ); 760 const pixelRatioInput = document.querySelector( 761 "#device-form-pixel-ratio input" 762 ); 763 const userAgentInput = document.querySelector( 764 "#device-form-user-agent input" 765 ); 766 const touchInput = document.querySelector("#device-form-touch input"); 767 768 nameInput.value = device.name; 769 Simulate.change(nameInput); 770 widthInput.value = device.width; 771 Simulate.change(widthInput); 772 Simulate.blur(widthInput); 773 heightInput.value = device.height; 774 Simulate.change(heightInput); 775 Simulate.blur(heightInput); 776 pixelRatioInput.value = device.pixelRatio; 777 Simulate.change(pixelRatioInput); 778 userAgentInput.value = device.userAgent; 779 Simulate.change(userAgentInput); 780 touchInput.checked = device.touch; 781 Simulate.change(touchInput); 782 783 const existingCustomDevices = store.getState().devices.custom.length; 784 const adderSave = document.querySelector("#device-form-save"); 785 const saved = waitUntilState( 786 store, 787 state => state.devices.custom.length == existingCustomDevices + 1 788 ); 789 Simulate.click(adderSave); 790 return saved; 791 } 792 793 async function editDeviceInModal(ui, device, newDevice) { 794 const { Simulate } = ui.toolWindow.require( 795 "resource://devtools/client/shared/vendor/react-dom-test-utils.js" 796 ); 797 const { document, store } = ui.toolWindow; 798 799 const nameInput = document.querySelector("#device-form-name input"); 800 const [widthInput, heightInput] = document.querySelectorAll( 801 "#device-form-size input" 802 ); 803 const pixelRatioInput = document.querySelector( 804 "#device-form-pixel-ratio input" 805 ); 806 const userAgentInput = document.querySelector( 807 "#device-form-user-agent input" 808 ); 809 const touchInput = document.querySelector("#device-form-touch input"); 810 811 nameInput.value = newDevice.name; 812 Simulate.change(nameInput); 813 widthInput.value = newDevice.width; 814 Simulate.change(widthInput); 815 Simulate.blur(widthInput); 816 heightInput.value = newDevice.height; 817 Simulate.change(heightInput); 818 Simulate.blur(heightInput); 819 pixelRatioInput.value = newDevice.pixelRatio; 820 Simulate.change(pixelRatioInput); 821 userAgentInput.value = newDevice.userAgent; 822 Simulate.change(userAgentInput); 823 touchInput.checked = newDevice.touch; 824 Simulate.change(touchInput); 825 826 const existingCustomDevices = store.getState().devices.custom.length; 827 const formSave = document.querySelector("#device-form-save"); 828 829 const saved = waitUntilState( 830 store, 831 state => 832 state.devices.custom.length == existingCustomDevices && 833 state.devices.custom.find(({ name }) => name == newDevice.name) && 834 !state.devices.custom.find(({ name }) => name == device.name) 835 ); 836 837 // Editing a custom device triggers a "device-change" message. 838 // Wait for the `device-changed` event to avoid unfinished requests during the 839 // tests. 840 const onDeviceChanged = ui.once("device-changed"); 841 842 Simulate.click(formSave); 843 844 await onDeviceChanged; 845 return saved; 846 } 847 848 function findMenuItem(menuItems, name) { 849 return menuItems.find(menuItem => menuItem.textContent.includes(name)); 850 } 851 852 function reloadOnUAChange(enabled) { 853 const pref = RELOAD_CONDITION_PREF_PREFIX + "userAgent"; 854 Services.prefs.setBoolPref(pref, enabled); 855 } 856 857 function reloadOnTouchChange(enabled) { 858 const pref = RELOAD_CONDITION_PREF_PREFIX + "touchSimulation"; 859 Services.prefs.setBoolPref(pref, enabled); 860 } 861 862 function rotateViewport(ui) { 863 const { document } = ui.toolWindow; 864 const rotateButton = document.getElementById("rotate-button"); 865 rotateButton.click(); 866 } 867 868 // Call this to switch between on/off support for meta viewports. 869 async function setTouchAndMetaViewportSupport(ui, value) { 870 await ui.updateTouchSimulation(value); 871 info("Reload so the new configuration applies cleanly to the page"); 872 await reloadBrowser(); 873 874 await promiseContentReflow(ui); 875 } 876 877 // This function checks that zoom, the initial containing block width and height 878 // are all as expected. 879 async function testViewportZoomWidthAndHeight(msg, ui, zoom, width, height) { 880 if (typeof zoom !== "undefined") { 881 const resolution = await spawnViewportTask(ui, {}, function () { 882 return content.windowUtils.getResolution(); 883 }); 884 is(resolution, zoom, msg + " should have expected zoom."); 885 } 886 887 if (typeof width !== "undefined" || typeof height !== "undefined") { 888 const innerSize = await spawnViewportTask(ui, {}, function () { 889 return { 890 width: content.document.documentElement.clientWidth, 891 height: content.document.documentElement.clientHeight, 892 }; 893 }); 894 if (typeof width !== "undefined") { 895 is(innerSize.width, width, msg + " should have expected inner width."); 896 } 897 if (typeof height !== "undefined") { 898 is(innerSize.height, height, msg + " should have expected inner height."); 899 } 900 } 901 } 902 903 function promiseContentReflow(ui) { 904 return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () { 905 return new Promise(resolve => { 906 content.window.requestAnimationFrame(() => { 907 content.window.requestAnimationFrame(resolve); 908 }); 909 }); 910 }); 911 } 912 913 // This function returns a promise that will be resolved when the 914 // RDM zoom has been set and the content has finished rescaling 915 // to the new size. 916 async function promiseRDMZoom(ui, browser, zoom) { 917 const currentZoom = ZoomManager.getZoomForBrowser(browser); 918 if (currentZoom.toFixed(2) == zoom.toFixed(2)) { 919 return; 920 } 921 922 const width = browser.getBoundingClientRect().width; 923 924 ZoomManager.setZoomForBrowser(browser, zoom); 925 926 // RDM resizes the browser as a result of a zoom change, so we wait for that. 927 // 928 // This also has the side effect of updating layout which ensures that any 929 // remote frame dimension update message gets there in time. 930 await BrowserTestUtils.waitForCondition(function () { 931 return browser.getBoundingClientRect().width != width; 932 }); 933 } 934 935 async function waitForDeviceAndViewportState(ui) { 936 const { store } = ui.toolWindow; 937 938 // Wait until the viewport has been added and the device list has been loaded 939 await waitUntilState( 940 store, 941 state => 942 state.viewports.length == 1 && 943 state.devices.listState == localTypes.loadableState.LOADED 944 ); 945 } 946 947 /** 948 * Wait for the content page to be rendered with the expected pixel ratio. 949 * 950 * @param {ResponsiveUI} ui 951 * The ResponsiveUI instance. 952 * @param {Integer} expected 953 * The expected dpr for the content page. 954 * @param {object} options 955 * @param {boolean} options.waitForTargetConfiguration 956 * If set to true, the function will wait for the targetConfigurationCommand configuration 957 * to reflect the ratio that was set. This can be used to prevent pending requests 958 * to the actor. 959 */ 960 async function waitForDevicePixelRatio( 961 ui, 962 expected, 963 { waitForTargetConfiguration } = {} 964 ) { 965 const dpx = await SpecialPowers.spawn( 966 ui.getViewportBrowser(), 967 [{ expected }], 968 function (args) { 969 const getDpr = function () { 970 return content.browsingContext.overrideDPPX || content.devicePixelRatio; 971 }; 972 const initial = getDpr(); 973 info( 974 `Listening for pixel ratio change ` + 975 `(current: ${initial}, expected: ${args.expected})` 976 ); 977 return new Promise(resolve => { 978 const mql = content.matchMedia(`(resolution: ${args.expected}dppx)`); 979 if (mql.matches) { 980 info(`Ratio already changed to ${args.expected}dppx`); 981 resolve(getDpr()); 982 return; 983 } 984 mql.addListener(function listener() { 985 info(`Ratio changed to ${args.expected}dppx`); 986 mql.removeListener(listener); 987 resolve(getDpr()); 988 }); 989 }); 990 } 991 ); 992 993 if (waitForTargetConfiguration) { 994 // Ensure the configuration was updated so we limit the risk of the client closing before 995 // the server sent back the result of the updateConfiguration call. 996 await waitFor(() => { 997 return ( 998 ui.commands.targetConfigurationCommand.configuration.overrideDPPX === 999 expected 1000 ); 1001 }); 1002 } 1003 1004 return dpx; 1005 }