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 const { loader, require } = ChromeUtils.importESModule(
58 "resource://devtools/shared/loader/Loader.sys.mjs"
60 const { sinon } = ChromeUtils.importESModule(
61 "resource://testing-common/Sinon.sys.mjs"
64 // When loaded from xpcshell test, this file is loaded via xpcshell.ini's head property
65 // and so it loaded first before anything else and isn't having access to Services global.
66 // Whereas many head.js files from mochitest import this file via loadSubScript
67 // and already expose Services as a global.
71 } = require("resource://devtools/client/framework/devtools.js");
74 } = require("resource://devtools/shared/commands/commands-factory.js");
75 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
77 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
79 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
81 loader.lazyRequireGetter(
83 "ResponsiveUIManager",
84 "resource://devtools/client/responsive/manager.js"
86 loader.lazyRequireGetter(
89 "resource://devtools/client/responsive/types.js"
91 loader.lazyRequireGetter(
93 "ResponsiveMessageHelper",
94 "resource://devtools/client/responsive/utils/message.js"
97 loader.lazyRequireGetter(
100 "resource://devtools/client/shared/vendor/fluent-react.js"
103 const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
104 const CHROME_URL_ROOT = TEST_DIR + "/";
105 const URL_ROOT = CHROME_URL_ROOT.replace(
106 "chrome://mochitests/content/",
107 "http://example.com/"
109 const URL_ROOT_SSL = CHROME_URL_ROOT.replace(
110 "chrome://mochitests/content/",
111 "https://example.com/"
114 // Add aliases which make it more explicit that URL_ROOT uses a com TLD.
115 const URL_ROOT_COM = URL_ROOT;
116 const URL_ROOT_COM_SSL = URL_ROOT_SSL;
118 // Also expose http://example.org, http://example.net, https://example.org to
119 // test Fission scenarios easily.
120 // Note: example.net is not available for https.
121 const URL_ROOT_ORG = CHROME_URL_ROOT.replace(
122 "chrome://mochitests/content/",
123 "http://example.org/"
125 const URL_ROOT_ORG_SSL = CHROME_URL_ROOT.replace(
126 "chrome://mochitests/content/",
127 "https://example.org/"
129 const URL_ROOT_NET = CHROME_URL_ROOT.replace(
130 "chrome://mochitests/content/",
131 "http://example.net/"
133 const URL_ROOT_NET_SSL = CHROME_URL_ROOT.replace(
134 "chrome://mochitests/content/",
135 "https://example.net/"
137 // mochi.test:8888 is the actual primary location where files are served.
138 const URL_ROOT_MOCHI_8888 = CHROME_URL_ROOT.replace(
139 "chrome://mochitests/content/",
140 "http://mochi.test:8888/"
145 Services.scriptloader.loadSubScript(
146 "chrome://mochitests/content/browser/devtools/client/shared/test/telemetry-test-helpers.js",
153 "MISSING DEPENDENCY ON telemetry-test-helpers.js\n" +
154 "Please add the following line in browser.ini:\n" +
155 " !/devtools/client/shared/test/telemetry-test-helpers.js\n"
160 // Force devtools to be initialized so menu items and keyboard shortcuts get installed
161 require("resource://devtools/client/framework/devtools-browser.js");
163 // All tests are asynchronous
165 waitForExplicitFinish();
168 var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
170 registerCleanupFunction(function () {
172 DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT
176 "Should have had the expected number of DevToolsUtils.assert() failures." +
178 EXPECTED_DTU_ASSERT_FAILURE_COUNT +
180 DevToolsUtils.assertionFailureCount
185 // Uncomment this pref to dump all devtools emitted events to the console.
186 // Services.prefs.setBoolPref("devtools.dump.emit", true);
189 * Watch console messages for failed propType definitions in React components.
191 function onConsoleMessage(subject) {
192 const message = subject.wrappedJSObject.arguments[0];
194 if (message && /Failed propType/.test(message.toString())) {
199 const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(
200 Ci.nsIConsoleAPIStorage
203 ConsoleAPIStorage.addLogEventListener(
205 Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
207 registerCleanupFunction(() => {
208 ConsoleAPIStorage.removeLogEventListener(onConsoleMessage);
211 Services.prefs.setBoolPref("devtools.inspector.three-pane-enabled", true);
213 // Disable this preference to reduce exceptions related to pending `listWorkers`
214 // requests occuring after a process is created/destroyed. See Bug 1620983.
215 Services.prefs.setBoolPref("dom.ipc.processPrelaunch.enabled", false);
217 // Disable this preference to capture async stacks across all locations during
218 // DevTools mochitests. Async stacks provide very valuable information to debug
219 // intermittents, but come with a performance overhead, which is why they are
220 // only captured in Debuggees by default.
221 Services.prefs.setBoolPref(
222 "javascript.options.asyncstack_capture_debuggee_only",
226 // On some Linux platforms, prefers-reduced-motion is enabled, which would
227 // trigger the notification to be displayed in the toolbox. Dismiss the message
229 Services.prefs.setBoolPref(
230 "devtools.inspector.simple-highlighters.message-dismissed",
234 registerCleanupFunction(() => {
235 Services.prefs.clearUserPref("devtools.dump.emit");
236 Services.prefs.clearUserPref("devtools.inspector.three-pane-enabled");
237 Services.prefs.clearUserPref("dom.ipc.processPrelaunch.enabled");
238 Services.prefs.clearUserPref("devtools.toolbox.host");
239 Services.prefs.clearUserPref("devtools.toolbox.previousHost");
240 Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
241 Services.prefs.clearUserPref("devtools.toolbox.splitconsoleHeight");
242 Services.prefs.clearUserPref(
243 "javascript.options.asyncstack_capture_debuggee_only"
245 Services.prefs.clearUserPref(
246 "devtools.inspector.simple-highlighters.message-dismissed"
251 BrowserConsoleManager,
252 } = require("resource://devtools/client/webconsole/browser-console-manager.js");
254 registerCleanupFunction(async function cleanup() {
255 // Closing the browser console if there's one
256 const browserConsole = BrowserConsoleManager.getBrowserConsole();
257 if (browserConsole) {
258 await safeCloseBrowserConsole({ clearOutput: true });
261 // Close any tab opened by the test.
262 // There should be only one tab opened by default when firefox starts the test.
263 while (isMochitest && gBrowser.tabs.length > 1) {
264 await closeTabAndToolbox(gBrowser.selectedTab);
267 // Note that this will run before cleanup functions registered by tests or other head.js files.
268 // So all connections must be cleaned up by the test when the test ends,
269 // before the harness starts invoking the cleanup functions
272 // All connections must be cleaned up by the test when the test ends.
275 } = require("resource://devtools/server/devtools-server.js");
277 !DevToolsServer.hasConnection(),
278 "The main process DevToolsServer has no pending connection when the test ends"
280 // If there is still open connection, close all of them so that following tests
282 if (DevToolsServer.hasConnection()) {
283 for (const conn of Object.values(DevToolsServer._connections)) {
289 async function safeCloseBrowserConsole({ clearOutput = false } = {}) {
290 const hud = BrowserConsoleManager.getBrowserConsole();
296 info("Clear the browser console output");
298 const promises = [ui.once("messages-cleared")];
299 // If there's an object inspector, we need to wait for the actors to be released.
300 if (ui.outputNode.querySelector(".object-inspector")) {
301 promises.push(ui.once("fronts-released"));
303 await ui.clearOutput(true);
304 await Promise.all(promises);
305 info("Browser console cleared");
308 info("Wait for all Browser Console targets to be attached");
309 // It might happen that waitForAllTargetsToBeAttached does not resolve, so we set a
310 // timeout of 1s before closing
312 waitForAllTargetsToBeAttached(hud.commands.targetCommand),
316 info("Close the Browser Console");
317 await BrowserConsoleManager.closeBrowserConsole();
318 info("Browser Console closed");
322 * Observer code to register the test actor in every DevTools server which
323 * starts registering its own actors.
325 * We require immediately the highlighter test actor file, because it will force to load and
326 * register the front and the spec for HighlighterTestActor. Normally specs and fronts are
327 * in separate files registered in specs/index.js. But here to simplify the
328 * setup everything is in the same file and we force to load it here.
330 * DevToolsServer will emit "devtools-server-initialized" after finishing its
331 * initialization. We watch this observable to add our custom actor.
333 * As a single test may create several DevTools servers, we keep the observer
334 * alive until the test ends.
336 * To avoid leaks, the observer needs to be removed at the end of each test.
337 * The test cleanup will send the async message "remove-devtools-highlightertestactor-observer",
338 * we listen to this message to cleanup the observer.
340 function highlighterTestActorBootstrap() {
341 /* eslint-env mozilla/process-script */
342 const HIGHLIGHTER_TEST_ACTOR_URL =
343 "chrome://mochitests/content/browser/devtools/client/shared/test/highlighter-test-actor.js";
345 const { require: _require } = ChromeUtils.importESModule(
346 "resource://devtools/shared/loader/Loader.sys.mjs"
348 _require(HIGHLIGHTER_TEST_ACTOR_URL);
350 const actorRegistryObserver = subject => {
351 const actorRegistry = subject.wrappedJSObject;
352 actorRegistry.registerModule(HIGHLIGHTER_TEST_ACTOR_URL, {
353 prefix: "highlighterTest",
354 constructor: "HighlighterTestActor",
355 type: { target: true },
358 Services.obs.addObserver(
359 actorRegistryObserver,
360 "devtools-server-initialized"
363 const unloadListener = () => {
364 Services.cpmm.removeMessageListener(
365 "remove-devtools-testactor-observer",
368 Services.obs.removeObserver(
369 actorRegistryObserver,
370 "devtools-server-initialized"
373 Services.cpmm.addMessageListener(
374 "remove-devtools-testactor-observer",
380 const highlighterTestActorBootstrapScript =
381 "data:,(" + highlighterTestActorBootstrap + ")()";
382 Services.ppmm.loadProcessScript(
383 highlighterTestActorBootstrapScript,
384 // Load this script in all processes (created or to be created)
388 registerCleanupFunction(() => {
389 Services.ppmm.broadcastAsyncMessage("remove-devtools-testactor-observer");
390 Services.ppmm.removeDelayedProcessScript(
391 highlighterTestActorBootstrapScript
397 * Spawn an instance of the highlighter test actor for the given toolbox
399 * @param {Toolbox} toolbox
400 * @param {Object} options
401 * @param {Function} options.target: Optional target to get the highlighterTestFront for.
402 * If not provided, the top level target will be used.
403 * @returns {HighlighterTestFront}
405 async function getHighlighterTestFront(toolbox, { target } = {}) {
406 // Loading the Inspector panel in order to overwrite the TestActor getter for the
407 // highlighter instance with a method that points to the currently visible
408 // Box Model Highlighter managed by the Inspector panel.
409 const inspector = await toolbox.loadTool("inspector");
411 const highlighterTestFront = await (target || toolbox.target).getFront(
414 // Override the highligher getter with a method to return the active box model
415 // highlighter. Adaptation for multi-process scenarios where there can be multiple
416 // highlighters, one per process.
417 highlighterTestFront.highlighter = () => {
418 return inspector.highlighters.getActiveHighlighter(
419 inspector.highlighters.TYPES.BOXMODEL
422 return highlighterTestFront;
426 * Spawn an instance of the highlighter test actor for the given tab, when we need the
427 * highlighter test front before opening or without a toolbox.
430 * @returns {HighlighterTestFront}
432 async function getHighlighterTestFrontWithoutToolbox(tab) {
433 const commands = await CommandsFactory.forTab(tab);
434 // Initialize the TargetCommands which require some async stuff to be done
435 // before being fully ready. This will define the `targetCommand.targetFront` attribute.
436 await commands.targetCommand.startListening();
438 const targetFront = commands.targetCommand.targetFront;
439 return targetFront.getFront("highlighterTest");
443 * Returns a Promise that resolves when all the targets are fully attached.
445 * @param {TargetCommand} targetCommand
447 function waitForAllTargetsToBeAttached(targetCommand) {
448 return Promise.allSettled(
450 .getAllTargets(targetCommand.ALL_TYPES)
451 .map(target => target.initialized)
456 * Add a new test tab in the browser and load the given url.
457 * @param {String} url The url to be loaded in the new tab
458 * @param {Object} options Object with various optional fields:
459 * - {Boolean} background If true, open the tab in background
460 * - {ChromeWindow} window Firefox top level window we should use to open the tab
461 * - {Number} userContextId The userContextId of the tab.
462 * - {String} preferredRemoteType
463 * - {Boolean} waitForLoad Wait for the page in the new tab to load. (Defaults to true.)
464 * @return a promise that resolves to the tab object when the url is loaded
466 async function addTab(url, options = {}) {
467 info("Adding a new tab with URL: " + url);
475 const { gBrowser } = options.window ? options.window : window;
477 const tab = BrowserTestUtils.addTab(gBrowser, url, {
483 gBrowser.selectedTab = tab;
487 await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
488 // Waiting for presShell helps with test timeouts in webrender platforms.
489 await waitForPresShell(tab.linkedBrowser);
490 info("Tab added and finished loading");
499 * Remove the given tab.
500 * @param {Object} tab The tab to be removed.
501 * @return Promise<undefined> resolved when the tab is successfully removed.
503 async function removeTab(tab) {
504 info("Removing tab.");
506 const { gBrowser } = tab.ownerDocument.defaultView;
507 const onClose = once(gBrowser.tabContainer, "TabClose");
508 gBrowser.removeTab(tab);
511 info("Tab removed and finished closing");
515 * Alias for navigateTo which will reuse the current URI of the provided browser
516 * to trigger a navigation.
518 async function reloadBrowser({
519 browser = gBrowser.selectedBrowser,
523 return navigateTo(browser.currentURI.spec, {
531 * Navigate the currently selected tab to a new URL and wait for it to load.
532 * Also wait for the toolbox to attach to the new target, if we navigated
535 * @param {String} url The url to be loaded in the current tab.
536 * @param {JSON} options Optional dictionary object with the following keys:
537 * - {XULBrowser} browser
538 * The browser element which should navigate. Defaults to the selected
540 * - {Boolean} isErrorPage
541 * You may pass `true` if the URL is an error page. Otherwise
542 * BrowserTestUtils.browserLoaded will wait for 'load' event, which
543 * never fires for error pages.
544 * - {Boolean} waitForLoad
545 * You may pass `false` if the page load is expected to be blocked by
546 * a script or a breakpoint.
548 * @return a promise that resolves when the page has fully loaded.
550 async function navigateTo(
553 browser = gBrowser.selectedBrowser,
558 const waitForDevToolsReload = await watchForDevToolsReload(browser, {
563 uri = uri.replaceAll("\n", "");
564 info(`Navigating to "${uri}"`);
566 const onBrowserLoaded = BrowserTestUtils.browserLoaded(
570 // resolve on this specific page to load (if null, it would be any page load)
572 // loadedUrl is encoded, while uri might not be.
573 return loadedUrl === uri || decodeURI(loadedUrl) === uri;
578 // if we're navigating to the same page we're already on, use reloadTab instead as the
579 // behavior slightly differs from loadURI (e.g. scroll position isn't keps with the latter).
580 if (uri === browser.currentURI.spec) {
581 gBrowser.reloadTab(gBrowser.getTabForBrowser(browser));
583 BrowserTestUtils.startLoadingURIString(browser, uri);
587 info(`Waiting for page to be loaded…`);
588 await onBrowserLoaded;
589 info(`→ page loaded`);
592 await waitForDevToolsReload();
596 * This method should be used to watch for completion of any browser navigation
597 * performed with a DevTools UI.
599 * It should watch for:
601 * - Toolbox commands reload
603 * - RDM commands reload
605 * And it should work both for target switching or old-style navigations.
607 * This method, similarly to all the other watch* navigation methods in this file,
608 * is async but returns another method which should be called after the navigation
609 * is done. Browser navigation might be monitored differently depending on the
610 * situation, so it's up to the caller to handle it as needed.
612 * Typically, this would be used as follows:
614 * async function someNavigationHelper(browser) {
615 * const waitForDevToolsFn = await watchForDevToolsReload(browser);
617 * // This step should wait for the load to be completed from the browser's
618 * // point of view, so that waitForDevToolsFn can compare pIds, browsing
619 * // contexts etc... and check if we should expect a target switch
620 * await performBrowserNavigation(browser);
622 * await waitForDevToolsFn();
626 async function watchForDevToolsReload(
628 { isErrorPage = false, waitForLoad = true } = {}
630 const waitForToolboxReload = await _watchForToolboxReload(browser, {
634 const waitForResponsiveReload = await _watchForResponsiveReload(browser, {
639 return async function () {
640 info("Wait for the toolbox to reload");
641 await waitForToolboxReload();
643 info("Wait for Responsive UI to reload");
644 await waitForResponsiveReload();
649 * Start watching for the toolbox reload to be completed:
650 * - watch for the toolbox's commands to be fully reloaded
651 * - watch for the toolbox's current panel to be reloaded
653 async function _watchForToolboxReload(
655 { isErrorPage, waitForLoad } = {}
657 const tab = gBrowser.getTabForBrowser(browser);
659 const toolbox = gDevTools.getToolboxForTab(tab);
662 // No toolbox to wait for
663 return function () {};
666 const waitForCurrentPanelReload = watchForCurrentPanelReload(toolbox);
667 const waitForToolboxCommandsReload = await watchForCommandsReload(
669 { isErrorPage, waitForLoad }
671 const checkTargetSwitching = await watchForTargetSwitching(
676 return async function () {
677 const isTargetSwitching = checkTargetSwitching();
679 info(`Waiting for toolbox commands to be reloaded…`);
680 await waitForToolboxCommandsReload(isTargetSwitching);
682 // TODO: We should wait for all loaded panels to reload here, because some
683 // of them might still perform background updates.
684 if (waitForCurrentPanelReload) {
685 info(`Waiting for ${toolbox.currentToolId} to be reloaded…`);
686 await waitForCurrentPanelReload();
687 info(`→ panel reloaded`);
693 * Start watching for Responsive UI (RDM) reload to be completed:
694 * - watch for the Responsive UI's commands to be fully reloaded
695 * - watch for the Responsive UI's target switch to be done
697 async function _watchForResponsiveReload(
699 { isErrorPage, waitForLoad } = {}
701 const tab = gBrowser.getTabForBrowser(browser);
702 const ui = ResponsiveUIManager.getResponsiveUIForTab(tab);
705 // No responsive UI to wait for
706 return function () {};
709 const onResponsiveTargetSwitch = ui.once("responsive-ui-target-switch-done");
710 const waitForResponsiveCommandsReload = await watchForCommandsReload(
712 { isErrorPage, waitForLoad }
714 const checkTargetSwitching = await watchForTargetSwitching(
719 return async function () {
720 const isTargetSwitching = checkTargetSwitching();
722 info(`Waiting for responsive ui commands to be reloaded…`);
723 await waitForResponsiveCommandsReload(isTargetSwitching);
725 if (isTargetSwitching) {
726 await onResponsiveTargetSwitch;
732 * Watch for the current panel selected in the provided toolbox to be reloaded.
733 * Some panels implement custom events that should be expected for every reload.
735 * Note about returning a method instead of a promise:
736 * In general this pattern is useful so that we can check if a target switch
737 * occurred or not, and decide which events to listen for. So far no panel is
738 * behaving differently whether there was a target switch or not. But to remain
739 * consistent with other watch* methods we still return a function here.
742 * The Toolbox instance which is going to experience a reload
743 * @return {function} An async method to be called and awaited after the reload
744 * started. Will return `null` for panels which don't implement any
745 * specific reload event.
747 function watchForCurrentPanelReload(toolbox) {
748 return _watchForPanelReload(toolbox, toolbox.currentToolId);
752 * Watch for all the panels loaded in the provided toolbox to be reloaded.
753 * Some panels implement custom events that should be expected for every reload.
755 * Note about returning a method instead of a promise:
756 * See comment for watchForCurrentPanelReload
759 * The Toolbox instance which is going to experience a reload
760 * @return {function} An async method to be called and awaited after the reload
763 function watchForLoadedPanelsReload(toolbox) {
764 const waitForPanels = [];
765 for (const [id] of toolbox.getToolPanels()) {
766 // Store a watcher method for each panel already loaded.
767 waitForPanels.push(_watchForPanelReload(toolbox, id));
772 waitForPanels.map(async watchPanel => {
773 // Wait for all panels to be reloaded.
782 function _watchForPanelReload(toolbox, toolId) {
783 const panel = toolbox.getPanel(toolId);
785 if (toolId == "inspector") {
786 const markuploaded = panel.once("markuploaded");
787 const onNewRoot = panel.once("new-root");
788 const onUpdated = panel.once("inspector-updated");
789 const onReloaded = panel.once("reloaded");
791 return async function () {
792 info("Waiting for markup view to load after navigation.");
795 info("Waiting for new root.");
798 info("Waiting for inspector to update after new-root event.");
801 info("Waiting for inspector updates after page reload");
805 ["netmonitor", "accessibility", "webconsole", "jsdebugger"].includes(toolId)
807 const onReloaded = panel.once("reloaded");
808 return async function () {
809 info(`Waiting for ${toolId} updates after page reload`);
817 * Watch for a Commands instance to be reloaded after a navigation.
819 * As for other navigation watch* methods, this should be called before the
820 * navigation starts, and the function it returns should be called after the
821 * navigation is done from a Browser point of view.
823 * !!! The wait function expects a `isTargetSwitching` argument to be provided,
824 * which needs to be monitored using watchForTargetSwitching !!!
826 async function watchForCommandsReload(
828 { isErrorPage = false, waitForLoad = true } = {}
830 // If we're switching origins, we need to wait for the 'switched-target'
831 // event to make sure everything is ready.
832 // Navigating from/to pages loaded in the parent process, like about:robots,
833 // also spawn new targets.
834 // (If target switching is disabled, the toolbox will reboot)
835 const onTargetSwitched = commands.targetCommand.once("switched-target");
837 // Wait until we received a page load resource:
838 // - dom-complete if we can wait for a full page load
839 // - dom-loading otherwise
840 // This allows to wait for page load for consumers calling directly
841 // waitForDevTools instead of navigateTo/reloadBrowser.
842 // This is also useful as an alternative to target switching, when no target
843 // switch is supposed to happen.
844 const waitForCompleteLoad = waitForLoad && !isErrorPage;
845 const documentEventName = waitForCompleteLoad
849 const { onResource: onTopLevelDomEvent } =
850 await commands.resourceCommand.waitForNextResource(
851 commands.resourceCommand.TYPES.DOCUMENT_EVENT,
853 ignoreExistingResources: true,
854 predicate: resource =>
855 resource.targetFront.isTopLevel &&
856 resource.name === documentEventName,
860 return async function (isTargetSwitching) {
861 if (typeof isTargetSwitching === "undefined") {
862 throw new Error("isTargetSwitching was not provided to the wait method");
865 if (isTargetSwitching) {
866 info(`Waiting for target switch…`);
867 await onTargetSwitched;
868 info(`→ switched-target emitted`);
871 info(`Waiting for '${documentEventName}' resource…`);
872 await onTopLevelDomEvent;
873 info(`→ '${documentEventName}' resource emitted`);
875 return isTargetSwitching;
880 * Watch if an upcoming navigation will trigger a target switching, for the
881 * provided Commands instance and the provided Browser.
883 * As for other navigation watch* methods, this should be called before the
884 * navigation starts, and the function it returns should be called after the
885 * navigation is done from a Browser point of view.
887 async function watchForTargetSwitching(commands, browser) {
888 browser = browser || gBrowser.selectedBrowser;
889 const currentPID = browser.browsingContext.currentWindowGlobal.osPid;
890 const currentBrowsingContextID = browser.browsingContext.id;
892 // If the current top-level target follows the window global lifecycle, a
893 // target switch will occur regardless of process changes.
894 const targetFollowsWindowLifecycle =
895 commands.targetCommand.targetFront.targetForm.followWindowGlobalLifeCycle;
898 // Compare the PIDs (and not the toolbox's targets) as PIDs are updated also immediately,
899 // while target may be updated slightly later.
900 const switchedProcess =
901 currentPID !== browser.browsingContext.currentWindowGlobal.osPid;
902 const switchedBrowsingContext =
903 currentBrowsingContextID !== browser.browsingContext.id;
906 targetFollowsWindowLifecycle || switchedProcess || switchedBrowsingContext
912 * Create a Target for the provided tab and attach to it before resolving.
913 * This should only be used for tests which don't involve the frontend or a
914 * toolbox. Typically, retrieving the target and attaching to it should be
915 * handled at framework level when a Toolbox is used.
917 * @param {XULTab} tab
918 * The tab for which a target should be created.
919 * @return {WindowGlobalTargetFront} The attached target front.
921 async function createAndAttachTargetForTab(tab) {
922 info("Creating and attaching to a local tab target");
924 const commands = await CommandsFactory.forTab(tab);
926 // Initialize the TargetCommands which require some async stuff to be done
927 // before being fully ready. This will define the `targetCommand.targetFront` attribute.
928 await commands.targetCommand.startListening();
930 const target = commands.targetCommand.targetFront;
934 function isFissionEnabled() {
935 return SpecialPowers.useRemoteSubframes;
938 function isEveryFrameTargetEnabled() {
939 return Services.prefs.getBoolPref(
940 "devtools.every-frame-target.enabled",
946 * Open the inspector in a tab with given URL.
947 * @param {string} url The URL to open.
948 * @param {String} hostType Optional hostType, as defined in Toolbox.HostType
949 * @return A promise that is resolved once the tab and inspector have loaded
950 * with an object: { tab, toolbox, inspector, highlighterTestFront }.
952 async function openInspectorForURL(url, hostType) {
953 const tab = await addTab(url);
954 const { inspector, toolbox, highlighterTestFront } = await openInspector(
957 return { tab, inspector, toolbox, highlighterTestFront };
960 function getActiveInspector() {
961 const toolbox = gDevTools.getToolboxForTab(gBrowser.selectedTab);
962 return toolbox.getPanel("inspector");
966 * Simulate a key event from an electron key shortcut string:
967 * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
969 * @param {String} key
970 * @param {DOMWindow} target
971 * Optional window where to fire the key event
973 function synthesizeKeyShortcut(key, target) {
974 // parseElectronKey requires any window, just to access `KeyboardEvent`
975 const window = Services.appShell.hiddenDOMWindow;
976 const shortcut = KeyShortcuts.parseElectronKey(window, key);
978 altKey: shortcut.alt,
979 ctrlKey: shortcut.ctrl,
980 metaKey: shortcut.meta,
981 shiftKey: shortcut.shift,
983 if (shortcut.keyCode) {
984 keyEvent.keyCode = shortcut.keyCode;
987 info("Synthesizing key shortcut: " + key);
988 EventUtils.synthesizeKey(shortcut.key || "", keyEvent, target);
991 var waitForTime = DevToolsUtils.waitForTime;
997 function waitForTick() {
998 return new Promise(resolve => DevToolsUtils.executeSoon(resolve));
1002 * This shouldn't be used in the tests, but is useful when writing new tests or
1003 * debugging existing tests in order to introduce delays in the test steps
1005 * @param {Number} ms
1007 * @return A promise that resolves when the time is passed
1010 return new Promise(resolve => {
1011 setTimeout(resolve, ms);
1012 info("Waiting " + ms / 1000 + " seconds.");
1017 * Wait for a predicate to return a result.
1019 * @param function condition
1020 * Invoked once in a while until it returns a truthy value. This should be an
1021 * idempotent function, since we have to run it a second time after it returns
1022 * true in order to return the value.
1023 * @param string message [optional]
1024 * A message to output if the condition fails.
1025 * @param number interval [optional]
1026 * How often the predicate is invoked, in milliseconds.
1027 * Can be set globally for a test via `waitFor.overrideIntervalForTestFile = someNumber;`.
1028 * @param number maxTries [optional]
1029 * How many times the predicate is invoked before timing out.
1030 * Can be set globally for a test via `waitFor.overrideMaxTriesForTestFile = someNumber;`.
1032 * A promise that is resolved with the result of the condition.
1034 async function waitFor(condition, message = "", interval = 10, maxTries = 500) {
1035 // Update interval & maxTries if overrides are defined on the waitFor object.
1037 typeof waitFor.overrideIntervalForTestFile !== "undefined"
1038 ? waitFor.overrideIntervalForTestFile
1041 typeof waitFor.overrideMaxTriesForTestFile !== "undefined"
1042 ? waitFor.overrideMaxTriesForTestFile
1046 const value = await BrowserTestUtils.waitForCondition(
1054 const errorMessage = `Failed waitFor(): ${message} \nFailed condition: ${condition} \nException Message: ${e}`;
1055 throw new Error(errorMessage);
1060 * Wait for eventName on target to be delivered a number of times.
1062 * @param {Object} target
1063 * An observable object that either supports on/off or
1064 * addEventListener/removeEventListener
1065 * @param {String} eventName
1066 * @param {Number} numTimes
1067 * Number of deliveries to wait for.
1068 * @param {Boolean} useCapture
1069 * Optional, for addEventListener/removeEventListener
1070 * @return A promise that resolves when the event has been handled
1072 function waitForNEvents(target, eventName, numTimes, useCapture = false) {
1073 info("Waiting for event: '" + eventName + "' on " + target + ".");
1077 return new Promise(resolve => {
1078 for (const [add, remove] of [
1080 ["addEventListener", "removeEventListener"],
1081 ["addListener", "removeListener"],
1082 ["addMessageListener", "removeMessageListener"],
1084 if (add in target && remove in target) {
1087 function onEvent(...args) {
1088 if (typeof info === "function") {
1089 info("Got event: '" + eventName + "' on " + target + ".");
1092 if (++count == numTimes) {
1093 target[remove](eventName, onEvent, useCapture);
1106 * Wait for DOM change on target.
1108 * @param {Object} target
1109 * The Node on which to observe DOM mutations.
1110 * @param {String} selector
1111 * Given a selector to watch whether the expected element is changed
1113 * @param {Number} expectedLength
1114 * Optional, default set to 1
1115 * There may be more than one element match an array match the selector,
1116 * give an expected length to wait for more elements.
1117 * @return A promise that resolves when the event has been handled
1119 function waitForDOM(target, selector, expectedLength = 1) {
1120 return new Promise(resolve => {
1121 const observer = new MutationObserver(mutations => {
1122 mutations.forEach(mutation => {
1123 const elements = mutation.target.querySelectorAll(selector);
1125 if (elements.length === expectedLength) {
1126 observer.disconnect();
1132 observer.observe(target, {
1141 * Wait for eventName on target.
1143 * @param {Object} target
1144 * An observable object that either supports on/off or
1145 * addEventListener/removeEventListener
1146 * @param {String} eventName
1147 * @param {Boolean} useCapture
1148 * Optional, for addEventListener/removeEventListener
1149 * @return A promise that resolves when the event has been handled
1151 function once(target, eventName, useCapture = false) {
1152 return waitForNEvents(target, eventName, 1, useCapture);
1156 * Some tests may need to import one or more of the test helper scripts.
1157 * A test helper script is simply a js file that contains common test code that
1158 * is either not common-enough to be in head.js, or that is located in a
1159 * separate directory.
1160 * The script will be loaded synchronously and in the test's scope.
1161 * @param {String} filePath The file path, relative to the current directory.
1163 * - "helper_attributes_test_runner.js"
1165 function loadHelperScript(filePath) {
1166 const testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
1167 Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
1171 * Open the toolbox in a given tab.
1172 * @param {XULNode} tab The tab the toolbox should be opened in.
1173 * @param {String} toolId Optional. The ID of the tool to be selected.
1174 * @param {String} hostType Optional. The type of toolbox host to be used.
1175 * @return {Promise} Resolves with the toolbox, when it has been opened.
1177 async function openToolboxForTab(tab, toolId, hostType) {
1178 info("Opening the toolbox");
1180 // Check if the toolbox is already loaded.
1181 let toolbox = gDevTools.getToolboxForTab(tab);
1183 if (!toolId || (toolId && toolbox.getPanel(toolId))) {
1184 info("Toolbox is already opened");
1189 // If not, load it now.
1190 toolbox = await gDevTools.showToolboxForTab(tab, { toolId, hostType });
1192 // Make sure that the toolbox frame is focused.
1193 await new Promise(resolve => waitForFocus(resolve, toolbox.win));
1195 info("Toolbox opened and focused");
1201 * Add a new tab and open the toolbox in it.
1202 * @param {String} url The URL for the tab to be opened.
1203 * @param {String} toolId Optional. The ID of the tool to be selected.
1204 * @param {String} hostType Optional. The type of toolbox host to be used.
1205 * @return {Promise} Resolves when the tab has been added, loaded and the
1206 * toolbox has been opened. Resolves to the toolbox.
1208 async function openNewTabAndToolbox(url, toolId, hostType) {
1209 const tab = await addTab(url);
1210 return openToolboxForTab(tab, toolId, hostType);
1214 * Close a tab and if necessary, the toolbox that belongs to it
1215 * @param {Tab} tab The tab to close.
1216 * @return {Promise} Resolves when the toolbox and tab have been destroyed and
1219 async function closeTabAndToolbox(tab = gBrowser.selectedTab) {
1220 if (gDevTools.hasToolboxForTab(tab)) {
1221 await gDevTools.closeToolboxForTab(tab);
1224 await removeTab(tab);
1226 await new Promise(resolve => setTimeout(resolve, 0));
1230 * Close a toolbox and the current tab.
1231 * @param {Toolbox} toolbox The toolbox to close.
1232 * @return {Promise} Resolves when the toolbox and tab have been destroyed and
1235 async function closeToolboxAndTab(toolbox) {
1236 await toolbox.destroy();
1237 await removeTab(gBrowser.selectedTab);
1241 * Waits until a predicate returns true.
1243 * @param function predicate
1244 * Invoked once in a while until it returns true.
1245 * @param number interval [optional]
1246 * How often the predicate is invoked, in milliseconds.
1248 function waitUntil(predicate, interval = 10) {
1250 return Promise.resolve(true);
1252 return new Promise(resolve => {
1253 setTimeout(function () {
1254 waitUntil(predicate, interval).then(() => resolve(true));
1260 * Variant of waitUntil that accepts a predicate returning a promise.
1262 async function asyncWaitUntil(predicate, interval = 10) {
1263 let success = await predicate();
1265 // Wait for X milliseconds.
1266 await new Promise(resolve => setTimeout(resolve, interval));
1267 // Test the predicate again.
1268 success = await predicate();
1273 * Wait for a context menu popup to open.
1275 * @param Element popup
1276 * The XUL popup you expect to open.
1277 * @param Element button
1278 * The button/element that receives the contextmenu event. This is
1279 * expected to open the popup.
1280 * @param function onShown
1281 * Function to invoke on popupshown event.
1282 * @param function onHidden
1283 * Function to invoke on popuphidden event.
1285 * A Promise object that is resolved after the popuphidden event
1286 * callback is invoked.
1288 function waitForContextMenu(popup, button, onShown, onHidden) {
1289 return new Promise(resolve => {
1290 function onPopupShown() {
1291 info("onPopupShown");
1292 popup.removeEventListener("popupshown", onPopupShown);
1294 onShown && onShown();
1296 // Use executeSoon() to get out of the popupshown event.
1297 popup.addEventListener("popuphidden", onPopupHidden);
1298 DevToolsUtils.executeSoon(() => popup.hidePopup());
1300 function onPopupHidden() {
1301 info("onPopupHidden");
1302 popup.removeEventListener("popuphidden", onPopupHidden);
1304 onHidden && onHidden();
1309 popup.addEventListener("popupshown", onPopupShown);
1311 info("wait for the context menu to open");
1312 synthesizeContextMenuEvent(button);
1316 function synthesizeContextMenuEvent(el) {
1317 el.scrollIntoView();
1318 const eventDetails = { type: "contextmenu", button: 2 };
1319 EventUtils.synthesizeMouse(
1324 el.ownerDocument.defaultView
1329 * Promise wrapper around SimpleTest.waitForClipboard
1331 function waitForClipboardPromise(setup, expected) {
1332 return new Promise((resolve, reject) => {
1333 SimpleTest.waitForClipboard(expected, setup, resolve, reject);
1338 * Simple helper to push a temporary preference. Wrapper on SpecialPowers
1339 * pushPrefEnv that returns a promise resolving when the preferences have been
1342 * @param {String} preferenceName
1343 * The name of the preference to updated
1345 * The preference value, type can vary
1346 * @return {Promise} resolves when the preferences have been updated
1348 function pushPref(preferenceName, value) {
1349 const options = { set: [[preferenceName, value]] };
1350 return SpecialPowers.pushPrefEnv(options);
1353 async function closeToolbox() {
1354 await gDevTools.closeToolboxForTab(gBrowser.selectedTab);
1358 * Clean the logical clipboard content. This method only clears the OS clipboard on
1359 * Windows (see Bug 666254).
1361 function emptyClipboard() {
1362 const clipboard = Services.clipboard;
1363 clipboard.emptyClipboard(clipboard.kGlobalClipboard);
1367 * Check if the current operating system is Windows.
1369 function isWindows() {
1370 return Services.appinfo.OS === "WINNT";
1374 * Create an HTTP server that can be used to simulate custom requests within
1375 * a test. It is automatically cleaned up when the test ends, so no need to
1378 * See https://developer.mozilla.org/en-US/docs/Httpd.js/HTTP_server_for_unit_tests
1379 * for more information about how to register handlers.
1381 * The server can be accessed like:
1383 * const server = createTestHTTPServer();
1384 * let url = "http://localhost: " + server.identity.primaryPort + "/path";
1385 * @returns {HttpServer}
1387 function createTestHTTPServer() {
1388 const { HttpServer } = ChromeUtils.importESModule(
1389 "resource://testing-common/httpd.sys.mjs"
1391 const server = new HttpServer();
1393 registerCleanupFunction(async function cleanup() {
1394 await new Promise(resolve => server.stop(resolve));
1402 * Register an actor in the content process of the current tab.
1404 * Calling ActorRegistry.registerModule only registers the actor in the current process.
1405 * As all test scripts are ran in the parent process, it is only registered here.
1406 * This function helps register them in the content process used for the current tab.
1408 * @param {string} url
1409 * Actor module URL or absolute require path
1410 * @param {json} options
1411 * Arguments to be passed to DevToolsServer.registerModule
1413 async function registerActorInContentProcess(url, options) {
1414 function convertChromeToFile(uri) {
1415 return Cc["@mozilla.org/chrome/chrome-registry;1"]
1416 .getService(Ci.nsIChromeRegistry)
1417 .convertChromeURL(Services.io.newURI(uri)).spec;
1419 // chrome://mochitests URI is registered only in the parent process, so convert these
1420 // URLs to file:// one in order to work in the content processes
1421 url = url.startsWith("chrome://mochitests") ? convertChromeToFile(url) : url;
1422 return SpecialPowers.spawn(
1423 gBrowser.selectedBrowser,
1426 // eslint-disable-next-line no-shadow
1427 const { require } = ChromeUtils.importESModule(
1428 "resource://devtools/shared/loader/Loader.sys.mjs"
1432 } = require("resource://devtools/server/actors/utils/actor-registry.js");
1433 ActorRegistry.registerModule(args.url, args.options);
1439 * Move the provided Window to the provided left, top coordinates and wait for
1440 * the window position to be updated.
1442 async function moveWindowTo(win, left, top) {
1443 // Check that the expected coordinates are within the window available area.
1444 left = Math.max(win.screen.availLeft, left);
1445 left = Math.min(win.screen.width, left);
1446 top = Math.max(win.screen.availTop, top);
1447 top = Math.min(win.screen.height, top);
1449 info(`Moving window to {${left}, ${top}}`);
1450 win.moveTo(left, top);
1452 // Bug 1600809: window move/resize can be async on Linux sometimes.
1453 // Wait so that the anchor's position is correctly measured.
1454 return waitUntil(() => {
1456 `Wait for window screenLeft and screenTop to be updated: (${win.screenLeft}, ${win.screenTop})`
1458 return win.screenLeft === left && win.screenTop === top;
1462 function getCurrentTestFilePath() {
1463 return gTestPath.replace("chrome://mochitests/content/browser/", "");
1467 * Unregister all registered service workers.
1469 * @param {DevToolsClient} client
1471 async function unregisterAllServiceWorkers(client) {
1472 info("Wait until all workers have a valid registrationFront");
1474 await asyncWaitUntil(async function () {
1475 workers = await client.mainRoot.listAllWorkers();
1476 const allWorkersRegistered = workers.service.every(
1477 worker => !!worker.registrationFront
1479 return allWorkersRegistered;
1482 info("Unregister all service workers");
1483 const promises = [];
1484 for (const worker of workers.service) {
1485 promises.push(worker.registrationFront.unregister());
1487 await Promise.all(promises);
1490 /**********************
1491 * Screenshot helpers *
1492 **********************/
1495 * Returns an object containing the r,g and b colors of the provided image at
1496 * the passed position
1498 * @param {Image} image
1501 * @returns Object with the following properties:
1502 * - {Int} r: The red component of the pixel
1503 * - {Int} g: The green component of the pixel
1504 * - {Int} b: The blue component of the pixel
1506 function colorAt(image, x, y) {
1507 // Create a test canvas element.
1508 const HTML_NS = "http://www.w3.org/1999/xhtml";
1509 const canvas = document.createElementNS(HTML_NS, "canvas");
1510 canvas.width = image.width;
1511 canvas.height = image.height;
1513 // Draw the image in the canvas
1514 const context = canvas.getContext("2d");
1515 context.drawImage(image, 0, 0, image.width, image.height);
1517 // Return the color found at the provided x,y coordinates as a "r, g, b" string.
1518 const [r, g, b] = context.getImageData(x, y, 1, 1).data;
1522 let allDownloads = [];
1524 * Returns a Promise that resolves when a new screenshot is available in the download folder.
1526 * @param {Object} [options]
1527 * @param {Boolean} options.isWindowPrivate: Set to true if the window from which the screenshot
1528 * is taken is a private window. This will ensure that we check that the
1529 * screenshot appears in the private window, not the non-private one (See Bug 1783373)
1531 async function waitUntilScreenshot({ isWindowPrivate = false } = {}) {
1532 const { Downloads } = ChromeUtils.importESModule(
1533 "resource://gre/modules/Downloads.sys.mjs"
1535 const list = await Downloads.getList(Downloads.ALL);
1537 return new Promise(function (resolve) {
1539 onDownloadAdded: async download => {
1540 await download.whenSucceeded();
1541 if (allDownloads.includes(download)) {
1546 !!download.source.isPrivate,
1548 `The download occured in the expected${
1549 isWindowPrivate ? " private" : ""
1553 allDownloads.push(download);
1554 resolve(download.target.path);
1555 list.removeView(view);
1564 * Clear all the download references.
1566 async function resetDownloads() {
1567 info("Reset downloads");
1568 const { Downloads } = ChromeUtils.importESModule(
1569 "resource://gre/modules/Downloads.sys.mjs"
1571 const downloadList = await Downloads.getList(Downloads.ALL);
1572 const downloads = await downloadList.getAll();
1573 for (const download of downloads) {
1574 downloadList.remove(download);
1575 await download.finalize(true);
1581 * Return a screenshot of the currently selected node in the inspector (using the internal
1582 * Inspector#screenshotNode method).
1584 * @param {Inspector} inspector
1587 async function takeNodeScreenshot(inspector) {
1588 // Cleanup all downloads at the end of the test.
1589 registerCleanupFunction(resetDownloads);
1592 "Call screenshotNode() and wait until the screenshot is found in the Downloads"
1594 const whenScreenshotSucceeded = waitUntilScreenshot();
1595 inspector.screenshotNode();
1596 const filePath = await whenScreenshotSucceeded;
1598 info("Create an image using the downloaded fileas source");
1599 const image = new Image();
1600 const onImageLoad = once(image, "load");
1601 image.src = PathUtils.toFileURI(filePath);
1604 info("Remove the downloaded screenshot file");
1605 await IOUtils.remove(filePath);
1607 // See intermittent Bug 1508435. Even after removing the file, tests still manage to
1608 // reuse files from the previous test if they have the same name. Since our file name
1609 // is based on a timestamp that has "second" precision, wait for one second to make sure
1610 // screenshots will have different names.
1612 "Wait for one second to make sure future screenshots will use a different name"
1614 await new Promise(r => setTimeout(r, 1000));
1620 * Check that the provided image has the expected width, height, and color.
1621 * NOTE: This test assumes that the image is only made of a single color and will only
1624 async function assertSingleColorScreenshotImage(
1630 info(`Assert ${image.src} content`);
1631 const ratio = await SpecialPowers.spawn(
1632 gBrowser.selectedBrowser,
1634 () => content.wrappedJSObject.devicePixelRatio
1640 `node screenshot has the expected width (dpr = ${ratio})`
1645 `node screenshot has the expected height (dpr = ${ratio})`
1648 const color = colorAt(image, 0, 0);
1649 is(color.r, r, "node screenshot has the expected red component");
1650 is(color.g, g, "node screenshot has the expected green component");
1651 is(color.b, b, "node screenshot has the expected blue component");
1655 * Check that the provided image has the expected color at a given position
1657 function checkImageColorAt({ image, x = 0, y, expectedColor, label }) {
1658 const color = colorAt(image, x, y);
1659 is(`rgb(${Object.values(color).join(", ")})`, expectedColor, label);
1663 * Wait until the store has reached a state that matches the predicate.
1664 * @param Store store
1665 * The Redux store being used.
1666 * @param function predicate
1667 * A function that returns true when the store has reached the expected
1670 * Resolved once the store reaches the expected state.
1672 function waitUntilState(store, predicate) {
1673 return new Promise(resolve => {
1674 const unsubscribe = store.subscribe(check);
1676 info(`Waiting for state predicate "${predicate}"`);
1678 if (predicate(store.getState())) {
1679 info(`Found state predicate "${predicate}"`);
1685 // Fire the check immediately in case the action has already occurred
1691 * Wait for a specific action type to be dispatched.
1693 * If the action is async and defines a `status` property, this helper will wait
1694 * for the status to reach either "error" or "done".
1696 * @param {Object} store
1697 * Redux store where the action should be dispatched.
1698 * @param {String} actionType
1699 * The actionType to wait for.
1700 * @param {Number} repeat
1701 * Optional, number of time the action is expected to be dispatched.
1705 function waitForDispatch(store, actionType, repeat = 1) {
1707 return new Promise(resolve => {
1709 type: "@@service/waitUntil",
1710 predicate: action => {
1713 action.status === "done" ||
1714 action.status === "error";
1716 if (action.type === actionType && isDone && ++count == repeat) {
1722 run: (dispatch, getState, action) => {
1730 * Retrieve a browsing context in nested frames.
1732 * @param {BrowsingContext|XULBrowser} browsingContext
1733 * The topmost browsing context under which we should search for the
1735 * @param {Array<String>} selectors
1736 * Array of CSS selectors that form a path to a specific nested frame.
1737 * @return {BrowsingContext} The nested browsing context.
1739 async function getBrowsingContextInFrames(browsingContext, selectors) {
1740 let context = browsingContext;
1742 if (!Array.isArray(selectors)) {
1744 "getBrowsingContextInFrames called with an invalid selectors argument"
1748 if (selectors.length === 0) {
1750 "getBrowsingContextInFrames called with an empty selectors array"
1754 const clonedSelectors = [...selectors];
1755 while (clonedSelectors.length) {
1756 const selector = clonedSelectors.shift();
1757 context = await SpecialPowers.spawn(context, [selector], _selector => {
1758 return content.document.querySelector(_selector).browsingContext;
1766 * Synthesize a mouse event on an element, after ensuring that it is visible
1769 * @param {String|Array} selector: The node selector to get the node target for the event.
1770 * To target an element in a specific iframe, pass an array of CSS selectors
1771 * (e.g. ["iframe", ".el-in-iframe"])
1774 * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse
1776 async function safeSynthesizeMouseEventInContentPage(
1782 let context = gBrowser.selectedBrowser.browsingContext;
1784 // If an array of selector is passed, we need to retrieve the context in which the node
1786 if (Array.isArray(selector)) {
1787 if (selector.length === 1) {
1788 selector = selector[0];
1790 context = await getBrowsingContextInFrames(
1792 // only pass the iframe path
1793 selector.slice(0, -1)
1795 // retrieve the last item of the selector, which should be the one for the node we want.
1796 selector = selector.at(-1);
1800 await scrollContentPageNodeIntoView(context, selector);
1801 BrowserTestUtils.synthesizeMouse(selector, x, y, options, context);
1805 * Synthesize a mouse event at the center of an element, after ensuring that it is visible
1808 * @param {String|Array} selector: The node selector to get the node target for the event.
1809 * To target an element in a specific iframe, pass an array of CSS selectors
1810 * (e.g. ["iframe", ".el-in-iframe"])
1811 * @param {object} options: Options that will be passed to BrowserTestUtils.synthesizeMouse
1813 async function safeSynthesizeMouseEventAtCenterInContentPage(
1817 let context = gBrowser.selectedBrowser.browsingContext;
1819 // If an array of selector is passed, we need to retrieve the context in which the node
1821 if (Array.isArray(selector)) {
1822 if (selector.length === 1) {
1823 selector = selector[0];
1825 context = await getBrowsingContextInFrames(
1827 // only pass the iframe path
1828 selector.slice(0, -1)
1830 // retrieve the last item of the selector, which should be the one for the node we want.
1831 selector = selector.at(-1);
1835 await scrollContentPageNodeIntoView(context, selector);
1836 BrowserTestUtils.synthesizeMouseAtCenter(selector, options, context);
1840 * Scroll into view an element in the content page matching the passed selector
1842 * @param {BrowsingContext} browsingContext: The browsing context the element lives in.
1843 * @param {String} selector: The node selector to get the node to scroll into view
1844 * @returns {Promise}
1846 function scrollContentPageNodeIntoView(browsingContext, selector) {
1847 return SpecialPowers.spawn(
1850 function (innerSelector) {
1852 content.wrappedJSObject.document.querySelector(innerSelector);
1853 node.scrollIntoView();
1859 * Change the zoom level of the selected page.
1861 * @param {Number} zoomLevel
1863 function setContentPageZoomLevel(zoomLevel) {
1864 gBrowser.selectedBrowser.fullZoom = zoomLevel;
1868 * Wait for the next DOCUMENT_EVENT dom-complete resource on a top-level target
1870 * @param {Object} commands
1871 * @return {Promise<Object>}
1872 * Return a promise which resolves once we fully settle the resource listener.
1873 * You should await for its resolution before doing the action which may fire
1875 * This promise will resolve with an object containing a `onDomCompleteResource` property,
1876 * which is also a promise, that will resolve once a "top-level" DOCUMENT_EVENT dom-complete
1879 async function waitForNextTopLevelDomCompleteResource(commands) {
1880 const { onResource: onDomCompleteResource } =
1881 await commands.resourceCommand.waitForNextResource(
1882 commands.resourceCommand.TYPES.DOCUMENT_EVENT,
1884 ignoreExistingResources: true,
1885 predicate: resource =>
1886 resource.name === "dom-complete" && resource.targetFront.isTopLevel,
1889 return { onDomCompleteResource };
1893 * Wait for the provided context to have a valid presShell. This can be useful
1894 * for tests which try to create popup panels or interact with the document very
1897 * @param {BrowsingContext} context
1899 function waitForPresShell(context) {
1900 return SpecialPowers.spawn(context, [], async () => {
1901 const winUtils = SpecialPowers.getDOMWindowUtils(content);
1902 await ContentTaskUtils.waitForCondition(() => {
1904 return !!winUtils.getPresShellId();
1908 }, "Waiting for a valid presShell");
1913 * In tests using Fluent localization, it is preferable to match DOM elements using
1914 * a message ID rather than the raw string as:
1916 * 1. It allows testing infrastructure to be multilingual if needed.
1917 * 2. It isolates the tests from localization changes.
1919 * @param {Array<string>} resourceIds A list of .ftl files to load.
1920 * @returns {(id: string, args?: Record<string, FluentVariable>) => string}
1922 async function getFluentStringHelper(resourceIds) {
1923 const locales = Services.locale.appLocalesAsBCP47;
1924 const generator = L10nRegistry.getInstance().generateBundles(
1930 for await (const bundle of generator) {
1931 bundles.push(bundle);
1934 const reactLocalization = new FluentReact.ReactLocalization(bundles);
1937 * Get the string from a message id. It throws when the message is not found.
1939 * @param {string} id
1940 * @param {string} attributeName: attribute name if you need to access a specific attribute
1941 * defined in the fluent string, e.g. setting "title" for this param
1942 * will retrieve the `title` string in
1943 * compatibility-issue-browsers-list =
1944 * .title = This is the title
1945 * @param {Record<string, FluentVariable>} [args] optional
1948 return (id, attributeName, args) => {
1951 if (!attributeName) {
1952 string = reactLocalization.getString(id, args);
1954 for (const bundle of reactLocalization.bundles) {
1955 const msg = bundle.getMessage(id);
1956 if (msg?.attributes[attributeName]) {
1957 string = bundle.formatPattern(
1958 msg.attributes[attributeName],
1969 `Could not find a string for "${id}"${
1970 attributeName ? ` and attribute "${attributeName}")` : ""
1971 }. Was the correct resource bundle loaded?`
1979 * Open responsive design mode for the given tab.
1981 async function openRDM(tab, { waitForDeviceList = true } = {}) {
1982 info("Opening responsive design mode");
1983 const manager = ResponsiveUIManager;
1984 const ui = await manager.openIfNeeded(tab.ownerGlobal, tab, {
1987 info("Responsive design mode opened");
1989 await ResponsiveMessageHelper.wait(ui.toolWindow, "post-init");
1990 info("Responsive design initialized");
1992 await waitForRDMLoaded(ui, { waitForDeviceList });
1994 return { ui, manager };
1997 async function waitForRDMLoaded(ui, { waitForDeviceList = true } = {}) {
1998 // Always wait for the viewport to be added.
1999 const { store } = ui.toolWindow;
2000 await waitUntilState(store, state => state.viewports.length == 1);
2002 if (waitForDeviceList) {
2003 // Wait until the device list has been loaded.
2004 await waitUntilState(
2006 state => state.devices.listState == localTypes.loadableState.LOADED
2012 * Close responsive design mode for the given tab.
2014 async function closeRDM(tab, options) {
2015 info("Closing responsive design mode");
2016 const manager = ResponsiveUIManager;
2017 await manager.closeIfNeeded(tab.ownerGlobal, tab, options);
2018 info("Responsive design mode closed");
2021 function getInputStream(data) {
2022 const BufferStream = Components.Constructor(
2023 "@mozilla.org/io/arraybuffer-input-stream;1",
2024 "nsIArrayBufferInputStream",
2027 const buffer = new TextEncoder().encode(data).buffer;
2028 return new BufferStream(buffer, 0, buffer.byteLength);
2032 * Wait for a specific target to have been fully processed by targetCommand.
2034 * @param {Commands} commands
2035 * The commands instance
2036 * @param {Function} isExpectedTargetFn
2037 * Predicate which will be called with a target front argument. Should
2038 * return true if the target front is the expected one, false otherwise.
2040 * Promise which resolves when a target matching `isExpectedTargetFn`
2041 * has been processed by targetCommand.
2043 function waitForTargetProcessed(commands, isExpectedTargetFn) {
2044 return new Promise(resolve => {
2045 const onProcessed = targetFront => {
2047 if (isExpectedTargetFn(targetFront)) {
2048 commands.targetCommand.off("processed-available-target", onProcessed);
2052 // Ignore errors from isExpectedTargetFn.
2056 commands.targetCommand.on("processed-available-target", onProcessed);
2061 * Instantiate a HTTP Server that serves files from a given test folder.
2062 * The test folder should be made of multiple sub folder named: v1, v2, v3,...
2063 * We will serve the content from one of these sub folder
2064 * and switch to the next one, each time `httpServer.switchToNextVersion()`
2067 * @return Object Test server with two functions:
2069 * Returns the absolute url for a given file.
2070 * - switchToNextVersion()
2071 * Start serving files from the next available sub folder.
2072 * - backToFirstVersion()
2073 * When running more than one test, helps restart from the first folder.
2075 function createVersionizedHttpTestServer(testFolderName) {
2076 const httpServer = createTestHTTPServer();
2078 let currentVersion = 1;
2080 httpServer.registerPrefixHandler("/", async (request, response) => {
2081 response.processAsync();
2082 response.setStatusLine(request.httpVersion, 200, "OK");
2083 if (request.path.endsWith(".js")) {
2084 response.setHeader("Content-Type", "application/javascript");
2085 } else if (request.path.endsWith(".js.map")) {
2086 response.setHeader("Content-Type", "application/json");
2088 if (request.path == "/" || request.path.endsWith(".html")) {
2089 response.setHeader("Content-Type", "text/html");
2091 // If a query string is passed, lookup with a matching file, if available
2092 // The '?' is replaced by '.'
2095 if (request.queryString) {
2096 const url = `${URL_ROOT_SSL}${testFolderName}/v${currentVersion}${request.path}.${request.queryString}`;
2098 fetchResponse = await fetch(url);
2099 // Log this only if the request succeed
2100 info(`[test-http-server] serving: ${url}`);
2102 // Ignore any error and proceed without the query string
2103 fetchResponse = null;
2107 if (!fetchResponse) {
2108 const url = `${URL_ROOT_SSL}${testFolderName}/v${currentVersion}${request.path}`;
2109 info(`[test-http-server] serving: ${url}`);
2110 fetchResponse = await fetch(url);
2113 // Ensure forwarding the response headers generated by the other http server
2114 // (this can be especially useful when query .sjs files)
2115 for (const [name, value] of fetchResponse.headers.entries()) {
2116 response.setHeader(name, value);
2119 // Override cache settings so that versionized requests are never cached
2120 // and we get brand new content for any request.
2121 response.setHeader("Cache-Control", "no-store");
2123 const text = await fetchResponse.text();
2124 response.write(text);
2129 switchToNextVersion() {
2132 backToFirstVersion() {
2136 const port = httpServer.identity.primaryPort;
2137 return `http://localhost:${port}/${path}`;
2143 * Fake clicking a link and return the URL we would have navigated to.
2144 * This function should be used to check external links since we can't access
2146 * This can also be used to test that a click will not be fired.
2148 * @param ElementNode element
2149 * The <a> element we want to simulate click on.
2151 * A Promise that is resolved when the link click simulation occured or
2152 * when the click is not dispatched.
2153 * The promise resolves with an object that holds the following properties
2154 * - link: url of the link or null(if event not fired)
2155 * - where: "tab" if tab is active or "tabshifted" if tab is inactive
2156 * or null(if event not fired)
2158 function simulateLinkClick(element) {
2159 const browserWindow = Services.wm.getMostRecentWindow(
2160 gDevTools.chromeWindowType
2163 const onOpenLink = new Promise(resolve => {
2164 const openLinkIn = (link, where) => resolve({ link, where });
2165 sinon.replace(browserWindow, "openTrustedLinkIn", openLinkIn);
2166 sinon.replace(browserWindow, "openWebLinkIn", openLinkIn);
2171 // Declare a timeout Promise that we can use to make sure spied methods were not called.
2172 const onTimeout = new Promise(function (resolve) {
2174 resolve({ link: null, where: null });
2178 const raceResult = Promise.race([onOpenLink, onTimeout]);
2184 * Since the MDN data is updated frequently, it might happen that the properties used in
2185 * this test are not in the dataset anymore/now have URLs.
2186 * This function will return properties in the dataset that don't have MDN url so you
2187 * can easily find a replacement.
2189 function logCssCompatDataPropertiesWithoutMDNUrl() {
2190 const cssPropertiesCompatData = require("resource://devtools/shared/compatibility/dataset/css-properties.json");
2192 function walk(node) {
2193 for (const propertyName in node) {
2194 const property = node[propertyName];
2195 if (property.__compat) {
2196 if (!property.__compat.mdn_url) {
2198 `"${propertyName}" - MDN URL: ${
2199 property.__compat.mdn_url || "❌"
2200 } - Spec URL: ${property.__compat.spec_url || "❌"}\n`
2203 } else if (typeof property == "object") {
2208 walk(cssPropertiesCompatData);
2212 * Craft a CssProperties instance without involving RDP for tests
2213 * manually spawning OutputParser, CssCompleter, Editor...
2215 * Otherwise this should instead be fetched from CssPropertiesFront.
2217 * @return {CssProperties}
2219 function getClientCssProperties() {
2221 generateCssProperties,
2222 } = require("resource://devtools/server/actors/css-properties.js");
2226 } = require("resource://devtools/client/fronts/css-properties.js");
2227 return new CssProperties(
2228 normalizeCssData({ properties: generateCssProperties() })