Bug 1704628 Part 1: Make selectContextMenuItem use .activateItem() semantics. r=ochameau
[gecko.git] / devtools / client / debugger / test / mochitest / helpers.js
blobef1bf42e1f7b4656a367742157ad8b0bd87c3a71
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/>. */
6 /**
7  * Helper methods to drive with the debugger during mochitests. This file can be safely
8  * required from other panel test files.
9  */
11 // Import helpers for the new debugger
12 Services.scriptloader.loadSubScript(
13   "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/helpers/context.js",
14   this
17 var { Toolbox } = require("devtools/client/framework/toolbox");
18 const { Task } = require("devtools/shared/task");
19 const asyncStorage = require("devtools/shared/async-storage");
21 const {
22   getSelectedLocation,
23 } = require("devtools/client/debugger/src/utils/selected-location");
25 const {
26   resetSchemaVersion,
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.`);
38   });
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 => {
47     store.dispatch({
48       // Normally we would use `services.WAIT_UNTIL`, but use the
49       // internal name here so tests aren't forced to always pass it
50       // in
51       type: "@@service/waitUntil",
52       predicate: action => action.type === actionType,
53       run: (dispatch, getState, action) => {
54         resolve(action);
55       },
56     });
57   });
60 /**
61  * Waits for `predicate()` to be true. `state` is the redux app state.
62  *
63  * @memberof mochitest/waits
64  * @param {Object} dbg
65  * @param {Function} predicate
66  * @return {Promise}
67  * @static
68  */
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 || ""}`);
74       return resolve();
75     }
77     const unsubscribe = dbg.store.subscribe(() => {
78       const result = predicate(dbg.store.getState());
79       if (result) {
80         info(`Finished waiting for state change: ${msg || ""}`);
81         unsubscribe();
82         resolve(result);
83       }
84     });
85   });
88 /**
89  * Waits for sources to be loaded.
90  *
91  * @memberof mochitest/waits
92  * @param {Object} dbg
93  * @param {Array} sources
94  * @return {Promise}
95  * @static
96  */
97 async function waitForSources(dbg, ...sources) {
98   if (sources.length === 0) {
99     return Promise.resolve();
100   }
102   info(`Waiting on sources: ${sources.join(", ")}`);
103   await Promise.all(
104     sources.map(url => {
105       if (!sourceExists(dbg, url)) {
106         return waitForState(
107           dbg,
108           () => sourceExists(dbg, url),
109           `source ${url} exists`
110         );
111       }
112     })
113   );
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
124  * @return {Promise}
125  * @static
126  */
127 function waitForSource(dbg, url) {
128   return waitForState(
129     dbg,
130     state => findSource(dbg, url, { silent: true }),
131     "source exists"
132   );
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).
150  */
151 async function waitForAllElements(
152   dbg,
153   name,
154   count = 1,
155   countStrictlyEqual = false
156 ) {
157   await waitUntil(() => {
158     const elsCount = findAllElements(dbg, name).length;
159     return countStrictlyEqual ? elsCount === count : elsCount >= count;
160   });
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) {
174   if (exists) {
175     ok(el.classList.contains(className), `${className} class exists`);
176   } else {
177     ok(!el.classList.contains(className), `${className} class does not exist`);
178   }
181 function waitForSelectedLocation(dbg, line, column) {
182   return waitForState(dbg, state => {
183     const location = dbg.selectors.getSelectedLocation();
184     return (
185       location &&
186       (line ? location.line == line : true) &&
187       (column ? location.column == column : true)
188     );
189   });
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`)
198  * @return {Promise}
199  * @static
200  */
201 function waitForSelectedSource(dbg, sourceOrUrl) {
202   const {
203     getSelectedSourceWithContent,
204     hasSymbols,
205     getBreakableLines,
206   } = dbg.selectors;
208   return waitForState(
209     dbg,
210     state => {
211       const source = getSelectedSourceWithContent() || {};
212       if (!source.content) {
213         return false;
214       }
216       if (sourceOrUrl) {
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)) {
221             return false;
222           }
223         } else {
224           if (source.id != sourceOrUrl.id) {
225             return false;
226           }
227         }
228       }
230       return hasSymbols(source) && getBreakableLines(source.id);
231     },
232     "selected source"
233   );
237  * Assert that the debugger is not currently paused.
238  * @memberof mochitest/asserts
239  * @static
240  */
241 function assertNotPaused(dbg) {
242   ok(!isPaused(dbg), "client is not paused");
246  * Assert that the debugger is currently paused.
247  * @memberof mochitest/asserts
248  * @static
249  */
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);
257   ok(
258     lines.every(line => !breakableLines.includes(line)),
259     "empty lines should match"
260   );
263 function getVisibleSelectedFrameLine(dbg) {
264   const {
265     selectors: { getVisibleSelectedFrame },
266   } = dbg;
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
282  * @static
283  */
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;
300     ok(
301       false,
302       `Looks like the source ${url} is still loading. Try adding waitForLoadedSource in the test.`
303     );
304     return;
305   }
307   if (!lineInfo.wrapClass) {
308     const pauseLine = getVisibleSelectedFrameLine(dbg);
309     ok(false, `Expected pause line on line ${line}, it is on ${pauseLine}`);
310     return;
311   }
313   ok(
314     lineInfo?.wrapClass.includes("new-debug-line"),
315     `Line ${line} is not highlighted as paused`
316   );
318   const debugLine =
319     findElement(dbg, "debugLine") || findElement(dbg, "debugErrorLine");
321   is(
322     findAllElements(dbg, "debugLine").length +
323       findAllElements(dbg, "debugErrorLine").length,
324     1,
325     "There is only one line"
326   );
328   ok(isVisibleInEditor(dbg, debugLine), "debug line is visible");
330   const markedSpans = lineInfo.handle.markedSpans;
331   if (markedSpans && markedSpans.length > 0) {
332     const classMatch =
333       markedSpans.filter(
334         span =>
335           span.marker.className &&
336           span.marker.className.includes("debug-expression")
337       ).length > 0;
339     if (column) {
340       const frame = dbg.selectors.getVisibleSelectedFrame();
341       is(frame.location.column, column, `Paused at column ${column}`);
342     }
344     ok(classMatch, "expression is highlighted as paused");
345   }
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
356  * @static
357  */
358 function assertHighlightLocation(dbg, source, line) {
359   source = findSource(dbg, source);
361   // Check the selected source
362   is(
363     dbg.selectors.getSelectedSource().url,
364     source.url,
365     "source url is correct"
366   );
368   // Check the highlight line
369   const lineEl = findElement(dbg, "highlightLine");
370   ok(lineEl, "Line is highlighted");
372   is(
373     findAllElements(dbg, "highlightLine").length,
374     1,
375     "Only 1 line is highlighted"
376   );
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
390  * @static
391  */
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) {
398   assertPaused(dbg);
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");
404   ok(
405     line == expectedLine,
406     `Frame paused at ${line}, but expected ${expectedLine}`
407   );
410 async function waitForThreadCount(dbg, count) {
411   return waitForState(
412     dbg,
413     state => dbg.selectors.getThreads(state).length == count
414   );
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) {
425   return waitForState(
426     dbg,
427     state => dbg.selectors.getBreakpointCount() == count
428   );
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
444  * @static
445  */
446 async function waitForPaused(dbg, url) {
447   const {
448     getSelectedScope,
449     getCurrentThread,
450     getCurrentThreadFrames,
451   } = dbg.selectors;
453   await waitForState(
454     dbg,
455     state => isPaused(dbg) && !!getSelectedScope(getCurrentThread()),
456     "paused"
457   );
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 =>
470     dbg.selectors
471       .getBreakpointsList()
472       .find(bp => bp.options.condition == condition)
473   );
476 function waitForLog(dbg, logValue) {
477   return waitForState(dbg, state =>
478     dbg.selectors
479       .getBreakpointsList()
480       .find(bp => bp.options.logValue == logValue)
481   );
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()
491  */
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)
501  */
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) {
515     return false;
516   }
518   return source.id == sourceId;
522  * Clear all the debugger related preferences.
523  */
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);
542   }
546  * Intilializes the debugger.
548  * @memberof mochitest
549  * @param {String} url
550  * @return {Promise} dbg
551  * @static
552  */
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);
567   return dbg;
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
585  * @static
586  */
587 function pauseTest() {
588   info("Test paused. Invoke resumeTest to continue.");
589   return new Promise(resolve => (resumeTest = resolve));
592 // Actions
594  * Returns a source that matches the URL.
596  * @memberof mochitest/actions
597  * @param {Object} dbg
598  * @param {String} url
599  * @return {Object} source
600  * @static
601  */
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
606     const source = url;
607     return source;
608   }
610   const sources = dbg.selectors.getSourceList();
611   const source = sources.find(s => (s.url || "").includes(url));
613   if (!source) {
614     if (silent) {
615       return false;
616     }
618     throw new Error(`Unable to find source: ${url}`);
619   }
621   return source;
624 function findSourceContent(dbg, url, opts) {
625   const source = findSource(dbg, url, opts);
627   if (!source) {
628     return null;
629   }
631   const content = dbg.selectors.getSourceContent(source.id);
633   if (!content) {
634     return null;
635   }
637   if (content.state !== "fulfilled") {
638     throw new Error("Expected loaded source, got" + content.value);
639   }
641   return content.value;
644 function sourceExists(dbg, url) {
645   return !!findSource(dbg, url, { silent: true });
648 function waitForLoadedSource(dbg, url) {
649   return waitForState(
650     dbg,
651     state => {
652       const source = findSource(dbg, url, { silent: true });
653       return source && dbg.selectors.getSourceContent(source.id);
654     },
655     "loaded source"
656   );
659 function waitForLoadedSources(dbg) {
660   return waitForState(
661     dbg,
662     state => {
663       const sources = dbg.selectors.getSourceList();
664       return sources.every(
665         source => !!dbg.selectors.getSourceContent(source.id)
666       );
667     },
668     "loaded source"
669   );
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
687  * @return {Promise}
688  * @static
689  */
690 async function selectSource(dbg, url, line, column) {
691   const source = findSource(dbg, url);
692   await dbg.actions.selectLocation(
693     getContext(dbg),
694     { sourceId: source.id, line, column },
695     { keepContext: false }
696   );
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;
709  * Steps over.
711  * @memberof mochitest/actions
712  * @param {Object} dbg
713  * @return {Promise}
714  * @static
715  */
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);
724  * Steps in.
726  * @memberof mochitest/actions
727  * @param {Object} dbg
728  * @return {Promise}
729  * @static
730  */
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);
739  * Steps out.
741  * @memberof mochitest/actions
742  * @param {Object} dbg
743  * @return {Promise}
744  * @static
745  */
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);
754  * Resumes.
756  * @memberof mochitest/actions
757  * @param {Object} dbg
758  * @return {Promise}
759  * @static
760  */
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));
766   await onResumed;
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
780  * @return {Promise}
781  * @static
782  */
783 async function reload(dbg, ...sources) {
784   const navigated = waitForDispatch(dbg.store, "NAVIGATE");
785   await dbg.client.reload();
786   await navigated;
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
797  * @return {Promise}
798  * @static
799  */
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);
804   await navigated;
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({
812     line,
813     sourceId,
814   });
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
827  * @return {Promise}
828  * @static
829  */
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(
836     getContext(dbg),
837     { sourceId, line, column },
838     options
839   );
840   await onBreakpoint;
841   is(
842     dbg.selectors.getBreakpointCount(),
843     bpCount + 1,
844     "a new breakpoint was created"
845   );
848 function disableBreakpoint(dbg, source, line, column) {
849   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(
861     getContext(dbg),
862     { sourceId, line, column },
863     options
864   );
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(
876     source.id,
877     line
878   );
879   return lineBreakpoints.find(bp => {
880     return bp.generatedLocation.column === column;
881   });
884 async function loadAndAddBreakpoint(dbg, filename, line, column) {
885   const {
886     selectors: { getBreakpoint, getBreakpointCount, getBreakpointsMap },
887   } = dbg;
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;
904     ok(
905       false,
906       `Breakpoint has correct line ${line}, column ${column}, but was line ${loc.line} column ${loc.column}`
907     );
908   }
910   return source;
913 async function invokeWithBreakpoint(
914   dbg,
915   fnName,
916   filename,
917   { line, column },
918   handler
919 ) {
920   const source = await loadAndAddBreakpoint(dbg, filename, line, column);
922   const invokeResult = invokeInTab(fnName);
924   const invokeFailed = await Promise.race([
925     waitForPaused(dbg),
926     invokeResult.then(
927       () => new Promise(() => {}),
928       () => true
929     ),
930   ]);
932   if (invokeFailed) {
933     return invokeResult;
934   }
936   assertPausedLocation(dbg);
938   await removeBreakpoint(dbg, source.id, line, column);
940   is(dbg.selectors.getBreakpointCount(), 0, "Breakpoint reverted");
942   await handler(source);
944   await resume(dbg);
946   // eslint-disable-next-line max-len
947   // If the invoke errored later somehow, capture here so the error is reported nicely.
948   await invokeResult;
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"])'
960   );
961   const indices = Array.from(scopeElements, el => {
962     return Array.prototype.indexOf.call(el.parentNode.childNodes, el);
963   }).reverse();
965   for (const index of indices) {
966     await toggleScopeNode(dbg, index + 1);
967   }
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]);
976       is(
977         getScopeValue(dbg, i + 1),
978         val[1],
979         `"${val[0]}" has the expected "${val[1]}" value`
980       );
981     } else {
982       is(getScopeLabel(dbg, i + 1), val);
983     }
984   }
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);
992   });
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
1008  * @return {Promise}
1009  * @static
1010  */
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
1026  * @return {Promise}
1027  * @static
1028  */
1029 async function togglePauseOnExceptions(
1030   dbg,
1031   pauseOnExceptions,
1032   pauseOnCaughtExceptions
1033 ) {
1034   return dbg.actions.pauseOnExceptions(
1035     pauseOnExceptions,
1036     pauseOnCaughtExceptions
1037   );
1040 function waitForActive(dbg) {
1041   return waitForState(dbg, state => !dbg.selectors.getIsCurrentThreadPaused());
1044 // Helpers
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.
1054  * @return {Promise}
1055  * @static
1056  */
1057 function invokeInTab(fnc, ...args) {
1058   info(`Invoking in tab: ${fnc}(${args.map(uneval).join(",")})`);
1059   return ContentTask.spawn(
1060     gBrowser.selectedBrowser,
1061     { fnc, args },
1062     ({ fnc, args }) => content.wrappedJSObject[fnc](...args)
1063   );
1066 function clickElementInTab(selector) {
1067   info(`click element ${selector} in tab`);
1069   return SpecialPowers.spawn(
1070     gBrowser.selectedBrowser,
1071     [{ selector }],
1072     function({ selector }) {
1073       content.wrappedJSObject.document.querySelector(selector).click();
1074     }
1075   );
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 },
1116   AltEnter: {
1117     code: "VK_RETURN",
1118     modifiers: { altKey: true },
1119   },
1120   Up: { code: "VK_UP" },
1121   Down: { code: "VK_DOWN" },
1122   Right: { code: "VK_RIGHT" },
1123   Left: { code: "VK_LEFT" },
1124   End: endKey,
1125   Start: startKey,
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 } },
1133   stepOutKey: {
1134     code: "VK_F11",
1135     modifiers: { ctrlKey: isLinux, shiftKey: true },
1136   },
1140  * Simulates a key press in the debugger window.
1142  * @memberof mochitest/helpers
1143  * @param {Object} dbg
1144  * @param {String} keyName
1145  * @return {Promise}
1146  * @static
1147  */
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
1165  * @return {boolean}
1166  * @static
1167  */
1169 function isVisibleInEditor(dbg, element) {
1170   return isVisible(findElement(dbg, "codeMirror"), element);
1174  * Checks to see if the inner element is visible inside the
1175  * outer element.
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
1185  * @return {boolean}
1186  * @static
1187  */
1188 function isVisible(outerEl, innerEl) {
1189   if (!innerEl || !outerEl) {
1190     return false;
1191   }
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;
1207   return visible;
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;
1219   }
1221   return el;
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
1230  * @static
1231  */
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
1245  * @static
1246  */
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");
1255   ok(
1256     !hasConditionClass,
1257     `Regular breakpoint doesn't have condition on line ${line}`
1258   );
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
1271  * @static
1272  */
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");
1285   ok(
1286     !hasLogClass,
1287     `Conditional breakpoint doesn't have log breakpoint on line ${line}`
1288   );
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
1297  * @static
1298  */
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");
1307   ok(
1308     !hasConditionClass,
1309     `Log breakpoint doesn't have condition on line ${line}`
1310   );
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`);
1322 const selectors = {
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",
1352   },
1353   editorContextMenu: {
1354     continueToHere: "#node-menu-continue-to-here",
1355   },
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`,
1360   scopeValue: i =>
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`,
1366   addConditionItem:
1367     "#node-menu-add-condition, #node-menu-add-conditional-breakpoint",
1368   editConditionItem:
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",
1407   popup: ".popover",
1408   tooltip: ".tooltip",
1409   previewPopup: ".preview-popup",
1410   openInspector: "button.open-inspector",
1411   outlineItem: i =>
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];
1444   if (!selector) {
1445     throw new Error(`The selector ${elementName} is not defined`);
1446   }
1448   if (typeof selector == "function") {
1449     selector = selector(...args);
1450   }
1452   return selector;
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)
1475     .textContent.trim()
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
1486  * @return {Promise}
1487  * @static
1488  */
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),
1511     { clickCount: 2 },
1512     dbg.win
1513   );
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");
1587   type(dbg, text);
1588   pressKey(dbg, "Enter");
1592  * Toggles the debugger call stack accordian.
1594  * @memberof mochitest/actions
1595  * @param {Object} dbg
1596  * @return {Promise}
1597  * @static
1598  */
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}`);
1632   node.click();
1633   return waitUntil(
1634     () => objectInspector.querySelectorAll(".node").length !== properties
1635   );
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);
1645   return waitUntil(
1646     () => objectInspector.querySelectorAll(".node").length !== properties
1647   );
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);
1684   });
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(
1707     left - leftOffset,
1708     top + lineHeightOffset
1709   );
1711   if (!tokenEl) {
1712     throw new Error(`Failed to find element for line ${line}`);
1713   }
1714   return tokenEl;
1717 async function clickAtPos(dbg, pos) {
1718   const tokenEl = await getTokenFromPosition(dbg, pos);
1720   if (!tokenEl) {
1721     return false;
1722   }
1724   const { top, left } = tokenEl.getBoundingClientRect();
1725   info(
1726     `Clicking on token ${tokenEl.innerText} in line ${tokenEl.parentNode.innerText}`
1727   );
1728   tokenEl.dispatchEvent(
1729     new MouseEvent("click", {
1730       bubbles: true,
1731       cancelable: true,
1732       view: dbg.win,
1733       clientX: left,
1734       clientY: top,
1735     })
1736   );
1739 async function rightClickAtPos(dbg, pos) {
1740   const el = await getTokenFromPosition(dbg, pos);
1741   if (!el) {
1742     return false;
1743   }
1745   EventUtils.synthesizeMouseAtCenter(el, { type: "contextmenu" }, dbg.win);
1748 async function hoverAtPos(dbg, pos) {
1749   const tokenEl = await getTokenFromPosition(dbg, pos);
1751   if (!tokenEl) {
1752     return false;
1753   }
1755   info(`Hovering on token ${tokenEl.innerText}`);
1756   tokenEl.dispatchEvent(
1757     new MouseEvent("mouseover", {
1758       bubbles: true,
1759       cancelable: true,
1760       view: dbg.win,
1761     })
1762   );
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);
1771   if (!tokenEl) {
1772     return false;
1773   }
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);
1788     let count = 0;
1790     element.then(() => {
1791       clearInterval(interval);
1792       resolve(element);
1793     });
1795     const interval = setInterval(() => {
1796       if (count++ == 5) {
1797         clearInterval(interval);
1798         reject("failed to preview");
1799       }
1801       hoverAtPos(dbg, { line, ch: column - 1 });
1802     }, 1000);
1803   });
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;
1832   const properties =
1833     resultGrip.preview.ownProperties || resultGrip.preview.items;
1834   const property = properties[field];
1835   return property.value || property;
1838 async function assertPreviewPopup(
1839   dbg,
1840   line,
1841   column,
1842   { field, value, expression }
1843 ) {
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");
1854     }
1856     if (fields) {
1857       for (const [field, value] of fields) {
1858         await assertPreviewPopup(dbg, line, column, {
1859           expression,
1860           field,
1861           value,
1862         });
1863       }
1864     } else {
1865       await assertPreviewTextValue(dbg, line, column, {
1866         expression,
1867         text: result,
1868       });
1869     }
1871     const { target } = dbg.selectors.getPreview(getContext(dbg));
1872     InspectorUtils.removePseudoClassLock(target, ":hover");
1873     dbg.actions.clearPreview(getContext(dbg));
1874   }
1877 async function waitForBreakableLine(dbg, source, lineNumber) {
1878   await waitForState(
1879     dbg,
1880     state => {
1881       const currentSource = findSource(dbg, source);
1883       const breakableLines =
1884         currentSource && dbg.selectors.getBreakableLines(currentSource.id);
1886       return breakableLines && breakableLines.includes(lineNumber);
1887     },
1888     `waiting for breakable line ${lineNumber}`
1889   );
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;
1898   });
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);
1910     if (element) {
1911       return element.classList.contains("focused");
1912     }
1914     return false;
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);
1928   if (plusIcon) {
1929     plusIcon.click();
1930   }
1931   findElementWithSelector(dbg, selectors.expressionInput).focus();
1932   type(dbg, input);
1933   const evaluated = waitForDispatch(dbg.store, "EVALUATE_EXPRESSION");
1934   pressKey(dbg, "Enter");
1935   await evaluated;
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");
1943   type(dbg, input);
1944   const evaluated = waitForDispatch(dbg.store, "EVALUATE_EXPRESSIONS");
1945   pressKey(dbg, "Enter");
1946   await evaluated;
1949 async function waitUntilPredicate(predicate) {
1950   let result;
1951   await waitUntil(() => {
1952     result = predicate();
1953     return result;
1954   });
1956   return result;
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;
1965   if (!win) {
1966     win = toolbox.win;
1967   }
1969   if (!toolbox.splitConsole) {
1970     pressKey(dbg, "Escape");
1971   }
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)
2009   );
2012 async function hasConsoleMessage({ toolbox }, msg) {
2013   return waitFor(async () => {
2014     const messages = await findConsoleMessages(toolbox, msg);
2015     return messages.length > 0;
2016   });
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);
2025           res(message.node);
2026         }
2027       }
2028     };
2029     hud.ui.on("new-messages", onNewMessage);
2030   });
2031   hud.ui.wrapper.dispatchEvaluateExpression(expression);
2032   return onResult;
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);
2046     if (elem) {
2047       if (text instanceof RegExp) {
2048         return text.test(elem.innerText);
2049       }
2050       return elem.innerText == text;
2051     }
2052     return false;
2053   });
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);
2060   return onValueSet;
2063 function assertMenuItemChecked(menuItem, isChecked) {
2064   is(
2065     !!menuItem.getAttribute("aria-checked"),
2066     isChecked,
2067     `Item has expected state: ${isChecked ? "checked" : "unchecked"}`
2068   );
2071 async function toggleDebbuggerSettingsMenuItem(dbg, { className, isChecked }) {
2072   const menuButton = findElementWithSelector(
2073     dbg,
2074     ".debugger-settings-menu-button"
2075   );
2076   const { parent } = dbg.panel.panelWin;
2077   const { document } = parent;
2079   menuButton.click();
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);
2087   menuItem.click();
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(
2096     dbg,
2097     `${selectors.addLogItem},${selectors.editLogItem}`
2098   );
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"
2113   );
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/
2121   );
2122   PromiseTestUtils.allowMatchingRejectionsGlobally(
2123     /Current thread has paused or resumed/
2124   );
2125   PromiseTestUtils.allowMatchingRejectionsGlobally(/Connection closed/);
2126   this.PromiseTestUtils = PromiseTestUtils;