/* eslint-disable no-unused-vars */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ /** * Helper methods to drive with the debugger during mochitests. This file can be safely * required from other panel test files. */ // Import helpers for the new debugger Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/helpers/context.js", this ); var { Toolbox } = require("devtools/client/framework/toolbox"); var { Task } = require("devtools/shared/task"); var asyncStorage = require("devtools/shared/async-storage"); const { getSelectedLocation, } = require("devtools/client/debugger/src/utils/selected-location"); const { resetSchemaVersion, } = require("devtools/client/debugger/src/utils/prefs"); function log(msg, data) { info(`${msg} ${!data ? "" : JSON.stringify(data)}`); } function logThreadEvents(dbg, event) { const thread = dbg.toolbox.threadFront; thread.on(event, function onEvent(...args) { info(`Thread event '${event}' fired.`); }); } /** * Wait for a predicate to return a result. * * @param function condition * Invoked once in a while until it returns a truthy value. This should be an * idempotent function, since we have to run it a second time after it returns * true in order to return the value. * @param string message [optional] * A message to output if the condition fails. * @param number interval [optional] * How often the predicate is invoked, in milliseconds. * @return object * A promise that is resolved with the result of the condition. */ async function waitFor( condition, message = "waitFor", interval = 10, maxTries = 500 ) { await BrowserTestUtils.waitForCondition( condition, message, interval, maxTries ); return condition(); } // Wait until an action of `type` is dispatched. This is different // then `waitForDispatch` because it doesn't wait for async actions // to be done/errored. Use this if you want to listen for the "start" // action of an async operation (somewhat rare). function waitForNextDispatch(store, actionType) { return new Promise(resolve => { store.dispatch({ // Normally we would use `services.WAIT_UNTIL`, but use the // internal name here so tests aren't forced to always pass it // in type: "@@service/waitUntil", predicate: action => action.type === actionType, run: (dispatch, getState, action) => { resolve(action); }, }); }); } /** * Wait for a specific action type to be dispatch. * If an async action, will wait for it to be done. * * @memberof mochitest/waits * @param {Object} dbg * @param {String} type * @param {Number} eventRepeat * @return {Promise} * @static */ function waitForDispatch(dbg, actionType, eventRepeat = 1) { let count = 0; return new Promise(resolve => { dbg.store.dispatch({ // Normally we would use `services.WAIT_UNTIL`, but use the // internal name here so tests aren't forced to always pass it // in type: "@@service/waitUntil", predicate: action => { const isDone = !action.status || action.status === "done" || action.status === "error"; if (action.type === actionType && isDone && ++count == eventRepeat) { return true; } }, run: (dispatch, getState, action) => { resolve(action); }, }); }); } /** * Waits for `predicate()` to be true. `state` is the redux app state. * * @memberof mochitest/waits * @param {Object} dbg * @param {Function} predicate * @return {Promise} * @static */ function waitForState(dbg, predicate, msg) { return new Promise(resolve => { info(`Waiting for state change: ${msg || ""}`); if (predicate(dbg.store.getState())) { info(`Finished waiting for state change: ${msg || ""}`); return resolve(); } const unsubscribe = dbg.store.subscribe(() => { const result = predicate(dbg.store.getState()); if (result) { info(`Finished waiting for state change: ${msg || ""}`); unsubscribe(); resolve(result); } }); }); } /** * Waits for sources to be loaded. * * @memberof mochitest/waits * @param {Object} dbg * @param {Array} sources * @return {Promise} * @static */ async function waitForSources(dbg, ...sources) { if (sources.length === 0) { return Promise.resolve(); } info(`Waiting on sources: ${sources.join(", ")}`); await Promise.all( sources.map(url => { if (!sourceExists(dbg, url)) { return waitForState( dbg, () => sourceExists(dbg, url), `source ${url} exists` ); } }) ); info(`Finished waiting on sources: ${sources.join(", ")}`); } /** * Waits for a source to be loaded. * * @memberof mochitest/waits * @param {Object} dbg * @param {String} source * @return {Promise} * @static */ function waitForSource(dbg, url) { return waitForState( dbg, state => findSource(dbg, url, { silent: true }), "source exists" ); } async function waitForElement(dbg, name, ...args) { await waitUntil(() => findElement(dbg, name, ...args)); return findElement(dbg, name, ...args); } /** * Wait for a count of given elements to be rendered on screen. * * @param {DebuggerPanel} dbg * @param {String} name * @param {Integer} count: Number of elements to match. Defaults to 1. * @param {Boolean} countStrictlyEqual: When set to true, will wait until the exact number * of elements is displayed on screen. When undefined or false, will wait * until there's at least `${count}` elements on screen (e.g. if count * is 1, it will resolve if there are 2 elements rendered). */ async function waitForAllElements( dbg, name, count = 1, countStrictlyEqual = false ) { await waitUntil(() => { const elsCount = findAllElements(dbg, name).length; return countStrictlyEqual ? elsCount === count : elsCount >= count; }); return findAllElements(dbg, name); } async function waitForElementWithSelector(dbg, selector) { await waitUntil(() => findElementWithSelector(dbg, selector)); return findElementWithSelector(dbg, selector); } function waitForRequestsToSettle(dbg) { return dbg.toolbox.target.client.waitForRequestsToSettle(); } function assertClass(el, className, exists = true) { if (exists) { ok(el.classList.contains(className), `${className} class exists`); } else { ok(!el.classList.contains(className), `${className} class does not exist`); } } function waitForSelectedLocation(dbg, line, column) { return waitForState(dbg, state => { const location = dbg.selectors.getSelectedLocation(); return ( location && (line ? location.line == line : true) && (column ? location.column == column : true) ); }); } function waitForSelectedSource(dbg, url) { const { getSelectedSourceWithContent, hasSymbols, getBreakableLines, } = dbg.selectors; return waitForState( dbg, state => { const source = getSelectedSourceWithContent() || {}; if (!source.content) { return false; } if (!url) { return true; } const newSource = findSource(dbg, url, { silent: true }); if (newSource.id != source.id) { return false; } return hasSymbols(source) && getBreakableLines(source.id); }, "selected source" ); } /** * Assert that the debugger is not currently paused. * @memberof mochitest/asserts * @static */ function assertNotPaused(dbg) { ok(!isPaused(dbg), "client is not paused"); } /** * Assert that the debugger is currently paused. * @memberof mochitest/asserts * @static */ function assertPaused(dbg) { ok(isPaused(dbg), "client is paused"); } function assertEmptyLines(dbg, lines) { const sourceId = dbg.selectors.getSelectedSourceId(); const breakableLines = dbg.selectors.getBreakableLines(sourceId); ok( lines.every(line => !breakableLines.includes(line)), "empty lines should match" ); } function getVisibleSelectedFrameLine(dbg) { const { selectors: { getVisibleSelectedFrame }, } = dbg; const frame = getVisibleSelectedFrame(); return frame && frame.location.line; } /** * Assert that the debugger is paused at the correct location. * * @memberof mochitest/asserts * @param {Object} dbg * @param {String} source * @param {Number} line * @static */ function assertPausedLocation(dbg) { ok(isSelectedFrameSelected(dbg), "top frame's source is selected"); // Check the pause location const pauseLine = getVisibleSelectedFrameLine(dbg); assertDebugLine(dbg, pauseLine); ok(isVisibleInEditor(dbg, getCM(dbg).display.gutters), "gutter is visible"); } function assertDebugLine(dbg, line, column) { // Check the debug line const lineInfo = getCM(dbg).lineInfo(line - 1); const source = dbg.selectors.getSelectedSourceWithContent() || {}; if (source && !source.content) { const url = source.url; ok( false, `Looks like the source ${url} is still loading. Try adding waitForLoadedSource in the test.` ); return; } if (!lineInfo.wrapClass) { const pauseLine = getVisibleSelectedFrameLine(dbg); ok(false, `Expected pause line on line ${line}, it is on ${pauseLine}`); return; } ok( lineInfo.wrapClass.includes("new-debug-line"), "Line is highlighted as paused" ); const debugLine = findElement(dbg, "debugLine") || findElement(dbg, "debugErrorLine"); is( findAllElements(dbg, "debugLine").length + findAllElements(dbg, "debugErrorLine").length, 1, "There is only one line" ); ok(isVisibleInEditor(dbg, debugLine), "debug line is visible"); const markedSpans = lineInfo.handle.markedSpans; if (markedSpans && markedSpans.length > 0) { const classMatch = markedSpans.filter( span => span.marker.className && span.marker.className.includes("debug-expression") ).length > 0; if (column) { const frame = dbg.selectors.getVisibleSelectedFrame(); is(frame.location.column, column, `Paused at column ${column}`); } ok(classMatch, "expression is highlighted as paused"); } } /** * Assert that the debugger is highlighting the correct location. * * @memberof mochitest/asserts * @param {Object} dbg * @param {String} source * @param {Number} line * @static */ function assertHighlightLocation(dbg, source, line) { source = findSource(dbg, source); // Check the selected source is( dbg.selectors.getSelectedSource().url, source.url, "source url is correct" ); // Check the highlight line const lineEl = findElement(dbg, "highlightLine"); ok(lineEl, "Line is highlighted"); is( findAllElements(dbg, "highlightLine").length, 1, "Only 1 line is highlighted" ); ok(isVisibleInEditor(dbg, lineEl), "Highlighted line is visible"); const cm = getCM(dbg); const lineInfo = cm.lineInfo(line - 1); ok(lineInfo.wrapClass.includes("highlight-line"), "Line is highlighted"); } /** * Returns boolean for whether the debugger is paused. * * @memberof mochitest/asserts * @param {Object} dbg * @static */ function isPaused(dbg) { return dbg.selectors.getIsPaused(dbg.selectors.getCurrentThread()); } // Make sure the debugger is paused at a certain source ID and line. function assertPausedAtSourceAndLine(dbg, expectedSourceId, expectedLine) { assertPaused(dbg); const frames = dbg.selectors.getCurrentThreadFrames(); ok(frames.length >= 1, "Got at least one frame"); const { sourceId, line } = frames[0].location; ok(sourceId == expectedSourceId, "Frame has correct source"); ok( line == expectedLine, `Frame paused at ${line}, but expected ${expectedLine}` ); } // Get any workers associated with the debugger. async function getThreads(dbg) { await dbg.actions.updateThreads(); return dbg.selectors.getThreads(); } async function waitForLoadedScopes(dbg) { const scopes = await waitForElement(dbg, "scopes"); // Since scopes auto-expand, we can assume they are loaded when there is a tree node // with the aria-level attribute equal to "2". await waitUntil(() => scopes.querySelector('.tree-node[aria-level="2"]')); } function waitForBreakpointCount(dbg, count) { return waitForState( dbg, state => dbg.selectors.getBreakpointCount() == count ); } function waitForBreakpoint(dbg, url, line) { return waitForState(dbg, () => findBreakpoint(dbg, url, line)); } function waitForBreakpointRemoved(dbg, url, line) { return waitForState(dbg, () => !findBreakpoint(dbg, url, line)); } /** * Waits for the debugger to be fully paused. * * @memberof mochitest/waits * @param {Object} dbg * @static */ async function waitForPaused(dbg, url) { const { getSelectedScope, getCurrentThread, getCurrentThreadFrames, } = dbg.selectors; await waitForState( dbg, state => isPaused(dbg) && !!getSelectedScope(getCurrentThread()), "paused" ); await waitForState(dbg, getCurrentThreadFrames, "fetched frames"); await waitForLoadedScopes(dbg); await waitForSelectedSource(dbg, url); } function waitForInlinePreviews(dbg) { return waitForState(dbg, () => dbg.selectors.getSelectedInlinePreviews()); } function waitForCondition(dbg, condition) { return waitForState(dbg, state => dbg.selectors .getBreakpointsList() .find(bp => bp.options.condition == condition) ); } function waitForLog(dbg, logValue) { return waitForState(dbg, state => dbg.selectors .getBreakpointsList() .find(bp => bp.options.logValue == logValue) ); } async function waitForPausedThread(dbg, thread) { return waitForState(dbg, state => dbg.selectors.getIsPaused(thread)); } /* * useful for when you want to see what is happening * e.g await waitForever() */ function waitForever() { return new Promise(r => {}); } /* * useful for waiting for a short amount of time as * a placeholder for a better waitForX handler. * * e.g await waitForTime(500) */ function waitForTime(ms) { return new Promise(r => setTimeout(r, ms)); } function isSelectedFrameSelected(dbg, state) { const frame = dbg.selectors.getVisibleSelectedFrame(); // Make sure the source text is completely loaded for the // source we are paused in. const sourceId = frame.location.sourceId; const source = dbg.selectors.getSelectedSourceWithContent() || {}; if (!source || !source.content) { return false; } return source.id == sourceId; } /** * Clear all the debugger related preferences. */ async function clearDebuggerPreferences(prefs = []) { resetSchemaVersion(); asyncStorage.clear(); Services.prefs.clearUserPref("devtools.recordreplay.enabled"); Services.prefs.clearUserPref("devtools.debugger.alphabetize-outline"); Services.prefs.clearUserPref("devtools.debugger.pause-on-exceptions"); Services.prefs.clearUserPref("devtools.debugger.pause-on-caught-exceptions"); Services.prefs.clearUserPref("devtools.debugger.ignore-caught-exceptions"); Services.prefs.clearUserPref("devtools.debugger.pending-selected-location"); Services.prefs.clearUserPref("devtools.debugger.expressions"); Services.prefs.clearUserPref("devtools.debugger.call-stack-visible"); Services.prefs.clearUserPref("devtools.debugger.scopes-visible"); Services.prefs.clearUserPref("devtools.debugger.skip-pausing"); Services.prefs.clearUserPref("devtools.debugger.map-scopes-enabled"); await pushPref("devtools.debugger.log-actions", true); for (const pref of prefs) { await pushPref(...pref); } } /** * Intilializes the debugger. * * @memberof mochitest * @param {String} url * @return {Promise} dbg * @static */ async function initDebugger(url, ...sources) { await clearDebuggerPreferences(); const toolbox = await openNewTabAndToolbox(EXAMPLE_URL + url, "jsdebugger"); const dbg = createDebuggerContext(toolbox); await waitForSources(dbg, ...sources); return dbg; } async function initPane(url, pane, prefs) { await clearDebuggerPreferences(prefs); return openNewTabAndToolbox(EXAMPLE_URL + url, pane); } window.resumeTest = undefined; registerCleanupFunction(() => { delete window.resumeTest; }); /** * Pause the test and let you interact with the debugger. * The test can be resumed by invoking `resumeTest` in the console. * * @memberof mochitest * @static */ function pauseTest() { info("Test paused. Invoke resumeTest to continue."); return new Promise(resolve => (resumeTest = resolve)); } // Actions /** * Returns a source that matches the URL. * * @memberof mochitest/actions * @param {Object} dbg * @param {String} url * @return {Object} source * @static */ function findSource(dbg, url, { silent } = { silent: false }) { if (typeof url !== "string") { // Support passing in a source object itelf all APIs that use this // function support both styles const source = url; return source; } const sources = dbg.selectors.getSourceList(); const source = sources.find(s => (s.url || "").includes(url)); if (!source) { if (silent) { return false; } throw new Error(`Unable to find source: ${url}`); } return source; } function findSourceContent(dbg, url, opts) { const source = findSource(dbg, url, opts); if (!source) { return null; } const content = dbg.selectors.getSourceContent(source.id); if (!content) { return null; } if (content.state !== "fulfilled") { throw new Error("Expected loaded source, got" + content.value); } return content.value; } function sourceExists(dbg, url) { return !!findSource(dbg, url, { silent: true }); } function waitForLoadedSource(dbg, url) { return waitForState( dbg, state => { const source = findSource(dbg, url, { silent: true }); return source && dbg.selectors.getSourceContent(source.id); }, "loaded source" ); } function waitForLoadedSources(dbg) { return waitForState( dbg, state => { const sources = dbg.selectors.getSourceList(); return sources.every( source => !!dbg.selectors.getSourceContent(source.id) ); }, "loaded source" ); } function getContext(dbg) { return dbg.selectors.getContext(); } function getThreadContext(dbg) { return dbg.selectors.getThreadContext(); } /** * Selects the source. * * @memberof mochitest/actions * @param {Object} dbg * @param {String} url * @param {Number} line * @return {Promise} * @static */ async function selectSource(dbg, url, line, column) { const source = findSource(dbg, url); await dbg.actions.selectLocation( getContext(dbg), { sourceId: source.id, line, column }, { keepContext: false } ); return waitForSelectedSource(dbg, url); } async function closeTab(dbg, url) { await dbg.actions.closeTab(getContext(dbg), findSource(dbg, url)); } function countTabs(dbg) { return findElement(dbg, "sourceTabs").children.length; } /** * Steps over. * * @memberof mochitest/actions * @param {Object} dbg * @return {Promise} * @static */ async function stepOver(dbg) { const pauseLine = getVisibleSelectedFrameLine(dbg); info(`Stepping over from ${pauseLine}`); await dbg.actions.stepOver(getThreadContext(dbg)); return waitForPaused(dbg); } /** * Steps in. * * @memberof mochitest/actions * @param {Object} dbg * @return {Promise} * @static */ async function stepIn(dbg) { const pauseLine = getVisibleSelectedFrameLine(dbg); info(`Stepping in from ${pauseLine}`); await dbg.actions.stepIn(getThreadContext(dbg)); return waitForPaused(dbg); } /** * Steps out. * * @memberof mochitest/actions * @param {Object} dbg * @return {Promise} * @static */ async function stepOut(dbg) { const pauseLine = getVisibleSelectedFrameLine(dbg); info(`Stepping out from ${pauseLine}`); await dbg.actions.stepOut(getThreadContext(dbg)); return waitForPaused(dbg); } /** * Resumes. * * @memberof mochitest/actions * @param {Object} dbg * @return {Promise} * @static */ function resume(dbg) { const pauseLine = getVisibleSelectedFrameLine(dbg); info(`Resuming from ${pauseLine}`); return dbg.actions.resume(getThreadContext(dbg)); } function deleteExpression(dbg, input) { info(`Delete expression "${input}"`); return dbg.actions.deleteExpression({ input }); } /** * Reloads the debuggee. * * @memberof mochitest/actions * @param {Object} dbg * @param {Array} sources * @return {Promise} * @static */ async function reload(dbg, ...sources) { const navigated = waitForDispatch(dbg, "NAVIGATE"); await dbg.client.reload(); await navigated; return waitForSources(dbg, ...sources); } /** * Navigates the debuggee to another url. * * @memberof mochitest/actions * @param {Object} dbg * @param {String} url * @param {Array} sources * @return {Promise} * @static */ async function navigate(dbg, url, ...sources) { info(`Navigating to ${url}`); const navigated = waitForDispatch(dbg, "NAVIGATE"); await dbg.client.navigate(url); await navigated; return waitForSources(dbg, ...sources); } function getFirstBreakpointColumn(dbg, { line, sourceId }) { const { getSource, getFirstBreakpointPosition } = dbg.selectors; const source = getSource(sourceId); const position = getFirstBreakpointPosition({ line, sourceId, }); return getSelectedLocation(position, source).column; } /** * Adds a breakpoint to a source at line/col. * * @memberof mochitest/actions * @param {Object} dbg * @param {String} source * @param {Number} line * @param {Number} col * @return {Promise} * @static */ async function addBreakpoint(dbg, source, line, column, options) { source = findSource(dbg, source); const sourceId = source.id; const bpCount = dbg.selectors.getBreakpointCount(); const onBreakpoint = waitForDispatch(dbg, "SET_BREAKPOINT"); await dbg.actions.addBreakpoint( getContext(dbg), { sourceId, line, column }, options ); await onBreakpoint; is( dbg.selectors.getBreakpointCount(), bpCount + 1, "a new breakpoint was created" ); } function disableBreakpoint(dbg, source, line, column) { column = column || getFirstBreakpointColumn(dbg, { line, sourceId: source.id }); const location = { sourceId: source.id, sourceUrl: source.url, line, column }; const bp = dbg.selectors.getBreakpointForLocation(location); return dbg.actions.disableBreakpoint(getContext(dbg), bp); } function setBreakpointOptions(dbg, source, line, column, options) { source = findSource(dbg, source); const sourceId = source.id; column = column || getFirstBreakpointColumn(dbg, { line, sourceId }); return dbg.actions.setBreakpointOptions( getContext(dbg), { sourceId, line, column }, options ); } function findBreakpoint(dbg, url, line) { const source = findSource(dbg, url); return dbg.selectors.getBreakpointsForSource(source.id, line)[0]; } // helper for finding column breakpoints. function findColumnBreakpoint(dbg, url, line, column) { const source = findSource(dbg, url); const lineBreakpoints = dbg.selectors.getBreakpointsForSource( source.id, line ); return lineBreakpoints.find(bp => { return bp.generatedLocation.column === column; }); } async function loadAndAddBreakpoint(dbg, filename, line, column) { const { selectors: { getBreakpoint, getBreakpointCount, getBreakpointsMap }, } = dbg; await waitForSources(dbg, filename); ok(true, "Original sources exist"); const source = findSource(dbg, filename); await selectSource(dbg, source); // Test that breakpoint is not off by a line. await addBreakpoint(dbg, source, line, column); is(getBreakpointCount(), 1, "One breakpoint exists"); if (!getBreakpoint({ sourceId: source.id, line, column })) { const breakpoints = getBreakpointsMap(); const id = Object.keys(breakpoints).pop(); const loc = breakpoints[id].location; ok( false, `Breakpoint has correct line ${line}, column ${column}, but was line ${ loc.line } column ${loc.column}` ); } return source; } async function invokeWithBreakpoint( dbg, fnName, filename, { line, column }, handler ) { const source = await loadAndAddBreakpoint(dbg, filename, line, column); const invokeResult = invokeInTab(fnName); const invokeFailed = await Promise.race([ waitForPaused(dbg), invokeResult.then(() => new Promise(() => {}), () => true), ]); if (invokeFailed) { return invokeResult; } assertPausedLocation(dbg); await removeBreakpoint(dbg, source.id, line, column); is(dbg.selectors.getBreakpointCount(), 0, "Breakpoint reverted"); await handler(source); await resume(dbg); // eslint-disable-next-line max-len // If the invoke errored later somehow, capture here so the error is reported nicely. await invokeResult; } function prettyPrint(dbg) { const sourceId = dbg.selectors.getSelectedSourceId(); return dbg.actions.togglePrettyPrint(getContext(dbg), sourceId); } async function expandAllScopes(dbg) { const scopes = await waitForElement(dbg, "scopes"); const scopeElements = scopes.querySelectorAll( '.tree-node[aria-level="1"][data-expandable="true"]:not([aria-expanded="true"])' ); const indices = Array.from(scopeElements, el => { return Array.prototype.indexOf.call(el.parentNode.childNodes, el); }).reverse(); for (const index of indices) { await toggleScopeNode(dbg, index + 1); } } async function assertScopes(dbg, items) { await expandAllScopes(dbg); for (const [i, val] of items.entries()) { if (Array.isArray(val)) { is(getScopeLabel(dbg, i + 1), val[0]); is( getScopeValue(dbg, i + 1), val[1], `"${val[0]}" has the expected "${val[1]}" value` ); } else { is(getScopeLabel(dbg, i + 1), val); } } is(getScopeLabel(dbg, items.length + 1), "Window"); } /** * Removes a breakpoint from a source at line/col. * * @memberof mochitest/actions * @param {Object} dbg * @param {String} source * @param {Number} line * @param {Number} col * @return {Promise} * @static */ function removeBreakpoint(dbg, sourceId, line, column) { const source = dbg.selectors.getSource(sourceId); column = column || getFirstBreakpointColumn(dbg, { line, sourceId }); const location = { sourceId, sourceUrl: source.url, line, column }; const bp = dbg.selectors.getBreakpointForLocation(location); return dbg.actions.removeBreakpoint(getContext(dbg), bp); } /** * Toggles the Pause on exceptions feature in the debugger. * * @memberof mochitest/actions * @param {Object} dbg * @param {Boolean} pauseOnExceptions * @param {Boolean} pauseOnCaughtExceptions * @return {Promise} * @static */ async function togglePauseOnExceptions( dbg, pauseOnExceptions, pauseOnCaughtExceptions ) { return dbg.actions.pauseOnExceptions( pauseOnExceptions, pauseOnCaughtExceptions ); } function waitForActive(dbg) { const { selectors: { getIsPaused, getCurrentThread }, } = dbg; return waitForState(dbg, state => !getIsPaused(getCurrentThread()), "active"); } // Helpers /** * Invokes a global function in the debuggee tab. * * @memberof mochitest/helpers * @param {String} fnc The name of a global function on the content window to * call. This is applied to structured clones of the * remaining arguments to invokeInTab. * @param {Any} ...args Remaining args to serialize and pass to fnc. * @return {Promise} * @static */ function invokeInTab(fnc, ...args) { info(`Invoking in tab: ${fnc}(${args.map(uneval).join(",")})`); return ContentTask.spawn( gBrowser.selectedBrowser, { fnc, args }, function({ fnc, args }) { return content.wrappedJSObject[fnc](...args); } ); } function clickElementInTab(selector) { info(`click element ${selector} in tab`); return SpecialPowers.spawn( gBrowser.selectedBrowser, [{ selector }], function({ selector }) { content.wrappedJSObject.document.querySelector(selector).click(); } ); } const isLinux = Services.appinfo.OS === "Linux"; const isMac = Services.appinfo.OS === "Darwin"; const cmdOrCtrl = isLinux ? { ctrlKey: true } : { metaKey: true }; const shiftOrAlt = isMac ? { accelKey: true, shiftKey: true } : { accelKey: true, altKey: true }; const cmdShift = isMac ? { accelKey: true, shiftKey: true, metaKey: true } : { accelKey: true, shiftKey: true, ctrlKey: true }; // On Mac, going to beginning/end only works with meta+left/right. On // Windows, it only works with home/end. On Linux, apparently, either // ctrl+left/right or home/end work. const endKey = isMac ? { code: "VK_RIGHT", modifiers: cmdOrCtrl } : { code: "VK_END" }; const startKey = isMac ? { code: "VK_LEFT", modifiers: cmdOrCtrl } : { code: "VK_HOME" }; const keyMappings = { close: { code: "w", modifiers: cmdOrCtrl }, debugger: { code: "s", modifiers: shiftOrAlt }, // test conditional panel shortcut toggleCondPanel: { code: "b", modifiers: cmdShift }, inspector: { code: "c", modifiers: shiftOrAlt }, quickOpen: { code: "p", modifiers: cmdOrCtrl }, quickOpenFunc: { code: "o", modifiers: cmdShift }, quickOpenLine: { code: ":", modifiers: cmdOrCtrl }, fileSearch: { code: "f", modifiers: cmdOrCtrl }, fileSearchNext: { code: "g", modifiers: { metaKey: true } }, fileSearchPrev: { code: "g", modifiers: cmdShift }, goToLine: { code: "g", modifiers: { ctrlKey: true } }, Enter: { code: "VK_RETURN" }, ShiftEnter: { code: "VK_RETURN", modifiers: shiftOrAlt }, AltEnter: { code: "VK_RETURN", modifiers: { altKey: true }, }, Up: { code: "VK_UP" }, Down: { code: "VK_DOWN" }, Right: { code: "VK_RIGHT" }, Left: { code: "VK_LEFT" }, End: endKey, Start: startKey, Tab: { code: "VK_TAB" }, Escape: { code: "VK_ESCAPE" }, Delete: { code: "VK_DELETE" }, pauseKey: { code: "VK_F8" }, resumeKey: { code: "VK_F8" }, stepOverKey: { code: "VK_F10" }, stepInKey: { code: "VK_F11", modifiers: { ctrlKey: isLinux } }, stepOutKey: { code: "VK_F11", modifiers: { ctrlKey: isLinux, shiftKey: true }, }, }; /** * Simulates a key press in the debugger window. * * @memberof mochitest/helpers * @param {Object} dbg * @param {String} keyName * @return {Promise} * @static */ function pressKey(dbg, keyName) { const keyEvent = keyMappings[keyName]; const { code, modifiers } = keyEvent; return EventUtils.synthesizeKey(code, modifiers || {}, dbg.win); } function type(dbg, string) { string.split("").forEach(char => EventUtils.synthesizeKey(char, {}, dbg.win)); } /* * Checks to see if the inner element is visible inside the editor. * * @memberof mochitest/helpers * @param {Object} dbg * @param {HTMLElement} inner element * @return {boolean} * @static */ function isVisibleInEditor(dbg, element) { return isVisible(findElement(dbg, "codeMirror"), element); } /* * Checks to see if the inner element is visible inside the * outer element. * * Note, the inner element does not need to be entirely visible, * it is possible for it to be somewhat clipped by the outer element's * bounding element or for it to span the entire length, starting before the * outer element and ending after. * * @memberof mochitest/helpers * @param {HTMLElement} outer element * @param {HTMLElement} inner element * @return {boolean} * @static */ function isVisible(outerEl, innerEl) { if (!innerEl || !outerEl) { return false; } const innerRect = innerEl.getBoundingClientRect(); const outerRect = outerEl.getBoundingClientRect(); const verticallyVisible = innerRect.top >= outerRect.top || innerRect.bottom <= outerRect.bottom || (innerRect.top < outerRect.top && innerRect.bottom > outerRect.bottom); const horizontallyVisible = innerRect.left >= outerRect.left || innerRect.right <= outerRect.right || (innerRect.left < outerRect.left && innerRect.right > outerRect.right); const visible = verticallyVisible && horizontallyVisible; return visible; } async function getEditorLineGutter(dbg, line) { const lineEl = await getEditorLineEl(dbg, line); return lineEl.firstChild; } async function getEditorLineEl(dbg, line) { let el = await codeMirrorGutterElement(dbg, line); while (el && !el.matches(".CodeMirror-code > div")) { el = el.parentElement; } return el; } async function assertEditorBreakpoint(dbg, line, shouldExist) { const el = await getEditorLineEl(dbg, line); const exists = !!el.querySelector(".new-breakpoint"); const existsStr = shouldExist ? "exists" : "does not exist"; ok(exists === shouldExist, `Breakpoint ${existsStr} on line ${line}`); } function assertBreakpointSnippet(dbg, index, snippet) { const actualSnippet = findElement(dbg, "breakpointLabel", 2).innerText; is(snippet, actualSnippet, `Breakpoint ${index} snippet`); } const selectors = { callStackHeader: ".call-stack-pane ._header", callStackBody: ".call-stack-pane .pane", domMutationItem: ".dom-mutation-list li", expressionNode: i => `.expressions-list .expression-container:nth-child(${i}) .object-label`, expressionValue: i => // eslint-disable-next-line max-len `.expressions-list .expression-container:nth-child(${i}) .object-delimiter + *`, expressionClose: i => `.expressions-list .expression-container:nth-child(${i}) .close`, expressionInput: ".expressions-list input.input-expression", expressionNodes: ".expressions-list .tree-node", expressionPlus: ".watch-expressions-pane button.plus", scopesHeader: ".scopes-pane ._header", breakpointItem: i => `.breakpoints-list div:nth-of-type(${i})`, breakpointLabel: i => `${selectors.breakpointItem(i)} .breakpoint-label`, breakpointItems: ".breakpoints-list .breakpoint", breakpointContextMenu: { disableSelf: "#node-menu-disable-self", disableAll: "#node-menu-disable-all", disableOthers: "#node-menu-disable-others", enableSelf: "#node-menu-enable-self", enableOthers: "#node-menu-enable-others", disableDbgStatement: "#node-menu-disable-dbgStatement", enableDbgStatement: "#node-menu-enable-dbgStatement", remove: "#node-menu-delete-self", removeOthers: "#node-menu-delete-other", removeCondition: "#node-menu-remove-condition", }, editorContextMenu: { continueToHere: "#node-menu-continue-to-here", }, columnBreakpoints: ".column-breakpoint", scopes: ".scopes-list", scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`, scopeValue: i => `.scopes-list .tree-node:nth-child(${i}) .object-delimiter + *`, mapScopesCheckbox: ".map-scopes-header input", frame: i => `.frames [role="list"] [role="listitem"]:nth-child(${i})`, frames: '.frames [role="list"] [role="listitem"]', gutter: i => `.CodeMirror-code *:nth-child(${i}) .CodeMirror-linenumber`, addConditionItem: "#node-menu-add-condition, #node-menu-add-conditional-breakpoint", editConditionItem: "#node-menu-edit-condition, #node-menu-edit-conditional-breakpoint", addLogItem: "#node-menu-add-log-point", editLogItem: "#node-menu-edit-log-point", disableItem: "#node-menu-disable-breakpoint", menuitem: i => `menupopup menuitem:nth-child(${i})`, pauseOnExceptions: ".pause-exceptions", breakpoint: ".CodeMirror-code > .new-breakpoint", highlightLine: ".CodeMirror-code > .highlight-line", debugLine: ".new-debug-line", debugErrorLine: ".new-debug-line-error", codeMirror: ".CodeMirror", resume: ".resume.active", pause: ".pause.active", sourceTabs: ".source-tabs", activeTab: ".source-tab.active", stepOver: ".stepOver.active", stepOut: ".stepOut.active", stepIn: ".stepIn.active", replayPrevious: ".replay-previous.active", replayNext: ".replay-next.active", toggleBreakpoints: ".breakpoints-toggle", prettyPrintButton: ".source-footer .prettyPrint", prettyPrintLoader: ".source-footer .spin", sourceMapLink: ".source-footer .mapped-source", sourcesFooter: ".sources-panel .source-footer", editorFooter: ".editor-pane .source-footer", sourceNode: i => `.sources-list .tree-node:nth-child(${i}) .node`, sourceNodes: ".sources-list .tree-node", threadSourceTree: i => `.threads-list .sources-pane:nth-child(${i})`, threadSourceTreeHeader: i => `${selectors.threadSourceTree(i)} .thread-header`, threadSourceTreeSourceNode: (i, j) => `${selectors.threadSourceTree(i)} .tree-node:nth-child(${j}) .node`, sourceDirectoryLabel: i => `.sources-list .tree-node:nth-child(${i}) .label`, resultItems: ".result-list .result-item", resultItemName: (name, i) => `${selectors.resultItems}:nth-child(${i})[title$="${name}"]`, fileMatch: ".project-text-search .line-value", popup: ".popover", tooltip: ".tooltip", previewPopup: ".preview-popup", openInspector: "button.open-inspector", outlineItem: i => `.outline-list__element:nth-child(${i}) .function-signature`, outlineItems: ".outline-list__element", conditionalPanel: ".conditional-breakpoint-panel", conditionalPanelInput: ".conditional-breakpoint-panel textarea", conditionalBreakpointInSecPane: ".breakpoint.is-conditional", logPointPanel: ".conditional-breakpoint-panel.log-point", logPointInSecPane: ".breakpoint.is-log", searchField: ".search-field", blackbox: ".action.black-box", projectSearchCollapsed: ".project-text-search .arrow:not(.expanded)", projectSerchExpandedResults: ".project-text-search .result", threadsPaneItems: ".threads-pane .thread", threadsPaneItem: i => `.threads-pane .thread:nth-child(${i})`, threadsPaneItemPause: i => `${selectors.threadsPaneItem(i)} .pause-badge`, CodeMirrorLines: ".CodeMirror-lines", inlinePreviewLabels: ".inline-preview .inline-preview-label", inlinePreviewValues: ".inline-preview .inline-preview-value", inlinePreviewOpenInspector: ".inline-preview-value button.open-inspector", watchpointsSubmenu: "#node-menu-watchpoints", addGetWatchpoint: "#node-menu-add-get-watchpoint", addSetWatchpoint: "#node-menu-add-set-watchpoint", removeWatchpoint: "#node-menu-remove-watchpoint", logEventsCheckbox: ".events-header input", }; function getSelector(elementName, ...args) { let selector = selectors[elementName]; if (!selector) { throw new Error(`The selector ${elementName} is not defined`); } if (typeof selector == "function") { selector = selector(...args); } return selector; } function findElement(dbg, elementName, ...args) { const selector = getSelector(elementName, ...args); return findElementWithSelector(dbg, selector); } function findElementWithSelector(dbg, selector) { return dbg.win.document.querySelector(selector); } function findAllElements(dbg, elementName, ...args) { const selector = getSelector(elementName, ...args); return findAllElementsWithSelector(dbg, selector); } function findAllElementsWithSelector(dbg, selector) { return dbg.win.document.querySelectorAll(selector); } function getSourceNodeLabel(dbg, index) { return findElement(dbg, "sourceNode", index) .textContent.trim() .replace(/^[\s\u200b]*/g, ""); } /** * Simulates a mouse click in the debugger DOM. * * @memberof mochitest/helpers * @param {Object} dbg * @param {String} elementName * @param {Array} args * @return {Promise} * @static */ async function clickElement(dbg, elementName, ...args) { const selector = getSelector(elementName, ...args); const el = await waitForElementWithSelector(dbg, selector); el.scrollIntoView(); return clickElementWithSelector(dbg, selector); } function clickElementWithSelector(dbg, selector) { clickDOMElement(dbg, findElementWithSelector(dbg, selector)); } function clickDOMElement(dbg, element) { EventUtils.synthesizeMouseAtCenter(element, {}, dbg.win); } function dblClickElement(dbg, elementName, ...args) { const selector = getSelector(elementName, ...args); return EventUtils.synthesizeMouseAtCenter( findElementWithSelector(dbg, selector), { clickCount: 2 }, dbg.win ); } function clickElementWithOptions(dbg, elementName, options, ...args) { const selector = getSelector(elementName, ...args); const el = findElementWithSelector(dbg, selector); el.scrollIntoView(); return EventUtils.synthesizeMouseAtCenter(el, options, dbg.win); } function altClickElement(dbg, elementName, ...args) { return clickElementWithOptions(dbg, elementName, { altKey: true }, ...args); } function shiftClickElement(dbg, elementName, ...args) { return clickElementWithOptions(dbg, elementName, { shiftKey: true }, ...args); } function rightClickElement(dbg, elementName, ...args) { const selector = getSelector(elementName, ...args); const doc = dbg.win.document; return rightClickEl(dbg, doc.querySelector(selector)); } function rightClickEl(dbg, el) { const doc = dbg.win.document; el.scrollIntoView(); EventUtils.synthesizeMouseAtCenter(el, { type: "contextmenu" }, dbg.win); } async function clickGutter(dbg, line) { const el = await codeMirrorGutterElement(dbg, line); clickDOMElement(dbg, el); } function selectContextMenuItem(dbg, selector) { // the context menu is in the toolbox window const doc = dbg.toolbox.topDoc; // there are several context menus, we want the one with the menu-api const popup = doc.querySelector('menupopup[menu-api="true"]'); const item = popup.querySelector(selector); return EventUtils.synthesizeMouseAtCenter(item, {}, dbg.toolbox.topWindow); } async function typeInPanel(dbg, text) { await waitForElement(dbg, "conditionalPanelInput"); // Position cursor reliably at the end of the text. pressKey(dbg, "End"); type(dbg, text); pressKey(dbg, "Enter"); } /** * Toggles the debugger call stack accordian. * * @memberof mochitest/actions * @param {Object} dbg * @return {Promise} * @static */ function toggleCallStack(dbg) { return findElement(dbg, "callStackHeader").click(); } function toggleScopes(dbg) { return findElement(dbg, "scopesHeader").click(); } function toggleExpressionNode(dbg, index) { return toggleObjectInspectorNode(findElement(dbg, "expressionNode", index)); } function toggleScopeNode(dbg, index) { return toggleObjectInspectorNode(findElement(dbg, "scopeNode", index)); } function rightClickScopeNode(dbg, index) { rightClickObjectInspectorNode(dbg, findElement(dbg, "scopeNode", index)); } function getScopeLabel(dbg, index) { return findElement(dbg, "scopeNode", index).innerText; } function getScopeValue(dbg, index) { return findElement(dbg, "scopeValue", index).innerText; } function toggleObjectInspectorNode(node) { const objectInspector = node.closest(".object-inspector"); const properties = objectInspector.querySelectorAll(".node").length; log(`Toggling node ${node.innerText}`); node.click(); return waitUntil( () => objectInspector.querySelectorAll(".node").length !== properties ); } function rightClickObjectInspectorNode(dbg, node) { const objectInspector = node.closest(".object-inspector"); const properties = objectInspector.querySelectorAll(".node").length; log(`Right clicking node ${node.innerText}`); rightClickEl(dbg, node); return waitUntil( () => objectInspector.querySelectorAll(".node").length !== properties ); } function getCM(dbg) { const el = dbg.win.document.querySelector(".CodeMirror"); return el.CodeMirror; } function getCoordsFromPosition(cm, { line, ch }) { return cm.charCoords({ line: ~~line, ch: ~~ch }); } async function getTokenFromPosition(dbg, { line, ch }) { info(`Get token at ${line}, ${ch}`); const cm = getCM(dbg); cm.scrollIntoView({ line: line - 1, ch }, 0); // Ensure the line is visible with margin because the bar at the bottom of // the editor overlaps into what the editor thinks is its own space, blocking // the click event below. await waitForScrolling(cm); const coords = getCoordsFromPosition(cm, { line: line - 1, ch }); const { left, top } = coords; // Adds a vertical offset due to increased line height // https://github.com/firefox-devtools/debugger/pull/7934 const lineHeightOffset = 3; return dbg.win.document.elementFromPoint(left, top + lineHeightOffset); } async function waitForScrolling(codeMirror) { return new Promise(resolve => { codeMirror.on("scroll", resolve); setTimeout(resolve, 500); }); } async function codeMirrorGutterElement(dbg, line) { info(`CodeMirror line ${line}`); const cm = getCM(dbg); const position = { line: line - 1, ch: 0 }; cm.scrollIntoView(position, 0); await waitForScrolling(cm); const coords = getCoordsFromPosition(cm, position); const { left, top } = coords; // Adds a vertical offset due to increased line height // https://github.com/firefox-devtools/debugger/pull/7934 const lineHeightOffset = 3; // Click in the center of the line/breakpoint const leftOffset = 10; const tokenEl = dbg.win.document.elementFromPoint( left - leftOffset, top + lineHeightOffset ); if (!tokenEl) { throw new Error(`Failed to find element for line ${line}`); } return tokenEl; } async function clickAtPos(dbg, pos) { const tokenEl = await getTokenFromPosition(dbg, pos); if (!tokenEl) { return false; } const { top, left } = tokenEl.getBoundingClientRect(); info( `Clicking on token ${tokenEl.innerText} in line ${ tokenEl.parentNode.innerText }` ); tokenEl.dispatchEvent( new MouseEvent("click", { bubbles: true, cancelable: true, view: dbg.win, clientX: left, clientY: top, }) ); } async function rightClickAtPos(dbg, pos) { const el = await getTokenFromPosition(dbg, pos); if (!el) { return false; } EventUtils.synthesizeMouseAtCenter(el, { type: "contextmenu" }, dbg.win); } async function hoverAtPos(dbg, pos) { const tokenEl = await getTokenFromPosition(dbg, pos); if (!tokenEl) { return false; } info(`Hovering on token ${tokenEl.innerText}`); tokenEl.dispatchEvent( new MouseEvent("mouseover", { bubbles: true, cancelable: true, view: dbg.win, }) ); InspectorUtils.addPseudoClassLock(tokenEl, ":hover"); } // tryHovering will hover at a position every second until we // see a preview element (popup, tooltip) appear. Once it appears, // it considers it a success. function tryHovering(dbg, line, column, elementName) { return new Promise((resolve, reject) => { const element = waitForElement(dbg, elementName); let count = 0; element.then(() => { clearInterval(interval); resolve(element); }); const interval = setInterval(() => { if (count++ == 5) { clearInterval(interval); reject("failed to preview"); } hoverAtPos(dbg, { line, ch: column - 1 }); }, 1000); }); } async function assertPreviewTextValue(dbg, line, column, { text, expression }) { const previewEl = await tryHovering(dbg, line, column, "previewPopup"); ok(previewEl.innerText.includes(text), "Preview text shown to user"); const preview = dbg.selectors.getPreview(); is(preview.expression, expression, "Preview.expression"); } async function assertPreviewTooltip(dbg, line, column, { result, expression }) { const previewEl = await tryHovering(dbg, line, column, "tooltip"); is(previewEl.innerText, result, "Preview text shown to user"); const preview = dbg.selectors.getPreview(); is(`${preview.resultGrip}`, result, "Preview.result"); is(preview.expression, expression, "Preview.expression"); } async function hoverOnToken(dbg, line, column, selector) { await tryHovering(dbg, line, column, selector); return dbg.selectors.getPreview(); } function getPreviewProperty(preview, field) { const { resultGrip } = preview; const properties = resultGrip.preview.ownProperties || resultGrip.preview.items; const property = properties[field]; return property.value || property; } async function assertPreviewPopup( dbg, line, column, { field, value, expression } ) { const preview = await hoverOnToken(dbg, line, column, "popup"); is(`${getPreviewProperty(preview, field)}`, value, "Preview.result"); is(preview.expression, expression, "Preview.expression"); } async function assertPreviews(dbg, previews) { for (const { line, column, expression, result, fields } of previews) { if (fields && result) { throw new Error("Invalid test fixture"); } if (fields) { for (const [field, value] of fields) { await assertPreviewPopup(dbg, line, column, { expression, field, value, }); } } else { await assertPreviewTextValue(dbg, line, column, { expression, text: result, }); } const { target } = dbg.selectors.getPreview(getContext(dbg)); InspectorUtils.removePseudoClassLock(target, ":hover"); dbg.actions.clearPreview(getContext(dbg)); } } async function waitForBreakableLine(dbg, source, lineNumber) { await waitForState( dbg, state => { const currentSource = findSource(dbg, source); const breakableLines = currentSource && dbg.selectors.getBreakableLines(currentSource.id); return breakableLines && breakableLines.includes(lineNumber); }, `waiting for breakable line ${lineNumber}` ); } async function waitForSourceCount(dbg, i) { // We are forced to wait until the DOM nodes appear because the // source tree batches its rendering. await waitUntil(() => { return findAllElements(dbg, "sourceNodes").length === i; }, `waiting for ${i} sources`); } async function assertSourceCount(dbg, count) { await waitForSourceCount(dbg, count); is(findAllElements(dbg, "sourceNodes").length, count, `${count} sources`); } async function waitForNodeToGainFocus(dbg, index) { await waitUntil(() => { const element = findElement(dbg, "sourceNode", index); if (element) { return element.classList.contains("focused"); } return false; }, `waiting for source node ${index} to be focused`); } async function assertNodeIsFocused(dbg, index) { await waitForNodeToGainFocus(dbg, index); const node = findElement(dbg, "sourceNode", index); ok(node.classList.contains("focused"), `node ${index} is focused`); } async function addExpression(dbg, input) { info("Adding an expression"); const plusIcon = findElementWithSelector(dbg, selectors.expressionPlus); if (plusIcon) { plusIcon.click(); } findElementWithSelector(dbg, selectors.expressionInput).focus(); type(dbg, input); pressKey(dbg, "Enter"); await waitForDispatch(dbg, "EVALUATE_EXPRESSION"); } async function editExpression(dbg, input) { info("Updating the expression"); dblClickElement(dbg, "expressionNode", 1); // Position cursor reliably at the end of the text. pressKey(dbg, "End"); type(dbg, input); const evaluated = waitForDispatch(dbg, "EVALUATE_EXPRESSIONS"); pressKey(dbg, "Enter"); await evaluated; } async function waitUntilPredicate(predicate) { let result; await waitUntil(() => { result = predicate(); return result; }); return result; } // Return a promise with a reference to jsterm, opening the split // console if necessary. This cleans up the split console pref so // it won't pollute other tests. async function getDebuggerSplitConsole(dbg) { let { toolbox, win } = dbg; if (!win) { win = toolbox.win; } if (!toolbox.splitConsole) { pressKey(dbg, "Escape"); } await toolbox.openSplitConsole(); return toolbox.getPanel("webconsole"); } // Return a promise that resolves with the result of a thread evaluating a // string in the topmost frame. async function evaluateInTopFrame(dbg, text) { const threadFront = dbg.toolbox.target.threadFront; const consoleFront = await dbg.toolbox.target.getFront("console"); const { frames } = await threadFront.getFrames(0, 1); ok(frames.length == 1, "Got one frame"); const options = { thread: threadFront.actor, frameActor: frames[0].actorID }; const response = await consoleFront.evaluateJSAsync(text, options); return response.result.type == "undefined" ? undefined : response.result; } // Return a promise that resolves when a thread evaluates a string in the // topmost frame, ensuring the result matches the expected value. async function checkEvaluateInTopFrame(dbg, text, expected) { const rval = await evaluateInTopFrame(dbg, text); ok(rval == expected, `Eval returned ${expected}`); } async function findConsoleMessage({ toolbox }, query) { const [message] = await findConsoleMessages(toolbox, query); const value = message.querySelector(".message-body").innerText; const link = message.querySelector(".frame-link-source-inner").innerText; return { value, link }; } async function findConsoleMessages(toolbox, query) { const webConsole = await toolbox.getPanel("webconsole"); const win = webConsole._frameWindow; return Array.prototype.filter.call( win.document.querySelectorAll(".message"), e => e.innerText.includes(query) ); } async function hasConsoleMessage({ toolbox }, msg) { return waitFor(async () => { const messages = await findConsoleMessages(toolbox, msg); return messages.length > 0; }); } function evaluateExpressionInConsole(hud, expression) { const onResult = new Promise(res => { const onNewMessage = messages => { for (let message of messages) { if (message.node.classList.contains("result")) { hud.ui.off("new-messages", onNewMessage); res(message.node); } } }; hud.ui.on("new-messages", onNewMessage); }); hud.ui.wrapper.dispatchEvaluateExpression(expression); return onResult; } function waitForInspectorPanelChange(dbg) { return dbg.toolbox.getPanelWhenReady("inspector"); } const { PromiseTestUtils } = ChromeUtils.import( "resource://testing-common/PromiseTestUtils.jsm" ); // Debugger operations that are canceled because they were rendered obsolete by // a navigation or pause/resume end up as uncaught rejections. These never // indicate errors and are whitelisted in all debugger tests. PromiseTestUtils.whitelistRejectionsGlobally(/Page has navigated/); PromiseTestUtils.whitelistRejectionsGlobally(/Current thread has changed/); PromiseTestUtils.whitelistRejectionsGlobally( /Current thread has paused or resumed/ ); PromiseTestUtils.whitelistRejectionsGlobally(/Connection closed/);