1 /* eslint-disable no-unused-vars */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
7 * Helper methods to drive with the debugger during mochitests. This file can be safely
8 * required from other panel test files.
11 // Import helpers for the new debugger
12 Services.scriptloader.loadSubScript(
13 "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/helpers/context.js",
17 var { Toolbox } = require("devtools/client/framework/toolbox");
18 const { Task } = require("devtools/shared/task");
19 const asyncStorage = require("devtools/shared/async-storage");
23 } = require("devtools/client/debugger/src/utils/selected-location");
27 } = require("devtools/client/debugger/src/utils/prefs");
29 function log(msg, data) {
30 info(`${msg} ${!data ? "" : JSON.stringify(data)}`);
33 function logThreadEvents(dbg, event) {
34 const thread = dbg.toolbox.threadFront;
36 thread.on(event, function onEvent(...args) {
37 info(`Thread event '${event}' fired.`);
41 // Wait until an action of `type` is dispatched. This is different
42 // then `waitForDispatch` because it doesn't wait for async actions
43 // to be done/errored. Use this if you want to listen for the "start"
44 // action of an async operation (somewhat rare).
45 function waitForNextDispatch(store, actionType) {
46 return new Promise(resolve => {
48 // Normally we would use `services.WAIT_UNTIL`, but use the
49 // internal name here so tests aren't forced to always pass it
51 type: "@@service/waitUntil",
52 predicate: action => action.type === actionType,
53 run: (dispatch, getState, action) => {
61 * Waits for `predicate()` to be true. `state` is the redux app state.
63 * @memberof mochitest/waits
65 * @param {Function} predicate
69 function waitForState(dbg, predicate, msg) {
70 return new Promise(resolve => {
71 info(`Waiting for state change: ${msg || ""}`);
72 if (predicate(dbg.store.getState())) {
73 info(`Finished waiting for state change: ${msg || ""}`);
77 const unsubscribe = dbg.store.subscribe(() => {
78 const result = predicate(dbg.store.getState());
80 info(`Finished waiting for state change: ${msg || ""}`);
89 * Waits for sources to be loaded.
91 * @memberof mochitest/waits
93 * @param {Array} sources
97 async function waitForSources(dbg, ...sources) {
98 if (sources.length === 0) {
99 return Promise.resolve();
102 info(`Waiting on sources: ${sources.join(", ")}`);
105 if (!sourceExists(dbg, url)) {
108 () => sourceExists(dbg, url),
109 `source ${url} exists`
115 info(`Finished waiting on sources: ${sources.join(", ")}`);
119 * Waits for a source to be loaded.
121 * @memberof mochitest/waits
122 * @param {Object} dbg
123 * @param {String} source
127 function waitForSource(dbg, url) {
130 state => findSource(dbg, url, { silent: true }),
135 async function waitForElement(dbg, name, ...args) {
136 await waitUntil(() => findElement(dbg, name, ...args));
137 return findElement(dbg, name, ...args);
141 * Wait for a count of given elements to be rendered on screen.
143 * @param {DebuggerPanel} dbg
144 * @param {String} name
145 * @param {Integer} count: Number of elements to match. Defaults to 1.
146 * @param {Boolean} countStrictlyEqual: When set to true, will wait until the exact number
147 * of elements is displayed on screen. When undefined or false, will wait
148 * until there's at least `${count}` elements on screen (e.g. if count
149 * is 1, it will resolve if there are 2 elements rendered).
151 async function waitForAllElements(
155 countStrictlyEqual = false
157 await waitUntil(() => {
158 const elsCount = findAllElements(dbg, name).length;
159 return countStrictlyEqual ? elsCount === count : elsCount >= count;
161 return findAllElements(dbg, name);
164 async function waitForElementWithSelector(dbg, selector) {
165 await waitUntil(() => findElementWithSelector(dbg, selector));
166 return findElementWithSelector(dbg, selector);
169 function waitForRequestsToSettle(dbg) {
170 return dbg.toolbox.target.client.waitForRequestsToSettle();
173 function assertClass(el, className, exists = true) {
175 ok(el.classList.contains(className), `${className} class exists`);
177 ok(!el.classList.contains(className), `${className} class does not exist`);
181 function waitForSelectedLocation(dbg, line, column) {
182 return waitForState(dbg, state => {
183 const location = dbg.selectors.getSelectedLocation();
186 (line ? location.line == line : true) &&
187 (column ? location.column == column : true)
193 * Wait for a given source to be selected and ready.
195 * @memberof mochitest/waits
196 * @param {Object} dbg
197 * @param {null|string|Source} sourceOrUrl Optional. Either a source URL (string) or a source object (typically fetched via `findSource`)
201 function waitForSelectedSource(dbg, sourceOrUrl) {
203 getSelectedSourceWithContent,
211 const source = getSelectedSourceWithContent() || {};
212 if (!source.content) {
217 // Second argument is either a source URL (string)
218 // or a Source object.
219 if (typeof sourceOrUrl == "string") {
220 if (!source.url.includes(sourceOrUrl)) {
224 if (source.id != sourceOrUrl.id) {
230 return hasSymbols(source) && getBreakableLines(source.id);
237 * Assert that the debugger is not currently paused.
238 * @memberof mochitest/asserts
241 function assertNotPaused(dbg) {
242 ok(!isPaused(dbg), "client is not paused");
246 * Assert that the debugger is currently paused.
247 * @memberof mochitest/asserts
250 function assertPaused(dbg) {
251 ok(isPaused(dbg), "client is paused");
254 function assertEmptyLines(dbg, lines) {
255 const sourceId = dbg.selectors.getSelectedSourceId();
256 const breakableLines = dbg.selectors.getBreakableLines(sourceId);
258 lines.every(line => !breakableLines.includes(line)),
259 "empty lines should match"
263 function getVisibleSelectedFrameLine(dbg) {
265 selectors: { getVisibleSelectedFrame },
267 const frame = getVisibleSelectedFrame();
268 return frame && frame.location.line;
271 function waitForPausedLine(dbg, line) {
272 return waitForState(dbg, () => getVisibleSelectedFrameLine(dbg) == line);
276 * Assert that the debugger is paused at the correct location.
278 * @memberof mochitest/asserts
279 * @param {Object} dbg
280 * @param {String} source
281 * @param {Number} line
284 function assertPausedLocation(dbg) {
285 ok(isSelectedFrameSelected(dbg), "top frame's source is selected");
287 // Check the pause location
288 const pauseLine = getVisibleSelectedFrameLine(dbg);
289 assertDebugLine(dbg, pauseLine);
291 ok(isVisibleInEditor(dbg, getCM(dbg).display.gutters), "gutter is visible");
294 function assertDebugLine(dbg, line, column) {
295 // Check the debug line
296 const lineInfo = getCM(dbg).lineInfo(line - 1);
297 const source = dbg.selectors.getSelectedSourceWithContent() || {};
298 if (source && !source.content) {
299 const url = source.url;
302 `Looks like the source ${url} is still loading. Try adding waitForLoadedSource in the test.`
307 if (!lineInfo.wrapClass) {
308 const pauseLine = getVisibleSelectedFrameLine(dbg);
309 ok(false, `Expected pause line on line ${line}, it is on ${pauseLine}`);
314 lineInfo?.wrapClass.includes("new-debug-line"),
315 `Line ${line} is not highlighted as paused`
319 findElement(dbg, "debugLine") || findElement(dbg, "debugErrorLine");
322 findAllElements(dbg, "debugLine").length +
323 findAllElements(dbg, "debugErrorLine").length,
325 "There is only one line"
328 ok(isVisibleInEditor(dbg, debugLine), "debug line is visible");
330 const markedSpans = lineInfo.handle.markedSpans;
331 if (markedSpans && markedSpans.length > 0) {
335 span.marker.className &&
336 span.marker.className.includes("debug-expression")
340 const frame = dbg.selectors.getVisibleSelectedFrame();
341 is(frame.location.column, column, `Paused at column ${column}`);
344 ok(classMatch, "expression is highlighted as paused");
346 info(`Paused on line ${line}`);
350 * Assert that the debugger is highlighting the correct location.
352 * @memberof mochitest/asserts
353 * @param {Object} dbg
354 * @param {String} source
355 * @param {Number} line
358 function assertHighlightLocation(dbg, source, line) {
359 source = findSource(dbg, source);
361 // Check the selected source
363 dbg.selectors.getSelectedSource().url,
365 "source url is correct"
368 // Check the highlight line
369 const lineEl = findElement(dbg, "highlightLine");
370 ok(lineEl, "Line is highlighted");
373 findAllElements(dbg, "highlightLine").length,
375 "Only 1 line is highlighted"
378 ok(isVisibleInEditor(dbg, lineEl), "Highlighted line is visible");
380 const cm = getCM(dbg);
381 const lineInfo = cm.lineInfo(line - 1);
382 ok(lineInfo.wrapClass.includes("highlight-line"), "Line is highlighted");
386 * Returns boolean for whether the debugger is paused.
388 * @memberof mochitest/asserts
389 * @param {Object} dbg
392 function isPaused(dbg) {
393 return dbg.selectors.getIsCurrentThreadPaused();
396 // Make sure the debugger is paused at a certain source ID and line.
397 function assertPausedAtSourceAndLine(dbg, expectedSourceId, expectedLine) {
400 const frames = dbg.selectors.getCurrentThreadFrames();
401 ok(frames.length >= 1, "Got at least one frame");
402 const { sourceId, line } = frames[0].location;
403 ok(sourceId == expectedSourceId, "Frame has correct source");
405 line == expectedLine,
406 `Frame paused at ${line}, but expected ${expectedLine}`
410 async function waitForThreadCount(dbg, count) {
413 state => dbg.selectors.getThreads(state).length == count
417 async function waitForLoadedScopes(dbg) {
418 const scopes = await waitForElement(dbg, "scopes");
419 // Since scopes auto-expand, we can assume they are loaded when there is a tree node
420 // with the aria-level attribute equal to "2".
421 await waitUntil(() => scopes.querySelector('.tree-node[aria-level="2"]'));
424 function waitForBreakpointCount(dbg, count) {
427 state => dbg.selectors.getBreakpointCount() == count
431 function waitForBreakpoint(dbg, url, line) {
432 return waitForState(dbg, () => findBreakpoint(dbg, url, line));
435 function waitForBreakpointRemoved(dbg, url, line) {
436 return waitForState(dbg, () => !findBreakpoint(dbg, url, line));
440 * Waits for the debugger to be fully paused.
442 * @memberof mochitest/waits
443 * @param {Object} dbg
446 async function waitForPaused(dbg, url) {
450 getCurrentThreadFrames,
455 state => isPaused(dbg) && !!getSelectedScope(getCurrentThread()),
459 await waitForState(dbg, getCurrentThreadFrames, "fetched frames");
460 await waitForLoadedScopes(dbg);
461 await waitForSelectedSource(dbg, url);
464 function waitForInlinePreviews(dbg) {
465 return waitForState(dbg, () => dbg.selectors.getSelectedInlinePreviews());
468 function waitForCondition(dbg, condition) {
469 return waitForState(dbg, state =>
471 .getBreakpointsList()
472 .find(bp => bp.options.condition == condition)
476 function waitForLog(dbg, logValue) {
477 return waitForState(dbg, state =>
479 .getBreakpointsList()
480 .find(bp => bp.options.logValue == logValue)
484 async function waitForPausedThread(dbg, thread) {
485 return waitForState(dbg, state => dbg.selectors.getIsPaused(thread));
489 * useful for when you want to see what is happening
490 * e.g await waitForever()
492 function waitForever() {
493 return new Promise(r => {});
497 * useful for waiting for a short amount of time as
498 * a placeholder for a better waitForX handler.
500 * e.g await waitForTime(500)
502 function waitForTime(ms) {
503 return new Promise(r => setTimeout(r, ms));
506 function isSelectedFrameSelected(dbg, state) {
507 const frame = dbg.selectors.getVisibleSelectedFrame();
509 // Make sure the source text is completely loaded for the
510 // source we are paused in.
511 const sourceId = frame.location.sourceId;
512 const source = dbg.selectors.getSelectedSourceWithContent() || {};
514 if (!source || !source.content) {
518 return source.id == sourceId;
522 * Clear all the debugger related preferences.
524 async function clearDebuggerPreferences(prefs = []) {
525 resetSchemaVersion();
526 asyncStorage.clear();
527 Services.prefs.clearUserPref("devtools.debugger.alphabetize-outline");
528 Services.prefs.clearUserPref("devtools.debugger.pause-on-exceptions");
529 Services.prefs.clearUserPref("devtools.debugger.pause-on-caught-exceptions");
530 Services.prefs.clearUserPref("devtools.debugger.ignore-caught-exceptions");
531 Services.prefs.clearUserPref("devtools.debugger.pending-selected-location");
532 Services.prefs.clearUserPref("devtools.debugger.expressions");
533 Services.prefs.clearUserPref("devtools.debugger.breakpoints-visible");
534 Services.prefs.clearUserPref("devtools.debugger.call-stack-visible");
535 Services.prefs.clearUserPref("devtools.debugger.scopes-visible");
536 Services.prefs.clearUserPref("devtools.debugger.skip-pausing");
537 Services.prefs.clearUserPref("devtools.debugger.map-scopes-enabled");
538 await pushPref("devtools.debugger.log-actions", true);
540 for (const pref of prefs) {
541 await pushPref(...pref);
546 * Intilializes the debugger.
548 * @memberof mochitest
549 * @param {String} url
550 * @return {Promise} dbg
554 async function initDebugger(url, ...sources) {
555 // We depend on EXAMPLE_URLs origin to do cross origin/process iframes via
556 // EXAMPLE_REMOTE_URL. If the top level document origin changes,
557 // we may break this. So be careful if you want to change EXAMPLE_URL.
558 return initDebuggerWithAbsoluteURL(EXAMPLE_URL + url, ...sources);
561 async function initDebuggerWithAbsoluteURL(url, ...sources) {
562 await clearDebuggerPreferences();
563 const toolbox = await openNewTabAndToolbox(url, "jsdebugger");
564 const dbg = createDebuggerContext(toolbox);
566 await waitForSources(dbg, ...sources);
570 async function initPane(url, pane, prefs) {
571 await clearDebuggerPreferences(prefs);
572 return openNewTabAndToolbox(EXAMPLE_URL + url, pane);
575 window.resumeTest = undefined;
576 registerCleanupFunction(() => {
577 delete window.resumeTest;
581 * Pause the test and let you interact with the debugger.
582 * The test can be resumed by invoking `resumeTest` in the console.
584 * @memberof mochitest
587 function pauseTest() {
588 info("Test paused. Invoke resumeTest to continue.");
589 return new Promise(resolve => (resumeTest = resolve));
594 * Returns a source that matches the URL.
596 * @memberof mochitest/actions
597 * @param {Object} dbg
598 * @param {String} url
599 * @return {Object} source
602 function findSource(dbg, url, { silent } = { silent: false }) {
603 if (typeof url !== "string") {
604 // Support passing in a source object itelf all APIs that use this
605 // function support both styles
610 const sources = dbg.selectors.getSourceList();
611 const source = sources.find(s => (s.url || "").includes(url));
618 throw new Error(`Unable to find source: ${url}`);
624 function findSourceContent(dbg, url, opts) {
625 const source = findSource(dbg, url, opts);
631 const content = dbg.selectors.getSourceContent(source.id);
637 if (content.state !== "fulfilled") {
638 throw new Error("Expected loaded source, got" + content.value);
641 return content.value;
644 function sourceExists(dbg, url) {
645 return !!findSource(dbg, url, { silent: true });
648 function waitForLoadedSource(dbg, url) {
652 const source = findSource(dbg, url, { silent: true });
653 return source && dbg.selectors.getSourceContent(source.id);
659 function waitForLoadedSources(dbg) {
663 const sources = dbg.selectors.getSourceList();
664 return sources.every(
665 source => !!dbg.selectors.getSourceContent(source.id)
672 function getContext(dbg) {
673 return dbg.selectors.getContext();
676 function getThreadContext(dbg) {
677 return dbg.selectors.getThreadContext();
681 * Selects the source.
683 * @memberof mochitest/actions
684 * @param {Object} dbg
685 * @param {String} url
686 * @param {Number} line
690 async function selectSource(dbg, url, line, column) {
691 const source = findSource(dbg, url);
692 await dbg.actions.selectLocation(
694 { sourceId: source.id, line, column },
695 { keepContext: false }
697 return waitForSelectedSource(dbg, url);
700 async function closeTab(dbg, url) {
701 await dbg.actions.closeTab(getContext(dbg), findSource(dbg, url));
704 function countTabs(dbg) {
705 return findElement(dbg, "sourceTabs").children.length;
711 * @memberof mochitest/actions
712 * @param {Object} dbg
716 async function stepOver(dbg) {
717 const pauseLine = getVisibleSelectedFrameLine(dbg);
718 info(`Stepping over from ${pauseLine}`);
719 await dbg.actions.stepOver(getThreadContext(dbg));
720 return waitForPaused(dbg);
726 * @memberof mochitest/actions
727 * @param {Object} dbg
731 async function stepIn(dbg) {
732 const pauseLine = getVisibleSelectedFrameLine(dbg);
733 info(`Stepping in from ${pauseLine}`);
734 await dbg.actions.stepIn(getThreadContext(dbg));
735 return waitForPaused(dbg);
741 * @memberof mochitest/actions
742 * @param {Object} dbg
746 async function stepOut(dbg) {
747 const pauseLine = getVisibleSelectedFrameLine(dbg);
748 info(`Stepping out from ${pauseLine}`);
749 await dbg.actions.stepOut(getThreadContext(dbg));
750 return waitForPaused(dbg);
756 * @memberof mochitest/actions
757 * @param {Object} dbg
761 async function resume(dbg) {
762 const pauseLine = getVisibleSelectedFrameLine(dbg);
763 info(`Resuming from ${pauseLine}`);
764 const onResumed = waitForActive(dbg);
765 await dbg.actions.resume(getThreadContext(dbg));
769 function deleteExpression(dbg, input) {
770 info(`Delete expression "${input}"`);
771 return dbg.actions.deleteExpression({ input });
775 * Reloads the debuggee.
777 * @memberof mochitest/actions
778 * @param {Object} dbg
779 * @param {Array} sources
783 async function reload(dbg, ...sources) {
784 const navigated = waitForDispatch(dbg.store, "NAVIGATE");
785 await dbg.client.reload();
787 return waitForSources(dbg, ...sources);
791 * Navigates the debuggee to another url.
793 * @memberof mochitest/actions
794 * @param {Object} dbg
795 * @param {String} url
796 * @param {Array} sources
800 async function navigate(dbg, url, ...sources) {
801 info(`Navigating to ${url}`);
802 const navigated = waitForDispatch(dbg.store, "NAVIGATE");
803 await dbg.client.navigate(url);
805 return waitForSources(dbg, ...sources);
808 function getFirstBreakpointColumn(dbg, { line, sourceId }) {
809 const { getSource, getFirstBreakpointPosition } = dbg.selectors;
810 const source = getSource(sourceId);
811 const position = getFirstBreakpointPosition({
816 return getSelectedLocation(position, source).column;
820 * Adds a breakpoint to a source at line/col.
822 * @memberof mochitest/actions
823 * @param {Object} dbg
824 * @param {String} source
825 * @param {Number} line
826 * @param {Number} col
830 async function addBreakpoint(dbg, source, line, column, options) {
831 source = findSource(dbg, source);
832 const sourceId = source.id;
833 const bpCount = dbg.selectors.getBreakpointCount();
834 const onBreakpoint = waitForDispatch(dbg.store, "SET_BREAKPOINT");
835 await dbg.actions.addBreakpoint(
837 { sourceId, line, column },
842 dbg.selectors.getBreakpointCount(),
844 "a new breakpoint was created"
848 function disableBreakpoint(dbg, source, line, column) {
850 column || getFirstBreakpointColumn(dbg, { line, sourceId: source.id });
851 const location = { sourceId: source.id, sourceUrl: source.url, line, column };
852 const bp = dbg.selectors.getBreakpointForLocation(location);
853 return dbg.actions.disableBreakpoint(getContext(dbg), bp);
856 function setBreakpointOptions(dbg, source, line, column, options) {
857 source = findSource(dbg, source);
858 const sourceId = source.id;
859 column = column || getFirstBreakpointColumn(dbg, { line, sourceId });
860 return dbg.actions.setBreakpointOptions(
862 { sourceId, line, column },
867 function findBreakpoint(dbg, url, line) {
868 const source = findSource(dbg, url);
869 return dbg.selectors.getBreakpointsForSource(source.id, line)[0];
872 // helper for finding column breakpoints.
873 function findColumnBreakpoint(dbg, url, line, column) {
874 const source = findSource(dbg, url);
875 const lineBreakpoints = dbg.selectors.getBreakpointsForSource(
879 return lineBreakpoints.find(bp => {
880 return bp.generatedLocation.column === column;
884 async function loadAndAddBreakpoint(dbg, filename, line, column) {
886 selectors: { getBreakpoint, getBreakpointCount, getBreakpointsMap },
889 await waitForSources(dbg, filename);
891 ok(true, "Original sources exist");
892 const source = findSource(dbg, filename);
894 await selectSource(dbg, source);
896 // Test that breakpoint is not off by a line.
897 await addBreakpoint(dbg, source, line, column);
899 is(getBreakpointCount(), 1, "One breakpoint exists");
900 if (!getBreakpoint({ sourceId: source.id, line, column })) {
901 const breakpoints = getBreakpointsMap();
902 const id = Object.keys(breakpoints).pop();
903 const loc = breakpoints[id].location;
906 `Breakpoint has correct line ${line}, column ${column}, but was line ${loc.line} column ${loc.column}`
913 async function invokeWithBreakpoint(
920 const source = await loadAndAddBreakpoint(dbg, filename, line, column);
922 const invokeResult = invokeInTab(fnName);
924 const invokeFailed = await Promise.race([
927 () => new Promise(() => {}),
936 assertPausedLocation(dbg);
938 await removeBreakpoint(dbg, source.id, line, column);
940 is(dbg.selectors.getBreakpointCount(), 0, "Breakpoint reverted");
942 await handler(source);
946 // eslint-disable-next-line max-len
947 // If the invoke errored later somehow, capture here so the error is reported nicely.
951 function prettyPrint(dbg) {
952 const sourceId = dbg.selectors.getSelectedSourceId();
953 return dbg.actions.togglePrettyPrint(getContext(dbg), sourceId);
956 async function expandAllScopes(dbg) {
957 const scopes = await waitForElement(dbg, "scopes");
958 const scopeElements = scopes.querySelectorAll(
959 '.tree-node[aria-level="1"][data-expandable="true"]:not([aria-expanded="true"])'
961 const indices = Array.from(scopeElements, el => {
962 return Array.prototype.indexOf.call(el.parentNode.childNodes, el);
965 for (const index of indices) {
966 await toggleScopeNode(dbg, index + 1);
970 async function assertScopes(dbg, items) {
971 await expandAllScopes(dbg);
973 for (const [i, val] of items.entries()) {
974 if (Array.isArray(val)) {
975 is(getScopeLabel(dbg, i + 1), val[0]);
977 getScopeValue(dbg, i + 1),
979 `"${val[0]}" has the expected "${val[1]}" value`
982 is(getScopeLabel(dbg, i + 1), val);
986 is(getScopeLabel(dbg, items.length + 1), "Window");
989 function findSourceNodeWithText(dbg, text) {
990 return [...findAllElements(dbg, "sourceNodes")].find(el => {
991 return el.textContent.includes(text);
995 function expandAllSourceNodes(dbg, treeNode) {
996 rightClickEl(dbg, treeNode);
997 selectContextMenuItem(dbg, "#node-menu-expand-all");
1001 * Removes a breakpoint from a source at line/col.
1003 * @memberof mochitest/actions
1004 * @param {Object} dbg
1005 * @param {String} source
1006 * @param {Number} line
1007 * @param {Number} col
1011 function removeBreakpoint(dbg, sourceId, line, column) {
1012 const source = dbg.selectors.getSource(sourceId);
1013 column = column || getFirstBreakpointColumn(dbg, { line, sourceId });
1014 const location = { sourceId, sourceUrl: source.url, line, column };
1015 const bp = dbg.selectors.getBreakpointForLocation(location);
1016 return dbg.actions.removeBreakpoint(getContext(dbg), bp);
1020 * Toggles the Pause on exceptions feature in the debugger.
1022 * @memberof mochitest/actions
1023 * @param {Object} dbg
1024 * @param {Boolean} pauseOnExceptions
1025 * @param {Boolean} pauseOnCaughtExceptions
1029 async function togglePauseOnExceptions(
1032 pauseOnCaughtExceptions
1034 return dbg.actions.pauseOnExceptions(
1036 pauseOnCaughtExceptions
1040 function waitForActive(dbg) {
1041 return waitForState(dbg, state => !dbg.selectors.getIsCurrentThreadPaused());
1047 * Invokes a global function in the debuggee tab.
1049 * @memberof mochitest/helpers
1050 * @param {String} fnc The name of a global function on the content window to
1051 * call. This is applied to structured clones of the
1052 * remaining arguments to invokeInTab.
1053 * @param {Any} ...args Remaining args to serialize and pass to fnc.
1057 function invokeInTab(fnc, ...args) {
1058 info(`Invoking in tab: ${fnc}(${args.map(uneval).join(",")})`);
1059 return ContentTask.spawn(
1060 gBrowser.selectedBrowser,
1062 ({ fnc, args }) => content.wrappedJSObject[fnc](...args)
1066 function clickElementInTab(selector) {
1067 info(`click element ${selector} in tab`);
1069 return SpecialPowers.spawn(
1070 gBrowser.selectedBrowser,
1072 function({ selector }) {
1073 content.wrappedJSObject.document.querySelector(selector).click();
1078 const isLinux = Services.appinfo.OS === "Linux";
1079 const isMac = Services.appinfo.OS === "Darwin";
1080 const cmdOrCtrl = isLinux ? { ctrlKey: true } : { metaKey: true };
1081 const shiftOrAlt = isMac
1082 ? { accelKey: true, shiftKey: true }
1083 : { accelKey: true, altKey: true };
1085 const cmdShift = isMac
1086 ? { accelKey: true, shiftKey: true, metaKey: true }
1087 : { accelKey: true, shiftKey: true, ctrlKey: true };
1089 // On Mac, going to beginning/end only works with meta+left/right. On
1090 // Windows, it only works with home/end. On Linux, apparently, either
1091 // ctrl+left/right or home/end work.
1092 const endKey = isMac
1093 ? { code: "VK_RIGHT", modifiers: cmdOrCtrl }
1094 : { code: "VK_END" };
1095 const startKey = isMac
1096 ? { code: "VK_LEFT", modifiers: cmdOrCtrl }
1097 : { code: "VK_HOME" };
1099 const keyMappings = {
1100 close: { code: "w", modifiers: cmdOrCtrl },
1101 commandKeyDown: { code: "VK_META", modifiers: { type: "keydown" } },
1102 commandKeyUp: { code: "VK_META", modifiers: { type: "keyup" } },
1103 debugger: { code: "s", modifiers: shiftOrAlt },
1104 // test conditional panel shortcut
1105 toggleCondPanel: { code: "b", modifiers: cmdShift },
1106 inspector: { code: "c", modifiers: shiftOrAlt },
1107 quickOpen: { code: "p", modifiers: cmdOrCtrl },
1108 quickOpenFunc: { code: "o", modifiers: cmdShift },
1109 quickOpenLine: { code: ":", modifiers: cmdOrCtrl },
1110 fileSearch: { code: "f", modifiers: cmdOrCtrl },
1111 fileSearchNext: { code: "g", modifiers: { metaKey: true } },
1112 fileSearchPrev: { code: "g", modifiers: cmdShift },
1113 goToLine: { code: "g", modifiers: { ctrlKey: true } },
1114 Enter: { code: "VK_RETURN" },
1115 ShiftEnter: { code: "VK_RETURN", modifiers: shiftOrAlt },
1118 modifiers: { altKey: true },
1120 Up: { code: "VK_UP" },
1121 Down: { code: "VK_DOWN" },
1122 Right: { code: "VK_RIGHT" },
1123 Left: { code: "VK_LEFT" },
1126 Tab: { code: "VK_TAB" },
1127 Escape: { code: "VK_ESCAPE" },
1128 Delete: { code: "VK_DELETE" },
1129 pauseKey: { code: "VK_F8" },
1130 resumeKey: { code: "VK_F8" },
1131 stepOverKey: { code: "VK_F10" },
1132 stepInKey: { code: "VK_F11", modifiers: { ctrlKey: isLinux } },
1135 modifiers: { ctrlKey: isLinux, shiftKey: true },
1140 * Simulates a key press in the debugger window.
1142 * @memberof mochitest/helpers
1143 * @param {Object} dbg
1144 * @param {String} keyName
1148 function pressKey(dbg, keyName) {
1149 const keyEvent = keyMappings[keyName];
1151 const { code, modifiers } = keyEvent;
1152 return EventUtils.synthesizeKey(code, modifiers || {}, dbg.win);
1155 function type(dbg, string) {
1156 string.split("").forEach(char => EventUtils.synthesizeKey(char, {}, dbg.win));
1160 * Checks to see if the inner element is visible inside the editor.
1162 * @memberof mochitest/helpers
1163 * @param {Object} dbg
1164 * @param {HTMLElement} inner element
1169 function isVisibleInEditor(dbg, element) {
1170 return isVisible(findElement(dbg, "codeMirror"), element);
1174 * Checks to see if the inner element is visible inside the
1177 * Note, the inner element does not need to be entirely visible,
1178 * it is possible for it to be somewhat clipped by the outer element's
1179 * bounding element or for it to span the entire length, starting before the
1180 * outer element and ending after.
1182 * @memberof mochitest/helpers
1183 * @param {HTMLElement} outer element
1184 * @param {HTMLElement} inner element
1188 function isVisible(outerEl, innerEl) {
1189 if (!innerEl || !outerEl) {
1193 const innerRect = innerEl.getBoundingClientRect();
1194 const outerRect = outerEl.getBoundingClientRect();
1196 const verticallyVisible =
1197 innerRect.top >= outerRect.top ||
1198 innerRect.bottom <= outerRect.bottom ||
1199 (innerRect.top < outerRect.top && innerRect.bottom > outerRect.bottom);
1201 const horizontallyVisible =
1202 innerRect.left >= outerRect.left ||
1203 innerRect.right <= outerRect.right ||
1204 (innerRect.left < outerRect.left && innerRect.right > outerRect.right);
1206 const visible = verticallyVisible && horizontallyVisible;
1210 async function getEditorLineGutter(dbg, line) {
1211 const lineEl = await getEditorLineEl(dbg, line);
1212 return lineEl.firstChild;
1215 async function getEditorLineEl(dbg, line) {
1216 let el = await codeMirrorGutterElement(dbg, line);
1217 while (el && !el.matches(".CodeMirror-code > div")) {
1218 el = el.parentElement;
1225 * Assert that no breakpoint is set on a given line.
1227 * @memberof mochitest/helpers
1228 * @param {Object} dbg
1229 * @param {Number} line Line where to check for a breakpoint in the editor
1232 async function assertNoBreakpoint(dbg, line) {
1233 const el = await getEditorLineEl(dbg, line);
1235 const exists = !!el.querySelector(".new-breakpoint");
1236 ok(!exists, `Breakpoint doesn't exists on line ${line}`);
1240 * Assert that a regular breakpoint is set. (no conditional, nor log breakpoint)
1242 * @memberof mochitest/helpers
1243 * @param {Object} dbg
1244 * @param {Number} line Line where to check for a breakpoint
1247 async function assertBreakpoint(dbg, line) {
1248 const el = await getEditorLineEl(dbg, line);
1250 const exists = !!el.querySelector(".new-breakpoint");
1251 ok(exists, `Breakpoint exists on line ${line}`);
1253 const hasConditionClass = el.classList.contains("has-condition");
1257 `Regular breakpoint doesn't have condition on line ${line}`
1260 const hasLogClass = el.classList.contains("has-log");
1262 ok(!hasLogClass, `Regular breakpoint doesn't have log on line ${line}`);
1266 * Assert that a conditionnal breakpoint is set.
1268 * @memberof mochitest/helpers
1269 * @param {Object} dbg
1270 * @param {Number} line Line where to check for a breakpoint
1273 async function assertConditionBreakpoint(dbg, line) {
1274 const el = await getEditorLineEl(dbg, line);
1276 const exists = !!el.querySelector(".new-breakpoint");
1277 ok(exists, `Breakpoint exists on line ${line}`);
1279 const hasConditionClass = el.classList.contains("has-condition");
1281 ok(hasConditionClass, `Conditional breakpoint on line ${line}`);
1283 const hasLogClass = el.classList.contains("has-log");
1287 `Conditional breakpoint doesn't have log breakpoint on line ${line}`
1292 * Assert that a log breakpoint is set.
1294 * @memberof mochitest/helpers
1295 * @param {Object} dbg
1296 * @param {Number} line Line where to check for a breakpoint
1299 async function assertLogBreakpoint(dbg, line) {
1300 const el = await getEditorLineEl(dbg, line);
1302 const exists = !!el.querySelector(".new-breakpoint");
1303 ok(exists, `Breakpoint exists on line ${line}`);
1305 const hasConditionClass = el.classList.contains("has-condition");
1309 `Log breakpoint doesn't have condition on line ${line}`
1312 const hasLogClass = el.classList.contains("has-log");
1314 ok(hasLogClass, `Log breakpoint on line ${line}`);
1317 function assertBreakpointSnippet(dbg, index, snippet) {
1318 const actualSnippet = findElement(dbg, "breakpointLabel", 2).innerText;
1319 is(snippet, actualSnippet, `Breakpoint ${index} snippet`);
1323 callStackHeader: ".call-stack-pane ._header",
1324 callStackBody: ".call-stack-pane .pane",
1325 domMutationItem: ".dom-mutation-list li",
1326 expressionNode: i =>
1327 `.expressions-list .expression-container:nth-child(${i}) .object-label`,
1328 expressionValue: i =>
1329 // eslint-disable-next-line max-len
1330 `.expressions-list .expression-container:nth-child(${i}) .object-delimiter + *`,
1331 expressionClose: i =>
1332 `.expressions-list .expression-container:nth-child(${i}) .close`,
1333 expressionInput: ".watch-expressions-pane input.input-expression",
1334 expressionNodes: ".expressions-list .tree-node",
1335 expressionPlus: ".watch-expressions-pane button.plus",
1336 expressionRefresh: ".watch-expressions-pane button.refresh",
1337 scopesHeader: ".scopes-pane ._header",
1338 breakpointItem: i => `.breakpoints-list div:nth-of-type(${i})`,
1339 breakpointLabel: i => `${selectors.breakpointItem(i)} .breakpoint-label`,
1340 breakpointItems: ".breakpoints-list .breakpoint",
1341 breakpointContextMenu: {
1342 disableSelf: "#node-menu-disable-self",
1343 disableAll: "#node-menu-disable-all",
1344 disableOthers: "#node-menu-disable-others",
1345 enableSelf: "#node-menu-enable-self",
1346 enableOthers: "#node-menu-enable-others",
1347 disableDbgStatement: "#node-menu-disable-dbgStatement",
1348 enableDbgStatement: "#node-menu-enable-dbgStatement",
1349 remove: "#node-menu-delete-self",
1350 removeOthers: "#node-menu-delete-other",
1351 removeCondition: "#node-menu-remove-condition",
1353 editorContextMenu: {
1354 continueToHere: "#node-menu-continue-to-here",
1356 columnBreakpoints: ".column-breakpoint",
1357 scopes: ".scopes-list",
1358 scopeNodes: ".scopes-list .object-label",
1359 scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`,
1361 `.scopes-list .tree-node:nth-child(${i}) .object-delimiter + *`,
1362 mapScopesCheckbox: ".map-scopes-header input",
1363 frame: i => `.frames [role="list"] [role="listitem"]:nth-child(${i})`,
1364 frames: '.frames [role="list"] [role="listitem"]',
1365 gutter: i => `.CodeMirror-code *:nth-child(${i}) .CodeMirror-linenumber`,
1367 "#node-menu-add-condition, #node-menu-add-conditional-breakpoint",
1369 "#node-menu-edit-condition, #node-menu-edit-conditional-breakpoint",
1370 addLogItem: "#node-menu-add-log-point",
1371 editLogItem: "#node-menu-edit-log-point",
1372 disableItem: "#node-menu-disable-breakpoint",
1373 menuitem: i => `menupopup menuitem:nth-child(${i})`,
1374 pauseOnExceptions: ".pause-exceptions",
1375 breakpoint: ".CodeMirror-code > .new-breakpoint",
1376 highlightLine: ".CodeMirror-code > .highlight-line",
1377 debugLine: ".new-debug-line",
1378 debugErrorLine: ".new-debug-line-error",
1379 codeMirror: ".CodeMirror",
1380 resume: ".resume.active",
1381 pause: ".pause.active",
1382 sourceTabs: ".source-tabs",
1383 activeTab: ".source-tab.active",
1384 stepOver: ".stepOver.active",
1385 stepOut: ".stepOut.active",
1386 stepIn: ".stepIn.active",
1387 replayPrevious: ".replay-previous.active",
1388 replayNext: ".replay-next.active",
1389 toggleBreakpoints: ".breakpoints-toggle",
1390 prettyPrintButton: ".source-footer .prettyPrint",
1391 prettyPrintLoader: ".source-footer .spin",
1392 sourceMapLink: ".source-footer .mapped-source",
1393 sourcesFooter: ".sources-panel .source-footer",
1394 editorFooter: ".editor-pane .source-footer",
1395 sourceNode: i => `.sources-list .tree-node:nth-child(${i}) .node`,
1396 sourceNodes: ".sources-list .tree-node",
1397 threadSourceTree: i => `.threads-list .sources-pane:nth-child(${i})`,
1398 threadSourceTreeHeader: i =>
1399 `${selectors.threadSourceTree(i)} .thread-header`,
1400 threadSourceTreeSourceNode: (i, j) =>
1401 `${selectors.threadSourceTree(i)} .tree-node:nth-child(${j}) .node`,
1402 sourceDirectoryLabel: i => `.sources-list .tree-node:nth-child(${i}) .label`,
1403 resultItems: ".result-list .result-item",
1404 resultItemName: (name, i) =>
1405 `${selectors.resultItems}:nth-child(${i})[title$="${name}"]`,
1406 fileMatch: ".project-text-search .line-value",
1408 tooltip: ".tooltip",
1409 previewPopup: ".preview-popup",
1410 openInspector: "button.open-inspector",
1412 `.outline-list__element:nth-child(${i}) .function-signature`,
1413 outlineItems: ".outline-list__element",
1414 conditionalPanel: ".conditional-breakpoint-panel",
1415 conditionalPanelInput: ".conditional-breakpoint-panel textarea",
1416 conditionalBreakpointInSecPane: ".breakpoint.is-conditional",
1417 logPointPanel: ".conditional-breakpoint-panel.log-point",
1418 logPointInSecPane: ".breakpoint.is-log",
1419 searchField: ".search-field",
1420 blackbox: ".action.black-box",
1421 projectSearchCollapsed: ".project-text-search .arrow:not(.expanded)",
1422 projectSerchExpandedResults: ".project-text-search .result",
1423 threadsPaneItems: ".threads-pane .thread",
1424 threadsPaneItem: i => `.threads-pane .thread:nth-child(${i})`,
1425 threadsPaneItemPause: i => `${selectors.threadsPaneItem(i)} .pause-badge`,
1426 CodeMirrorLines: ".CodeMirror-lines",
1427 inlinePreviewLabels: ".inline-preview .inline-preview-label",
1428 inlinePreviewValues: ".inline-preview .inline-preview-value",
1429 inlinePreviewOpenInspector: ".inline-preview-value button.open-inspector",
1430 watchpointsSubmenu: "#node-menu-watchpoints",
1431 addGetWatchpoint: "#node-menu-add-get-watchpoint",
1432 addSetWatchpoint: "#node-menu-add-set-watchpoint",
1433 removeWatchpoint: "#node-menu-remove-watchpoint",
1434 logEventsCheckbox: ".events-header input",
1435 previewPopupInvokeGetterButton: ".preview-popup .invoke-getter",
1436 previewPopupObjectNumber: ".preview-popup .objectBox-number",
1437 previewPopupObjectObject: ".preview-popup .objectBox-object",
1438 sourceTreeRootNode: ".sources-panel .node .window",
1439 sourceTreeFolderNode: ".sources-panel .node .folder",
1442 function getSelector(elementName, ...args) {
1443 let selector = selectors[elementName];
1445 throw new Error(`The selector ${elementName} is not defined`);
1448 if (typeof selector == "function") {
1449 selector = selector(...args);
1455 function findElement(dbg, elementName, ...args) {
1456 const selector = getSelector(elementName, ...args);
1457 return findElementWithSelector(dbg, selector);
1460 function findElementWithSelector(dbg, selector) {
1461 return dbg.win.document.querySelector(selector);
1464 function findAllElements(dbg, elementName, ...args) {
1465 const selector = getSelector(elementName, ...args);
1466 return findAllElementsWithSelector(dbg, selector);
1469 function findAllElementsWithSelector(dbg, selector) {
1470 return dbg.win.document.querySelectorAll(selector);
1473 function getSourceNodeLabel(dbg, index) {
1474 return findElement(dbg, "sourceNode", index)
1476 .replace(/^[\s\u200b]*/g, "");
1480 * Simulates a mouse click in the debugger DOM.
1482 * @memberof mochitest/helpers
1483 * @param {Object} dbg
1484 * @param {String} elementName
1485 * @param {Array} args
1489 async function clickElement(dbg, elementName, ...args) {
1490 const selector = getSelector(elementName, ...args);
1491 const el = await waitForElementWithSelector(dbg, selector);
1493 el.scrollIntoView();
1495 return clickElementWithSelector(dbg, selector);
1498 function clickElementWithSelector(dbg, selector) {
1499 clickDOMElement(dbg, findElementWithSelector(dbg, selector));
1502 function clickDOMElement(dbg, element, options = {}) {
1503 EventUtils.synthesizeMouseAtCenter(element, options, dbg.win);
1506 function dblClickElement(dbg, elementName, ...args) {
1507 const selector = getSelector(elementName, ...args);
1509 return EventUtils.synthesizeMouseAtCenter(
1510 findElementWithSelector(dbg, selector),
1516 function clickElementWithOptions(dbg, elementName, options, ...args) {
1517 const selector = getSelector(elementName, ...args);
1518 const el = findElementWithSelector(dbg, selector);
1519 el.scrollIntoView();
1521 return EventUtils.synthesizeMouseAtCenter(el, options, dbg.win);
1524 function altClickElement(dbg, elementName, ...args) {
1525 return clickElementWithOptions(dbg, elementName, { altKey: true }, ...args);
1528 function shiftClickElement(dbg, elementName, ...args) {
1529 return clickElementWithOptions(dbg, elementName, { shiftKey: true }, ...args);
1532 function rightClickElement(dbg, elementName, ...args) {
1533 const selector = getSelector(elementName, ...args);
1534 const doc = dbg.win.document;
1535 return rightClickEl(dbg, doc.querySelector(selector));
1538 function rightClickEl(dbg, el) {
1539 const doc = dbg.win.document;
1540 el.scrollIntoView();
1541 EventUtils.synthesizeMouseAtCenter(el, { type: "contextmenu" }, dbg.win);
1544 async function clickGutter(dbg, line) {
1545 const el = await codeMirrorGutterElement(dbg, line);
1546 clickDOMElement(dbg, el);
1549 async function cmdClickGutter(dbg, line) {
1550 const el = await codeMirrorGutterElement(dbg, line);
1551 clickDOMElement(dbg, el, cmdOrCtrl);
1554 function findContextMenuPopup(dbg) {
1555 // the context menu is in the toolbox window
1556 const doc = dbg.toolbox.topDoc;
1558 // there are several context menus, we want the one with the menu-api
1559 return doc.querySelector('menupopup[menu-api="true"]');
1562 function findContextMenu(dbg, selector) {
1563 const popup = findContextMenuPopup(dbg)
1565 return popup.querySelector(selector);
1568 async function waitForContextMenu(dbg, selector) {
1569 await waitFor(() => findContextMenu(dbg, selector));
1570 return findContextMenu(dbg, selector);
1573 function selectContextMenuItem(dbg, selector) {
1574 const popup = findContextMenuPopup(dbg);
1575 popup.activateItem(popup.querySelector(selector));
1578 async function assertContextMenuLabel(dbg, selector, label) {
1579 const item = await waitForContextMenu(dbg, selector);
1580 is(item.label, label, "The label of the context menu item shown to the user");
1583 async function typeInPanel(dbg, text) {
1584 await waitForElement(dbg, "conditionalPanelInput");
1585 // Position cursor reliably at the end of the text.
1586 pressKey(dbg, "End");
1588 pressKey(dbg, "Enter");
1592 * Toggles the debugger call stack accordian.
1594 * @memberof mochitest/actions
1595 * @param {Object} dbg
1599 function toggleCallStack(dbg) {
1600 return findElement(dbg, "callStackHeader").click();
1603 function toggleScopes(dbg) {
1604 return findElement(dbg, "scopesHeader").click();
1607 function toggleExpressionNode(dbg, index) {
1608 return toggleObjectInspectorNode(findElement(dbg, "expressionNode", index));
1611 function toggleScopeNode(dbg, index) {
1612 return toggleObjectInspectorNode(findElement(dbg, "scopeNode", index));
1615 function rightClickScopeNode(dbg, index) {
1616 rightClickObjectInspectorNode(dbg, findElement(dbg, "scopeNode", index));
1619 function getScopeLabel(dbg, index) {
1620 return findElement(dbg, "scopeNode", index).innerText;
1623 function getScopeValue(dbg, index) {
1624 return findElement(dbg, "scopeValue", index).innerText;
1627 function toggleObjectInspectorNode(node) {
1628 const objectInspector = node.closest(".object-inspector");
1629 const properties = objectInspector.querySelectorAll(".node").length;
1631 log(`Toggling node ${node.innerText}`);
1634 () => objectInspector.querySelectorAll(".node").length !== properties
1638 function rightClickObjectInspectorNode(dbg, node) {
1639 const objectInspector = node.closest(".object-inspector");
1640 const properties = objectInspector.querySelectorAll(".node").length;
1642 log(`Right clicking node ${node.innerText}`);
1643 rightClickEl(dbg, node);
1646 () => objectInspector.querySelectorAll(".node").length !== properties
1650 function getCM(dbg) {
1651 const el = dbg.win.document.querySelector(".CodeMirror");
1652 return el.CodeMirror;
1655 function getCoordsFromPosition(cm, { line, ch }) {
1656 return cm.charCoords({ line: ~~line, ch: ~~ch });
1659 async function getTokenFromPosition(dbg, { line, ch }) {
1660 info(`Get token at ${line}, ${ch}`);
1661 const cm = getCM(dbg);
1662 cm.scrollIntoView({ line: line - 1, ch }, 0);
1664 // Ensure the line is visible with margin because the bar at the bottom of
1665 // the editor overlaps into what the editor thinks is its own space, blocking
1666 // the click event below.
1667 await waitForScrolling(cm);
1669 const coords = getCoordsFromPosition(cm, { line: line - 1, ch });
1671 const { left, top } = coords;
1673 // Adds a vertical offset due to increased line height
1674 // https://github.com/firefox-devtools/debugger/pull/7934
1675 const lineHeightOffset = 3;
1677 return dbg.win.document.elementFromPoint(left, top + lineHeightOffset);
1680 async function waitForScrolling(codeMirror) {
1681 return new Promise(resolve => {
1682 codeMirror.on("scroll", resolve);
1683 setTimeout(resolve, 500);
1687 async function codeMirrorGutterElement(dbg, line) {
1688 info(`CodeMirror line ${line}`);
1689 const cm = getCM(dbg);
1691 const position = { line: line - 1, ch: 0 };
1692 cm.scrollIntoView(position, 0);
1693 await waitForScrolling(cm);
1695 const coords = getCoordsFromPosition(cm, position);
1697 const { left, top } = coords;
1699 // Adds a vertical offset due to increased line height
1700 // https://github.com/firefox-devtools/debugger/pull/7934
1701 const lineHeightOffset = 3;
1703 // Click in the center of the line/breakpoint
1704 const leftOffset = 10;
1706 const tokenEl = dbg.win.document.elementFromPoint(
1708 top + lineHeightOffset
1712 throw new Error(`Failed to find element for line ${line}`);
1717 async function clickAtPos(dbg, pos) {
1718 const tokenEl = await getTokenFromPosition(dbg, pos);
1724 const { top, left } = tokenEl.getBoundingClientRect();
1726 `Clicking on token ${tokenEl.innerText} in line ${tokenEl.parentNode.innerText}`
1728 tokenEl.dispatchEvent(
1729 new MouseEvent("click", {
1739 async function rightClickAtPos(dbg, pos) {
1740 const el = await getTokenFromPosition(dbg, pos);
1745 EventUtils.synthesizeMouseAtCenter(el, { type: "contextmenu" }, dbg.win);
1748 async function hoverAtPos(dbg, pos) {
1749 const tokenEl = await getTokenFromPosition(dbg, pos);
1755 info(`Hovering on token ${tokenEl.innerText}`);
1756 tokenEl.dispatchEvent(
1757 new MouseEvent("mouseover", {
1764 InspectorUtils.addPseudoClassLock(tokenEl, ":hover");
1767 async function closePreviewAtPos(dbg, line, column) {
1768 const pos = { line, ch: column - 1 };
1769 const tokenEl = await getTokenFromPosition(dbg, pos);
1775 InspectorUtils.removePseudoClassLock(tokenEl, ":hover");
1777 const gutterEl = await getEditorLineGutter(dbg, line);
1778 EventUtils.synthesizeMouseAtCenter(gutterEl, { type: "mousemove" }, dbg.win);
1779 await waitUntil(() => findElement(dbg, "previewPopup") == null);
1782 // tryHovering will hover at a position every second until we
1783 // see a preview element (popup, tooltip) appear. Once it appears,
1784 // it considers it a success.
1785 function tryHovering(dbg, line, column, elementName) {
1786 return new Promise((resolve, reject) => {
1787 const element = waitForElement(dbg, elementName);
1790 element.then(() => {
1791 clearInterval(interval);
1795 const interval = setInterval(() => {
1797 clearInterval(interval);
1798 reject("failed to preview");
1801 hoverAtPos(dbg, { line, ch: column - 1 });
1806 async function assertPreviewTextValue(dbg, line, column, { text, expression }) {
1807 const previewEl = await tryHovering(dbg, line, column, "previewPopup");
1809 ok(previewEl.innerText.includes(text), "Preview text shown to user");
1811 const preview = dbg.selectors.getPreview();
1812 is(preview.expression, expression, "Preview.expression");
1815 async function assertPreviewTooltip(dbg, line, column, { result, expression }) {
1816 const previewEl = await tryHovering(dbg, line, column, "tooltip");
1818 is(previewEl.innerText, result, "Preview text shown to user");
1820 const preview = dbg.selectors.getPreview();
1821 is(`${preview.resultGrip}`, result, "Preview.result");
1822 is(preview.expression, expression, "Preview.expression");
1825 async function hoverOnToken(dbg, line, column, selector) {
1826 await tryHovering(dbg, line, column, selector);
1827 return dbg.selectors.getPreview();
1830 function getPreviewProperty(preview, field) {
1831 const { resultGrip } = preview;
1833 resultGrip.preview.ownProperties || resultGrip.preview.items;
1834 const property = properties[field];
1835 return property.value || property;
1838 async function assertPreviewPopup(
1842 { field, value, expression }
1844 const preview = await hoverOnToken(dbg, line, column, "popup");
1845 is(`${getPreviewProperty(preview, field)}`, value, "Preview.result");
1847 is(preview.expression, expression, "Preview.expression");
1850 async function assertPreviews(dbg, previews) {
1851 for (const { line, column, expression, result, fields } of previews) {
1852 if (fields && result) {
1853 throw new Error("Invalid test fixture");
1857 for (const [field, value] of fields) {
1858 await assertPreviewPopup(dbg, line, column, {
1865 await assertPreviewTextValue(dbg, line, column, {
1871 const { target } = dbg.selectors.getPreview(getContext(dbg));
1872 InspectorUtils.removePseudoClassLock(target, ":hover");
1873 dbg.actions.clearPreview(getContext(dbg));
1877 async function waitForBreakableLine(dbg, source, lineNumber) {
1881 const currentSource = findSource(dbg, source);
1883 const breakableLines =
1884 currentSource && dbg.selectors.getBreakableLines(currentSource.id);
1886 return breakableLines && breakableLines.includes(lineNumber);
1888 `waiting for breakable line ${lineNumber}`
1892 async function waitForSourceCount(dbg, i) {
1893 // We are forced to wait until the DOM nodes appear because the
1894 // source tree batches its rendering.
1895 info(`waiting for ${i} sources`);
1896 await waitUntil(() => {
1897 return findAllElements(dbg, "sourceNodes").length === i;
1901 async function assertSourceCount(dbg, count) {
1902 await waitForSourceCount(dbg, count);
1903 is(findAllElements(dbg, "sourceNodes").length, count, `${count} sources`);
1906 async function waitForNodeToGainFocus(dbg, index) {
1907 await waitUntil(() => {
1908 const element = findElement(dbg, "sourceNode", index);
1911 return element.classList.contains("focused");
1915 }, `waiting for source node ${index} to be focused`);
1918 async function assertNodeIsFocused(dbg, index) {
1919 await waitForNodeToGainFocus(dbg, index);
1920 const node = findElement(dbg, "sourceNode", index);
1921 ok(node.classList.contains("focused"), `node ${index} is focused`);
1924 async function addExpression(dbg, input) {
1925 info("Adding an expression");
1927 const plusIcon = findElementWithSelector(dbg, selectors.expressionPlus);
1931 findElementWithSelector(dbg, selectors.expressionInput).focus();
1933 const evaluated = waitForDispatch(dbg.store, "EVALUATE_EXPRESSION");
1934 pressKey(dbg, "Enter");
1938 async function editExpression(dbg, input) {
1939 info("Updating the expression");
1940 dblClickElement(dbg, "expressionNode", 1);
1941 // Position cursor reliably at the end of the text.
1942 pressKey(dbg, "End");
1944 const evaluated = waitForDispatch(dbg.store, "EVALUATE_EXPRESSIONS");
1945 pressKey(dbg, "Enter");
1949 async function waitUntilPredicate(predicate) {
1951 await waitUntil(() => {
1952 result = predicate();
1959 // Return a promise with a reference to jsterm, opening the split
1960 // console if necessary. This cleans up the split console pref so
1961 // it won't pollute other tests.
1962 async function getDebuggerSplitConsole(dbg) {
1963 let { toolbox, win } = dbg;
1969 if (!toolbox.splitConsole) {
1970 pressKey(dbg, "Escape");
1973 await toolbox.openSplitConsole();
1974 return toolbox.getPanel("webconsole");
1977 // Return a promise that resolves with the result of a thread evaluating a
1978 // string in the topmost frame.
1979 async function evaluateInTopFrame(dbg, text) {
1980 const threadFront = dbg.toolbox.target.threadFront;
1981 const consoleFront = await dbg.toolbox.target.getFront("console");
1982 const { frames } = await threadFront.getFrames(0, 1);
1983 ok(frames.length == 1, "Got one frame");
1984 const options = { thread: threadFront.actor, frameActor: frames[0].actorID };
1985 const response = await consoleFront.evaluateJSAsync(text, options);
1986 return response.result.type == "undefined" ? undefined : response.result;
1989 // Return a promise that resolves when a thread evaluates a string in the
1990 // topmost frame, ensuring the result matches the expected value.
1991 async function checkEvaluateInTopFrame(dbg, text, expected) {
1992 const rval = await evaluateInTopFrame(dbg, text);
1993 ok(rval == expected, `Eval returned ${expected}`);
1996 async function findConsoleMessage({ toolbox }, query) {
1997 const [message] = await findConsoleMessages(toolbox, query);
1998 const value = message.querySelector(".message-body").innerText;
1999 const link = message.querySelector(".frame-link-source-inner").innerText;
2000 return { value, link };
2003 async function findConsoleMessages(toolbox, query) {
2004 const webConsole = await toolbox.getPanel("webconsole");
2005 const win = webConsole._frameWindow;
2006 return Array.prototype.filter.call(
2007 win.document.querySelectorAll(".message"),
2008 e => e.innerText.includes(query)
2012 async function hasConsoleMessage({ toolbox }, msg) {
2013 return waitFor(async () => {
2014 const messages = await findConsoleMessages(toolbox, msg);
2015 return messages.length > 0;
2019 function evaluateExpressionInConsole(hud, expression) {
2020 const onResult = new Promise(res => {
2021 const onNewMessage = messages => {
2022 for (let message of messages) {
2023 if (message.node.classList.contains("result")) {
2024 hud.ui.off("new-messages", onNewMessage);
2029 hud.ui.on("new-messages", onNewMessage);
2031 hud.ui.wrapper.dispatchEvaluateExpression(expression);
2035 function waitForInspectorPanelChange(dbg) {
2036 return dbg.toolbox.getPanelWhenReady("inspector");
2039 function getEagerEvaluationElement(hud) {
2040 return hud.ui.outputNode.querySelector(".eager-evaluation-result");
2043 async function waitForEagerEvaluationResult(hud, text) {
2044 await waitUntil(() => {
2045 const elem = getEagerEvaluationElement(hud);
2047 if (text instanceof RegExp) {
2048 return text.test(elem.innerText);
2050 return elem.innerText == text;
2054 ok(true, `Got eager evaluation result ${text}`);
2057 function setInputValue(hud, value) {
2058 const onValueSet = hud.jsterm.once("set-input-value");
2059 hud.jsterm._setValue(value);
2063 function assertMenuItemChecked(menuItem, isChecked) {
2065 !!menuItem.getAttribute("aria-checked"),
2067 `Item has expected state: ${isChecked ? "checked" : "unchecked"}`
2071 async function toggleDebbuggerSettingsMenuItem(dbg, { className, isChecked }) {
2072 const menuButton = findElementWithSelector(
2074 ".debugger-settings-menu-button"
2076 const { parent } = dbg.panel.panelWin;
2077 const { document } = parent;
2080 // Waits for the debugger settings panel to appear.
2081 await waitFor(() => document.querySelector("#debugger-settings-menu-list"));
2083 const menuItem = document.querySelector(className);
2085 assertMenuItemChecked(menuItem, isChecked);
2089 // Waits for the debugger settings panel to disappear.
2090 await waitFor(() => menuButton.getAttribute("aria-expanded") === "false");
2093 async function setLogPoint(dbg, index, value) {
2094 rightClickElement(dbg, "gutter", index);
2095 selectContextMenuItem(
2097 `${selectors.addLogItem},${selectors.editLogItem}`
2099 const onBreakpointSet = waitForDispatch(dbg.store, "SET_BREAKPOINT");
2100 await typeInPanel(dbg, value);
2101 await onBreakpointSet;
2104 // This module is also loaded for Browser Toolbox tests, within the browser toolbox process
2105 // which doesn't contain mochitests resource://testing-common URL.
2106 // This isn't important to allow rejections in the context of the browser toolbox tests.
2107 const protocolHandler = Services.io
2108 .getProtocolHandler("resource")
2109 .QueryInterface(Ci.nsIResProtocolHandler);
2110 if (protocolHandler.hasSubstitution("testing-common")) {
2111 const { PromiseTestUtils } = ChromeUtils.import(
2112 "resource://testing-common/PromiseTestUtils.jsm"
2115 // Debugger operations that are canceled because they were rendered obsolete by
2116 // a navigation or pause/resume end up as uncaught rejections. These never
2117 // indicate errors and are allowed in all debugger tests.
2118 PromiseTestUtils.allowMatchingRejectionsGlobally(/Page has navigated/);
2119 PromiseTestUtils.allowMatchingRejectionsGlobally(
2120 /Current thread has changed/
2122 PromiseTestUtils.allowMatchingRejectionsGlobally(
2123 /Current thread has paused or resumed/
2125 PromiseTestUtils.allowMatchingRejectionsGlobally(/Connection closed/);
2126 this.PromiseTestUtils = PromiseTestUtils;