diff options
Diffstat (limited to 'devtools/client/debugger/new/test/mochitest/head.js')
-rw-r--r-- | devtools/client/debugger/new/test/mochitest/head.js | 684 |
1 files changed, 684 insertions, 0 deletions
diff --git a/devtools/client/debugger/new/test/mochitest/head.js b/devtools/client/debugger/new/test/mochitest/head.js new file mode 100644 index 000000000..b0964d890 --- /dev/null +++ b/devtools/client/debugger/new/test/mochitest/head.js @@ -0,0 +1,684 @@ +/* -*- 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 => { + return s.get("url").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 + ok(dbg.win.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 win = toolbox.getPanel("jsdebugger").panelWin; + const store = win.Debugger.store; + + return { + actions: win.Debugger.actions, + selectors: win.Debugger.selectors, + getState: store.getState, + store: store, + client: win.Debugger.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* () { + 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 => s.get("url").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"); +} + +/** + * 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 cmdOrCtrl = isLinux ? { ctrlKey: true } : { metaKey: true }; +const keyMappings = { + sourceSearch: { code: "p", modifiers: cmdOrCtrl}, + fileSearch: { code: "f", modifiers: cmdOrCtrl}, + "Enter": { code: "VK_RETURN" }, + "Up": { code: "VK_UP" }, + "Down": { code: "VK_DOWN" }, + 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", + scopesHeader: ".scopes-pane ._header", + breakpointItem: i => `.breakpoints-list .breakpoint:nth-child(${i})`, + scopeNode: i => `.scopes-list .tree-node:nth-child(${i}) .object-label`, + 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: ".toggleBreakpoints", + 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); + return EventUtils.synthesizeMouseAtCenter( + findElementWithSelector(dbg, selector), + {}, + 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(); +} |