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 */
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;
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,...)
32 useDistinctSystemPrincipalLoader,
33 releaseDistinctSystemPrincipalLoader,
34 } = ChromeUtils.importESModule(
35 "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
38 const loader = useDistinctSystemPrincipalLoader(requester);
39 registerCleanupFunction(() =>
40 releaseDistinctSystemPrincipalLoader(requester)
43 const { allocationTracker } = loader.require(
44 "resource://devtools/shared/test-helpers/allocation-tracker.js"
46 const tracker = allocationTracker({ watchAllGlobals: true });
47 registerCleanupFunction(() => {
48 if (DEBUG_ALLOCATIONS == "normal") {
50 } else if (DEBUG_ALLOCATIONS == "verbose") {
51 tracker.logAllocationSites();
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");
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,...)
78 useDistinctSystemPrincipalLoader,
79 releaseDistinctSystemPrincipalLoader,
80 } = ChromeUtils.importESModule(
81 "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
84 const loader = useDistinctSystemPrincipalLoader(requester);
86 const stepper = loader.require(
87 "resource://devtools/shared/test-helpers/test-stepper.js"
89 stepper.start(globalThis, gTestPath, DEBUG_STEP);
90 registerCleanupFunction(() => {
92 releaseDistinctSystemPrincipalLoader(requester);
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,...)
102 useDistinctSystemPrincipalLoader,
103 releaseDistinctSystemPrincipalLoader,
104 } = ChromeUtils.importESModule(
105 "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
107 const requester = {};
108 const loader = useDistinctSystemPrincipalLoader(requester);
110 const lineTracer = loader.require(
111 "resource://devtools/shared/test-helpers/test-line-tracer.js"
113 lineTracer.start(globalThis, gTestPath, DEBUG_TRACE_LINE);
114 registerCleanupFunction(() => {
116 releaseDistinctSystemPrincipalLoader(requester);
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.
134 } = require("resource://devtools/client/framework/devtools.js");
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(
146 "ResponsiveUIManager",
147 "resource://devtools/client/responsive/manager.js"
149 loader.lazyRequireGetter(
152 "resource://devtools/client/responsive/types.js"
154 loader.lazyRequireGetter(
156 "ResponsiveMessageHelper",
157 "resource://devtools/client/responsive/utils/message.js"
160 loader.lazyRequireGetter(
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/"
208 Services.scriptloader.loadSubScript(
209 "chrome://mochitests/content/browser/devtools/client/shared/test/telemetry-test-helpers.js",
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"
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
228 waitForExplicitFinish();
231 var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
233 registerCleanupFunction(function () {
235 DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT
239 "Should have had the expected number of DevToolsUtils.assert() failures." +
241 EXPECTED_DTU_ASSERT_FAILURE_COUNT +
243 DevToolsUtils.assertionFailureCount
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.
254 function onConsoleMessage(subject) {
255 const message = subject.wrappedJSObject.arguments[0];
257 if (message && /Failed propType/.test(message.toString())) {
262 const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(
263 Ci.nsIConsoleAPIStorage
266 ConsoleAPIStorage.addLogEventListener(
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",
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
292 Services.prefs.setBoolPref(
293 "devtools.inspector.simple-highlighters.message-dismissed",
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"
308 Services.prefs.clearUserPref(
309 "devtools.inspector.simple-highlighters.message-dismissed"
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 });
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);
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
335 // All connections must be cleaned up by the test when the test ends.
338 } = require("resource://devtools/server/devtools-server.js");
340 !DevToolsServer.hasConnection(),
341 "The main process DevToolsServer has no pending connection when the test ends"
343 // If there is still open connection, close all of them so that following tests
345 if (DevToolsServer.hasConnection()) {
346 for (const conn of Object.values(DevToolsServer._connections)) {
352 async function safeCloseBrowserConsole({ clearOutput = false } = {}) {
353 const hud = BrowserConsoleManager.getBrowserConsole();
359 info("Clear the browser console output");
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"));
366 await ui.clearOutput(true);
367 await Promise.all(promises);
368 info("Browser console cleared");
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
375 waitForAllTargetsToBeAttached(hud.commands.targetCommand),
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.
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"
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 },
421 Services.obs.addObserver(
422 actorRegistryObserver,
423 "devtools-server-initialized"
426 const unloadListener = () => {
427 Services.cpmm.removeMessageListener(
428 "remove-devtools-testactor-observer",
431 Services.obs.removeObserver(
432 actorRegistryObserver,
433 "devtools-server-initialized"
436 Services.cpmm.addMessageListener(
437 "remove-devtools-testactor-observer",
443 const highlighterTestActorBootstrapScript =
444 "data:,(" + highlighterTestActorBootstrap + ")()";
445 Services.ppmm.loadProcessScript(
446 highlighterTestActorBootstrapScript,
447 // Load this script in all processes (created or to be created)
451 registerCleanupFunction(() => {
452 Services.ppmm.broadcastAsyncMessage("remove-devtools-testactor-observer");
453 Services.ppmm.removeDelayedProcessScript(
454 highlighterTestActorBootstrapScript
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}
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(
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
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.
493 * @returns {HighlighterTestFront}
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
510 function waitForAllTargetsToBeAttached(targetCommand) {
511 return Promise.allSettled(
513 .getAllTargets(targetCommand.ALL_TYPES)
514 .map(target => target.initialized)
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
529 async function addTab(url, options = {}) {
530 info("Adding a new tab with URL: " + url);
538 const { gBrowser } = options.window ? options.window : window;
540 const tab = BrowserTestUtils.addTab(gBrowser, url, {
546 gBrowser.selectedTab = tab;
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");
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.
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);
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.
581 async function reloadBrowser({
582 browser = gBrowser.selectedBrowser,
586 return navigateTo(browser.currentURI.spec, {
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
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
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.
613 async function navigateTo(
616 browser = gBrowser.selectedBrowser,
621 const waitForDevToolsReload = await watchForDevToolsReload(browser, {
626 uri = uri.replaceAll("\n", "");
627 info(`Navigating to "${uri}"`);
629 const onBrowserLoaded = BrowserTestUtils.browserLoaded(
633 // resolve on this specific page to load (if null, it would be any page load)
635 // loadedUrl is encoded, while uri might not be.
636 return loadedUrl === uri || decodeURI(loadedUrl) === uri;
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));
646 BrowserTestUtils.startLoadingURIString(browser, uri);
650 info(`Waiting for page to be loaded…`);
651 await onBrowserLoaded;
652 info(`→ page loaded`);
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:
664 * - Toolbox commands 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:
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();
689 async function watchForDevToolsReload(
691 { isErrorPage = false, waitForLoad = true } = {}
693 const waitForToolboxReload = await _watchForToolboxReload(browser, {
697 const waitForResponsiveReload = await _watchForResponsiveReload(browser, {
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();
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
716 async function _watchForToolboxReload(
718 { isErrorPage, waitForLoad } = {}
720 const tab = gBrowser.getTabForBrowser(browser);
722 const toolbox = gDevTools.getToolboxForTab(tab);
725 // No toolbox to wait for
726 return function () {};
729 const waitForCurrentPanelReload = watchForCurrentPanelReload(toolbox);
730 const waitForToolboxCommandsReload = await watchForCommandsReload(
732 { isErrorPage, waitForLoad }
734 const checkTargetSwitching = await watchForTargetSwitching(
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`);
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
760 async function _watchForResponsiveReload(
762 { isErrorPage, waitForLoad } = {}
764 const tab = gBrowser.getTabForBrowser(browser);
765 const ui = ResponsiveUIManager.getResponsiveUIForTab(tab);
768 // No responsive UI to wait for
769 return function () {};
772 const onResponsiveTargetSwitch = ui.once("responsive-ui-target-switch-done");
773 const waitForResponsiveCommandsReload = await watchForCommandsReload(
775 { isErrorPage, waitForLoad }
777 const checkTargetSwitching = await watchForTargetSwitching(
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;
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.
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.
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
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
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));
835 waitForPanels.map(async watchPanel => {
836 // Wait for all panels to be reloaded.
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.");
858 info("Waiting for new root.");
861 info("Waiting for inspector to update after new-root event.");
864 info("Waiting for inspector updates after page reload");
868 ["netmonitor", "accessibility", "webconsole", "jsdebugger"].includes(toolId)
870 const onReloaded = panel.once("reloaded");
871 return async function () {
872 info(`Waiting for ${toolId} updates after page reload`);
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 !!!
889 async function watchForCommandsReload(
891 { isErrorPage = false, waitForLoad = true } = {}
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
912 const { onResource: onTopLevelDomEvent } =
913 await commands.resourceCommand.waitForNextResource(
914 commands.resourceCommand.TYPES.DOCUMENT_EVENT,
916 ignoreExistingResources: true,
917 predicate: resource =>
918 resource.targetFront.isTopLevel &&
919 resource.name === documentEventName,
923 return async function (isTargetSwitching) {
924 if (typeof isTargetSwitching === "undefined") {
925 throw new Error("isTargetSwitching was not provided to the wait method");
928 if (isTargetSwitching) {
929 info(`Waiting for target switch…`);
930 await onTargetSwitched;
931 info(`→ switched-target emitted`);
934 info(`Waiting for '${documentEventName}' resource…`);
935 await onTopLevelDomEvent;
936 info(`→ '${documentEventName}' resource emitted`);
938 return isTargetSwitching;
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.
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;
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;
969 targetFollowsWindowLifecycle || switchedProcess || switchedBrowsingContext
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.
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;
997 function isFissionEnabled() {
998 return SpecialPowers.useRemoteSubframes;
1001 function isEveryFrameTargetEnabled() {
1002 return Services.prefs.getBoolPref(
1003 "devtools.every-frame-target.enabled",
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 }.
1015 async function openInspectorForURL(url, hostType) {
1016 const tab = await addTab(url);
1017 const { inspector, toolbox, highlighterTestFront } = await openInspector(
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
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);
1041 altKey: shortcut.alt,
1042 ctrlKey: shortcut.ctrl,
1043 metaKey: shortcut.meta,
1044 shiftKey: shortcut.shift,
1046 if (shortcut.keyCode) {
1047 keyEvent.keyCode = shortcut.keyCode;
1050 info("Synthesizing key shortcut: " + key);
1051 EventUtils.synthesizeKey(shortcut.key || "", keyEvent, target);
1054 var waitForTime = DevToolsUtils.waitForTime;
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
1070 * @return A promise that resolves when the time is passed
1073 return new Promise(resolve => {
1074 setTimeout(resolve, ms);
1075 info("Waiting " + ms / 1000 + " seconds.");
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;`.
1095 * A promise that is resolved with the result of the condition.
1097 async function waitFor(condition, message = "", interval = 10, maxTries = 500) {
1098 // Update interval & maxTries if overrides are defined on the waitFor object.
1100 typeof waitFor.overrideIntervalForTestFile !== "undefined"
1101 ? waitFor.overrideIntervalForTestFile
1104 typeof waitFor.overrideMaxTriesForTestFile !== "undefined"
1105 ? waitFor.overrideMaxTriesForTestFile
1109 const value = await BrowserTestUtils.waitForCondition(
1117 const errorMessage = `Failed waitFor(): ${message} \nFailed condition: ${condition} \nException Message: ${e}`;
1118 throw new Error(errorMessage);
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
1135 function waitForNEvents(target, eventName, numTimes, useCapture = false) {
1136 info("Waiting for event: '" + eventName + "' on " + target + ".");
1140 return new Promise(resolve => {
1141 for (const [add, remove] of [
1143 ["addEventListener", "removeEventListener"],
1144 ["addListener", "removeListener"],
1145 ["addMessageListener", "removeMessageListener"],
1147 if (add in target && remove in target) {
1150 function onEvent(...args) {
1151 if (typeof info === "function") {
1152 info("Got event: '" + eventName + "' on " + target + ".");
1155 if (++count == numTimes) {
1156 target[remove](eventName, onEvent, useCapture);
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
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
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();
1195 observer.observe(target, {
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
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.
1226 * - "helper_attributes_test_runner.js"
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.
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);
1246 if (!toolId || (toolId && toolbox.getPanel(toolId))) {
1247 info("Toolbox is already opened");
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");
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.
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
1282 async function closeTabAndToolbox(tab = gBrowser.selectedTab) {
1283 if (gDevTools.hasToolboxForTab(tab)) {
1284 await gDevTools.closeToolboxForTab(tab);
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
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.
1311 function waitUntil(predicate, interval = 10) {
1313 return Promise.resolve(true);
1315 return new Promise(resolve => {
1316 setTimeout(function () {
1317 waitUntil(predicate, interval).then(() => resolve(true));
1323 * Variant of waitUntil that accepts a predicate returning a promise.
1325 async function asyncWaitUntil(predicate, interval = 10) {
1326 let success = await predicate();
1328 // Wait for X milliseconds.
1329 await new Promise(resolve => setTimeout(resolve, interval));
1330 // Test the predicate again.
1331 success = await predicate();
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.
1348 * A Promise object that is resolved after the popuphidden event
1349 * callback is invoked.
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());
1363 function onPopupHidden() {
1364 info("onPopupHidden");
1365 popup.removeEventListener("popuphidden", onPopupHidden);
1367 onHidden && onHidden();
1372 popup.addEventListener("popupshown", onPopupShown);
1374 info("wait for the context menu to open");
1375 synthesizeContextMenuEvent(button);
1379 function synthesizeContextMenuEvent(el) {
1380 el.scrollIntoView();
1381 const eventDetails = { type: "contextmenu", button: 2 };
1382 EventUtils.synthesizeMouse(
1387 el.ownerDocument.defaultView
1392 * Promise wrapper around SimpleTest.waitForClipboard
1394 function waitForClipboardPromise(setup, expected) {
1395 return new Promise((resolve, reject) => {
1396 SimpleTest.waitForClipboard(expected, setup, resolve, reject);
1401 * Simple helper to push a temporary preference. Wrapper on SpecialPowers
1402 * pushPrefEnv that returns a promise resolving when the preferences have been
1405 * @param {String} preferenceName
1406 * The name of the preference to updated
1408 * The preference value, type can vary
1409 * @return {Promise} resolves when the preferences have been updated
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).
1424 function emptyClipboard() {
1425 const clipboard = Services.clipboard;
1426 clipboard.emptyClipboard(clipboard.kGlobalClipboard);
1430 * Check if the current operating system is Windows.
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
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}
1450 function createTestHTTPServer() {
1451 const { HttpServer } = ChromeUtils.importESModule(
1452 "resource://testing-common/httpd.sys.mjs"
1454 const server = new HttpServer();
1456 registerCleanupFunction(async function cleanup() {
1457 await new Promise(resolve => server.stop(resolve));
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
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;
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,
1489 // eslint-disable-next-line no-shadow
1490 const { require } = ChromeUtils.importESModule(
1491 "resource://devtools/shared/loader/Loader.sys.mjs"
1495 } = require("resource://devtools/server/actors/utils/actor-registry.js");
1496 ActorRegistry.registerModule(args.url, args.options);
1502 * Move the provided Window to the provided left, top coordinates and wait for
1503 * the window position to be updated.
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(() => {
1519 `Wait for window screenLeft and screenTop to be updated: (${win.screenLeft}, ${win.screenTop})`
1521 return win.screenLeft === left && win.screenTop === top;
1525 function getCurrentTestFilePath() {
1526 return gTestPath.replace("chrome://mochitests/content/browser/", "");
1530 * Unregister all registered service workers.
1532 * @param {DevToolsClient} client
1534 async function unregisterAllServiceWorkers(client) {
1535 info("Wait until all workers have a valid registrationFront");
1537 await asyncWaitUntil(async function () {
1538 workers = await client.mainRoot.listAllWorkers();
1539 const allWorkersRegistered = workers.service.every(
1540 worker => !!worker.registrationFront
1542 return allWorkersRegistered;
1545 info("Unregister all service workers");
1546 const promises = [];
1547 for (const worker of workers.service) {
1548 promises.push(worker.registrationFront.unregister());
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
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
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;
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)
1594 async function waitUntilScreenshot({ isWindowPrivate = false } = {}) {
1595 const { Downloads } = ChromeUtils.importESModule(
1596 "resource://gre/modules/Downloads.sys.mjs"
1598 const list = await Downloads.getList(Downloads.ALL);
1600 return new Promise(function (resolve) {
1602 onDownloadAdded: async download => {
1603 await download.whenSucceeded();
1604 if (allDownloads.includes(download)) {
1609 !!download.source.isPrivate,
1611 `The download occured in the expected${
1612 isWindowPrivate ? " private" : ""
1616 allDownloads.push(download);
1617 resolve(download.target.path);
1618 list.removeView(view);
1627 * Clear all the download references.
1629 async function resetDownloads() {
1630 info("Reset downloads");
1631 const { Downloads } = ChromeUtils.importESModule(
1632 "resource://gre/modules/Downloads.sys.mjs"
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);
1644 * Return a screenshot of the currently selected node in the inspector (using the internal
1645 * Inspector#screenshotNode method).
1647 * @param {Inspector} inspector
1650 async function takeNodeScreenshot(inspector) {
1651 // Cleanup all downloads at the end of the test.
1652 registerCleanupFunction(resetDownloads);
1655 "Call screenshotNode() and wait until the screenshot is found in the Downloads"
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);
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.
1675 "Wait for one second to make sure future screenshots will use a different name"
1677 await new Promise(r => setTimeout(r, 1000));
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
1687 async function assertSingleColorScreenshotImage(
1693 info(`Assert ${image.src} content`);
1694 const ratio = await SpecialPowers.spawn(
1695 gBrowser.selectedBrowser,
1697 () => content.wrappedJSObject.devicePixelRatio
1703 `node screenshot has the expected width (dpr = ${ratio})`
1708 `node screenshot has the expected height (dpr = ${ratio})`
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
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
1733 * Resolved once the store reaches the expected state.
1735 function waitUntilState(store, predicate) {
1736 return new Promise(resolve => {
1737 const unsubscribe = store.subscribe(check);
1739 info(`Waiting for state predicate "${predicate}"`);
1741 if (predicate(store.getState())) {
1742 info(`Found state predicate "${predicate}"`);
1748 // Fire the check immediately in case the action has already occurred
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.
1768 function waitForDispatch(store, actionType, repeat = 1) {
1770 return new Promise(resolve => {
1772 type: "@@service/waitUntil",
1773 predicate: action => {
1776 action.status === "done" ||
1777 action.status === "error";
1779 if (action.type === actionType && isDone && ++count == repeat) {
1785 run: (dispatch, getState, action) => {
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
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.
1802 async function getBrowsingContextInFrames(browsingContext, selectors) {
1803 let context = browsingContext;
1805 if (!Array.isArray(selectors)) {
1807 "getBrowsingContextInFrames called with an invalid selectors argument"
1811 if (selectors.length === 0) {
1813 "getBrowsingContextInFrames called with an empty selectors array"
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;
1829 * Synthesize a mouse event on an element, after ensuring that it is visible
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"])
1837 * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse
1839 async function safeSynthesizeMouseEventInContentPage(
1845 let context = gBrowser.selectedBrowser.browsingContext;
1847 // If an array of selector is passed, we need to retrieve the context in which the node
1849 if (Array.isArray(selector)) {
1850 if (selector.length === 1) {
1851 selector = selector[0];
1853 context = await getBrowsingContextInFrames(
1855 // only pass the iframe path
1856 selector.slice(0, -1)
1858 // retrieve the last item of the selector, which should be the one for the node we want.
1859 selector = selector.at(-1);
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
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
1876 async function safeSynthesizeMouseEventAtCenterInContentPage(
1880 let context = gBrowser.selectedBrowser.browsingContext;
1882 // If an array of selector is passed, we need to retrieve the context in which the node
1884 if (Array.isArray(selector)) {
1885 if (selector.length === 1) {
1886 selector = selector[0];
1888 context = await getBrowsingContextInFrames(
1890 // only pass the iframe path
1891 selector.slice(0, -1)
1893 // retrieve the last item of the selector, which should be the one for the node we want.
1894 selector = selector.at(-1);
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}
1909 function scrollContentPageNodeIntoView(browsingContext, selector) {
1910 return SpecialPowers.spawn(
1913 function (innerSelector) {
1915 content.wrappedJSObject.document.querySelector(innerSelector);
1916 node.scrollIntoView();
1922 * Change the zoom level of the selected page.
1924 * @param {Number} zoomLevel
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
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
1942 async function waitForNextTopLevelDomCompleteResource(commands) {
1943 const { onResource: onDomCompleteResource } =
1944 await commands.resourceCommand.waitForNextResource(
1945 commands.resourceCommand.TYPES.DOCUMENT_EVENT,
1947 ignoreExistingResources: true,
1948 predicate: resource =>
1949 resource.name === "dom-complete" && resource.targetFront.isTopLevel,
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
1960 * @param {BrowsingContext} context
1962 function waitForPresShell(context) {
1963 return SpecialPowers.spawn(context, [], async () => {
1964 const winUtils = SpecialPowers.getDOMWindowUtils(content);
1965 await ContentTaskUtils.waitForCondition(() => {
1967 return !!winUtils.getPresShellId();
1971 }, "Waiting for a valid presShell");
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}
1985 async function getFluentStringHelper(resourceIds) {
1986 const locales = Services.locale.appLocalesAsBCP47;
1987 const generator = L10nRegistry.getInstance().generateBundles(
1993 for await (const bundle of generator) {
1994 bundles.push(bundle);
1997 const reactLocalization = new FluentReact.ReactLocalization(bundles);
2000 * Get the string from a message id. It throws when the message is not found.
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
2011 return (id, attributeName, args) => {
2014 if (!attributeName) {
2015 string = reactLocalization.getString(id, args);
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],
2032 `Could not find a string for "${id}"${
2033 attributeName ? ` and attribute "${attributeName}")` : ""
2034 }. Was the correct resource bundle loaded?`
2042 * Open responsive design mode for the given tab.
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, {
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(
2069 state => state.devices.listState == localTypes.loadableState.LOADED
2075 * Close responsive design mode for the given tab.
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",
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.
2103 * Promise which resolves when a target matching `isExpectedTargetFn`
2104 * has been processed by targetCommand.
2106 function waitForTargetProcessed(commands, isExpectedTargetFn) {
2107 return new Promise(resolve => {
2108 const onProcessed = targetFront => {
2110 if (isExpectedTargetFn(targetFront)) {
2111 commands.targetCommand.off("processed-available-target", onProcessed);
2115 // Ignore errors from isExpectedTargetFn.
2119 commands.targetCommand.on("processed-available-target", onProcessed);
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()`
2130 * @return Object Test server with two functions:
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.
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");
2151 if (request.path == "/" || request.path.endsWith(".html")) {
2152 response.setHeader("Content-Type", "text/html");
2154 // If a query string is passed, lookup with a matching file, if available
2155 // The '?' is replaced by '.'
2158 if (request.queryString) {
2159 const url = `${URL_ROOT_SSL}${testFolderName}/v${currentVersion}${request.path}.${request.queryString}`;
2161 fetchResponse = await fetch(url);
2162 // Log this only if the request succeed
2163 info(`[test-http-server] serving: ${url}`);
2165 // Ignore any error and proceed without the query string
2166 fetchResponse = null;
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);
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);
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);
2192 switchToNextVersion() {
2195 backToFirstVersion() {
2199 const port = httpServer.identity.primaryPort;
2200 return `http://localhost:${port}/${path}`;
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
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.
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)
2221 function simulateLinkClick(element) {
2222 const browserWindow = Services.wm.getMostRecentWindow(
2223 gDevTools.chromeWindowType
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);
2234 // Declare a timeout Promise that we can use to make sure spied methods were not called.
2235 const onTimeout = new Promise(function (resolve) {
2237 resolve({ link: null, where: null });
2241 const raceResult = Promise.race([onOpenLink, onTimeout]);
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.
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) {
2261 `"${propertyName}" - MDN URL: ${
2262 property.__compat.mdn_url || "❌"
2263 } - Spec URL: ${property.__compat.spec_url || "❌"}\n`
2266 } else if (typeof property == "object") {
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}
2282 function getClientCssProperties() {
2284 generateCssProperties,
2285 } = require("resource://devtools/server/actors/css-properties.js");
2289 } = require("resource://devtools/client/fronts/css-properties.js");
2290 return new CssProperties(
2291 normalizeCssData({ properties: generateCssProperties(document) })
2296 * Helper method to stop a Service Worker promptly.
2298 * @param {String} workerUrl
2299 * Absolute Worker URL to stop.
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
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();
2318 for (let i = 0; i < registrations.length; i++) {
2319 const info = registrations.queryElementAt(
2321 Ci.nsIServiceWorkerRegistrationInfo
2323 // Lookup for an exact URL match.
2324 if (info.scriptSpec === workerUrl) {
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();
2341 resetWorkerTimeout(matchedInfo.activeWorker);
2342 // Also reset all the other possible worker instances
2343 if (matchedInfo.evaluatingWorker) {
2344 resetWorkerTimeout(matchedInfo.evaluatingWorker);
2346 if (matchedInfo.installingWorker) {
2347 resetWorkerTimeout(matchedInfo.installingWorker);
2349 if (matchedInfo.waitingWorker) {
2350 resetWorkerTimeout(matchedInfo.waitingWorker);
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.
2362 * Helper method to stop and unregister a Service Worker promptly.
2364 * @param {String} workerUrl
2365 * Absolute Worker URL to unregister.
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
2375 const unregisterSuccess = await new Promise(resolve => {
2379 unregisterSucceeded(success) {
2386 ok(unregisterSuccess, "Service worker successfully unregistered");