summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/new/test/mochitest/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/new/test/mochitest/head.js')
-rw-r--r--devtools/client/debugger/new/test/mochitest/head.js684
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();
+}