forked from mirrors/gecko-dev
MozReview-Commit-ID: H2XDuj14Ayc --HG-- extra : rebase_source : 80ffdd6a881daa6bee3b571e73d7ffd50bafdeb3
748 lines
19 KiB
JavaScript
748 lines
19 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
|
/* Any copyright is dedicated to the Public Domain.
|
|
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
/**
|
|
* The Mochitest API documentation
|
|
* @module mochitest
|
|
*/
|
|
|
|
/**
|
|
* The mochitest API to wait for certain events.
|
|
* @module mochitest/waits
|
|
* @parent mochitest
|
|
*/
|
|
|
|
/**
|
|
* The mochitest API predefined asserts.
|
|
* @module mochitest/asserts
|
|
* @parent mochitest
|
|
*/
|
|
|
|
/**
|
|
* The mochitest API for interacting with the debugger.
|
|
* @module mochitest/actions
|
|
* @parent mochitest
|
|
*/
|
|
|
|
/**
|
|
* Helper methods for the mochitest API.
|
|
* @module mochitest/helpers
|
|
* @parent mochitest
|
|
*/
|
|
|
|
// shared-head.js handles imports, constants, and utility functions
|
|
Services.scriptloader.loadSubScript(
|
|
"chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
|
|
this
|
|
);
|
|
var { Toolbox } = require("devtools/client/framework/toolbox");
|
|
const EXAMPLE_URL =
|
|
"http://example.com/browser/devtools/client/debugger/new/test/mochitest/examples/";
|
|
|
|
Services.prefs.setBoolPref("devtools.debugger.new-debugger-frontend", true);
|
|
|
|
registerCleanupFunction(() => {
|
|
Services.prefs.clearUserPref("devtools.debugger.new-debugger-frontend");
|
|
delete window.resumeTest;
|
|
});
|
|
|
|
// Wait until an action of `type` is dispatched. This is different
|
|
// then `_afterDispatchDone` 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, type) {
|
|
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 === type,
|
|
run: (dispatch, getState, action) => {
|
|
resolve(action);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Wait until an action of `type` is dispatched. If it's part of an
|
|
// async operation, wait until the `status` field is "done" or "error"
|
|
function _afterDispatchDone(store, type) {
|
|
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 => {
|
|
if (action.type === type) {
|
|
return action.status
|
|
? action.status === "done" || action.status === "error"
|
|
: true;
|
|
}
|
|
},
|
|
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, type, eventRepeat = 1) {
|
|
let count = 0;
|
|
|
|
return Task.spawn(function*() {
|
|
info("Waiting for " + type + " to dispatch " + eventRepeat + " time(s)");
|
|
while (count < eventRepeat) {
|
|
yield _afterDispatchDone(dbg.store, type);
|
|
count++;
|
|
info(type + " dispatched " + count + " time(s)");
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Waits for specific thread events.
|
|
*
|
|
* @memberof mochitest/waits
|
|
* @param {Object} dbg
|
|
* @param {String} eventName
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function waitForThreadEvents(dbg, eventName) {
|
|
info("Waiting for thread event '" + eventName + "' to fire.");
|
|
const thread = dbg.toolbox.threadClient;
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
thread.addListener(eventName, function onEvent(eventName, ...args) {
|
|
info("Thread event '" + eventName + "' fired.");
|
|
thread.removeListener(eventName, onEvent);
|
|
resolve.apply(resolve, args);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Waits for `predicate(state)` 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) {
|
|
return new Promise(resolve => {
|
|
const unsubscribe = dbg.store.subscribe(() => {
|
|
if (predicate(dbg.store.getState())) {
|
|
unsubscribe();
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Waits for sources to be loaded.
|
|
*
|
|
* @memberof mochitest/waits
|
|
* @param {Object} dbg
|
|
* @param {Array} sources
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function waitForSources(dbg, ...sources) {
|
|
if (sources.length === 0) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
info("Waiting on sources: " + sources.join(", "));
|
|
const { selectors: { getSources }, store } = dbg;
|
|
return Promise.all(
|
|
sources.map(url => {
|
|
function sourceExists(state) {
|
|
return getSources(state).some(s => {
|
|
let u = s.get("url");
|
|
return u && u.includes(url);
|
|
});
|
|
}
|
|
|
|
if (!sourceExists(store.getState())) {
|
|
return waitForState(dbg, sourceExists);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
function waitForElement(dbg, selector) {
|
|
return waitUntil(() => findElementWithSelector(dbg, selector));
|
|
}
|
|
|
|
/**
|
|
* 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, source, line) {
|
|
const { selectors: { getSelectedSource, getPause }, getState } = dbg;
|
|
source = findSource(dbg, source);
|
|
|
|
// Check the selected source
|
|
is(getSelectedSource(getState()).get("id"), source.id);
|
|
|
|
// Check the pause location
|
|
const location = getPause(getState()).getIn(["frame", "location"]);
|
|
is(location.get("sourceId"), source.id);
|
|
is(location.get("line"), line);
|
|
|
|
// Check the debug line
|
|
let cm = dbg.win.document.querySelector(".CodeMirror").CodeMirror;
|
|
ok(
|
|
cm.lineInfo(line - 1).wrapClass.includes("debug-line"),
|
|
"Line 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) {
|
|
const { selectors: { getSelectedSource, getPause }, getState } = dbg;
|
|
source = findSource(dbg, source);
|
|
|
|
// Check the selected source
|
|
is(getSelectedSource(getState()).get("url"), source.url);
|
|
|
|
// Check the highlight line
|
|
const lineEl = findElement(dbg, "highlightLine");
|
|
ok(lineEl, "Line is highlighted");
|
|
ok(
|
|
isVisibleWithin(findElement(dbg, "codeMirror"), lineEl),
|
|
"Highlighted line is visible"
|
|
);
|
|
ok(
|
|
dbg.win.cm.lineInfo(line - 1).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) {
|
|
const { selectors: { getPause }, getState } = dbg;
|
|
return !!getPause(getState());
|
|
}
|
|
|
|
/**
|
|
* Waits for the debugger to be fully paused.
|
|
*
|
|
* @memberof mochitest/waits
|
|
* @param {Object} dbg
|
|
* @static
|
|
*/
|
|
function waitForPaused(dbg) {
|
|
return Task.spawn(function*() {
|
|
// We want to make sure that we get both a real paused event and
|
|
// that the state is fully populated. The client may do some more
|
|
// work (call other client methods) before populating the state.
|
|
yield waitForThreadEvents(dbg, "paused"), yield waitForState(dbg, state => {
|
|
const pause = dbg.selectors.getPause(state);
|
|
// Make sure we have the paused state.
|
|
if (!pause) {
|
|
return false;
|
|
}
|
|
// Make sure the source text is completely loaded for the
|
|
// source we are paused in.
|
|
const sourceId = pause.getIn(["frame", "location", "sourceId"]);
|
|
const sourceText = dbg.selectors.getSourceText(dbg.getState(), sourceId);
|
|
return sourceText && !sourceText.get("loading");
|
|
});
|
|
});
|
|
}
|
|
|
|
function createDebuggerContext(toolbox) {
|
|
const panel = toolbox.getPanel("jsdebugger");
|
|
const win = panel.panelWin;
|
|
const { store, client, selectors, actions } = panel.getVarsForTests();
|
|
|
|
return {
|
|
actions: actions,
|
|
selectors: selectors,
|
|
getState: store.getState,
|
|
store: store,
|
|
client: client,
|
|
toolbox: toolbox,
|
|
win: win
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Intilializes the debugger.
|
|
*
|
|
* @memberof mochitest
|
|
* @param {String} url
|
|
* @param {Array} sources
|
|
* @return {Promise} dbg
|
|
* @static
|
|
*/
|
|
function initDebugger(url, ...sources) {
|
|
return Task.spawn(function*() {
|
|
Services.prefs.clearUserPref("devtools.debugger.pause-on-exceptions");
|
|
Services.prefs.clearUserPref("devtools.debugger.ignore-caught-exceptions");
|
|
Services.prefs.clearUserPref("devtools.debugger.tabs");
|
|
Services.prefs.clearUserPref("devtools.debugger.pending-selected-location");
|
|
Services.prefs.clearUserPref("devtools.debugger.pending-breakpoints");
|
|
Services.prefs.clearUserPref("devtools.debugger.expressions");
|
|
const toolbox = yield openNewTabAndToolbox(EXAMPLE_URL + url, "jsdebugger");
|
|
return createDebuggerContext(toolbox);
|
|
});
|
|
}
|
|
|
|
window.resumeTest = undefined;
|
|
/**
|
|
* 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) {
|
|
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.getSources(dbg.getState());
|
|
const source = sources.find(s => {
|
|
let u = s.get("url");
|
|
return u && u.includes(url);
|
|
});
|
|
|
|
if (!source) {
|
|
throw new Error("Unable to find source: " + url);
|
|
}
|
|
|
|
return source.toJS();
|
|
}
|
|
|
|
/**
|
|
* Selects the source.
|
|
*
|
|
* @memberof mochitest/actions
|
|
* @param {Object} dbg
|
|
* @param {String} url
|
|
* @param {Number} line
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function selectSource(dbg, url, line) {
|
|
info("Selecting source: " + url);
|
|
const source = findSource(dbg, url);
|
|
const hasText = !!dbg.selectors.getSourceText(dbg.getState(), source.id);
|
|
dbg.actions.selectSource(source.id, { line });
|
|
|
|
if (!hasText) {
|
|
return waitForDispatch(dbg, "LOAD_SOURCE_TEXT");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Steps over.
|
|
*
|
|
* @memberof mochitest/actions
|
|
* @param {Object} dbg
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function stepOver(dbg) {
|
|
info("Stepping over");
|
|
dbg.actions.stepOver();
|
|
return waitForPaused(dbg);
|
|
}
|
|
|
|
/**
|
|
* Steps in.
|
|
*
|
|
* @memberof mochitest/actions
|
|
* @param {Object} dbg
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function stepIn(dbg) {
|
|
info("Stepping in");
|
|
dbg.actions.stepIn();
|
|
return waitForPaused(dbg);
|
|
}
|
|
|
|
/**
|
|
* Steps out.
|
|
*
|
|
* @memberof mochitest/actions
|
|
* @param {Object} dbg
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function stepOut(dbg) {
|
|
info("Stepping out");
|
|
dbg.actions.stepOut();
|
|
return waitForPaused(dbg);
|
|
}
|
|
|
|
/**
|
|
* Resumes.
|
|
*
|
|
* @memberof mochitest/actions
|
|
* @param {Object} dbg
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function resume(dbg) {
|
|
info("Resuming");
|
|
dbg.actions.resume();
|
|
return waitForThreadEvents(dbg, "resumed");
|
|
}
|
|
|
|
function deleteExpression(dbg, input) {
|
|
info("Resuming");
|
|
return dbg.actions.deleteExpression({ input });
|
|
}
|
|
|
|
/**
|
|
* Reloads the debuggee.
|
|
*
|
|
* @memberof mochitest/actions
|
|
* @param {Object} dbg
|
|
* @param {Array} sources
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function reload(dbg, ...sources) {
|
|
return dbg.client.reload().then(() => waitForSources(...sources));
|
|
}
|
|
|
|
/**
|
|
* Navigates the debuggee to another url.
|
|
*
|
|
* @memberof mochitest/actions
|
|
* @param {Object} dbg
|
|
* @param {String} url
|
|
* @param {Array} sources
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function navigate(dbg, url, ...sources) {
|
|
dbg.client.navigate(url);
|
|
return waitForSources(dbg, ...sources);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function addBreakpoint(dbg, source, line, col) {
|
|
source = findSource(dbg, source);
|
|
const sourceId = source.id;
|
|
dbg.actions.addBreakpoint({ sourceId, line, col });
|
|
return waitForDispatch(dbg, "ADD_BREAKPOINT");
|
|
}
|
|
|
|
/**
|
|
* 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, col) {
|
|
return dbg.actions.removeBreakpoint({ sourceId, line, col });
|
|
}
|
|
|
|
/**
|
|
* Toggles the Pause on exceptions feature in the debugger.
|
|
*
|
|
* @memberof mochitest/actions
|
|
* @param {Object} dbg
|
|
* @param {Boolean} pauseOnExceptions
|
|
* @param {Boolean} ignoreCaughtExceptions
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function togglePauseOnExceptions(
|
|
dbg,
|
|
pauseOnExceptions,
|
|
ignoreCaughtExceptions
|
|
) {
|
|
const command = dbg.actions.pauseOnExceptions(
|
|
pauseOnExceptions,
|
|
ignoreCaughtExceptions
|
|
);
|
|
|
|
if (!isPaused(dbg)) {
|
|
return waitForThreadEvents(dbg, "resumed");
|
|
}
|
|
|
|
return command;
|
|
}
|
|
|
|
// Helpers
|
|
|
|
/**
|
|
* Invokes a global function in the debuggee tab.
|
|
*
|
|
* @memberof mochitest/helpers
|
|
* @param {String} fnc
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function invokeInTab(fnc) {
|
|
info(`Invoking function ${fnc} in tab`);
|
|
return ContentTask.spawn(gBrowser.selectedBrowser, fnc, function*(fnc) {
|
|
content.wrappedJSObject[fnc](); // eslint-disable-line mozilla/no-cpows-in-tests, max-len
|
|
});
|
|
}
|
|
|
|
const isLinux = Services.appinfo.OS === "Linux";
|
|
const isMac = Services.appinfo.OS === "Darwin";
|
|
const cmdOrCtrl = isLinux ? { ctrlKey: true } : { metaKey: 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 = {
|
|
sourceSearch: { code: "p", modifiers: cmdOrCtrl },
|
|
fileSearch: { code: "f", modifiers: cmdOrCtrl },
|
|
Enter: { code: "VK_RETURN" },
|
|
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" },
|
|
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) {
|
|
let 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);
|
|
});
|
|
}
|
|
|
|
function isVisibleWithin(outerEl, innerEl) {
|
|
const innerRect = innerEl.getBoundingClientRect();
|
|
const outerRect = outerEl.getBoundingClientRect();
|
|
return innerRect.top > outerRect.top && innerRect.bottom < outerRect.bottom;
|
|
}
|
|
|
|
const selectors = {
|
|
callStackHeader: ".call-stack-pane ._header",
|
|
callStackBody: ".call-stack-pane .pane",
|
|
expressionNode: i => `.expressions-list .tree-node:nth-child(${i}) .object-label`,
|
|
expressionValue: i => `.expressions-list .tree-node:nth-child(${i}) .object-value`,
|
|
expressionClose: i => `.expressions-list .expression-container:nth-child(${i}) .close`,
|
|
expressionNodes: ".expressions-list .tree-node",
|
|
scopesHeader: ".scopes-pane ._header",
|
|
breakpointItem: i => `.breakpoints-list .breakpoint:nth-child(${i})`,
|
|
scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`,
|
|
scopeValue: i => `.scopes-list .tree-node:nth-child(${i}) .object-value`,
|
|
frame: i => `.frames ul li:nth-child(${i})`,
|
|
frames: ".frames ul li",
|
|
gutter: i => `.CodeMirror-code *:nth-child(${i}) .CodeMirror-linenumber`,
|
|
menuitem: i => `menupopup menuitem:nth-child(${i})`,
|
|
pauseOnExceptions: ".pause-exceptions",
|
|
breakpoint: ".CodeMirror-code > .new-breakpoint",
|
|
highlightLine: ".CodeMirror-code > .highlight-line",
|
|
codeMirror: ".CodeMirror",
|
|
resume: ".resume.active",
|
|
stepOver: ".stepOver.active",
|
|
stepOut: ".stepOut.active",
|
|
stepIn: ".stepIn.active",
|
|
toggleBreakpoints: ".breakpoints-toggle",
|
|
prettyPrintButton: ".prettyPrint",
|
|
sourceFooter: ".source-footer",
|
|
sourceNode: i => `.sources-list .tree-node:nth-child(${i})`,
|
|
sourceNodes: ".sources-list .tree-node",
|
|
sourceArrow: i => `.sources-list .tree-node:nth-child(${i}) .arrow`
|
|
};
|
|
|
|
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 dbg.win.document.querySelectorAll(selector);
|
|
}
|
|
|
|
/**
|
|
* Simulates a mouse click in the debugger DOM.
|
|
*
|
|
* @memberof mochitest/helpers
|
|
* @param {Object} dbg
|
|
* @param {String} elementName
|
|
* @param {Array} args
|
|
* @return {Promise}
|
|
* @static
|
|
*/
|
|
function clickElement(dbg, elementName, ...args) {
|
|
const selector = getSelector(elementName, ...args);
|
|
const el = findElement(dbg, elementName, ...args);
|
|
el.scrollIntoView();
|
|
|
|
return EventUtils.synthesizeMouseAtCenter(
|
|
findElementWithSelector(dbg, selector),
|
|
{},
|
|
dbg.win
|
|
);
|
|
}
|
|
|
|
function dblClickElement(dbg, elementName, ...args) {
|
|
const selector = getSelector(elementName, ...args);
|
|
|
|
return EventUtils.synthesizeMouseAtCenter(
|
|
findElementWithSelector(dbg, selector),
|
|
{ clickCount: 2 },
|
|
dbg.win
|
|
);
|
|
}
|
|
|
|
function rightClickElement(dbg, elementName, ...args) {
|
|
const selector = getSelector(elementName, ...args);
|
|
const doc = dbg.win.document;
|
|
return EventUtils.synthesizeMouseAtCenter(
|
|
doc.querySelector(selector),
|
|
{ type: "contextmenu" },
|
|
dbg.win
|
|
);
|
|
}
|
|
|
|
function selectMenuItem(dbg, index) {
|
|
// the context menu is in the toolbox window
|
|
const doc = dbg.toolbox.win.document;
|
|
|
|
// 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(`menuitem:nth-child(${index})`);
|
|
return EventUtils.synthesizeMouseAtCenter(item, {}, dbg.toolbox.win);
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|