Backed out 4 changesets (bug 1651522) for causing dt failures on devtools/shared...
[gecko.git] / devtools / client / shared / test / shared-head.js
blob5e01e3f9b84ddb966c2651bbc1cfbf5de9f5f1ab
1 /* Any copyright is dedicated to the Public Domain.
2  * http://creativecommons.org/publicdomain/zero/1.0/ */
3 /* eslint no-unused-vars: [2, {"vars": "local"}] */
5 /* import-globals-from ../../inspector/test/shared-head.js */
7 "use strict";
9 // This shared-head.js file is used by most mochitests
10 // and we start using it in xpcshell tests as well.
11 // It contains various common helper functions.
13 const isMochitest = "gTestPath" in this;
14 const isXpcshell = !isMochitest;
15 if (isXpcshell) {
16   // gTestPath isn't exposed to xpcshell tests
17   // _TEST_FILE is an array for a unique string
18   /* global _TEST_FILE */
19   this.gTestPath = _TEST_FILE[0];
22 const { Constructor: CC } = Components;
24 // Print allocation count if DEBUG_DEVTOOLS_ALLOCATIONS is set to "normal",
25 // and allocation sites if DEBUG_DEVTOOLS_ALLOCATIONS is set to "verbose".
26 const DEBUG_ALLOCATIONS = Services.env.get("DEBUG_DEVTOOLS_ALLOCATIONS");
27 if (DEBUG_ALLOCATIONS) {
28   // Use a custom loader with `invisibleToDebugger` flag for the allocation tracker
29   // as it instantiates custom Debugger API instances and has to be running in a distinct
30   // compartments from DevTools and system scopes (JSMs, XPCOM,...)
31   const {
32     useDistinctSystemPrincipalLoader,
33     releaseDistinctSystemPrincipalLoader,
34   } = ChromeUtils.importESModule(
35     "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
36   );
37   const requester = {};
38   const loader = useDistinctSystemPrincipalLoader(requester);
39   registerCleanupFunction(() =>
40     releaseDistinctSystemPrincipalLoader(requester)
41   );
43   const { allocationTracker } = loader.require(
44     "resource://devtools/shared/test-helpers/allocation-tracker.js"
45   );
46   const tracker = allocationTracker({ watchAllGlobals: true });
47   registerCleanupFunction(() => {
48     if (DEBUG_ALLOCATIONS == "normal") {
49       tracker.logCount();
50     } else if (DEBUG_ALLOCATIONS == "verbose") {
51       tracker.logAllocationSites();
52     }
53     tracker.stop();
54   });
57 const { loader, require } = ChromeUtils.importESModule(
58   "resource://devtools/shared/loader/Loader.sys.mjs"
60 const { sinon } = ChromeUtils.importESModule(
61   "resource://testing-common/Sinon.sys.mjs"
64 // When loaded from xpcshell test, this file is loaded via xpcshell.ini's head property
65 // and so it loaded first before anything else and isn't having access to Services global.
66 // Whereas many head.js files from mochitest import this file via loadSubScript
67 // and already expose Services as a global.
69 const {
70   gDevTools,
71 } = require("resource://devtools/client/framework/devtools.js");
72 const {
73   CommandsFactory,
74 } = require("resource://devtools/shared/commands/commands-factory.js");
75 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
77 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
79 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
81 loader.lazyRequireGetter(
82   this,
83   "ResponsiveUIManager",
84   "resource://devtools/client/responsive/manager.js"
86 loader.lazyRequireGetter(
87   this,
88   "localTypes",
89   "resource://devtools/client/responsive/types.js"
91 loader.lazyRequireGetter(
92   this,
93   "ResponsiveMessageHelper",
94   "resource://devtools/client/responsive/utils/message.js"
97 loader.lazyRequireGetter(
98   this,
99   "FluentReact",
100   "resource://devtools/client/shared/vendor/fluent-react.js"
103 const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
104 const CHROME_URL_ROOT = TEST_DIR + "/";
105 const URL_ROOT = CHROME_URL_ROOT.replace(
106   "chrome://mochitests/content/",
107   "http://example.com/"
109 const URL_ROOT_SSL = CHROME_URL_ROOT.replace(
110   "chrome://mochitests/content/",
111   "https://example.com/"
114 // Add aliases which make it more explicit that URL_ROOT uses a com TLD.
115 const URL_ROOT_COM = URL_ROOT;
116 const URL_ROOT_COM_SSL = URL_ROOT_SSL;
118 // Also expose http://example.org, http://example.net, https://example.org to
119 // test Fission scenarios easily.
120 // Note: example.net is not available for https.
121 const URL_ROOT_ORG = CHROME_URL_ROOT.replace(
122   "chrome://mochitests/content/",
123   "http://example.org/"
125 const URL_ROOT_ORG_SSL = CHROME_URL_ROOT.replace(
126   "chrome://mochitests/content/",
127   "https://example.org/"
129 const URL_ROOT_NET = CHROME_URL_ROOT.replace(
130   "chrome://mochitests/content/",
131   "http://example.net/"
133 const URL_ROOT_NET_SSL = CHROME_URL_ROOT.replace(
134   "chrome://mochitests/content/",
135   "https://example.net/"
137 // mochi.test:8888 is the actual primary location where files are served.
138 const URL_ROOT_MOCHI_8888 = CHROME_URL_ROOT.replace(
139   "chrome://mochitests/content/",
140   "http://mochi.test:8888/"
143 try {
144   if (isMochitest) {
145     Services.scriptloader.loadSubScript(
146       "chrome://mochitests/content/browser/devtools/client/shared/test/telemetry-test-helpers.js",
147       this
148     );
149   }
150 } catch (e) {
151   ok(
152     false,
153     "MISSING DEPENDENCY ON telemetry-test-helpers.js\n" +
154       "Please add the following line in browser.ini:\n" +
155       "  !/devtools/client/shared/test/telemetry-test-helpers.js\n"
156   );
157   throw e;
160 // Force devtools to be initialized so menu items and keyboard shortcuts get installed
161 require("resource://devtools/client/framework/devtools-browser.js");
163 // All tests are asynchronous
164 if (isMochitest) {
165   waitForExplicitFinish();
168 var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
170 registerCleanupFunction(function () {
171   if (
172     DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT
173   ) {
174     ok(
175       false,
176       "Should have had the expected number of DevToolsUtils.assert() failures." +
177         " Expected " +
178         EXPECTED_DTU_ASSERT_FAILURE_COUNT +
179         ", got " +
180         DevToolsUtils.assertionFailureCount
181     );
182   }
185 // Uncomment this pref to dump all devtools emitted events to the console.
186 // Services.prefs.setBoolPref("devtools.dump.emit", true);
189  * Watch console messages for failed propType definitions in React components.
190  */
191 function onConsoleMessage(subject) {
192   const message = subject.wrappedJSObject.arguments[0];
194   if (message && /Failed propType/.test(message.toString())) {
195     ok(false, message);
196   }
199 const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(
200   Ci.nsIConsoleAPIStorage
203 ConsoleAPIStorage.addLogEventListener(
204   onConsoleMessage,
205   Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
207 registerCleanupFunction(() => {
208   ConsoleAPIStorage.removeLogEventListener(onConsoleMessage);
211 Services.prefs.setBoolPref("devtools.inspector.three-pane-enabled", true);
213 // Disable this preference to reduce exceptions related to pending `listWorkers`
214 // requests occuring after a process is created/destroyed. See Bug 1620983.
215 Services.prefs.setBoolPref("dom.ipc.processPrelaunch.enabled", false);
217 // Disable this preference to capture async stacks across all locations during
218 // DevTools mochitests. Async stacks provide very valuable information to debug
219 // intermittents, but come with a performance overhead, which is why they are
220 // only captured in Debuggees by default.
221 Services.prefs.setBoolPref(
222   "javascript.options.asyncstack_capture_debuggee_only",
223   false
226 // On some Linux platforms, prefers-reduced-motion is enabled, which would
227 // trigger the notification to be displayed in the toolbox. Dismiss the message
228 // by default.
229 Services.prefs.setBoolPref(
230   "devtools.inspector.simple-highlighters.message-dismissed",
231   true
234 registerCleanupFunction(() => {
235   Services.prefs.clearUserPref("devtools.dump.emit");
236   Services.prefs.clearUserPref("devtools.inspector.three-pane-enabled");
237   Services.prefs.clearUserPref("dom.ipc.processPrelaunch.enabled");
238   Services.prefs.clearUserPref("devtools.toolbox.host");
239   Services.prefs.clearUserPref("devtools.toolbox.previousHost");
240   Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
241   Services.prefs.clearUserPref("devtools.toolbox.splitconsoleHeight");
242   Services.prefs.clearUserPref(
243     "javascript.options.asyncstack_capture_debuggee_only"
244   );
245   Services.prefs.clearUserPref(
246     "devtools.inspector.simple-highlighters.message-dismissed"
247   );
250 var {
251   BrowserConsoleManager,
252 } = require("resource://devtools/client/webconsole/browser-console-manager.js");
254 registerCleanupFunction(async function cleanup() {
255   // Closing the browser console if there's one
256   const browserConsole = BrowserConsoleManager.getBrowserConsole();
257   if (browserConsole) {
258     await safeCloseBrowserConsole({ clearOutput: true });
259   }
261   // Close any tab opened by the test.
262   // There should be only one tab opened by default when firefox starts the test.
263   while (isMochitest && gBrowser.tabs.length > 1) {
264     await closeTabAndToolbox(gBrowser.selectedTab);
265   }
267   // Note that this will run before cleanup functions registered by tests or other head.js files.
268   // So all connections must be cleaned up by the test when the test ends,
269   // before the harness starts invoking the cleanup functions
270   await waitForTick();
272   // All connections must be cleaned up by the test when the test ends.
273   const {
274     DevToolsServer,
275   } = require("resource://devtools/server/devtools-server.js");
276   ok(
277     !DevToolsServer.hasConnection(),
278     "The main process DevToolsServer has no pending connection when the test ends"
279   );
280   // If there is still open connection, close all of them so that following tests
281   // could pass.
282   if (DevToolsServer.hasConnection()) {
283     for (const conn of Object.values(DevToolsServer._connections)) {
284       conn.close();
285     }
286   }
289 async function safeCloseBrowserConsole({ clearOutput = false } = {}) {
290   const hud = BrowserConsoleManager.getBrowserConsole();
291   if (!hud) {
292     return;
293   }
295   if (clearOutput) {
296     info("Clear the browser console output");
297     const { ui } = hud;
298     const promises = [ui.once("messages-cleared")];
299     // If there's an object inspector, we need to wait for the actors to be released.
300     if (ui.outputNode.querySelector(".object-inspector")) {
301       promises.push(ui.once("fronts-released"));
302     }
303     await ui.clearOutput(true);
304     await Promise.all(promises);
305     info("Browser console cleared");
306   }
308   info("Wait for all Browser Console targets to be attached");
309   // It might happen that waitForAllTargetsToBeAttached does not resolve, so we set a
310   // timeout of 1s before closing
311   await Promise.race([
312     waitForAllTargetsToBeAttached(hud.commands.targetCommand),
313     wait(1000),
314   ]);
316   info("Close the Browser Console");
317   await BrowserConsoleManager.closeBrowserConsole();
318   info("Browser Console closed");
322  * Observer code to register the test actor in every DevTools server which
323  * starts registering its own actors.
325  * We require immediately the highlighter test actor file, because it will force to load and
326  * register the front and the spec for HighlighterTestActor. Normally specs and fronts are
327  * in separate files registered in specs/index.js. But here to simplify the
328  * setup everything is in the same file and we force to load it here.
330  * DevToolsServer will emit "devtools-server-initialized" after finishing its
331  * initialization. We watch this observable to add our custom actor.
333  * As a single test may create several DevTools servers, we keep the observer
334  * alive until the test ends.
336  * To avoid leaks, the observer needs to be removed at the end of each test.
337  * The test cleanup will send the async message "remove-devtools-highlightertestactor-observer",
338  * we listen to this message to cleanup the observer.
339  */
340 function highlighterTestActorBootstrap() {
341   /* eslint-env mozilla/process-script */
342   const HIGHLIGHTER_TEST_ACTOR_URL =
343     "chrome://mochitests/content/browser/devtools/client/shared/test/highlighter-test-actor.js";
345   const { require: _require } = ChromeUtils.importESModule(
346     "resource://devtools/shared/loader/Loader.sys.mjs"
347   );
348   _require(HIGHLIGHTER_TEST_ACTOR_URL);
350   const actorRegistryObserver = subject => {
351     const actorRegistry = subject.wrappedJSObject;
352     actorRegistry.registerModule(HIGHLIGHTER_TEST_ACTOR_URL, {
353       prefix: "highlighterTest",
354       constructor: "HighlighterTestActor",
355       type: { target: true },
356     });
357   };
358   Services.obs.addObserver(
359     actorRegistryObserver,
360     "devtools-server-initialized"
361   );
363   const unloadListener = () => {
364     Services.cpmm.removeMessageListener(
365       "remove-devtools-testactor-observer",
366       unloadListener
367     );
368     Services.obs.removeObserver(
369       actorRegistryObserver,
370       "devtools-server-initialized"
371     );
372   };
373   Services.cpmm.addMessageListener(
374     "remove-devtools-testactor-observer",
375     unloadListener
376   );
379 if (isMochitest) {
380   const highlighterTestActorBootstrapScript =
381     "data:,(" + highlighterTestActorBootstrap + ")()";
382   Services.ppmm.loadProcessScript(
383     highlighterTestActorBootstrapScript,
384     // Load this script in all processes (created or to be created)
385     true
386   );
388   registerCleanupFunction(() => {
389     Services.ppmm.broadcastAsyncMessage("remove-devtools-testactor-observer");
390     Services.ppmm.removeDelayedProcessScript(
391       highlighterTestActorBootstrapScript
392     );
393   });
397  * Spawn an instance of the highlighter test actor for the given toolbox
399  * @param {Toolbox} toolbox
400  * @param {Object} options
401  * @param {Function} options.target: Optional target to get the highlighterTestFront for.
402  *        If not provided, the top level target will be used.
403  * @returns {HighlighterTestFront}
404  */
405 async function getHighlighterTestFront(toolbox, { target } = {}) {
406   // Loading the Inspector panel in order to overwrite the TestActor getter for the
407   // highlighter instance with a method that points to the currently visible
408   // Box Model Highlighter managed by the Inspector panel.
409   const inspector = await toolbox.loadTool("inspector");
411   const highlighterTestFront = await (target || toolbox.target).getFront(
412     "highlighterTest"
413   );
414   // Override the highligher getter with a method to return the active box model
415   // highlighter. Adaptation for multi-process scenarios where there can be multiple
416   // highlighters, one per process.
417   highlighterTestFront.highlighter = () => {
418     return inspector.highlighters.getActiveHighlighter(
419       inspector.highlighters.TYPES.BOXMODEL
420     );
421   };
422   return highlighterTestFront;
426  * Spawn an instance of the highlighter test actor for the given tab, when we need the
427  * highlighter test front before opening or without a toolbox.
429  * @param {Tab} tab
430  * @returns {HighlighterTestFront}
431  */
432 async function getHighlighterTestFrontWithoutToolbox(tab) {
433   const commands = await CommandsFactory.forTab(tab);
434   // Initialize the TargetCommands which require some async stuff to be done
435   // before being fully ready. This will define the `targetCommand.targetFront` attribute.
436   await commands.targetCommand.startListening();
438   const targetFront = commands.targetCommand.targetFront;
439   return targetFront.getFront("highlighterTest");
443  * Returns a Promise that resolves when all the targets are fully attached.
445  * @param {TargetCommand} targetCommand
446  */
447 function waitForAllTargetsToBeAttached(targetCommand) {
448   return Promise.allSettled(
449     targetCommand
450       .getAllTargets(targetCommand.ALL_TYPES)
451       .map(target => target.initialized)
452   );
456  * Add a new test tab in the browser and load the given url.
457  * @param {String} url The url to be loaded in the new tab
458  * @param {Object} options Object with various optional fields:
459  *   - {Boolean} background If true, open the tab in background
460  *   - {ChromeWindow} window Firefox top level window we should use to open the tab
461  *   - {Number} userContextId The userContextId of the tab.
462  *   - {String} preferredRemoteType
463  *   - {Boolean} waitForLoad Wait for the page in the new tab to load. (Defaults to true.)
464  * @return a promise that resolves to the tab object when the url is loaded
465  */
466 async function addTab(url, options = {}) {
467   info("Adding a new tab with URL: " + url);
469   const {
470     background = false,
471     userContextId,
472     preferredRemoteType,
473     waitForLoad = true,
474   } = options;
475   const { gBrowser } = options.window ? options.window : window;
477   const tab = BrowserTestUtils.addTab(gBrowser, url, {
478     userContextId,
479     preferredRemoteType,
480   });
482   if (!background) {
483     gBrowser.selectedTab = tab;
484   }
486   if (waitForLoad) {
487     await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
488     // Waiting for presShell helps with test timeouts in webrender platforms.
489     await waitForPresShell(tab.linkedBrowser);
490     info("Tab added and finished loading");
491   } else {
492     info("Tab added");
493   }
495   return tab;
499  * Remove the given tab.
500  * @param {Object} tab The tab to be removed.
501  * @return Promise<undefined> resolved when the tab is successfully removed.
502  */
503 async function removeTab(tab) {
504   info("Removing tab.");
506   const { gBrowser } = tab.ownerDocument.defaultView;
507   const onClose = once(gBrowser.tabContainer, "TabClose");
508   gBrowser.removeTab(tab);
509   await onClose;
511   info("Tab removed and finished closing");
515  * Alias for navigateTo which will reuse the current URI of the provided browser
516  * to trigger a navigation.
517  */
518 async function reloadBrowser({
519   browser = gBrowser.selectedBrowser,
520   isErrorPage = false,
521   waitForLoad = true,
522 } = {}) {
523   return navigateTo(browser.currentURI.spec, {
524     browser,
525     isErrorPage,
526     waitForLoad,
527   });
531  * Navigate the currently selected tab to a new URL and wait for it to load.
532  * Also wait for the toolbox to attach to the new target, if we navigated
533  * to a new process.
535  * @param {String} url The url to be loaded in the current tab.
536  * @param {JSON} options Optional dictionary object with the following keys:
537  *        - {XULBrowser} browser
538  *          The browser element which should navigate. Defaults to the selected
539  *          browser.
540  *        - {Boolean} isErrorPage
541  *          You may pass `true` if the URL is an error page. Otherwise
542  *          BrowserTestUtils.browserLoaded will wait for 'load' event, which
543  *          never fires for error pages.
544  *        - {Boolean} waitForLoad
545  *          You may pass `false` if the page load is expected to be blocked by
546  *          a script or a breakpoint.
548  * @return a promise that resolves when the page has fully loaded.
549  */
550 async function navigateTo(
551   uri,
552   {
553     browser = gBrowser.selectedBrowser,
554     isErrorPage = false,
555     waitForLoad = true,
556   } = {}
557 ) {
558   const waitForDevToolsReload = await watchForDevToolsReload(browser, {
559     isErrorPage,
560     waitForLoad,
561   });
563   uri = uri.replaceAll("\n", "");
564   info(`Navigating to "${uri}"`);
566   const onBrowserLoaded = BrowserTestUtils.browserLoaded(
567     browser,
568     // includeSubFrames
569     false,
570     // resolve on this specific page to load (if null, it would be any page load)
571     loadedUrl => {
572       // loadedUrl is encoded, while uri might not be.
573       return loadedUrl === uri || decodeURI(loadedUrl) === uri;
574     },
575     isErrorPage
576   );
578   // if we're navigating to the same page we're already on, use reloadTab instead as the
579   // behavior slightly differs from loadURI (e.g. scroll position isn't keps with the latter).
580   if (uri === browser.currentURI.spec) {
581     gBrowser.reloadTab(gBrowser.getTabForBrowser(browser));
582   } else {
583     BrowserTestUtils.startLoadingURIString(browser, uri);
584   }
586   if (waitForLoad) {
587     info(`Waiting for page to be loaded…`);
588     await onBrowserLoaded;
589     info(`→ page loaded`);
590   }
592   await waitForDevToolsReload();
596  * This method should be used to watch for completion of any browser navigation
597  * performed with a DevTools UI.
599  * It should watch for:
600  * - Toolbox reload
601  * - Toolbox commands reload
602  * - RDM reload
603  * - RDM commands reload
605  * And it should work both for target switching or old-style navigations.
607  * This method, similarly to all the other watch* navigation methods in this file,
608  * is async but returns another method which should be called after the navigation
609  * is done. Browser navigation might be monitored differently depending on the
610  * situation, so it's up to the caller to handle it as needed.
612  * Typically, this would be used as follows:
613  * ```
614  *   async function someNavigationHelper(browser) {
615  *     const waitForDevToolsFn = await watchForDevToolsReload(browser);
617  *     // This step should wait for the load to be completed from the browser's
618  *     // point of view, so that waitForDevToolsFn can compare pIds, browsing
619  *     // contexts etc... and check if we should expect a target switch
620  *     await performBrowserNavigation(browser);
622  *     await waitForDevToolsFn();
623  *   }
624  * ```
625  */
626 async function watchForDevToolsReload(
627   browser,
628   { isErrorPage = false, waitForLoad = true } = {}
629 ) {
630   const waitForToolboxReload = await _watchForToolboxReload(browser, {
631     isErrorPage,
632     waitForLoad,
633   });
634   const waitForResponsiveReload = await _watchForResponsiveReload(browser, {
635     isErrorPage,
636     waitForLoad,
637   });
639   return async function () {
640     info("Wait for the toolbox to reload");
641     await waitForToolboxReload();
643     info("Wait for Responsive UI to reload");
644     await waitForResponsiveReload();
645   };
649  * Start watching for the toolbox reload to be completed:
650  * - watch for the toolbox's commands to be fully reloaded
651  * - watch for the toolbox's current panel to be reloaded
652  */
653 async function _watchForToolboxReload(
654   browser,
655   { isErrorPage, waitForLoad } = {}
656 ) {
657   const tab = gBrowser.getTabForBrowser(browser);
659   const toolbox = gDevTools.getToolboxForTab(tab);
661   if (!toolbox) {
662     // No toolbox to wait for
663     return function () {};
664   }
666   const waitForCurrentPanelReload = watchForCurrentPanelReload(toolbox);
667   const waitForToolboxCommandsReload = await watchForCommandsReload(
668     toolbox.commands,
669     { isErrorPage, waitForLoad }
670   );
671   const checkTargetSwitching = await watchForTargetSwitching(
672     toolbox.commands,
673     browser
674   );
676   return async function () {
677     const isTargetSwitching = checkTargetSwitching();
679     info(`Waiting for toolbox commands to be reloaded…`);
680     await waitForToolboxCommandsReload(isTargetSwitching);
682     // TODO: We should wait for all loaded panels to reload here, because some
683     // of them might still perform background updates.
684     if (waitForCurrentPanelReload) {
685       info(`Waiting for ${toolbox.currentToolId} to be reloaded…`);
686       await waitForCurrentPanelReload();
687       info(`→ panel reloaded`);
688     }
689   };
693  * Start watching for Responsive UI (RDM) reload to be completed:
694  * - watch for the Responsive UI's commands to be fully reloaded
695  * - watch for the Responsive UI's target switch to be done
696  */
697 async function _watchForResponsiveReload(
698   browser,
699   { isErrorPage, waitForLoad } = {}
700 ) {
701   const tab = gBrowser.getTabForBrowser(browser);
702   const ui = ResponsiveUIManager.getResponsiveUIForTab(tab);
704   if (!ui) {
705     // No responsive UI to wait for
706     return function () {};
707   }
709   const onResponsiveTargetSwitch = ui.once("responsive-ui-target-switch-done");
710   const waitForResponsiveCommandsReload = await watchForCommandsReload(
711     ui.commands,
712     { isErrorPage, waitForLoad }
713   );
714   const checkTargetSwitching = await watchForTargetSwitching(
715     ui.commands,
716     browser
717   );
719   return async function () {
720     const isTargetSwitching = checkTargetSwitching();
722     info(`Waiting for responsive ui commands to be reloaded…`);
723     await waitForResponsiveCommandsReload(isTargetSwitching);
725     if (isTargetSwitching) {
726       await onResponsiveTargetSwitch;
727     }
728   };
732  * Watch for the current panel selected in the provided toolbox to be reloaded.
733  * Some panels implement custom events that should be expected for every reload.
735  * Note about returning a method instead of a promise:
736  * In general this pattern is useful so that we can check if a target switch
737  * occurred or not, and decide which events to listen for. So far no panel is
738  * behaving differently whether there was a target switch or not. But to remain
739  * consistent with other watch* methods we still return a function here.
741  * @param {Toolbox}
742  *        The Toolbox instance which is going to experience a reload
743  * @return {function} An async method to be called and awaited after the reload
744  *         started. Will return `null` for panels which don't implement any
745  *         specific reload event.
746  */
747 function watchForCurrentPanelReload(toolbox) {
748   return _watchForPanelReload(toolbox, toolbox.currentToolId);
752  * Watch for all the panels loaded in the provided toolbox to be reloaded.
753  * Some panels implement custom events that should be expected for every reload.
755  * Note about returning a method instead of a promise:
756  * See comment for watchForCurrentPanelReload
758  * @param {Toolbox}
759  *        The Toolbox instance which is going to experience a reload
760  * @return {function} An async method to be called and awaited after the reload
761  *         started.
762  */
763 function watchForLoadedPanelsReload(toolbox) {
764   const waitForPanels = [];
765   for (const [id] of toolbox.getToolPanels()) {
766     // Store a watcher method for each panel already loaded.
767     waitForPanels.push(_watchForPanelReload(toolbox, id));
768   }
770   return function () {
771     return Promise.all(
772       waitForPanels.map(async watchPanel => {
773         // Wait for all panels to be reloaded.
774         if (watchPanel) {
775           await watchPanel();
776         }
777       })
778     );
779   };
782 function _watchForPanelReload(toolbox, toolId) {
783   const panel = toolbox.getPanel(toolId);
785   if (toolId == "inspector") {
786     const markuploaded = panel.once("markuploaded");
787     const onNewRoot = panel.once("new-root");
788     const onUpdated = panel.once("inspector-updated");
789     const onReloaded = panel.once("reloaded");
791     return async function () {
792       info("Waiting for markup view to load after navigation.");
793       await markuploaded;
795       info("Waiting for new root.");
796       await onNewRoot;
798       info("Waiting for inspector to update after new-root event.");
799       await onUpdated;
801       info("Waiting for inspector updates after page reload");
802       await onReloaded;
803     };
804   } else if (
805     ["netmonitor", "accessibility", "webconsole", "jsdebugger"].includes(toolId)
806   ) {
807     const onReloaded = panel.once("reloaded");
808     return async function () {
809       info(`Waiting for ${toolId} updates after page reload`);
810       await onReloaded;
811     };
812   }
813   return null;
817  * Watch for a Commands instance to be reloaded after a navigation.
819  * As for other navigation watch* methods, this should be called before the
820  * navigation starts, and the function it returns should be called after the
821  * navigation is done from a Browser point of view.
823  * !!! The wait function expects a `isTargetSwitching` argument to be provided,
824  * which needs to be monitored using watchForTargetSwitching !!!
825  */
826 async function watchForCommandsReload(
827   commands,
828   { isErrorPage = false, waitForLoad = true } = {}
829 ) {
830   // If we're switching origins, we need to wait for the 'switched-target'
831   // event to make sure everything is ready.
832   // Navigating from/to pages loaded in the parent process, like about:robots,
833   // also spawn new targets.
834   // (If target switching is disabled, the toolbox will reboot)
835   const onTargetSwitched = commands.targetCommand.once("switched-target");
837   // Wait until we received a page load resource:
838   // - dom-complete if we can wait for a full page load
839   // - dom-loading otherwise
840   // This allows to wait for page load for consumers calling directly
841   // waitForDevTools instead of navigateTo/reloadBrowser.
842   // This is also useful as an alternative to target switching, when no target
843   // switch is supposed to happen.
844   const waitForCompleteLoad = waitForLoad && !isErrorPage;
845   const documentEventName = waitForCompleteLoad
846     ? "dom-complete"
847     : "dom-loading";
849   const { onResource: onTopLevelDomEvent } =
850     await commands.resourceCommand.waitForNextResource(
851       commands.resourceCommand.TYPES.DOCUMENT_EVENT,
852       {
853         ignoreExistingResources: true,
854         predicate: resource =>
855           resource.targetFront.isTopLevel &&
856           resource.name === documentEventName,
857       }
858     );
860   return async function (isTargetSwitching) {
861     if (typeof isTargetSwitching === "undefined") {
862       throw new Error("isTargetSwitching was not provided to the wait method");
863     }
865     if (isTargetSwitching) {
866       info(`Waiting for target switch…`);
867       await onTargetSwitched;
868       info(`→ switched-target emitted`);
869     }
871     info(`Waiting for '${documentEventName}' resource…`);
872     await onTopLevelDomEvent;
873     info(`→ '${documentEventName}' resource emitted`);
875     return isTargetSwitching;
876   };
880  * Watch if an upcoming navigation will trigger a target switching, for the
881  * provided Commands instance and the provided Browser.
883  * As for other navigation watch* methods, this should be called before the
884  * navigation starts, and the function it returns should be called after the
885  * navigation is done from a Browser point of view.
886  */
887 async function watchForTargetSwitching(commands, browser) {
888   browser = browser || gBrowser.selectedBrowser;
889   const currentPID = browser.browsingContext.currentWindowGlobal.osPid;
890   const currentBrowsingContextID = browser.browsingContext.id;
892   // If the current top-level target follows the window global lifecycle, a
893   // target switch will occur regardless of process changes.
894   const targetFollowsWindowLifecycle =
895     commands.targetCommand.targetFront.targetForm.followWindowGlobalLifeCycle;
897   return function () {
898     // Compare the PIDs (and not the toolbox's targets) as PIDs are updated also immediately,
899     // while target may be updated slightly later.
900     const switchedProcess =
901       currentPID !== browser.browsingContext.currentWindowGlobal.osPid;
902     const switchedBrowsingContext =
903       currentBrowsingContextID !== browser.browsingContext.id;
905     return (
906       targetFollowsWindowLifecycle || switchedProcess || switchedBrowsingContext
907     );
908   };
912  * Create a Target for the provided tab and attach to it before resolving.
913  * This should only be used for tests which don't involve the frontend or a
914  * toolbox. Typically, retrieving the target and attaching to it should be
915  * handled at framework level when a Toolbox is used.
917  * @param {XULTab} tab
918  *        The tab for which a target should be created.
919  * @return {WindowGlobalTargetFront} The attached target front.
920  */
921 async function createAndAttachTargetForTab(tab) {
922   info("Creating and attaching to a local tab target");
924   const commands = await CommandsFactory.forTab(tab);
926   // Initialize the TargetCommands which require some async stuff to be done
927   // before being fully ready. This will define the `targetCommand.targetFront` attribute.
928   await commands.targetCommand.startListening();
930   const target = commands.targetCommand.targetFront;
931   return target;
934 function isFissionEnabled() {
935   return SpecialPowers.useRemoteSubframes;
938 function isEveryFrameTargetEnabled() {
939   return Services.prefs.getBoolPref(
940     "devtools.every-frame-target.enabled",
941     false
942   );
946  * Open the inspector in a tab with given URL.
947  * @param {string} url  The URL to open.
948  * @param {String} hostType Optional hostType, as defined in Toolbox.HostType
949  * @return A promise that is resolved once the tab and inspector have loaded
950  *         with an object: { tab, toolbox, inspector, highlighterTestFront }.
951  */
952 async function openInspectorForURL(url, hostType) {
953   const tab = await addTab(url);
954   const { inspector, toolbox, highlighterTestFront } = await openInspector(
955     hostType
956   );
957   return { tab, inspector, toolbox, highlighterTestFront };
960 function getActiveInspector() {
961   const toolbox = gDevTools.getToolboxForTab(gBrowser.selectedTab);
962   return toolbox.getPanel("inspector");
966  * Simulate a key event from an electron key shortcut string:
967  * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
969  * @param {String} key
970  * @param {DOMWindow} target
971  *        Optional window where to fire the key event
972  */
973 function synthesizeKeyShortcut(key, target) {
974   // parseElectronKey requires any window, just to access `KeyboardEvent`
975   const window = Services.appShell.hiddenDOMWindow;
976   const shortcut = KeyShortcuts.parseElectronKey(window, key);
977   const keyEvent = {
978     altKey: shortcut.alt,
979     ctrlKey: shortcut.ctrl,
980     metaKey: shortcut.meta,
981     shiftKey: shortcut.shift,
982   };
983   if (shortcut.keyCode) {
984     keyEvent.keyCode = shortcut.keyCode;
985   }
987   info("Synthesizing key shortcut: " + key);
988   EventUtils.synthesizeKey(shortcut.key || "", keyEvent, target);
991 var waitForTime = DevToolsUtils.waitForTime;
994  * Wait for a tick.
995  * @return {Promise}
996  */
997 function waitForTick() {
998   return new Promise(resolve => DevToolsUtils.executeSoon(resolve));
1002  * This shouldn't be used in the tests, but is useful when writing new tests or
1003  * debugging existing tests in order to introduce delays in the test steps
1005  * @param {Number} ms
1006  *        The time to wait
1007  * @return A promise that resolves when the time is passed
1008  */
1009 function wait(ms) {
1010   return new Promise(resolve => {
1011     setTimeout(resolve, ms);
1012     info("Waiting " + ms / 1000 + " seconds.");
1013   });
1017  * Wait for a predicate to return a result.
1019  * @param function condition
1020  *        Invoked once in a while until it returns a truthy value. This should be an
1021  *        idempotent function, since we have to run it a second time after it returns
1022  *        true in order to return the value.
1023  * @param string message [optional]
1024  *        A message to output if the condition fails.
1025  * @param number interval [optional]
1026  *        How often the predicate is invoked, in milliseconds.
1027  *        Can be set globally for a test via `waitFor.overrideIntervalForTestFile = someNumber;`.
1028  * @param number maxTries [optional]
1029  *        How many times the predicate is invoked before timing out.
1030  *        Can be set globally for a test via `waitFor.overrideMaxTriesForTestFile = someNumber;`.
1031  * @return object
1032  *         A promise that is resolved with the result of the condition.
1033  */
1034 async function waitFor(condition, message = "", interval = 10, maxTries = 500) {
1035   // Update interval & maxTries if overrides are defined on the waitFor object.
1036   interval =
1037     typeof waitFor.overrideIntervalForTestFile !== "undefined"
1038       ? waitFor.overrideIntervalForTestFile
1039       : interval;
1040   maxTries =
1041     typeof waitFor.overrideMaxTriesForTestFile !== "undefined"
1042       ? waitFor.overrideMaxTriesForTestFile
1043       : maxTries;
1045   try {
1046     const value = await BrowserTestUtils.waitForCondition(
1047       condition,
1048       message,
1049       interval,
1050       maxTries
1051     );
1052     return value;
1053   } catch (e) {
1054     const errorMessage = `Failed waitFor(): ${message} \nFailed condition: ${condition} \nException Message: ${e}`;
1055     throw new Error(errorMessage);
1056   }
1060  * Wait for eventName on target to be delivered a number of times.
1062  * @param {Object} target
1063  *        An observable object that either supports on/off or
1064  *        addEventListener/removeEventListener
1065  * @param {String} eventName
1066  * @param {Number} numTimes
1067  *        Number of deliveries to wait for.
1068  * @param {Boolean} useCapture
1069  *        Optional, for addEventListener/removeEventListener
1070  * @return A promise that resolves when the event has been handled
1071  */
1072 function waitForNEvents(target, eventName, numTimes, useCapture = false) {
1073   info("Waiting for event: '" + eventName + "' on " + target + ".");
1075   let count = 0;
1077   return new Promise(resolve => {
1078     for (const [add, remove] of [
1079       ["on", "off"],
1080       ["addEventListener", "removeEventListener"],
1081       ["addListener", "removeListener"],
1082       ["addMessageListener", "removeMessageListener"],
1083     ]) {
1084       if (add in target && remove in target) {
1085         target[add](
1086           eventName,
1087           function onEvent(...args) {
1088             if (typeof info === "function") {
1089               info("Got event: '" + eventName + "' on " + target + ".");
1090             }
1092             if (++count == numTimes) {
1093               target[remove](eventName, onEvent, useCapture);
1094               resolve(...args);
1095             }
1096           },
1097           useCapture
1098         );
1099         break;
1100       }
1101     }
1102   });
1106  * Wait for DOM change on target.
1108  * @param {Object} target
1109  *        The Node on which to observe DOM mutations.
1110  * @param {String} selector
1111  *        Given a selector to watch whether the expected element is changed
1112  *        on target.
1113  * @param {Number} expectedLength
1114  *        Optional, default set to 1
1115  *        There may be more than one element match an array match the selector,
1116  *        give an expected length to wait for more elements.
1117  * @return A promise that resolves when the event has been handled
1118  */
1119 function waitForDOM(target, selector, expectedLength = 1) {
1120   return new Promise(resolve => {
1121     const observer = new MutationObserver(mutations => {
1122       mutations.forEach(mutation => {
1123         const elements = mutation.target.querySelectorAll(selector);
1125         if (elements.length === expectedLength) {
1126           observer.disconnect();
1127           resolve(elements);
1128         }
1129       });
1130     });
1132     observer.observe(target, {
1133       attributes: true,
1134       childList: true,
1135       subtree: true,
1136     });
1137   });
1141  * Wait for eventName on target.
1143  * @param {Object} target
1144  *        An observable object that either supports on/off or
1145  *        addEventListener/removeEventListener
1146  * @param {String} eventName
1147  * @param {Boolean} useCapture
1148  *        Optional, for addEventListener/removeEventListener
1149  * @return A promise that resolves when the event has been handled
1150  */
1151 function once(target, eventName, useCapture = false) {
1152   return waitForNEvents(target, eventName, 1, useCapture);
1156  * Some tests may need to import one or more of the test helper scripts.
1157  * A test helper script is simply a js file that contains common test code that
1158  * is either not common-enough to be in head.js, or that is located in a
1159  * separate directory.
1160  * The script will be loaded synchronously and in the test's scope.
1161  * @param {String} filePath The file path, relative to the current directory.
1162  *                 Examples:
1163  *                 - "helper_attributes_test_runner.js"
1164  */
1165 function loadHelperScript(filePath) {
1166   const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
1167   Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
1171  * Open the toolbox in a given tab.
1172  * @param {XULNode} tab The tab the toolbox should be opened in.
1173  * @param {String} toolId Optional. The ID of the tool to be selected.
1174  * @param {String} hostType Optional. The type of toolbox host to be used.
1175  * @return {Promise} Resolves with the toolbox, when it has been opened.
1176  */
1177 async function openToolboxForTab(tab, toolId, hostType) {
1178   info("Opening the toolbox");
1180   // Check if the toolbox is already loaded.
1181   let toolbox = gDevTools.getToolboxForTab(tab);
1182   if (toolbox) {
1183     if (!toolId || (toolId && toolbox.getPanel(toolId))) {
1184       info("Toolbox is already opened");
1185       return toolbox;
1186     }
1187   }
1189   // If not, load it now.
1190   toolbox = await gDevTools.showToolboxForTab(tab, { toolId, hostType });
1192   // Make sure that the toolbox frame is focused.
1193   await new Promise(resolve => waitForFocus(resolve, toolbox.win));
1195   info("Toolbox opened and focused");
1197   return toolbox;
1201  * Add a new tab and open the toolbox in it.
1202  * @param {String} url The URL for the tab to be opened.
1203  * @param {String} toolId Optional. The ID of the tool to be selected.
1204  * @param {String} hostType Optional. The type of toolbox host to be used.
1205  * @return {Promise} Resolves when the tab has been added, loaded and the
1206  * toolbox has been opened. Resolves to the toolbox.
1207  */
1208 async function openNewTabAndToolbox(url, toolId, hostType) {
1209   const tab = await addTab(url);
1210   return openToolboxForTab(tab, toolId, hostType);
1214  * Close a tab and if necessary, the toolbox that belongs to it
1215  * @param {Tab} tab The tab to close.
1216  * @return {Promise} Resolves when the toolbox and tab have been destroyed and
1217  * closed.
1218  */
1219 async function closeTabAndToolbox(tab = gBrowser.selectedTab) {
1220   if (gDevTools.hasToolboxForTab(tab)) {
1221     await gDevTools.closeToolboxForTab(tab);
1222   }
1224   await removeTab(tab);
1226   await new Promise(resolve => setTimeout(resolve, 0));
1230  * Close a toolbox and the current tab.
1231  * @param {Toolbox} toolbox The toolbox to close.
1232  * @return {Promise} Resolves when the toolbox and tab have been destroyed and
1233  * closed.
1234  */
1235 async function closeToolboxAndTab(toolbox) {
1236   await toolbox.destroy();
1237   await removeTab(gBrowser.selectedTab);
1241  * Waits until a predicate returns true.
1243  * @param function predicate
1244  *        Invoked once in a while until it returns true.
1245  * @param number interval [optional]
1246  *        How often the predicate is invoked, in milliseconds.
1247  */
1248 function waitUntil(predicate, interval = 10) {
1249   if (predicate()) {
1250     return Promise.resolve(true);
1251   }
1252   return new Promise(resolve => {
1253     setTimeout(function () {
1254       waitUntil(predicate, interval).then(() => resolve(true));
1255     }, interval);
1256   });
1260  * Variant of waitUntil that accepts a predicate returning a promise.
1261  */
1262 async function asyncWaitUntil(predicate, interval = 10) {
1263   let success = await predicate();
1264   while (!success) {
1265     // Wait for X milliseconds.
1266     await new Promise(resolve => setTimeout(resolve, interval));
1267     // Test the predicate again.
1268     success = await predicate();
1269   }
1273  * Wait for a context menu popup to open.
1275  * @param Element popup
1276  *        The XUL popup you expect to open.
1277  * @param Element button
1278  *        The button/element that receives the contextmenu event. This is
1279  *        expected to open the popup.
1280  * @param function onShown
1281  *        Function to invoke on popupshown event.
1282  * @param function onHidden
1283  *        Function to invoke on popuphidden event.
1284  * @return object
1285  *         A Promise object that is resolved after the popuphidden event
1286  *         callback is invoked.
1287  */
1288 function waitForContextMenu(popup, button, onShown, onHidden) {
1289   return new Promise(resolve => {
1290     function onPopupShown() {
1291       info("onPopupShown");
1292       popup.removeEventListener("popupshown", onPopupShown);
1294       onShown && onShown();
1296       // Use executeSoon() to get out of the popupshown event.
1297       popup.addEventListener("popuphidden", onPopupHidden);
1298       DevToolsUtils.executeSoon(() => popup.hidePopup());
1299     }
1300     function onPopupHidden() {
1301       info("onPopupHidden");
1302       popup.removeEventListener("popuphidden", onPopupHidden);
1304       onHidden && onHidden();
1306       resolve(popup);
1307     }
1309     popup.addEventListener("popupshown", onPopupShown);
1311     info("wait for the context menu to open");
1312     synthesizeContextMenuEvent(button);
1313   });
1316 function synthesizeContextMenuEvent(el) {
1317   el.scrollIntoView();
1318   const eventDetails = { type: "contextmenu", button: 2 };
1319   EventUtils.synthesizeMouse(
1320     el,
1321     5,
1322     2,
1323     eventDetails,
1324     el.ownerDocument.defaultView
1325   );
1329  * Promise wrapper around SimpleTest.waitForClipboard
1330  */
1331 function waitForClipboardPromise(setup, expected) {
1332   return new Promise((resolve, reject) => {
1333     SimpleTest.waitForClipboard(expected, setup, resolve, reject);
1334   });
1338  * Simple helper to push a temporary preference. Wrapper on SpecialPowers
1339  * pushPrefEnv that returns a promise resolving when the preferences have been
1340  * updated.
1342  * @param {String} preferenceName
1343  *        The name of the preference to updated
1344  * @param {} value
1345  *        The preference value, type can vary
1346  * @return {Promise} resolves when the preferences have been updated
1347  */
1348 function pushPref(preferenceName, value) {
1349   const options = { set: [[preferenceName, value]] };
1350   return SpecialPowers.pushPrefEnv(options);
1353 async function closeToolbox() {
1354   await gDevTools.closeToolboxForTab(gBrowser.selectedTab);
1358  * Clean the logical clipboard content. This method only clears the OS clipboard on
1359  * Windows (see Bug 666254).
1360  */
1361 function emptyClipboard() {
1362   const clipboard = Services.clipboard;
1363   clipboard.emptyClipboard(clipboard.kGlobalClipboard);
1367  * Check if the current operating system is Windows.
1368  */
1369 function isWindows() {
1370   return Services.appinfo.OS === "WINNT";
1374  * Create an HTTP server that can be used to simulate custom requests within
1375  * a test.  It is automatically cleaned up when the test ends, so no need to
1376  * call `destroy`.
1378  * See https://developer.mozilla.org/en-US/docs/Httpd.js/HTTP_server_for_unit_tests
1379  * for more information about how to register handlers.
1381  * The server can be accessed like:
1383  *   const server = createTestHTTPServer();
1384  *   let url = "http://localhost: " + server.identity.primaryPort + "/path";
1385  * @returns {HttpServer}
1386  */
1387 function createTestHTTPServer() {
1388   const { HttpServer } = ChromeUtils.importESModule(
1389     "resource://testing-common/httpd.sys.mjs"
1390   );
1391   const server = new HttpServer();
1393   registerCleanupFunction(async function cleanup() {
1394     await new Promise(resolve => server.stop(resolve));
1395   });
1397   server.start(-1);
1398   return server;
1402  * Register an actor in the content process of the current tab.
1404  * Calling ActorRegistry.registerModule only registers the actor in the current process.
1405  * As all test scripts are ran in the parent process, it is only registered here.
1406  * This function helps register them in the content process used for the current tab.
1408  * @param {string} url
1409  *        Actor module URL or absolute require path
1410  * @param {json} options
1411  *        Arguments to be passed to DevToolsServer.registerModule
1412  */
1413 async function registerActorInContentProcess(url, options) {
1414   function convertChromeToFile(uri) {
1415     return Cc["@mozilla.org/chrome/chrome-registry;1"]
1416       .getService(Ci.nsIChromeRegistry)
1417       .convertChromeURL(Services.io.newURI(uri)).spec;
1418   }
1419   // chrome://mochitests URI is registered only in the parent process, so convert these
1420   // URLs to file:// one in order to work in the content processes
1421   url = url.startsWith("chrome://mochitests") ? convertChromeToFile(url) : url;
1422   return SpecialPowers.spawn(
1423     gBrowser.selectedBrowser,
1424     [{ url, options }],
1425     args => {
1426       // eslint-disable-next-line no-shadow
1427       const { require } = ChromeUtils.importESModule(
1428         "resource://devtools/shared/loader/Loader.sys.mjs"
1429       );
1430       const {
1431         ActorRegistry,
1432       } = require("resource://devtools/server/actors/utils/actor-registry.js");
1433       ActorRegistry.registerModule(args.url, args.options);
1434     }
1435   );
1439  * Move the provided Window to the provided left, top coordinates and wait for
1440  * the window position to be updated.
1441  */
1442 async function moveWindowTo(win, left, top) {
1443   // Check that the expected coordinates are within the window available area.
1444   left = Math.max(win.screen.availLeft, left);
1445   left = Math.min(win.screen.width, left);
1446   top = Math.max(win.screen.availTop, top);
1447   top = Math.min(win.screen.height, top);
1449   info(`Moving window to {${left}, ${top}}`);
1450   win.moveTo(left, top);
1452   // Bug 1600809: window move/resize can be async on Linux sometimes.
1453   // Wait so that the anchor's position is correctly measured.
1454   return waitUntil(() => {
1455     info(
1456       `Wait for window screenLeft and screenTop to be updated: (${win.screenLeft}, ${win.screenTop})`
1457     );
1458     return win.screenLeft === left && win.screenTop === top;
1459   });
1462 function getCurrentTestFilePath() {
1463   return gTestPath.replace("chrome://mochitests/content/browser/", "");
1467  * Unregister all registered service workers.
1469  * @param {DevToolsClient} client
1470  */
1471 async function unregisterAllServiceWorkers(client) {
1472   info("Wait until all workers have a valid registrationFront");
1473   let workers;
1474   await asyncWaitUntil(async function () {
1475     workers = await client.mainRoot.listAllWorkers();
1476     const allWorkersRegistered = workers.service.every(
1477       worker => !!worker.registrationFront
1478     );
1479     return allWorkersRegistered;
1480   });
1482   info("Unregister all service workers");
1483   const promises = [];
1484   for (const worker of workers.service) {
1485     promises.push(worker.registrationFront.unregister());
1486   }
1487   await Promise.all(promises);
1490 /**********************
1491  * Screenshot helpers *
1492  **********************/
1495  * Returns an object containing the r,g and b colors of the provided image at
1496  * the passed position
1498  * @param {Image} image
1499  * @param {Int} x
1500  * @param {Int} y
1501  * @returns Object with the following properties:
1502  *           - {Int} r: The red component of the pixel
1503  *           - {Int} g: The green component of the pixel
1504  *           - {Int} b: The blue component of the pixel
1505  */
1506 function colorAt(image, x, y) {
1507   // Create a test canvas element.
1508   const HTML_NS = "http://www.w3.org/1999/xhtml";
1509   const canvas = document.createElementNS(HTML_NS, "canvas");
1510   canvas.width = image.width;
1511   canvas.height = image.height;
1513   // Draw the image in the canvas
1514   const context = canvas.getContext("2d");
1515   context.drawImage(image, 0, 0, image.width, image.height);
1517   // Return the color found at the provided x,y coordinates as a "r, g, b" string.
1518   const [r, g, b] = context.getImageData(x, y, 1, 1).data;
1519   return { r, g, b };
1522 let allDownloads = [];
1524  * Returns a Promise that resolves when a new screenshot is available in the download folder.
1526  * @param {Object} [options]
1527  * @param {Boolean} options.isWindowPrivate: Set to true if the window from which the screenshot
1528  *                  is taken is a private window. This will ensure that we check that the
1529  *                  screenshot appears in the private window, not the non-private one (See Bug 1783373)
1530  */
1531 async function waitUntilScreenshot({ isWindowPrivate = false } = {}) {
1532   const { Downloads } = ChromeUtils.importESModule(
1533     "resource://gre/modules/Downloads.sys.mjs"
1534   );
1535   const list = await Downloads.getList(Downloads.ALL);
1537   return new Promise(function (resolve) {
1538     const view = {
1539       onDownloadAdded: async download => {
1540         await download.whenSucceeded();
1541         if (allDownloads.includes(download)) {
1542           return;
1543         }
1545         is(
1546           !!download.source.isPrivate,
1547           isWindowPrivate,
1548           `The download occured in the expected${
1549             isWindowPrivate ? " private" : ""
1550           } window`
1551         );
1553         allDownloads.push(download);
1554         resolve(download.target.path);
1555         list.removeView(view);
1556       },
1557     };
1559     list.addView(view);
1560   });
1564  * Clear all the download references.
1565  */
1566 async function resetDownloads() {
1567   info("Reset downloads");
1568   const { Downloads } = ChromeUtils.importESModule(
1569     "resource://gre/modules/Downloads.sys.mjs"
1570   );
1571   const downloadList = await Downloads.getList(Downloads.ALL);
1572   const downloads = await downloadList.getAll();
1573   for (const download of downloads) {
1574     downloadList.remove(download);
1575     await download.finalize(true);
1576   }
1577   allDownloads = [];
1581  * Return a screenshot of the currently selected node in the inspector (using the internal
1582  * Inspector#screenshotNode method).
1584  * @param {Inspector} inspector
1585  * @returns {Image}
1586  */
1587 async function takeNodeScreenshot(inspector) {
1588   // Cleanup all downloads at the end of the test.
1589   registerCleanupFunction(resetDownloads);
1591   info(
1592     "Call screenshotNode() and wait until the screenshot is found in the Downloads"
1593   );
1594   const whenScreenshotSucceeded = waitUntilScreenshot();
1595   inspector.screenshotNode();
1596   const filePath = await whenScreenshotSucceeded;
1598   info("Create an image using the downloaded fileas source");
1599   const image = new Image();
1600   const onImageLoad = once(image, "load");
1601   image.src = PathUtils.toFileURI(filePath);
1602   await onImageLoad;
1604   info("Remove the downloaded screenshot file");
1605   await IOUtils.remove(filePath);
1607   // See intermittent Bug 1508435. Even after removing the file, tests still manage to
1608   // reuse files from the previous test if they have the same name. Since our file name
1609   // is based on a timestamp that has "second" precision, wait for one second to make sure
1610   // screenshots will have different names.
1611   info(
1612     "Wait for one second to make sure future screenshots will use a different name"
1613   );
1614   await new Promise(r => setTimeout(r, 1000));
1616   return image;
1620  * Check that the provided image has the expected width, height, and color.
1621  * NOTE: This test assumes that the image is only made of a single color and will only
1622  * check one pixel.
1623  */
1624 async function assertSingleColorScreenshotImage(
1625   image,
1626   width,
1627   height,
1628   { r, g, b }
1629 ) {
1630   info(`Assert ${image.src} content`);
1631   const ratio = await SpecialPowers.spawn(
1632     gBrowser.selectedBrowser,
1633     [],
1634     () => content.wrappedJSObject.devicePixelRatio
1635   );
1637   is(
1638     image.width,
1639     ratio * width,
1640     `node screenshot has the expected width (dpr = ${ratio})`
1641   );
1642   is(
1643     image.height,
1644     height * ratio,
1645     `node screenshot has the expected height (dpr = ${ratio})`
1646   );
1648   const color = colorAt(image, 0, 0);
1649   is(color.r, r, "node screenshot has the expected red component");
1650   is(color.g, g, "node screenshot has the expected green component");
1651   is(color.b, b, "node screenshot has the expected blue component");
1655  * Check that the provided image has the expected color at a given position
1656  */
1657 function checkImageColorAt({ image, x = 0, y, expectedColor, label }) {
1658   const color = colorAt(image, x, y);
1659   is(`rgb(${Object.values(color).join(", ")})`, expectedColor, label);
1663  * Wait until the store has reached a state that matches the predicate.
1664  * @param Store store
1665  *        The Redux store being used.
1666  * @param function predicate
1667  *        A function that returns true when the store has reached the expected
1668  *        state.
1669  * @return Promise
1670  *         Resolved once the store reaches the expected state.
1671  */
1672 function waitUntilState(store, predicate) {
1673   return new Promise(resolve => {
1674     const unsubscribe = store.subscribe(check);
1676     info(`Waiting for state predicate "${predicate}"`);
1677     function check() {
1678       if (predicate(store.getState())) {
1679         info(`Found state predicate "${predicate}"`);
1680         unsubscribe();
1681         resolve();
1682       }
1683     }
1685     // Fire the check immediately in case the action has already occurred
1686     check();
1687   });
1691  * Wait for a specific action type to be dispatched.
1693  * If the action is async and defines a `status` property, this helper will wait
1694  * for the status to reach either "error" or "done".
1696  * @param {Object} store
1697  *        Redux store where the action should be dispatched.
1698  * @param {String} actionType
1699  *        The actionType to wait for.
1700  * @param {Number} repeat
1701  *        Optional, number of time the action is expected to be dispatched.
1702  *        Defaults to 1
1703  * @return {Promise}
1704  */
1705 function waitForDispatch(store, actionType, repeat = 1) {
1706   let count = 0;
1707   return new Promise(resolve => {
1708     store.dispatch({
1709       type: "@@service/waitUntil",
1710       predicate: action => {
1711         const isDone =
1712           !action.status ||
1713           action.status === "done" ||
1714           action.status === "error";
1716         if (action.type === actionType && isDone && ++count == repeat) {
1717           return true;
1718         }
1720         return false;
1721       },
1722       run: (dispatch, getState, action) => {
1723         resolve(action);
1724       },
1725     });
1726   });
1730  * Retrieve a browsing context in nested frames.
1732  * @param {BrowsingContext|XULBrowser} browsingContext
1733  *        The topmost browsing context under which we should search for the
1734  *        browsing context.
1735  * @param {Array<String>} selectors
1736  *        Array of CSS selectors that form a path to a specific nested frame.
1737  * @return {BrowsingContext} The nested browsing context.
1738  */
1739 async function getBrowsingContextInFrames(browsingContext, selectors) {
1740   let context = browsingContext;
1742   if (!Array.isArray(selectors)) {
1743     throw new Error(
1744       "getBrowsingContextInFrames called with an invalid selectors argument"
1745     );
1746   }
1748   if (selectors.length === 0) {
1749     throw new Error(
1750       "getBrowsingContextInFrames called with an empty selectors array"
1751     );
1752   }
1754   const clonedSelectors = [...selectors];
1755   while (clonedSelectors.length) {
1756     const selector = clonedSelectors.shift();
1757     context = await SpecialPowers.spawn(context, [selector], _selector => {
1758       return content.document.querySelector(_selector).browsingContext;
1759     });
1760   }
1762   return context;
1766  * Synthesize a mouse event on an element, after ensuring that it is visible
1767  * in the viewport.
1769  * @param {String|Array} selector: The node selector to get the node target for the event.
1770  *        To target an element in a specific iframe, pass an array of CSS selectors
1771  *        (e.g. ["iframe", ".el-in-iframe"])
1772  * @param {number} x
1773  * @param {number} y
1774  * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse
1775  */
1776 async function safeSynthesizeMouseEventInContentPage(
1777   selector,
1778   x,
1779   y,
1780   options = {}
1781 ) {
1782   let context = gBrowser.selectedBrowser.browsingContext;
1784   // If an array of selector is passed, we need to retrieve the context in which the node
1785   // lives in.
1786   if (Array.isArray(selector)) {
1787     if (selector.length === 1) {
1788       selector = selector[0];
1789     } else {
1790       context = await getBrowsingContextInFrames(
1791         context,
1792         // only pass the iframe path
1793         selector.slice(0, -1)
1794       );
1795       // retrieve the last item of the selector, which should be the one for the node we want.
1796       selector = selector.at(-1);
1797     }
1798   }
1800   await scrollContentPageNodeIntoView(context, selector);
1801   BrowserTestUtils.synthesizeMouse(selector, x, y, options, context);
1805  * Synthesize a mouse event at the center of an element, after ensuring that it is visible
1806  * in the viewport.
1808  * @param {String|Array} selector: The node selector to get the node target for the event.
1809  *        To target an element in a specific iframe, pass an array of CSS selectors
1810  *        (e.g. ["iframe", ".el-in-iframe"])
1811  * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse
1812  */
1813 async function safeSynthesizeMouseEventAtCenterInContentPage(
1814   selector,
1815   options = {}
1816 ) {
1817   let context = gBrowser.selectedBrowser.browsingContext;
1819   // If an array of selector is passed, we need to retrieve the context in which the node
1820   // lives in.
1821   if (Array.isArray(selector)) {
1822     if (selector.length === 1) {
1823       selector = selector[0];
1824     } else {
1825       context = await getBrowsingContextInFrames(
1826         context,
1827         // only pass the iframe path
1828         selector.slice(0, -1)
1829       );
1830       // retrieve the last item of the selector, which should be the one for the node we want.
1831       selector = selector.at(-1);
1832     }
1833   }
1835   await scrollContentPageNodeIntoView(context, selector);
1836   BrowserTestUtils.synthesizeMouseAtCenter(selector, options, context);
1840  * Scroll into view an element in the content page matching the passed selector
1842  * @param {BrowsingContext} browsingContext: The browsing context the element lives in.
1843  * @param {String} selector: The node selector to get the node to scroll into view
1844  * @returns {Promise}
1845  */
1846 function scrollContentPageNodeIntoView(browsingContext, selector) {
1847   return SpecialPowers.spawn(
1848     browsingContext,
1849     [selector],
1850     function (innerSelector) {
1851       const node =
1852         content.wrappedJSObject.document.querySelector(innerSelector);
1853       node.scrollIntoView();
1854     }
1855   );
1859  * Change the zoom level of the selected page.
1861  * @param {Number} zoomLevel
1862  */
1863 function setContentPageZoomLevel(zoomLevel) {
1864   gBrowser.selectedBrowser.fullZoom = zoomLevel;
1868  * Wait for the next DOCUMENT_EVENT dom-complete resource on a top-level target
1870  * @param {Object} commands
1871  * @return {Promise<Object>}
1872  *         Return a promise which resolves once we fully settle the resource listener.
1873  *         You should await for its resolution before doing the action which may fire
1874  *         your resource.
1875  *         This promise will resolve with an object containing a `onDomCompleteResource` property,
1876  *         which is also a promise, that will resolve once a "top-level" DOCUMENT_EVENT dom-complete
1877  *         is received.
1878  */
1879 async function waitForNextTopLevelDomCompleteResource(commands) {
1880   const { onResource: onDomCompleteResource } =
1881     await commands.resourceCommand.waitForNextResource(
1882       commands.resourceCommand.TYPES.DOCUMENT_EVENT,
1883       {
1884         ignoreExistingResources: true,
1885         predicate: resource =>
1886           resource.name === "dom-complete" && resource.targetFront.isTopLevel,
1887       }
1888     );
1889   return { onDomCompleteResource };
1893  * Wait for the provided context to have a valid presShell. This can be useful
1894  * for tests which try to create popup panels or interact with the document very
1895  * early.
1897  * @param {BrowsingContext} context
1898  **/
1899 function waitForPresShell(context) {
1900   return SpecialPowers.spawn(context, [], async () => {
1901     const winUtils = SpecialPowers.getDOMWindowUtils(content);
1902     await ContentTaskUtils.waitForCondition(() => {
1903       try {
1904         return !!winUtils.getPresShellId();
1905       } catch (e) {
1906         return false;
1907       }
1908     }, "Waiting for a valid presShell");
1909   });
1913  * In tests using Fluent localization, it is preferable to match DOM elements using
1914  * a message ID rather than the raw string as:
1916  *  1. It allows testing infrastructure to be multilingual if needed.
1917  *  2. It isolates the tests from localization changes.
1919  * @param {Array<string>} resourceIds A list of .ftl files to load.
1920  * @returns {(id: string, args?: Record<string, FluentVariable>) => string}
1921  */
1922 async function getFluentStringHelper(resourceIds) {
1923   const locales = Services.locale.appLocalesAsBCP47;
1924   const generator = L10nRegistry.getInstance().generateBundles(
1925     locales,
1926     resourceIds
1927   );
1929   const bundles = [];
1930   for await (const bundle of generator) {
1931     bundles.push(bundle);
1932   }
1934   const reactLocalization = new FluentReact.ReactLocalization(bundles);
1936   /**
1937    * Get the string from a message id. It throws when the message is not found.
1938    *
1939    * @param {string} id
1940    * @param {string} attributeName: attribute name if you need to access a specific attribute
1941    *                 defined in the fluent string, e.g. setting "title" for this param
1942    *                 will retrieve the `title` string in
1943    *                    compatibility-issue-browsers-list =
1944    *                      .title = This is the title
1945    * @param {Record<string, FluentVariable>} [args] optional
1946    * @returns {string}
1947    */
1948   return (id, attributeName, args) => {
1949     let string;
1951     if (!attributeName) {
1952       string = reactLocalization.getString(id, args);
1953     } else {
1954       for (const bundle of reactLocalization.bundles) {
1955         const msg = bundle.getMessage(id);
1956         if (msg?.attributes[attributeName]) {
1957           string = bundle.formatPattern(
1958             msg.attributes[attributeName],
1959             args,
1960             []
1961           );
1962           break;
1963         }
1964       }
1965     }
1967     if (!string) {
1968       throw new Error(
1969         `Could not find a string for "${id}"${
1970           attributeName ? ` and attribute "${attributeName}")` : ""
1971         }. Was the correct resource bundle loaded?`
1972       );
1973     }
1974     return string;
1975   };
1979  * Open responsive design mode for the given tab.
1980  */
1981 async function openRDM(tab, { waitForDeviceList = true } = {}) {
1982   info("Opening responsive design mode");
1983   const manager = ResponsiveUIManager;
1984   const ui = await manager.openIfNeeded(tab.ownerGlobal, tab, {
1985     trigger: "test",
1986   });
1987   info("Responsive design mode opened");
1989   await ResponsiveMessageHelper.wait(ui.toolWindow, "post-init");
1990   info("Responsive design initialized");
1992   await waitForRDMLoaded(ui, { waitForDeviceList });
1994   return { ui, manager };
1997 async function waitForRDMLoaded(ui, { waitForDeviceList = true } = {}) {
1998   // Always wait for the viewport to be added.
1999   const { store } = ui.toolWindow;
2000   await waitUntilState(store, state => state.viewports.length == 1);
2002   if (waitForDeviceList) {
2003     // Wait until the device list has been loaded.
2004     await waitUntilState(
2005       store,
2006       state => state.devices.listState == localTypes.loadableState.LOADED
2007     );
2008   }
2012  * Close responsive design mode for the given tab.
2013  */
2014 async function closeRDM(tab, options) {
2015   info("Closing responsive design mode");
2016   const manager = ResponsiveUIManager;
2017   await manager.closeIfNeeded(tab.ownerGlobal, tab, options);
2018   info("Responsive design mode closed");
2021 function getInputStream(data) {
2022   const BufferStream = Components.Constructor(
2023     "@mozilla.org/io/arraybuffer-input-stream;1",
2024     "nsIArrayBufferInputStream",
2025     "setData"
2026   );
2027   const buffer = new TextEncoder().encode(data).buffer;
2028   return new BufferStream(buffer, 0, buffer.byteLength);
2032  * Wait for a specific target to have been fully processed by targetCommand.
2034  * @param {Commands} commands
2035  *        The commands instance
2036  * @param {Function} isExpectedTargetFn
2037  *        Predicate which will be called with a target front argument. Should
2038  *        return true if the target front is the expected one, false otherwise.
2039  * @return {Promise}
2040  *         Promise which resolves when a target matching `isExpectedTargetFn`
2041  *         has been processed by targetCommand.
2042  */
2043 function waitForTargetProcessed(commands, isExpectedTargetFn) {
2044   return new Promise(resolve => {
2045     const onProcessed = targetFront => {
2046       try {
2047         if (isExpectedTargetFn(targetFront)) {
2048           commands.targetCommand.off("processed-available-target", onProcessed);
2049           resolve();
2050         }
2051       } catch {
2052         // Ignore errors from isExpectedTargetFn.
2053       }
2054     };
2056     commands.targetCommand.on("processed-available-target", onProcessed);
2057   });
2061  * Instantiate a HTTP Server that serves files from a given test folder.
2062  * The test folder should be made of multiple sub folder named: v1, v2, v3,...
2063  * We will serve the content from one of these sub folder
2064  * and switch to the next one, each time `httpServer.switchToNextVersion()`
2065  * is called.
2067  * @return Object Test server with two functions:
2068  *   - urlFor(path)
2069  *     Returns the absolute url for a given file.
2070  *   - switchToNextVersion()
2071  *     Start serving files from the next available sub folder.
2072  *   - backToFirstVersion()
2073  *     When running more than one test, helps restart from the first folder.
2074  */
2075 function createVersionizedHttpTestServer(testFolderName) {
2076   const httpServer = createTestHTTPServer();
2078   let currentVersion = 1;
2080   httpServer.registerPrefixHandler("/", async (request, response) => {
2081     response.processAsync();
2082     response.setStatusLine(request.httpVersion, 200, "OK");
2083     if (request.path.endsWith(".js")) {
2084       response.setHeader("Content-Type", "application/javascript");
2085     } else if (request.path.endsWith(".js.map")) {
2086       response.setHeader("Content-Type", "application/json");
2087     }
2088     if (request.path == "/" || request.path.endsWith(".html")) {
2089       response.setHeader("Content-Type", "text/html");
2090     }
2091     // If a query string is passed, lookup with a matching file, if available
2092     // The '?' is replaced by '.'
2093     let fetchResponse;
2095     if (request.queryString) {
2096       const url = `${URL_ROOT_SSL}${testFolderName}/v${currentVersion}${request.path}.${request.queryString}`;
2097       try {
2098         fetchResponse = await fetch(url);
2099         // Log this only if the request succeed
2100         info(`[test-http-server] serving: ${url}`);
2101       } catch (e) {
2102         // Ignore any error and proceed without the query string
2103         fetchResponse = null;
2104       }
2105     }
2107     if (!fetchResponse) {
2108       const url = `${URL_ROOT_SSL}${testFolderName}/v${currentVersion}${request.path}`;
2109       info(`[test-http-server] serving: ${url}`);
2110       fetchResponse = await fetch(url);
2111     }
2113     // Ensure forwarding the response headers generated by the other http server
2114     // (this can be especially useful when query .sjs files)
2115     for (const [name, value] of fetchResponse.headers.entries()) {
2116       response.setHeader(name, value);
2117     }
2119     // Override cache settings so that versionized requests are never cached
2120     // and we get brand new content for any request.
2121     response.setHeader("Cache-Control", "no-store");
2123     const text = await fetchResponse.text();
2124     response.write(text);
2125     response.finish();
2126   });
2128   return {
2129     switchToNextVersion() {
2130       currentVersion++;
2131     },
2132     backToFirstVersion() {
2133       currentVersion = 1;
2134     },
2135     urlFor(path) {
2136       const port = httpServer.identity.primaryPort;
2137       return `http://localhost:${port}/${path}`;
2138     },
2139   };
2143  * Fake clicking a link and return the URL we would have navigated to.
2144  * This function should be used to check external links since we can't access
2145  * network in tests.
2146  * This can also be used to test that a click will not be fired.
2148  * @param ElementNode element
2149  *        The <a> element we want to simulate click on.
2150  * @returns Promise
2151  *          A Promise that is resolved when the link click simulation occured or
2152  *          when the click is not dispatched.
2153  *          The promise resolves with an object that holds the following properties
2154  *          - link: url of the link or null(if event not fired)
2155  *          - where: "tab" if tab is active or "tabshifted" if tab is inactive
2156  *            or null(if event not fired)
2157  */
2158 function simulateLinkClick(element) {
2159   const browserWindow = Services.wm.getMostRecentWindow(
2160     gDevTools.chromeWindowType
2161   );
2163   const onOpenLink = new Promise(resolve => {
2164     const openLinkIn = (link, where) => resolve({ link, where });
2165     sinon.replace(browserWindow, "openTrustedLinkIn", openLinkIn);
2166     sinon.replace(browserWindow, "openWebLinkIn", openLinkIn);
2167   });
2169   element.click();
2171   // Declare a timeout Promise that we can use to make sure spied methods were not called.
2172   const onTimeout = new Promise(function (resolve) {
2173     setTimeout(() => {
2174       resolve({ link: null, where: null });
2175     }, 1000);
2176   });
2178   const raceResult = Promise.race([onOpenLink, onTimeout]);
2179   sinon.restore();
2180   return raceResult;
2184  * Since the MDN data is updated frequently, it might happen that the properties used in
2185  * this test are not in the dataset anymore/now have URLs.
2186  * This function will return properties in the dataset that don't have MDN url so you
2187  * can easily find a replacement.
2188  */
2189 function logCssCompatDataPropertiesWithoutMDNUrl() {
2190   const cssPropertiesCompatData = require("resource://devtools/shared/compatibility/dataset/css-properties.json");
2192   function walk(node) {
2193     for (const propertyName in node) {
2194       const property = node[propertyName];
2195       if (property.__compat) {
2196         if (!property.__compat.mdn_url) {
2197           dump(
2198             `"${propertyName}" - MDN URL: ${
2199               property.__compat.mdn_url || "❌"
2200             } - Spec URL: ${property.__compat.spec_url || "❌"}\n`
2201           );
2202         }
2203       } else if (typeof property == "object") {
2204         walk(property);
2205       }
2206     }
2207   }
2208   walk(cssPropertiesCompatData);
2212  * Craft a CssProperties instance without involving RDP for tests
2213  * manually spawning OutputParser, CssCompleter, Editor...
2215  * Otherwise this should instead be fetched from CssPropertiesFront.
2217  * @return {CssProperties}
2218  */
2219 function getClientCssProperties() {
2220   const {
2221     generateCssProperties,
2222   } = require("resource://devtools/server/actors/css-properties.js");
2223   const {
2224     CssProperties,
2225     normalizeCssData,
2226   } = require("resource://devtools/client/fronts/css-properties.js");
2227   return new CssProperties(
2228     normalizeCssData({ properties: generateCssProperties() })
2229   );