Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / devtools / client / shared / test / shared-head.js
blob0657ede75e0b56e325ca59572683678cc8fabe5c
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 // When DEBUG_STEP environment variable is set,
58 // automatically start a tracer which will log all line being executed
59 // in the running test (and nothing else) and also pause its execution
60 // for the given amount of milliseconds.
62 // Be careful that these pause have significant side effect.
63 // This will pause the test script event loop and allow running the other
64 // tasks queued in the parent process's main thread event loop queue.
66 // Passing any non-number value, like `DEBUG_STEP=true` will still
67 // log the executed lines without any pause, and without this side effect.
69 // For now, the tracer can only work once per thread.
70 // So when using this feature you will not be able to use the JS tracer
71 // in any other way on parent process's main thread.
72 const DEBUG_STEP = Services.env.get("DEBUG_STEP");
73 if (DEBUG_STEP) {
74   // Use a custom loader with `invisibleToDebugger` flag for the allocation tracker
75   // as it instantiates custom Debugger API instances and has to be running in a distinct
76   // compartments from DevTools and system scopes (JSMs, XPCOM,...)
77   const {
78     useDistinctSystemPrincipalLoader,
79     releaseDistinctSystemPrincipalLoader,
80   } = ChromeUtils.importESModule(
81     "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
82   );
83   const requester = {};
84   const loader = useDistinctSystemPrincipalLoader(requester);
86   const stepper = loader.require(
87     "resource://devtools/shared/test-helpers/test-stepper.js"
88   );
89   stepper.start(globalThis, gTestPath, DEBUG_STEP);
90   registerCleanupFunction(() => {
91     stepper.stop();
92     releaseDistinctSystemPrincipalLoader(requester);
93   });
96 const DEBUG_TRACE_LINE = Services.env.get("DEBUG_TRACE_LINE");
97 if (DEBUG_TRACE_LINE) {
98   // Use a custom loader with `invisibleToDebugger` flag for the allocation tracker
99   // as it instantiates custom Debugger API instances and has to be running in a distinct
100   // compartments from DevTools and system scopes (ESMs, XPCOM,...)
101   const {
102     useDistinctSystemPrincipalLoader,
103     releaseDistinctSystemPrincipalLoader,
104   } = ChromeUtils.importESModule(
105     "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
106   );
107   const requester = {};
108   const loader = useDistinctSystemPrincipalLoader(requester);
110   const lineTracer = loader.require(
111     "resource://devtools/shared/test-helpers/test-line-tracer.js"
112   );
113   lineTracer.start(globalThis, gTestPath, DEBUG_TRACE_LINE);
114   registerCleanupFunction(() => {
115     lineTracer.stop();
116     releaseDistinctSystemPrincipalLoader(requester);
117   });
120 const { loader, require } = ChromeUtils.importESModule(
121   "resource://devtools/shared/loader/Loader.sys.mjs"
123 const { sinon } = ChromeUtils.importESModule(
124   "resource://testing-common/Sinon.sys.mjs"
127 // When loaded from xpcshell test, this file is loaded via xpcshell.ini's head property
128 // and so it loaded first before anything else and isn't having access to Services global.
129 // Whereas many head.js files from mochitest import this file via loadSubScript
130 // and already expose Services as a global.
132 const {
133   gDevTools,
134 } = require("resource://devtools/client/framework/devtools.js");
135 const {
136   CommandsFactory,
137 } = require("resource://devtools/shared/commands/commands-factory.js");
138 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
140 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
142 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
144 loader.lazyRequireGetter(
145   this,
146   "ResponsiveUIManager",
147   "resource://devtools/client/responsive/manager.js"
149 loader.lazyRequireGetter(
150   this,
151   "localTypes",
152   "resource://devtools/client/responsive/types.js"
154 loader.lazyRequireGetter(
155   this,
156   "ResponsiveMessageHelper",
157   "resource://devtools/client/responsive/utils/message.js"
160 loader.lazyRequireGetter(
161   this,
162   "FluentReact",
163   "resource://devtools/client/shared/vendor/fluent-react.js"
166 const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
167 const CHROME_URL_ROOT = TEST_DIR + "/";
168 const URL_ROOT = CHROME_URL_ROOT.replace(
169   "chrome://mochitests/content/",
170   "http://example.com/"
172 const URL_ROOT_SSL = CHROME_URL_ROOT.replace(
173   "chrome://mochitests/content/",
174   "https://example.com/"
177 // Add aliases which make it more explicit that URL_ROOT uses a com TLD.
178 const URL_ROOT_COM = URL_ROOT;
179 const URL_ROOT_COM_SSL = URL_ROOT_SSL;
181 // Also expose http://example.org, http://example.net, https://example.org to
182 // test Fission scenarios easily.
183 // Note: example.net is not available for https.
184 const URL_ROOT_ORG = CHROME_URL_ROOT.replace(
185   "chrome://mochitests/content/",
186   "http://example.org/"
188 const URL_ROOT_ORG_SSL = CHROME_URL_ROOT.replace(
189   "chrome://mochitests/content/",
190   "https://example.org/"
192 const URL_ROOT_NET = CHROME_URL_ROOT.replace(
193   "chrome://mochitests/content/",
194   "http://example.net/"
196 const URL_ROOT_NET_SSL = CHROME_URL_ROOT.replace(
197   "chrome://mochitests/content/",
198   "https://example.net/"
200 // mochi.test:8888 is the actual primary location where files are served.
201 const URL_ROOT_MOCHI_8888 = CHROME_URL_ROOT.replace(
202   "chrome://mochitests/content/",
203   "http://mochi.test:8888/"
206 try {
207   if (isMochitest) {
208     Services.scriptloader.loadSubScript(
209       "chrome://mochitests/content/browser/devtools/client/shared/test/telemetry-test-helpers.js",
210       this
211     );
212   }
213 } catch (e) {
214   ok(
215     false,
216     "MISSING DEPENDENCY ON telemetry-test-helpers.js\n" +
217       "Please add the following line in browser.ini:\n" +
218       "  !/devtools/client/shared/test/telemetry-test-helpers.js\n"
219   );
220   throw e;
223 // Force devtools to be initialized so menu items and keyboard shortcuts get installed
224 require("resource://devtools/client/framework/devtools-browser.js");
226 // All tests are asynchronous
227 if (isMochitest) {
228   waitForExplicitFinish();
231 var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
233 registerCleanupFunction(function () {
234   if (
235     DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT
236   ) {
237     ok(
238       false,
239       "Should have had the expected number of DevToolsUtils.assert() failures." +
240         " Expected " +
241         EXPECTED_DTU_ASSERT_FAILURE_COUNT +
242         ", got " +
243         DevToolsUtils.assertionFailureCount
244     );
245   }
248 // Uncomment this pref to dump all devtools emitted events to the console.
249 // Services.prefs.setBoolPref("devtools.dump.emit", true);
252  * Watch console messages for failed propType definitions in React components.
253  */
254 function onConsoleMessage(subject) {
255   const message = subject.wrappedJSObject.arguments[0];
257   if (message && /Failed propType/.test(message.toString())) {
258     ok(false, message);
259   }
262 const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(
263   Ci.nsIConsoleAPIStorage
266 ConsoleAPIStorage.addLogEventListener(
267   onConsoleMessage,
268   Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
270 registerCleanupFunction(() => {
271   ConsoleAPIStorage.removeLogEventListener(onConsoleMessage);
274 Services.prefs.setBoolPref("devtools.inspector.three-pane-enabled", true);
276 // Disable this preference to reduce exceptions related to pending `listWorkers`
277 // requests occuring after a process is created/destroyed. See Bug 1620983.
278 Services.prefs.setBoolPref("dom.ipc.processPrelaunch.enabled", false);
280 // Disable this preference to capture async stacks across all locations during
281 // DevTools mochitests. Async stacks provide very valuable information to debug
282 // intermittents, but come with a performance overhead, which is why they are
283 // only captured in Debuggees by default.
284 Services.prefs.setBoolPref(
285   "javascript.options.asyncstack_capture_debuggee_only",
286   false
289 // On some Linux platforms, prefers-reduced-motion is enabled, which would
290 // trigger the notification to be displayed in the toolbox. Dismiss the message
291 // by default.
292 Services.prefs.setBoolPref(
293   "devtools.inspector.simple-highlighters.message-dismissed",
294   true
297 registerCleanupFunction(() => {
298   Services.prefs.clearUserPref("devtools.dump.emit");
299   Services.prefs.clearUserPref("devtools.inspector.three-pane-enabled");
300   Services.prefs.clearUserPref("dom.ipc.processPrelaunch.enabled");
301   Services.prefs.clearUserPref("devtools.toolbox.host");
302   Services.prefs.clearUserPref("devtools.toolbox.previousHost");
303   Services.prefs.clearUserPref("devtools.toolbox.splitconsole.open");
304   Services.prefs.clearUserPref("devtools.toolbox.splitconsoleHeight");
305   Services.prefs.clearUserPref(
306     "javascript.options.asyncstack_capture_debuggee_only"
307   );
308   Services.prefs.clearUserPref(
309     "devtools.inspector.simple-highlighters.message-dismissed"
310   );
313 var {
314   BrowserConsoleManager,
315 } = require("resource://devtools/client/webconsole/browser-console-manager.js");
317 registerCleanupFunction(async function cleanup() {
318   // Closing the browser console if there's one
319   const browserConsole = BrowserConsoleManager.getBrowserConsole();
320   if (browserConsole) {
321     await safeCloseBrowserConsole({ clearOutput: true });
322   }
324   // Close any tab opened by the test.
325   // There should be only one tab opened by default when firefox starts the test.
326   while (isMochitest && gBrowser.tabs.length > 1) {
327     await closeTabAndToolbox(gBrowser.selectedTab);
328   }
330   // Note that this will run before cleanup functions registered by tests or other head.js files.
331   // So all connections must be cleaned up by the test when the test ends,
332   // before the harness starts invoking the cleanup functions
333   await waitForTick();
335   // All connections must be cleaned up by the test when the test ends.
336   const {
337     DevToolsServer,
338   } = require("resource://devtools/server/devtools-server.js");
339   ok(
340     !DevToolsServer.hasConnection(),
341     "The main process DevToolsServer has no pending connection when the test ends"
342   );
343   // If there is still open connection, close all of them so that following tests
344   // could pass.
345   if (DevToolsServer.hasConnection()) {
346     for (const conn of Object.values(DevToolsServer._connections)) {
347       conn.close();
348     }
349   }
352 async function safeCloseBrowserConsole({ clearOutput = false } = {}) {
353   const hud = BrowserConsoleManager.getBrowserConsole();
354   if (!hud) {
355     return;
356   }
358   if (clearOutput) {
359     info("Clear the browser console output");
360     const { ui } = hud;
361     const promises = [ui.once("messages-cleared")];
362     // If there's an object inspector, we need to wait for the actors to be released.
363     if (ui.outputNode.querySelector(".object-inspector")) {
364       promises.push(ui.once("fronts-released"));
365     }
366     await ui.clearOutput(true);
367     await Promise.all(promises);
368     info("Browser console cleared");
369   }
371   info("Wait for all Browser Console targets to be attached");
372   // It might happen that waitForAllTargetsToBeAttached does not resolve, so we set a
373   // timeout of 1s before closing
374   await Promise.race([
375     waitForAllTargetsToBeAttached(hud.commands.targetCommand),
376     wait(1000),
377   ]);
379   info("Close the Browser Console");
380   await BrowserConsoleManager.closeBrowserConsole();
381   info("Browser Console closed");
385  * Observer code to register the test actor in every DevTools server which
386  * starts registering its own actors.
388  * We require immediately the highlighter test actor file, because it will force to load and
389  * register the front and the spec for HighlighterTestActor. Normally specs and fronts are
390  * in separate files registered in specs/index.js. But here to simplify the
391  * setup everything is in the same file and we force to load it here.
393  * DevToolsServer will emit "devtools-server-initialized" after finishing its
394  * initialization. We watch this observable to add our custom actor.
396  * As a single test may create several DevTools servers, we keep the observer
397  * alive until the test ends.
399  * To avoid leaks, the observer needs to be removed at the end of each test.
400  * The test cleanup will send the async message "remove-devtools-highlightertestactor-observer",
401  * we listen to this message to cleanup the observer.
402  */
403 function highlighterTestActorBootstrap() {
404   /* eslint-env mozilla/process-script */
405   const HIGHLIGHTER_TEST_ACTOR_URL =
406     "chrome://mochitests/content/browser/devtools/client/shared/test/highlighter-test-actor.js";
408   const { require: _require } = ChromeUtils.importESModule(
409     "resource://devtools/shared/loader/Loader.sys.mjs"
410   );
411   _require(HIGHLIGHTER_TEST_ACTOR_URL);
413   const actorRegistryObserver = subject => {
414     const actorRegistry = subject.wrappedJSObject;
415     actorRegistry.registerModule(HIGHLIGHTER_TEST_ACTOR_URL, {
416       prefix: "highlighterTest",
417       constructor: "HighlighterTestActor",
418       type: { target: true },
419     });
420   };
421   Services.obs.addObserver(
422     actorRegistryObserver,
423     "devtools-server-initialized"
424   );
426   const unloadListener = () => {
427     Services.cpmm.removeMessageListener(
428       "remove-devtools-testactor-observer",
429       unloadListener
430     );
431     Services.obs.removeObserver(
432       actorRegistryObserver,
433       "devtools-server-initialized"
434     );
435   };
436   Services.cpmm.addMessageListener(
437     "remove-devtools-testactor-observer",
438     unloadListener
439   );
442 if (isMochitest) {
443   const highlighterTestActorBootstrapScript =
444     "data:,(" + highlighterTestActorBootstrap + ")()";
445   Services.ppmm.loadProcessScript(
446     highlighterTestActorBootstrapScript,
447     // Load this script in all processes (created or to be created)
448     true
449   );
451   registerCleanupFunction(() => {
452     Services.ppmm.broadcastAsyncMessage("remove-devtools-testactor-observer");
453     Services.ppmm.removeDelayedProcessScript(
454       highlighterTestActorBootstrapScript
455     );
456   });
460  * Spawn an instance of the highlighter test actor for the given toolbox
462  * @param {Toolbox} toolbox
463  * @param {Object} options
464  * @param {Function} options.target: Optional target to get the highlighterTestFront for.
465  *        If not provided, the top level target will be used.
466  * @returns {HighlighterTestFront}
467  */
468 async function getHighlighterTestFront(toolbox, { target } = {}) {
469   // Loading the Inspector panel in order to overwrite the TestActor getter for the
470   // highlighter instance with a method that points to the currently visible
471   // Box Model Highlighter managed by the Inspector panel.
472   const inspector = await toolbox.loadTool("inspector");
474   const highlighterTestFront = await (target || toolbox.target).getFront(
475     "highlighterTest"
476   );
477   // Override the highligher getter with a method to return the active box model
478   // highlighter. Adaptation for multi-process scenarios where there can be multiple
479   // highlighters, one per process.
480   highlighterTestFront.highlighter = () => {
481     return inspector.highlighters.getActiveHighlighter(
482       inspector.highlighters.TYPES.BOXMODEL
483     );
484   };
485   return highlighterTestFront;
489  * Spawn an instance of the highlighter test actor for the given tab, when we need the
490  * highlighter test front before opening or without a toolbox.
492  * @param {Tab} tab
493  * @returns {HighlighterTestFront}
494  */
495 async function getHighlighterTestFrontWithoutToolbox(tab) {
496   const commands = await CommandsFactory.forTab(tab);
497   // Initialize the TargetCommands which require some async stuff to be done
498   // before being fully ready. This will define the `targetCommand.targetFront` attribute.
499   await commands.targetCommand.startListening();
501   const targetFront = commands.targetCommand.targetFront;
502   return targetFront.getFront("highlighterTest");
506  * Returns a Promise that resolves when all the targets are fully attached.
508  * @param {TargetCommand} targetCommand
509  */
510 function waitForAllTargetsToBeAttached(targetCommand) {
511   return Promise.allSettled(
512     targetCommand
513       .getAllTargets(targetCommand.ALL_TYPES)
514       .map(target => target.initialized)
515   );
519  * Add a new test tab in the browser and load the given url.
520  * @param {String} url The url to be loaded in the new tab
521  * @param {Object} options Object with various optional fields:
522  *   - {Boolean} background If true, open the tab in background
523  *   - {ChromeWindow} window Firefox top level window we should use to open the tab
524  *   - {Number} userContextId The userContextId of the tab.
525  *   - {String} preferredRemoteType
526  *   - {Boolean} waitForLoad Wait for the page in the new tab to load. (Defaults to true.)
527  * @return a promise that resolves to the tab object when the url is loaded
528  */
529 async function addTab(url, options = {}) {
530   info("Adding a new tab with URL: " + url);
532   const {
533     background = false,
534     userContextId,
535     preferredRemoteType,
536     waitForLoad = true,
537   } = options;
538   const { gBrowser } = options.window ? options.window : window;
540   const tab = BrowserTestUtils.addTab(gBrowser, url, {
541     userContextId,
542     preferredRemoteType,
543   });
545   if (!background) {
546     gBrowser.selectedTab = tab;
547   }
549   if (waitForLoad) {
550     await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
551     // Waiting for presShell helps with test timeouts in webrender platforms.
552     await waitForPresShell(tab.linkedBrowser);
553     info("Tab added and finished loading");
554   } else {
555     info("Tab added");
556   }
558   return tab;
562  * Remove the given tab.
563  * @param {Object} tab The tab to be removed.
564  * @return Promise<undefined> resolved when the tab is successfully removed.
565  */
566 async function removeTab(tab) {
567   info("Removing tab.");
569   const { gBrowser } = tab.ownerDocument.defaultView;
570   const onClose = once(gBrowser.tabContainer, "TabClose");
571   gBrowser.removeTab(tab);
572   await onClose;
574   info("Tab removed and finished closing");
578  * Alias for navigateTo which will reuse the current URI of the provided browser
579  * to trigger a navigation.
580  */
581 async function reloadBrowser({
582   browser = gBrowser.selectedBrowser,
583   isErrorPage = false,
584   waitForLoad = true,
585 } = {}) {
586   return navigateTo(browser.currentURI.spec, {
587     browser,
588     isErrorPage,
589     waitForLoad,
590   });
594  * Navigate the currently selected tab to a new URL and wait for it to load.
595  * Also wait for the toolbox to attach to the new target, if we navigated
596  * to a new process.
598  * @param {String} url The url to be loaded in the current tab.
599  * @param {JSON} options Optional dictionary object with the following keys:
600  *        - {XULBrowser} browser
601  *          The browser element which should navigate. Defaults to the selected
602  *          browser.
603  *        - {Boolean} isErrorPage
604  *          You may pass `true` if the URL is an error page. Otherwise
605  *          BrowserTestUtils.browserLoaded will wait for 'load' event, which
606  *          never fires for error pages.
607  *        - {Boolean} waitForLoad
608  *          You may pass `false` if the page load is expected to be blocked by
609  *          a script or a breakpoint.
611  * @return a promise that resolves when the page has fully loaded.
612  */
613 async function navigateTo(
614   uri,
615   {
616     browser = gBrowser.selectedBrowser,
617     isErrorPage = false,
618     waitForLoad = true,
619   } = {}
620 ) {
621   const waitForDevToolsReload = await watchForDevToolsReload(browser, {
622     isErrorPage,
623     waitForLoad,
624   });
626   uri = uri.replaceAll("\n", "");
627   info(`Navigating to "${uri}"`);
629   const onBrowserLoaded = BrowserTestUtils.browserLoaded(
630     browser,
631     // includeSubFrames
632     false,
633     // resolve on this specific page to load (if null, it would be any page load)
634     loadedUrl => {
635       // loadedUrl is encoded, while uri might not be.
636       return loadedUrl === uri || decodeURI(loadedUrl) === uri;
637     },
638     isErrorPage
639   );
641   // if we're navigating to the same page we're already on, use reloadTab instead as the
642   // behavior slightly differs from loadURI (e.g. scroll position isn't keps with the latter).
643   if (uri === browser.currentURI.spec) {
644     gBrowser.reloadTab(gBrowser.getTabForBrowser(browser));
645   } else {
646     BrowserTestUtils.startLoadingURIString(browser, uri);
647   }
649   if (waitForLoad) {
650     info(`Waiting for page to be loaded…`);
651     await onBrowserLoaded;
652     info(`→ page loaded`);
653   }
655   await waitForDevToolsReload();
659  * This method should be used to watch for completion of any browser navigation
660  * performed with a DevTools UI.
662  * It should watch for:
663  * - Toolbox reload
664  * - Toolbox commands reload
665  * - RDM reload
666  * - RDM commands reload
668  * And it should work both for target switching or old-style navigations.
670  * This method, similarly to all the other watch* navigation methods in this file,
671  * is async but returns another method which should be called after the navigation
672  * is done. Browser navigation might be monitored differently depending on the
673  * situation, so it's up to the caller to handle it as needed.
675  * Typically, this would be used as follows:
676  * ```
677  *   async function someNavigationHelper(browser) {
678  *     const waitForDevToolsFn = await watchForDevToolsReload(browser);
680  *     // This step should wait for the load to be completed from the browser's
681  *     // point of view, so that waitForDevToolsFn can compare pIds, browsing
682  *     // contexts etc... and check if we should expect a target switch
683  *     await performBrowserNavigation(browser);
685  *     await waitForDevToolsFn();
686  *   }
687  * ```
688  */
689 async function watchForDevToolsReload(
690   browser,
691   { isErrorPage = false, waitForLoad = true } = {}
692 ) {
693   const waitForToolboxReload = await _watchForToolboxReload(browser, {
694     isErrorPage,
695     waitForLoad,
696   });
697   const waitForResponsiveReload = await _watchForResponsiveReload(browser, {
698     isErrorPage,
699     waitForLoad,
700   });
702   return async function () {
703     info("Wait for the toolbox to reload");
704     await waitForToolboxReload();
706     info("Wait for Responsive UI to reload");
707     await waitForResponsiveReload();
708   };
712  * Start watching for the toolbox reload to be completed:
713  * - watch for the toolbox's commands to be fully reloaded
714  * - watch for the toolbox's current panel to be reloaded
715  */
716 async function _watchForToolboxReload(
717   browser,
718   { isErrorPage, waitForLoad } = {}
719 ) {
720   const tab = gBrowser.getTabForBrowser(browser);
722   const toolbox = gDevTools.getToolboxForTab(tab);
724   if (!toolbox) {
725     // No toolbox to wait for
726     return function () {};
727   }
729   const waitForCurrentPanelReload = watchForCurrentPanelReload(toolbox);
730   const waitForToolboxCommandsReload = await watchForCommandsReload(
731     toolbox.commands,
732     { isErrorPage, waitForLoad }
733   );
734   const checkTargetSwitching = await watchForTargetSwitching(
735     toolbox.commands,
736     browser
737   );
739   return async function () {
740     const isTargetSwitching = checkTargetSwitching();
742     info(`Waiting for toolbox commands to be reloaded…`);
743     await waitForToolboxCommandsReload(isTargetSwitching);
745     // TODO: We should wait for all loaded panels to reload here, because some
746     // of them might still perform background updates.
747     if (waitForCurrentPanelReload) {
748       info(`Waiting for ${toolbox.currentToolId} to be reloaded…`);
749       await waitForCurrentPanelReload();
750       info(`→ panel reloaded`);
751     }
752   };
756  * Start watching for Responsive UI (RDM) reload to be completed:
757  * - watch for the Responsive UI's commands to be fully reloaded
758  * - watch for the Responsive UI's target switch to be done
759  */
760 async function _watchForResponsiveReload(
761   browser,
762   { isErrorPage, waitForLoad } = {}
763 ) {
764   const tab = gBrowser.getTabForBrowser(browser);
765   const ui = ResponsiveUIManager.getResponsiveUIForTab(tab);
767   if (!ui) {
768     // No responsive UI to wait for
769     return function () {};
770   }
772   const onResponsiveTargetSwitch = ui.once("responsive-ui-target-switch-done");
773   const waitForResponsiveCommandsReload = await watchForCommandsReload(
774     ui.commands,
775     { isErrorPage, waitForLoad }
776   );
777   const checkTargetSwitching = await watchForTargetSwitching(
778     ui.commands,
779     browser
780   );
782   return async function () {
783     const isTargetSwitching = checkTargetSwitching();
785     info(`Waiting for responsive ui commands to be reloaded…`);
786     await waitForResponsiveCommandsReload(isTargetSwitching);
788     if (isTargetSwitching) {
789       await onResponsiveTargetSwitch;
790     }
791   };
795  * Watch for the current panel selected in the provided toolbox to be reloaded.
796  * Some panels implement custom events that should be expected for every reload.
798  * Note about returning a method instead of a promise:
799  * In general this pattern is useful so that we can check if a target switch
800  * occurred or not, and decide which events to listen for. So far no panel is
801  * behaving differently whether there was a target switch or not. But to remain
802  * consistent with other watch* methods we still return a function here.
804  * @param {Toolbox}
805  *        The Toolbox instance which is going to experience a reload
806  * @return {function} An async method to be called and awaited after the reload
807  *         started. Will return `null` for panels which don't implement any
808  *         specific reload event.
809  */
810 function watchForCurrentPanelReload(toolbox) {
811   return _watchForPanelReload(toolbox, toolbox.currentToolId);
815  * Watch for all the panels loaded in the provided toolbox to be reloaded.
816  * Some panels implement custom events that should be expected for every reload.
818  * Note about returning a method instead of a promise:
819  * See comment for watchForCurrentPanelReload
821  * @param {Toolbox}
822  *        The Toolbox instance which is going to experience a reload
823  * @return {function} An async method to be called and awaited after the reload
824  *         started.
825  */
826 function watchForLoadedPanelsReload(toolbox) {
827   const waitForPanels = [];
828   for (const [id] of toolbox.getToolPanels()) {
829     // Store a watcher method for each panel already loaded.
830     waitForPanels.push(_watchForPanelReload(toolbox, id));
831   }
833   return function () {
834     return Promise.all(
835       waitForPanels.map(async watchPanel => {
836         // Wait for all panels to be reloaded.
837         if (watchPanel) {
838           await watchPanel();
839         }
840       })
841     );
842   };
845 function _watchForPanelReload(toolbox, toolId) {
846   const panel = toolbox.getPanel(toolId);
848   if (toolId == "inspector") {
849     const markuploaded = panel.once("markuploaded");
850     const onNewRoot = panel.once("new-root");
851     const onUpdated = panel.once("inspector-updated");
852     const onReloaded = panel.once("reloaded");
854     return async function () {
855       info("Waiting for markup view to load after navigation.");
856       await markuploaded;
858       info("Waiting for new root.");
859       await onNewRoot;
861       info("Waiting for inspector to update after new-root event.");
862       await onUpdated;
864       info("Waiting for inspector updates after page reload");
865       await onReloaded;
866     };
867   } else if (
868     ["netmonitor", "accessibility", "webconsole", "jsdebugger"].includes(toolId)
869   ) {
870     const onReloaded = panel.once("reloaded");
871     return async function () {
872       info(`Waiting for ${toolId} updates after page reload`);
873       await onReloaded;
874     };
875   }
876   return null;
880  * Watch for a Commands instance to be reloaded after a navigation.
882  * As for other navigation watch* methods, this should be called before the
883  * navigation starts, and the function it returns should be called after the
884  * navigation is done from a Browser point of view.
886  * !!! The wait function expects a `isTargetSwitching` argument to be provided,
887  * which needs to be monitored using watchForTargetSwitching !!!
888  */
889 async function watchForCommandsReload(
890   commands,
891   { isErrorPage = false, waitForLoad = true } = {}
892 ) {
893   // If we're switching origins, we need to wait for the 'switched-target'
894   // event to make sure everything is ready.
895   // Navigating from/to pages loaded in the parent process, like about:robots,
896   // also spawn new targets.
897   // (If target switching is disabled, the toolbox will reboot)
898   const onTargetSwitched = commands.targetCommand.once("switched-target");
900   // Wait until we received a page load resource:
901   // - dom-complete if we can wait for a full page load
902   // - dom-loading otherwise
903   // This allows to wait for page load for consumers calling directly
904   // waitForDevTools instead of navigateTo/reloadBrowser.
905   // This is also useful as an alternative to target switching, when no target
906   // switch is supposed to happen.
907   const waitForCompleteLoad = waitForLoad && !isErrorPage;
908   const documentEventName = waitForCompleteLoad
909     ? "dom-complete"
910     : "dom-loading";
912   const { onResource: onTopLevelDomEvent } =
913     await commands.resourceCommand.waitForNextResource(
914       commands.resourceCommand.TYPES.DOCUMENT_EVENT,
915       {
916         ignoreExistingResources: true,
917         predicate: resource =>
918           resource.targetFront.isTopLevel &&
919           resource.name === documentEventName,
920       }
921     );
923   return async function (isTargetSwitching) {
924     if (typeof isTargetSwitching === "undefined") {
925       throw new Error("isTargetSwitching was not provided to the wait method");
926     }
928     if (isTargetSwitching) {
929       info(`Waiting for target switch…`);
930       await onTargetSwitched;
931       info(`→ switched-target emitted`);
932     }
934     info(`Waiting for '${documentEventName}' resource…`);
935     await onTopLevelDomEvent;
936     info(`→ '${documentEventName}' resource emitted`);
938     return isTargetSwitching;
939   };
943  * Watch if an upcoming navigation will trigger a target switching, for the
944  * provided Commands instance and the provided Browser.
946  * As for other navigation watch* methods, this should be called before the
947  * navigation starts, and the function it returns should be called after the
948  * navigation is done from a Browser point of view.
949  */
950 async function watchForTargetSwitching(commands, browser) {
951   browser = browser || gBrowser.selectedBrowser;
952   const currentPID = browser.browsingContext.currentWindowGlobal.osPid;
953   const currentBrowsingContextID = browser.browsingContext.id;
955   // If the current top-level target follows the window global lifecycle, a
956   // target switch will occur regardless of process changes.
957   const targetFollowsWindowLifecycle =
958     commands.targetCommand.targetFront.targetForm.followWindowGlobalLifeCycle;
960   return function () {
961     // Compare the PIDs (and not the toolbox's targets) as PIDs are updated also immediately,
962     // while target may be updated slightly later.
963     const switchedProcess =
964       currentPID !== browser.browsingContext.currentWindowGlobal.osPid;
965     const switchedBrowsingContext =
966       currentBrowsingContextID !== browser.browsingContext.id;
968     return (
969       targetFollowsWindowLifecycle || switchedProcess || switchedBrowsingContext
970     );
971   };
975  * Create a Target for the provided tab and attach to it before resolving.
976  * This should only be used for tests which don't involve the frontend or a
977  * toolbox. Typically, retrieving the target and attaching to it should be
978  * handled at framework level when a Toolbox is used.
980  * @param {XULTab} tab
981  *        The tab for which a target should be created.
982  * @return {WindowGlobalTargetFront} The attached target front.
983  */
984 async function createAndAttachTargetForTab(tab) {
985   info("Creating and attaching to a local tab target");
987   const commands = await CommandsFactory.forTab(tab);
989   // Initialize the TargetCommands which require some async stuff to be done
990   // before being fully ready. This will define the `targetCommand.targetFront` attribute.
991   await commands.targetCommand.startListening();
993   const target = commands.targetCommand.targetFront;
994   return target;
997 function isFissionEnabled() {
998   return SpecialPowers.useRemoteSubframes;
1001 function isEveryFrameTargetEnabled() {
1002   return Services.prefs.getBoolPref(
1003     "devtools.every-frame-target.enabled",
1004     false
1005   );
1009  * Open the inspector in a tab with given URL.
1010  * @param {string} url  The URL to open.
1011  * @param {String} hostType Optional hostType, as defined in Toolbox.HostType
1012  * @return A promise that is resolved once the tab and inspector have loaded
1013  *         with an object: { tab, toolbox, inspector, highlighterTestFront }.
1014  */
1015 async function openInspectorForURL(url, hostType) {
1016   const tab = await addTab(url);
1017   const { inspector, toolbox, highlighterTestFront } = await openInspector(
1018     hostType
1019   );
1020   return { tab, inspector, toolbox, highlighterTestFront };
1023 function getActiveInspector() {
1024   const toolbox = gDevTools.getToolboxForTab(gBrowser.selectedTab);
1025   return toolbox.getPanel("inspector");
1029  * Simulate a key event from an electron key shortcut string:
1030  * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
1032  * @param {String} key
1033  * @param {DOMWindow} target
1034  *        Optional window where to fire the key event
1035  */
1036 function synthesizeKeyShortcut(key, target) {
1037   // parseElectronKey requires any window, just to access `KeyboardEvent`
1038   const window = Services.appShell.hiddenDOMWindow;
1039   const shortcut = KeyShortcuts.parseElectronKey(window, key);
1040   const keyEvent = {
1041     altKey: shortcut.alt,
1042     ctrlKey: shortcut.ctrl,
1043     metaKey: shortcut.meta,
1044     shiftKey: shortcut.shift,
1045   };
1046   if (shortcut.keyCode) {
1047     keyEvent.keyCode = shortcut.keyCode;
1048   }
1050   info("Synthesizing key shortcut: " + key);
1051   EventUtils.synthesizeKey(shortcut.key || "", keyEvent, target);
1054 var waitForTime = DevToolsUtils.waitForTime;
1057  * Wait for a tick.
1058  * @return {Promise}
1059  */
1060 function waitForTick() {
1061   return new Promise(resolve => DevToolsUtils.executeSoon(resolve));
1065  * This shouldn't be used in the tests, but is useful when writing new tests or
1066  * debugging existing tests in order to introduce delays in the test steps
1068  * @param {Number} ms
1069  *        The time to wait
1070  * @return A promise that resolves when the time is passed
1071  */
1072 function wait(ms) {
1073   return new Promise(resolve => {
1074     setTimeout(resolve, ms);
1075     info("Waiting " + ms / 1000 + " seconds.");
1076   });
1080  * Wait for a predicate to return a result.
1082  * @param function condition
1083  *        Invoked once in a while until it returns a truthy value. This should be an
1084  *        idempotent function, since we have to run it a second time after it returns
1085  *        true in order to return the value.
1086  * @param string message [optional]
1087  *        A message to output if the condition fails.
1088  * @param number interval [optional]
1089  *        How often the predicate is invoked, in milliseconds.
1090  *        Can be set globally for a test via `waitFor.overrideIntervalForTestFile = someNumber;`.
1091  * @param number maxTries [optional]
1092  *        How many times the predicate is invoked before timing out.
1093  *        Can be set globally for a test via `waitFor.overrideMaxTriesForTestFile = someNumber;`.
1094  * @return object
1095  *         A promise that is resolved with the result of the condition.
1096  */
1097 async function waitFor(condition, message = "", interval = 10, maxTries = 500) {
1098   // Update interval & maxTries if overrides are defined on the waitFor object.
1099   interval =
1100     typeof waitFor.overrideIntervalForTestFile !== "undefined"
1101       ? waitFor.overrideIntervalForTestFile
1102       : interval;
1103   maxTries =
1104     typeof waitFor.overrideMaxTriesForTestFile !== "undefined"
1105       ? waitFor.overrideMaxTriesForTestFile
1106       : maxTries;
1108   try {
1109     const value = await BrowserTestUtils.waitForCondition(
1110       condition,
1111       message,
1112       interval,
1113       maxTries
1114     );
1115     return value;
1116   } catch (e) {
1117     const errorMessage = `Failed waitFor(): ${message} \nFailed condition: ${condition} \nException Message: ${e}`;
1118     throw new Error(errorMessage);
1119   }
1123  * Wait for eventName on target to be delivered a number of times.
1125  * @param {Object} target
1126  *        An observable object that either supports on/off or
1127  *        addEventListener/removeEventListener
1128  * @param {String} eventName
1129  * @param {Number} numTimes
1130  *        Number of deliveries to wait for.
1131  * @param {Boolean} useCapture
1132  *        Optional, for addEventListener/removeEventListener
1133  * @return A promise that resolves when the event has been handled
1134  */
1135 function waitForNEvents(target, eventName, numTimes, useCapture = false) {
1136   info("Waiting for event: '" + eventName + "' on " + target + ".");
1138   let count = 0;
1140   return new Promise(resolve => {
1141     for (const [add, remove] of [
1142       ["on", "off"],
1143       ["addEventListener", "removeEventListener"],
1144       ["addListener", "removeListener"],
1145       ["addMessageListener", "removeMessageListener"],
1146     ]) {
1147       if (add in target && remove in target) {
1148         target[add](
1149           eventName,
1150           function onEvent(...args) {
1151             if (typeof info === "function") {
1152               info("Got event: '" + eventName + "' on " + target + ".");
1153             }
1155             if (++count == numTimes) {
1156               target[remove](eventName, onEvent, useCapture);
1157               resolve(...args);
1158             }
1159           },
1160           useCapture
1161         );
1162         break;
1163       }
1164     }
1165   });
1169  * Wait for DOM change on target.
1171  * @param {Object} target
1172  *        The Node on which to observe DOM mutations.
1173  * @param {String} selector
1174  *        Given a selector to watch whether the expected element is changed
1175  *        on target.
1176  * @param {Number} expectedLength
1177  *        Optional, default set to 1
1178  *        There may be more than one element match an array match the selector,
1179  *        give an expected length to wait for more elements.
1180  * @return A promise that resolves when the event has been handled
1181  */
1182 function waitForDOM(target, selector, expectedLength = 1) {
1183   return new Promise(resolve => {
1184     const observer = new MutationObserver(mutations => {
1185       mutations.forEach(mutation => {
1186         const elements = mutation.target.querySelectorAll(selector);
1188         if (elements.length === expectedLength) {
1189           observer.disconnect();
1190           resolve(elements);
1191         }
1192       });
1193     });
1195     observer.observe(target, {
1196       attributes: true,
1197       childList: true,
1198       subtree: true,
1199     });
1200   });
1204  * Wait for eventName on target.
1206  * @param {Object} target
1207  *        An observable object that either supports on/off or
1208  *        addEventListener/removeEventListener
1209  * @param {String} eventName
1210  * @param {Boolean} useCapture
1211  *        Optional, for addEventListener/removeEventListener
1212  * @return A promise that resolves when the event has been handled
1213  */
1214 function once(target, eventName, useCapture = false) {
1215   return waitForNEvents(target, eventName, 1, useCapture);
1219  * Some tests may need to import one or more of the test helper scripts.
1220  * A test helper script is simply a js file that contains common test code that
1221  * is either not common-enough to be in head.js, or that is located in a
1222  * separate directory.
1223  * The script will be loaded synchronously and in the test's scope.
1224  * @param {String} filePath The file path, relative to the current directory.
1225  *                 Examples:
1226  *                 - "helper_attributes_test_runner.js"
1227  */
1228 function loadHelperScript(filePath) {
1229   const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
1230   Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
1234  * Open the toolbox in a given tab.
1235  * @param {XULNode} tab The tab the toolbox should be opened in.
1236  * @param {String} toolId Optional. The ID of the tool to be selected.
1237  * @param {String} hostType Optional. The type of toolbox host to be used.
1238  * @return {Promise} Resolves with the toolbox, when it has been opened.
1239  */
1240 async function openToolboxForTab(tab, toolId, hostType) {
1241   info("Opening the toolbox");
1243   // Check if the toolbox is already loaded.
1244   let toolbox = gDevTools.getToolboxForTab(tab);
1245   if (toolbox) {
1246     if (!toolId || (toolId && toolbox.getPanel(toolId))) {
1247       info("Toolbox is already opened");
1248       return toolbox;
1249     }
1250   }
1252   // If not, load it now.
1253   toolbox = await gDevTools.showToolboxForTab(tab, { toolId, hostType });
1255   // Make sure that the toolbox frame is focused.
1256   await new Promise(resolve => waitForFocus(resolve, toolbox.win));
1258   info("Toolbox opened and focused");
1260   return toolbox;
1264  * Add a new tab and open the toolbox in it.
1265  * @param {String} url The URL for the tab to be opened.
1266  * @param {String} toolId Optional. The ID of the tool to be selected.
1267  * @param {String} hostType Optional. The type of toolbox host to be used.
1268  * @return {Promise} Resolves when the tab has been added, loaded and the
1269  * toolbox has been opened. Resolves to the toolbox.
1270  */
1271 async function openNewTabAndToolbox(url, toolId, hostType) {
1272   const tab = await addTab(url);
1273   return openToolboxForTab(tab, toolId, hostType);
1277  * Close a tab and if necessary, the toolbox that belongs to it
1278  * @param {Tab} tab The tab to close.
1279  * @return {Promise} Resolves when the toolbox and tab have been destroyed and
1280  * closed.
1281  */
1282 async function closeTabAndToolbox(tab = gBrowser.selectedTab) {
1283   if (gDevTools.hasToolboxForTab(tab)) {
1284     await gDevTools.closeToolboxForTab(tab);
1285   }
1287   await removeTab(tab);
1289   await new Promise(resolve => setTimeout(resolve, 0));
1293  * Close a toolbox and the current tab.
1294  * @param {Toolbox} toolbox The toolbox to close.
1295  * @return {Promise} Resolves when the toolbox and tab have been destroyed and
1296  * closed.
1297  */
1298 async function closeToolboxAndTab(toolbox) {
1299   await toolbox.destroy();
1300   await removeTab(gBrowser.selectedTab);
1304  * Waits until a predicate returns true.
1306  * @param function predicate
1307  *        Invoked once in a while until it returns true.
1308  * @param number interval [optional]
1309  *        How often the predicate is invoked, in milliseconds.
1310  */
1311 function waitUntil(predicate, interval = 10) {
1312   if (predicate()) {
1313     return Promise.resolve(true);
1314   }
1315   return new Promise(resolve => {
1316     setTimeout(function () {
1317       waitUntil(predicate, interval).then(() => resolve(true));
1318     }, interval);
1319   });
1323  * Variant of waitUntil that accepts a predicate returning a promise.
1324  */
1325 async function asyncWaitUntil(predicate, interval = 10) {
1326   let success = await predicate();
1327   while (!success) {
1328     // Wait for X milliseconds.
1329     await new Promise(resolve => setTimeout(resolve, interval));
1330     // Test the predicate again.
1331     success = await predicate();
1332   }
1336  * Wait for a context menu popup to open.
1338  * @param Element popup
1339  *        The XUL popup you expect to open.
1340  * @param Element button
1341  *        The button/element that receives the contextmenu event. This is
1342  *        expected to open the popup.
1343  * @param function onShown
1344  *        Function to invoke on popupshown event.
1345  * @param function onHidden
1346  *        Function to invoke on popuphidden event.
1347  * @return object
1348  *         A Promise object that is resolved after the popuphidden event
1349  *         callback is invoked.
1350  */
1351 function waitForContextMenu(popup, button, onShown, onHidden) {
1352   return new Promise(resolve => {
1353     function onPopupShown() {
1354       info("onPopupShown");
1355       popup.removeEventListener("popupshown", onPopupShown);
1357       onShown && onShown();
1359       // Use executeSoon() to get out of the popupshown event.
1360       popup.addEventListener("popuphidden", onPopupHidden);
1361       DevToolsUtils.executeSoon(() => popup.hidePopup());
1362     }
1363     function onPopupHidden() {
1364       info("onPopupHidden");
1365       popup.removeEventListener("popuphidden", onPopupHidden);
1367       onHidden && onHidden();
1369       resolve(popup);
1370     }
1372     popup.addEventListener("popupshown", onPopupShown);
1374     info("wait for the context menu to open");
1375     synthesizeContextMenuEvent(button);
1376   });
1379 function synthesizeContextMenuEvent(el) {
1380   el.scrollIntoView();
1381   const eventDetails = { type: "contextmenu", button: 2 };
1382   EventUtils.synthesizeMouse(
1383     el,
1384     5,
1385     2,
1386     eventDetails,
1387     el.ownerDocument.defaultView
1388   );
1392  * Promise wrapper around SimpleTest.waitForClipboard
1393  */
1394 function waitForClipboardPromise(setup, expected) {
1395   return new Promise((resolve, reject) => {
1396     SimpleTest.waitForClipboard(expected, setup, resolve, reject);
1397   });
1401  * Simple helper to push a temporary preference. Wrapper on SpecialPowers
1402  * pushPrefEnv that returns a promise resolving when the preferences have been
1403  * updated.
1405  * @param {String} preferenceName
1406  *        The name of the preference to updated
1407  * @param {} value
1408  *        The preference value, type can vary
1409  * @return {Promise} resolves when the preferences have been updated
1410  */
1411 function pushPref(preferenceName, value) {
1412   const options = { set: [[preferenceName, value]] };
1413   return SpecialPowers.pushPrefEnv(options);
1416 async function closeToolbox() {
1417   await gDevTools.closeToolboxForTab(gBrowser.selectedTab);
1421  * Clean the logical clipboard content. This method only clears the OS clipboard on
1422  * Windows (see Bug 666254).
1423  */
1424 function emptyClipboard() {
1425   const clipboard = Services.clipboard;
1426   clipboard.emptyClipboard(clipboard.kGlobalClipboard);
1430  * Check if the current operating system is Windows.
1431  */
1432 function isWindows() {
1433   return Services.appinfo.OS === "WINNT";
1437  * Create an HTTP server that can be used to simulate custom requests within
1438  * a test.  It is automatically cleaned up when the test ends, so no need to
1439  * call `destroy`.
1441  * See https://developer.mozilla.org/en-US/docs/Httpd.js/HTTP_server_for_unit_tests
1442  * for more information about how to register handlers.
1444  * The server can be accessed like:
1446  *   const server = createTestHTTPServer();
1447  *   let url = "http://localhost: " + server.identity.primaryPort + "/path";
1448  * @returns {HttpServer}
1449  */
1450 function createTestHTTPServer() {
1451   const { HttpServer } = ChromeUtils.importESModule(
1452     "resource://testing-common/httpd.sys.mjs"
1453   );
1454   const server = new HttpServer();
1456   registerCleanupFunction(async function cleanup() {
1457     await new Promise(resolve => server.stop(resolve));
1458   });
1460   server.start(-1);
1461   return server;
1465  * Register an actor in the content process of the current tab.
1467  * Calling ActorRegistry.registerModule only registers the actor in the current process.
1468  * As all test scripts are ran in the parent process, it is only registered here.
1469  * This function helps register them in the content process used for the current tab.
1471  * @param {string} url
1472  *        Actor module URL or absolute require path
1473  * @param {json} options
1474  *        Arguments to be passed to DevToolsServer.registerModule
1475  */
1476 async function registerActorInContentProcess(url, options) {
1477   function convertChromeToFile(uri) {
1478     return Cc["@mozilla.org/chrome/chrome-registry;1"]
1479       .getService(Ci.nsIChromeRegistry)
1480       .convertChromeURL(Services.io.newURI(uri)).spec;
1481   }
1482   // chrome://mochitests URI is registered only in the parent process, so convert these
1483   // URLs to file:// one in order to work in the content processes
1484   url = url.startsWith("chrome://mochitests") ? convertChromeToFile(url) : url;
1485   return SpecialPowers.spawn(
1486     gBrowser.selectedBrowser,
1487     [{ url, options }],
1488     args => {
1489       // eslint-disable-next-line no-shadow
1490       const { require } = ChromeUtils.importESModule(
1491         "resource://devtools/shared/loader/Loader.sys.mjs"
1492       );
1493       const {
1494         ActorRegistry,
1495       } = require("resource://devtools/server/actors/utils/actor-registry.js");
1496       ActorRegistry.registerModule(args.url, args.options);
1497     }
1498   );
1502  * Move the provided Window to the provided left, top coordinates and wait for
1503  * the window position to be updated.
1504  */
1505 async function moveWindowTo(win, left, top) {
1506   // Check that the expected coordinates are within the window available area.
1507   left = Math.max(win.screen.availLeft, left);
1508   left = Math.min(win.screen.width, left);
1509   top = Math.max(win.screen.availTop, top);
1510   top = Math.min(win.screen.height, top);
1512   info(`Moving window to {${left}, ${top}}`);
1513   win.moveTo(left, top);
1515   // Bug 1600809: window move/resize can be async on Linux sometimes.
1516   // Wait so that the anchor's position is correctly measured.
1517   return waitUntil(() => {
1518     info(
1519       `Wait for window screenLeft and screenTop to be updated: (${win.screenLeft}, ${win.screenTop})`
1520     );
1521     return win.screenLeft === left && win.screenTop === top;
1522   });
1525 function getCurrentTestFilePath() {
1526   return gTestPath.replace("chrome://mochitests/content/browser/", "");
1530  * Unregister all registered service workers.
1532  * @param {DevToolsClient} client
1533  */
1534 async function unregisterAllServiceWorkers(client) {
1535   info("Wait until all workers have a valid registrationFront");
1536   let workers;
1537   await asyncWaitUntil(async function () {
1538     workers = await client.mainRoot.listAllWorkers();
1539     const allWorkersRegistered = workers.service.every(
1540       worker => !!worker.registrationFront
1541     );
1542     return allWorkersRegistered;
1543   });
1545   info("Unregister all service workers");
1546   const promises = [];
1547   for (const worker of workers.service) {
1548     promises.push(worker.registrationFront.unregister());
1549   }
1550   await Promise.all(promises);
1553 /**********************
1554  * Screenshot helpers *
1555  **********************/
1558  * Returns an object containing the r,g and b colors of the provided image at
1559  * the passed position
1561  * @param {Image} image
1562  * @param {Int} x
1563  * @param {Int} y
1564  * @returns Object with the following properties:
1565  *           - {Int} r: The red component of the pixel
1566  *           - {Int} g: The green component of the pixel
1567  *           - {Int} b: The blue component of the pixel
1568  */
1569 function colorAt(image, x, y) {
1570   // Create a test canvas element.
1571   const HTML_NS = "http://www.w3.org/1999/xhtml";
1572   const canvas = document.createElementNS(HTML_NS, "canvas");
1573   canvas.width = image.width;
1574   canvas.height = image.height;
1576   // Draw the image in the canvas
1577   const context = canvas.getContext("2d");
1578   context.drawImage(image, 0, 0, image.width, image.height);
1580   // Return the color found at the provided x,y coordinates as a "r, g, b" string.
1581   const [r, g, b] = context.getImageData(x, y, 1, 1).data;
1582   return { r, g, b };
1585 let allDownloads = [];
1587  * Returns a Promise that resolves when a new screenshot is available in the download folder.
1589  * @param {Object} [options]
1590  * @param {Boolean} options.isWindowPrivate: Set to true if the window from which the screenshot
1591  *                  is taken is a private window. This will ensure that we check that the
1592  *                  screenshot appears in the private window, not the non-private one (See Bug 1783373)
1593  */
1594 async function waitUntilScreenshot({ isWindowPrivate = false } = {}) {
1595   const { Downloads } = ChromeUtils.importESModule(
1596     "resource://gre/modules/Downloads.sys.mjs"
1597   );
1598   const list = await Downloads.getList(Downloads.ALL);
1600   return new Promise(function (resolve) {
1601     const view = {
1602       onDownloadAdded: async download => {
1603         await download.whenSucceeded();
1604         if (allDownloads.includes(download)) {
1605           return;
1606         }
1608         is(
1609           !!download.source.isPrivate,
1610           isWindowPrivate,
1611           `The download occured in the expected${
1612             isWindowPrivate ? " private" : ""
1613           } window`
1614         );
1616         allDownloads.push(download);
1617         resolve(download.target.path);
1618         list.removeView(view);
1619       },
1620     };
1622     list.addView(view);
1623   });
1627  * Clear all the download references.
1628  */
1629 async function resetDownloads() {
1630   info("Reset downloads");
1631   const { Downloads } = ChromeUtils.importESModule(
1632     "resource://gre/modules/Downloads.sys.mjs"
1633   );
1634   const downloadList = await Downloads.getList(Downloads.ALL);
1635   const downloads = await downloadList.getAll();
1636   for (const download of downloads) {
1637     downloadList.remove(download);
1638     await download.finalize(true);
1639   }
1640   allDownloads = [];
1644  * Return a screenshot of the currently selected node in the inspector (using the internal
1645  * Inspector#screenshotNode method).
1647  * @param {Inspector} inspector
1648  * @returns {Image}
1649  */
1650 async function takeNodeScreenshot(inspector) {
1651   // Cleanup all downloads at the end of the test.
1652   registerCleanupFunction(resetDownloads);
1654   info(
1655     "Call screenshotNode() and wait until the screenshot is found in the Downloads"
1656   );
1657   const whenScreenshotSucceeded = waitUntilScreenshot();
1658   inspector.screenshotNode();
1659   const filePath = await whenScreenshotSucceeded;
1661   info("Create an image using the downloaded fileas source");
1662   const image = new Image();
1663   const onImageLoad = once(image, "load");
1664   image.src = PathUtils.toFileURI(filePath);
1665   await onImageLoad;
1667   info("Remove the downloaded screenshot file");
1668   await IOUtils.remove(filePath);
1670   // See intermittent Bug 1508435. Even after removing the file, tests still manage to
1671   // reuse files from the previous test if they have the same name. Since our file name
1672   // is based on a timestamp that has "second" precision, wait for one second to make sure
1673   // screenshots will have different names.
1674   info(
1675     "Wait for one second to make sure future screenshots will use a different name"
1676   );
1677   await new Promise(r => setTimeout(r, 1000));
1679   return image;
1683  * Check that the provided image has the expected width, height, and color.
1684  * NOTE: This test assumes that the image is only made of a single color and will only
1685  * check one pixel.
1686  */
1687 async function assertSingleColorScreenshotImage(
1688   image,
1689   width,
1690   height,
1691   { r, g, b }
1692 ) {
1693   info(`Assert ${image.src} content`);
1694   const ratio = await SpecialPowers.spawn(
1695     gBrowser.selectedBrowser,
1696     [],
1697     () => content.wrappedJSObject.devicePixelRatio
1698   );
1700   is(
1701     image.width,
1702     ratio * width,
1703     `node screenshot has the expected width (dpr = ${ratio})`
1704   );
1705   is(
1706     image.height,
1707     height * ratio,
1708     `node screenshot has the expected height (dpr = ${ratio})`
1709   );
1711   const color = colorAt(image, 0, 0);
1712   is(color.r, r, "node screenshot has the expected red component");
1713   is(color.g, g, "node screenshot has the expected green component");
1714   is(color.b, b, "node screenshot has the expected blue component");
1718  * Check that the provided image has the expected color at a given position
1719  */
1720 function checkImageColorAt({ image, x = 0, y, expectedColor, label }) {
1721   const color = colorAt(image, x, y);
1722   is(`rgb(${Object.values(color).join(", ")})`, expectedColor, label);
1726  * Wait until the store has reached a state that matches the predicate.
1727  * @param Store store
1728  *        The Redux store being used.
1729  * @param function predicate
1730  *        A function that returns true when the store has reached the expected
1731  *        state.
1732  * @return Promise
1733  *         Resolved once the store reaches the expected state.
1734  */
1735 function waitUntilState(store, predicate) {
1736   return new Promise(resolve => {
1737     const unsubscribe = store.subscribe(check);
1739     info(`Waiting for state predicate "${predicate}"`);
1740     function check() {
1741       if (predicate(store.getState())) {
1742         info(`Found state predicate "${predicate}"`);
1743         unsubscribe();
1744         resolve();
1745       }
1746     }
1748     // Fire the check immediately in case the action has already occurred
1749     check();
1750   });
1754  * Wait for a specific action type to be dispatched.
1756  * If the action is async and defines a `status` property, this helper will wait
1757  * for the status to reach either "error" or "done".
1759  * @param {Object} store
1760  *        Redux store where the action should be dispatched.
1761  * @param {String} actionType
1762  *        The actionType to wait for.
1763  * @param {Number} repeat
1764  *        Optional, number of time the action is expected to be dispatched.
1765  *        Defaults to 1
1766  * @return {Promise}
1767  */
1768 function waitForDispatch(store, actionType, repeat = 1) {
1769   let count = 0;
1770   return new Promise(resolve => {
1771     store.dispatch({
1772       type: "@@service/waitUntil",
1773       predicate: action => {
1774         const isDone =
1775           !action.status ||
1776           action.status === "done" ||
1777           action.status === "error";
1779         if (action.type === actionType && isDone && ++count == repeat) {
1780           return true;
1781         }
1783         return false;
1784       },
1785       run: (dispatch, getState, action) => {
1786         resolve(action);
1787       },
1788     });
1789   });
1793  * Retrieve a browsing context in nested frames.
1795  * @param {BrowsingContext|XULBrowser} browsingContext
1796  *        The topmost browsing context under which we should search for the
1797  *        browsing context.
1798  * @param {Array<String>} selectors
1799  *        Array of CSS selectors that form a path to a specific nested frame.
1800  * @return {BrowsingContext} The nested browsing context.
1801  */
1802 async function getBrowsingContextInFrames(browsingContext, selectors) {
1803   let context = browsingContext;
1805   if (!Array.isArray(selectors)) {
1806     throw new Error(
1807       "getBrowsingContextInFrames called with an invalid selectors argument"
1808     );
1809   }
1811   if (selectors.length === 0) {
1812     throw new Error(
1813       "getBrowsingContextInFrames called with an empty selectors array"
1814     );
1815   }
1817   const clonedSelectors = [...selectors];
1818   while (clonedSelectors.length) {
1819     const selector = clonedSelectors.shift();
1820     context = await SpecialPowers.spawn(context, [selector], _selector => {
1821       return content.document.querySelector(_selector).browsingContext;
1822     });
1823   }
1825   return context;
1829  * Synthesize a mouse event on an element, after ensuring that it is visible
1830  * in the viewport.
1832  * @param {String|Array} selector: The node selector to get the node target for the event.
1833  *        To target an element in a specific iframe, pass an array of CSS selectors
1834  *        (e.g. ["iframe", ".el-in-iframe"])
1835  * @param {number} x
1836  * @param {number} y
1837  * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse
1838  */
1839 async function safeSynthesizeMouseEventInContentPage(
1840   selector,
1841   x,
1842   y,
1843   options = {}
1844 ) {
1845   let context = gBrowser.selectedBrowser.browsingContext;
1847   // If an array of selector is passed, we need to retrieve the context in which the node
1848   // lives in.
1849   if (Array.isArray(selector)) {
1850     if (selector.length === 1) {
1851       selector = selector[0];
1852     } else {
1853       context = await getBrowsingContextInFrames(
1854         context,
1855         // only pass the iframe path
1856         selector.slice(0, -1)
1857       );
1858       // retrieve the last item of the selector, which should be the one for the node we want.
1859       selector = selector.at(-1);
1860     }
1861   }
1863   await scrollContentPageNodeIntoView(context, selector);
1864   BrowserTestUtils.synthesizeMouse(selector, x, y, options, context);
1868  * Synthesize a mouse event at the center of an element, after ensuring that it is visible
1869  * in the viewport.
1871  * @param {String|Array} selector: The node selector to get the node target for the event.
1872  *        To target an element in a specific iframe, pass an array of CSS selectors
1873  *        (e.g. ["iframe", ".el-in-iframe"])
1874  * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse
1875  */
1876 async function safeSynthesizeMouseEventAtCenterInContentPage(
1877   selector,
1878   options = {}
1879 ) {
1880   let context = gBrowser.selectedBrowser.browsingContext;
1882   // If an array of selector is passed, we need to retrieve the context in which the node
1883   // lives in.
1884   if (Array.isArray(selector)) {
1885     if (selector.length === 1) {
1886       selector = selector[0];
1887     } else {
1888       context = await getBrowsingContextInFrames(
1889         context,
1890         // only pass the iframe path
1891         selector.slice(0, -1)
1892       );
1893       // retrieve the last item of the selector, which should be the one for the node we want.
1894       selector = selector.at(-1);
1895     }
1896   }
1898   await scrollContentPageNodeIntoView(context, selector);
1899   BrowserTestUtils.synthesizeMouseAtCenter(selector, options, context);
1903  * Scroll into view an element in the content page matching the passed selector
1905  * @param {BrowsingContext} browsingContext: The browsing context the element lives in.
1906  * @param {String} selector: The node selector to get the node to scroll into view
1907  * @returns {Promise}
1908  */
1909 function scrollContentPageNodeIntoView(browsingContext, selector) {
1910   return SpecialPowers.spawn(
1911     browsingContext,
1912     [selector],
1913     function (innerSelector) {
1914       const node =
1915         content.wrappedJSObject.document.querySelector(innerSelector);
1916       node.scrollIntoView();
1917     }
1918   );
1922  * Change the zoom level of the selected page.
1924  * @param {Number} zoomLevel
1925  */
1926 function setContentPageZoomLevel(zoomLevel) {
1927   gBrowser.selectedBrowser.fullZoom = zoomLevel;
1931  * Wait for the next DOCUMENT_EVENT dom-complete resource on a top-level target
1933  * @param {Object} commands
1934  * @return {Promise<Object>}
1935  *         Return a promise which resolves once we fully settle the resource listener.
1936  *         You should await for its resolution before doing the action which may fire
1937  *         your resource.
1938  *         This promise will resolve with an object containing a `onDomCompleteResource` property,
1939  *         which is also a promise, that will resolve once a "top-level" DOCUMENT_EVENT dom-complete
1940  *         is received.
1941  */
1942 async function waitForNextTopLevelDomCompleteResource(commands) {
1943   const { onResource: onDomCompleteResource } =
1944     await commands.resourceCommand.waitForNextResource(
1945       commands.resourceCommand.TYPES.DOCUMENT_EVENT,
1946       {
1947         ignoreExistingResources: true,
1948         predicate: resource =>
1949           resource.name === "dom-complete" && resource.targetFront.isTopLevel,
1950       }
1951     );
1952   return { onDomCompleteResource };
1956  * Wait for the provided context to have a valid presShell. This can be useful
1957  * for tests which try to create popup panels or interact with the document very
1958  * early.
1960  * @param {BrowsingContext} context
1961  **/
1962 function waitForPresShell(context) {
1963   return SpecialPowers.spawn(context, [], async () => {
1964     const winUtils = SpecialPowers.getDOMWindowUtils(content);
1965     await ContentTaskUtils.waitForCondition(() => {
1966       try {
1967         return !!winUtils.getPresShellId();
1968       } catch (e) {
1969         return false;
1970       }
1971     }, "Waiting for a valid presShell");
1972   });
1976  * In tests using Fluent localization, it is preferable to match DOM elements using
1977  * a message ID rather than the raw string as:
1979  *  1. It allows testing infrastructure to be multilingual if needed.
1980  *  2. It isolates the tests from localization changes.
1982  * @param {Array<string>} resourceIds A list of .ftl files to load.
1983  * @returns {(id: string, args?: Record<string, FluentVariable>) => string}
1984  */
1985 async function getFluentStringHelper(resourceIds) {
1986   const locales = Services.locale.appLocalesAsBCP47;
1987   const generator = L10nRegistry.getInstance().generateBundles(
1988     locales,
1989     resourceIds
1990   );
1992   const bundles = [];
1993   for await (const bundle of generator) {
1994     bundles.push(bundle);
1995   }
1997   const reactLocalization = new FluentReact.ReactLocalization(bundles);
1999   /**
2000    * Get the string from a message id. It throws when the message is not found.
2001    *
2002    * @param {string} id
2003    * @param {string} attributeName: attribute name if you need to access a specific attribute
2004    *                 defined in the fluent string, e.g. setting "title" for this param
2005    *                 will retrieve the `title` string in
2006    *                    compatibility-issue-browsers-list =
2007    *                      .title = This is the title
2008    * @param {Record<string, FluentVariable>} [args] optional
2009    * @returns {string}
2010    */
2011   return (id, attributeName, args) => {
2012     let string;
2014     if (!attributeName) {
2015       string = reactLocalization.getString(id, args);
2016     } else {
2017       for (const bundle of reactLocalization.bundles) {
2018         const msg = bundle.getMessage(id);
2019         if (msg?.attributes[attributeName]) {
2020           string = bundle.formatPattern(
2021             msg.attributes[attributeName],
2022             args,
2023             []
2024           );
2025           break;
2026         }
2027       }
2028     }
2030     if (!string) {
2031       throw new Error(
2032         `Could not find a string for "${id}"${
2033           attributeName ? ` and attribute "${attributeName}")` : ""
2034         }. Was the correct resource bundle loaded?`
2035       );
2036     }
2037     return string;
2038   };
2042  * Open responsive design mode for the given tab.
2043  */
2044 async function openRDM(tab, { waitForDeviceList = true } = {}) {
2045   info("Opening responsive design mode");
2046   const manager = ResponsiveUIManager;
2047   const ui = await manager.openIfNeeded(tab.ownerGlobal, tab, {
2048     trigger: "test",
2049   });
2050   info("Responsive design mode opened");
2052   await ResponsiveMessageHelper.wait(ui.toolWindow, "post-init");
2053   info("Responsive design initialized");
2055   await waitForRDMLoaded(ui, { waitForDeviceList });
2057   return { ui, manager };
2060 async function waitForRDMLoaded(ui, { waitForDeviceList = true } = {}) {
2061   // Always wait for the viewport to be added.
2062   const { store } = ui.toolWindow;
2063   await waitUntilState(store, state => state.viewports.length == 1);
2065   if (waitForDeviceList) {
2066     // Wait until the device list has been loaded.
2067     await waitUntilState(
2068       store,
2069       state => state.devices.listState == localTypes.loadableState.LOADED
2070     );
2071   }
2075  * Close responsive design mode for the given tab.
2076  */
2077 async function closeRDM(tab, options) {
2078   info("Closing responsive design mode");
2079   const manager = ResponsiveUIManager;
2080   await manager.closeIfNeeded(tab.ownerGlobal, tab, options);
2081   info("Responsive design mode closed");
2084 function getInputStream(data) {
2085   const BufferStream = Components.Constructor(
2086     "@mozilla.org/io/arraybuffer-input-stream;1",
2087     "nsIArrayBufferInputStream",
2088     "setData"
2089   );
2090   const buffer = new TextEncoder().encode(data).buffer;
2091   return new BufferStream(buffer, 0, buffer.byteLength);
2095  * Wait for a specific target to have been fully processed by targetCommand.
2097  * @param {Commands} commands
2098  *        The commands instance
2099  * @param {Function} isExpectedTargetFn
2100  *        Predicate which will be called with a target front argument. Should
2101  *        return true if the target front is the expected one, false otherwise.
2102  * @return {Promise}
2103  *         Promise which resolves when a target matching `isExpectedTargetFn`
2104  *         has been processed by targetCommand.
2105  */
2106 function waitForTargetProcessed(commands, isExpectedTargetFn) {
2107   return new Promise(resolve => {
2108     const onProcessed = targetFront => {
2109       try {
2110         if (isExpectedTargetFn(targetFront)) {
2111           commands.targetCommand.off("processed-available-target", onProcessed);
2112           resolve();
2113         }
2114       } catch {
2115         // Ignore errors from isExpectedTargetFn.
2116       }
2117     };
2119     commands.targetCommand.on("processed-available-target", onProcessed);
2120   });
2124  * Instantiate a HTTP Server that serves files from a given test folder.
2125  * The test folder should be made of multiple sub folder named: v1, v2, v3,...
2126  * We will serve the content from one of these sub folder
2127  * and switch to the next one, each time `httpServer.switchToNextVersion()`
2128  * is called.
2130  * @return Object Test server with two functions:
2131  *   - urlFor(path)
2132  *     Returns the absolute url for a given file.
2133  *   - switchToNextVersion()
2134  *     Start serving files from the next available sub folder.
2135  *   - backToFirstVersion()
2136  *     When running more than one test, helps restart from the first folder.
2137  */
2138 function createVersionizedHttpTestServer(testFolderName) {
2139   const httpServer = createTestHTTPServer();
2141   let currentVersion = 1;
2143   httpServer.registerPrefixHandler("/", async (request, response) => {
2144     response.processAsync();
2145     response.setStatusLine(request.httpVersion, 200, "OK");
2146     if (request.path.endsWith(".js")) {
2147       response.setHeader("Content-Type", "application/javascript");
2148     } else if (request.path.endsWith(".js.map")) {
2149       response.setHeader("Content-Type", "application/json");
2150     }
2151     if (request.path == "/" || request.path.endsWith(".html")) {
2152       response.setHeader("Content-Type", "text/html");
2153     }
2154     // If a query string is passed, lookup with a matching file, if available
2155     // The '?' is replaced by '.'
2156     let fetchResponse;
2158     if (request.queryString) {
2159       const url = `${URL_ROOT_SSL}${testFolderName}/v${currentVersion}${request.path}.${request.queryString}`;
2160       try {
2161         fetchResponse = await fetch(url);
2162         // Log this only if the request succeed
2163         info(`[test-http-server] serving: ${url}`);
2164       } catch (e) {
2165         // Ignore any error and proceed without the query string
2166         fetchResponse = null;
2167       }
2168     }
2170     if (!fetchResponse) {
2171       const url = `${URL_ROOT_SSL}${testFolderName}/v${currentVersion}${request.path}`;
2172       info(`[test-http-server] serving: ${url}`);
2173       fetchResponse = await fetch(url);
2174     }
2176     // Ensure forwarding the response headers generated by the other http server
2177     // (this can be especially useful when query .sjs files)
2178     for (const [name, value] of fetchResponse.headers.entries()) {
2179       response.setHeader(name, value);
2180     }
2182     // Override cache settings so that versionized requests are never cached
2183     // and we get brand new content for any request.
2184     response.setHeader("Cache-Control", "no-store");
2186     const text = await fetchResponse.text();
2187     response.write(text);
2188     response.finish();
2189   });
2191   return {
2192     switchToNextVersion() {
2193       currentVersion++;
2194     },
2195     backToFirstVersion() {
2196       currentVersion = 1;
2197     },
2198     urlFor(path) {
2199       const port = httpServer.identity.primaryPort;
2200       return `http://localhost:${port}/${path}`;
2201     },
2202   };
2206  * Fake clicking a link and return the URL we would have navigated to.
2207  * This function should be used to check external links since we can't access
2208  * network in tests.
2209  * This can also be used to test that a click will not be fired.
2211  * @param ElementNode element
2212  *        The <a> element we want to simulate click on.
2213  * @returns Promise
2214  *          A Promise that is resolved when the link click simulation occured or
2215  *          when the click is not dispatched.
2216  *          The promise resolves with an object that holds the following properties
2217  *          - link: url of the link or null(if event not fired)
2218  *          - where: "tab" if tab is active or "tabshifted" if tab is inactive
2219  *            or null(if event not fired)
2220  */
2221 function simulateLinkClick(element) {
2222   const browserWindow = Services.wm.getMostRecentWindow(
2223     gDevTools.chromeWindowType
2224   );
2226   const onOpenLink = new Promise(resolve => {
2227     const openLinkIn = (link, where) => resolve({ link, where });
2228     sinon.replace(browserWindow, "openTrustedLinkIn", openLinkIn);
2229     sinon.replace(browserWindow, "openWebLinkIn", openLinkIn);
2230   });
2232   element.click();
2234   // Declare a timeout Promise that we can use to make sure spied methods were not called.
2235   const onTimeout = new Promise(function (resolve) {
2236     setTimeout(() => {
2237       resolve({ link: null, where: null });
2238     }, 1000);
2239   });
2241   const raceResult = Promise.race([onOpenLink, onTimeout]);
2242   sinon.restore();
2243   return raceResult;
2247  * Since the MDN data is updated frequently, it might happen that the properties used in
2248  * this test are not in the dataset anymore/now have URLs.
2249  * This function will return properties in the dataset that don't have MDN url so you
2250  * can easily find a replacement.
2251  */
2252 function logCssCompatDataPropertiesWithoutMDNUrl() {
2253   const cssPropertiesCompatData = require("resource://devtools/shared/compatibility/dataset/css-properties.json");
2255   function walk(node) {
2256     for (const propertyName in node) {
2257       const property = node[propertyName];
2258       if (property.__compat) {
2259         if (!property.__compat.mdn_url) {
2260           dump(
2261             `"${propertyName}" - MDN URL: ${
2262               property.__compat.mdn_url || "❌"
2263             } - Spec URL: ${property.__compat.spec_url || "❌"}\n`
2264           );
2265         }
2266       } else if (typeof property == "object") {
2267         walk(property);
2268       }
2269     }
2270   }
2271   walk(cssPropertiesCompatData);
2275  * Craft a CssProperties instance without involving RDP for tests
2276  * manually spawning OutputParser, CssCompleter, Editor...
2278  * Otherwise this should instead be fetched from CssPropertiesFront.
2280  * @return {CssProperties}
2281  */
2282 function getClientCssProperties() {
2283   const {
2284     generateCssProperties,
2285   } = require("resource://devtools/server/actors/css-properties.js");
2286   const {
2287     CssProperties,
2288     normalizeCssData,
2289   } = require("resource://devtools/client/fronts/css-properties.js");
2290   return new CssProperties(
2291     normalizeCssData({ properties: generateCssProperties(document) })
2292   );
2296  * Helper method to stop a Service Worker promptly.
2298  * @param {String} workerUrl
2299  *        Absolute Worker URL to stop.
2300  */
2301 async function stopServiceWorker(workerUrl) {
2302   info(`Stop Service Worker: ${workerUrl}\n`);
2304   // Help the SW to be immediately destroyed after unregistering it.
2305   Services.prefs.setIntPref("dom.serviceWorkers.idle_timeout", 0);
2307   const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
2308     Ci.nsIServiceWorkerManager
2309   );
2310   // Unfortunately we can't use swm.getRegistrationByPrincipal, as it requires a "scope", which doesn't seem to be the worker URL.
2311   // So let's use getAllRegistrations to find the nsIServiceWorkerInfo in order to:
2312   // - retrieve its active worker,
2313   // - call attach+detachDebugger,
2314   // - reset the idle timeout.
2315   // This way, the unregister instruction is immediate, thanks to the 0 dom.serviceWorkers.idle_timeout we set at the beginning of the function
2316   const registrations = swm.getAllRegistrations();
2317   let matchedInfo;
2318   for (let i = 0; i < registrations.length; i++) {
2319     const info = registrations.queryElementAt(
2320       i,
2321       Ci.nsIServiceWorkerRegistrationInfo
2322     );
2323     // Lookup for an exact URL match.
2324     if (info.scriptSpec === workerUrl) {
2325       matchedInfo = info;
2326       break;
2327     }
2328   }
2329   ok(!!matchedInfo, "Found the service worker info");
2331   info("Wait for the worker to be active");
2332   await waitFor(() => matchedInfo.activeWorker, "Wait for the SW to be active");
2334   // We need to attach+detach the debugger in order to reset the idle timeout.
2335   // Otherwise the worker would still be waiting for a previously registered timeout
2336   // which would be the 0ms one we set by tweaking the preference.
2337   function resetWorkerTimeout(worker) {
2338     worker.attachDebugger();
2339     worker.detachDebugger();
2340   }
2341   resetWorkerTimeout(matchedInfo.activeWorker);
2342   // Also reset all the other possible worker instances
2343   if (matchedInfo.evaluatingWorker) {
2344     resetWorkerTimeout(matchedInfo.evaluatingWorker);
2345   }
2346   if (matchedInfo.installingWorker) {
2347     resetWorkerTimeout(matchedInfo.installingWorker);
2348   }
2349   if (matchedInfo.waitingWorker) {
2350     resetWorkerTimeout(matchedInfo.waitingWorker);
2351   }
2352   // Reset this preference in order to ensure other SW are not immediately destroyed.
2353   Services.prefs.clearUserPref("dom.serviceWorkers.idle_timeout");
2355   // Spin the event loop to ensure the worker had time to really be shut down.
2356   await wait(0);
2358   return matchedInfo;
2362  * Helper method to stop and unregister a Service Worker promptly.
2364  * @param {String} workerUrl
2365  *        Absolute Worker URL to unregister.
2366  */
2367 async function unregisterServiceWorker(workerUrl) {
2368   const swInfo = await stopServiceWorker(workerUrl);
2370   info(`Unregister Service Worker: ${workerUrl}\n`);
2371   // Now call unregister on that worker so that it can be destroyed immediately
2372   const swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService(
2373     Ci.nsIServiceWorkerManager
2374   );
2375   const unregisterSuccess = await new Promise(resolve => {
2376     swm.unregister(
2377       swInfo.principal,
2378       {
2379         unregisterSucceeded(success) {
2380           resolve(success);
2381         },
2382       },
2383       swInfo.scope
2384     );
2385   });
2386   ok(unregisterSuccess, "Service worker successfully unregistered");