summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions/pause
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/actions/pause')
-rw-r--r--devtools/client/debugger/src/actions/pause/breakOnNext.js18
-rw-r--r--devtools/client/debugger/src/actions/pause/commands.js157
-rw-r--r--devtools/client/debugger/src/actions/pause/continueToHere.js62
-rw-r--r--devtools/client/debugger/src/actions/pause/expandScopes.js17
-rw-r--r--devtools/client/debugger/src/actions/pause/fetchFrames.js23
-rw-r--r--devtools/client/debugger/src/actions/pause/fetchScopes.js30
-rw-r--r--devtools/client/debugger/src/actions/pause/highlightCalls.js89
-rw-r--r--devtools/client/debugger/src/actions/pause/index.js33
-rw-r--r--devtools/client/debugger/src/actions/pause/inlinePreview.js244
-rw-r--r--devtools/client/debugger/src/actions/pause/mapDisplayNames.js49
-rw-r--r--devtools/client/debugger/src/actions/pause/mapFrames.js157
-rw-r--r--devtools/client/debugger/src/actions/pause/mapScopes.js194
-rw-r--r--devtools/client/debugger/src/actions/pause/moz.build27
-rw-r--r--devtools/client/debugger/src/actions/pause/pauseOnExceptions.js34
-rw-r--r--devtools/client/debugger/src/actions/pause/paused.js73
-rw-r--r--devtools/client/debugger/src/actions/pause/resetBreakpointsPaneState.js18
-rw-r--r--devtools/client/debugger/src/actions/pause/resumed.js28
-rw-r--r--devtools/client/debugger/src/actions/pause/selectFrame.js39
-rw-r--r--devtools/client/debugger/src/actions/pause/skipPausing.js33
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap10
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/pause.spec.js413
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js24
-rw-r--r--devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js18
23 files changed, 1790 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/actions/pause/breakOnNext.js b/devtools/client/debugger/src/actions/pause/breakOnNext.js
new file mode 100644
index 0000000000..02df827cb1
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/breakOnNext.js
@@ -0,0 +1,18 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+/**
+ * Debugger breakOnNext command.
+ * It's different from the comand action because we also want to
+ * highlight the pause icon.
+ *
+ * @memberof actions/pause
+ * @static
+ */
+export function breakOnNext(cx) {
+ return async ({ dispatch, getState, client }) => {
+ await client.breakOnNext(cx.thread);
+ return dispatch({ type: "BREAK_ON_NEXT", thread: cx.thread });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/commands.js b/devtools/client/debugger/src/actions/pause/commands.js
new file mode 100644
index 0000000000..27478d6ad2
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/commands.js
@@ -0,0 +1,157 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ getSelectedFrame,
+ getThreadContext,
+ getCurrentThread,
+ getIsCurrentThreadPaused,
+} from "../../selectors";
+import { PROMISE } from "../utils/middleware/promise";
+import { evaluateExpressions } from "../expressions";
+import { selectLocation } from "../sources";
+import { fetchScopes } from "./fetchScopes";
+import { fetchFrames } from "./fetchFrames";
+import { recordEvent } from "../../utils/telemetry";
+import assert from "../../utils/assert";
+
+export function selectThread(cx, thread) {
+ return async ({ dispatch, getState, client }) => {
+ if (getCurrentThread(getState()) === thread) {
+ return;
+ }
+
+ dispatch({ cx, type: "SELECT_THREAD", thread });
+
+ // Get a new context now that the current thread has changed.
+ const threadcx = getThreadContext(getState());
+ // Note that this is a rethorical assertion as threadcx.thread is updated by SELECT_THREAD action
+ assert(threadcx.thread == thread, "Thread mismatch");
+
+ const serverRequests = [];
+ // Update the watched expressions as we may never have evaluated them against this thread
+ serverRequests.push(dispatch(evaluateExpressions(threadcx)));
+
+ // If we were paused on the newly selected thread, ensure:
+ // - select the source where we are paused,
+ // - fetching the paused stackframes,
+ // - fetching the paused scope, so that variable preview are working on the selected source.
+ // (frames and scopes is supposed to be fetched on pause,
+ // but if two threads pause concurrently, it might be cancelled)
+ const frame = getSelectedFrame(getState(), thread);
+ if (frame) {
+ serverRequests.push(dispatch(selectLocation(threadcx, frame.location)));
+ serverRequests.push(dispatch(fetchFrames(threadcx)));
+ serverRequests.push(dispatch(fetchScopes(threadcx)));
+ }
+
+ await Promise.all(serverRequests);
+ };
+}
+
+/**
+ * Debugger commands like stepOver, stepIn, stepUp
+ *
+ * @param string $0.type
+ * @memberof actions/pause
+ * @static
+ */
+export function command(type) {
+ return async ({ dispatch, getState, client }) => {
+ if (!type) {
+ return null;
+ }
+ // For now, all commands are by default against the currently selected thread
+ const thread = getCurrentThread(getState());
+
+ const frame = getSelectedFrame(getState(), thread);
+
+ return dispatch({
+ type: "COMMAND",
+ command: type,
+ thread,
+ [PROMISE]: client[type](thread, frame?.id),
+ });
+ };
+}
+
+/**
+ * StepIn
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+export function stepIn() {
+ return ({ dispatch, getState }) => {
+ if (!getIsCurrentThreadPaused(getState())) {
+ return null;
+ }
+ return dispatch(command("stepIn"));
+ };
+}
+
+/**
+ * stepOver
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+export function stepOver() {
+ return ({ dispatch, getState }) => {
+ if (!getIsCurrentThreadPaused(getState())) {
+ return null;
+ }
+ return dispatch(command("stepOver"));
+ };
+}
+
+/**
+ * stepOut
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+export function stepOut() {
+ return ({ dispatch, getState }) => {
+ if (!getIsCurrentThreadPaused(getState())) {
+ return null;
+ }
+ return dispatch(command("stepOut"));
+ };
+}
+
+/**
+ * resume
+ * @memberof actions/pause
+ * @static
+ * @returns {Function} {@link command}
+ */
+export function resume() {
+ return ({ dispatch, getState }) => {
+ if (!getIsCurrentThreadPaused(getState())) {
+ return null;
+ }
+ recordEvent("continue");
+ return dispatch(command("resume"));
+ };
+}
+
+/**
+ * restart frame
+ * @memberof actions/pause
+ * @static
+ */
+export function restart(cx, frame) {
+ return async ({ dispatch, getState, client }) => {
+ if (!getIsCurrentThreadPaused(getState())) {
+ return null;
+ }
+ return dispatch({
+ type: "COMMAND",
+ command: "restart",
+ thread: cx.thread,
+ [PROMISE]: client.restart(cx.thread, frame.id),
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/continueToHere.js b/devtools/client/debugger/src/actions/pause/continueToHere.js
new file mode 100644
index 0000000000..56aa117eab
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/continueToHere.js
@@ -0,0 +1,62 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ getSelectedSource,
+ getSelectedFrame,
+ getClosestBreakpointPosition,
+ getBreakpoint,
+} from "../../selectors";
+import { createLocation } from "../../utils/location";
+import { addHiddenBreakpoint } from "../breakpoints";
+import { setBreakpointPositions } from "../breakpoints/breakpointPositions";
+
+import { resume } from "./commands";
+
+export function continueToHere(cx, location) {
+ return async function ({ dispatch, getState }) {
+ const { line, column } = location;
+ const selectedSource = getSelectedSource(getState());
+ const selectedFrame = getSelectedFrame(getState(), cx.thread);
+
+ if (!selectedFrame || !selectedSource) {
+ return;
+ }
+
+ const debugLine = selectedFrame.location.line;
+ // If the user selects a line to continue to,
+ // it must be different than the currently paused line.
+ if (!column && debugLine == line) {
+ return;
+ }
+
+ await dispatch(setBreakpointPositions({ cx, location }));
+ const position = getClosestBreakpointPosition(getState(), location);
+
+ // If the user selects a location in the editor,
+ // there must be a place we can pause on that line.
+ if (column && !position) {
+ return;
+ }
+
+ const pauseLocation = column && position ? position.location : location;
+
+ // Set a hidden breakpoint if we do not already have a breakpoint
+ // at the closest position
+ if (!getBreakpoint(getState(), pauseLocation)) {
+ await dispatch(
+ addHiddenBreakpoint(
+ cx,
+ createLocation({
+ source: selectedSource,
+ line: pauseLocation.line,
+ column: pauseLocation.column,
+ })
+ )
+ );
+ }
+
+ dispatch(resume(cx));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/expandScopes.js b/devtools/client/debugger/src/actions/pause/expandScopes.js
new file mode 100644
index 0000000000..fa431ee0b9
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/expandScopes.js
@@ -0,0 +1,17 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import { getScopeItemPath } from "../../utils/pause/scopes/utils";
+
+export function setExpandedScope(cx, item, expanded) {
+ return function ({ dispatch, getState }) {
+ return dispatch({
+ type: "SET_EXPANDED_SCOPE",
+ cx,
+ thread: cx.thread,
+ path: getScopeItemPath(item),
+ expanded,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/fetchFrames.js b/devtools/client/debugger/src/actions/pause/fetchFrames.js
new file mode 100644
index 0000000000..42295ae026
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/fetchFrames.js
@@ -0,0 +1,23 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import { isValidThreadContext } from "../../utils/context";
+
+export function fetchFrames(cx) {
+ return async function ({ dispatch, client, getState }) {
+ const { thread } = cx;
+ let frames;
+ try {
+ frames = await client.getFrames(thread);
+ } catch (e) {
+ // getFrames will fail if the thread has resumed. In this case the thread
+ // should no longer be valid and the frames we would have fetched would be
+ // discarded anyways.
+ if (isValidThreadContext(getState(), cx)) {
+ throw e;
+ }
+ }
+ dispatch({ type: "FETCHED_FRAMES", thread, frames, cx });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/fetchScopes.js b/devtools/client/debugger/src/actions/pause/fetchScopes.js
new file mode 100644
index 0000000000..691b3ce006
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/fetchScopes.js
@@ -0,0 +1,30 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import { getSelectedFrame, getGeneratedFrameScope } from "../../selectors";
+import { mapScopes } from "./mapScopes";
+import { generateInlinePreview } from "./inlinePreview";
+import { PROMISE } from "../utils/middleware/promise";
+
+export function fetchScopes(cx) {
+ return async function ({ dispatch, getState, client }) {
+ const frame = getSelectedFrame(getState(), cx.thread);
+ if (!frame || getGeneratedFrameScope(getState(), frame.id)) {
+ return;
+ }
+
+ const scopes = dispatch({
+ type: "ADD_SCOPES",
+ cx,
+ thread: cx.thread,
+ frame,
+ [PROMISE]: client.getFrameScopes(frame),
+ });
+
+ scopes.then(() => {
+ dispatch(generateInlinePreview(cx, frame));
+ });
+ await dispatch(mapScopes(cx, scopes, frame));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/highlightCalls.js b/devtools/client/debugger/src/actions/pause/highlightCalls.js
new file mode 100644
index 0000000000..aec82fe35b
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/highlightCalls.js
@@ -0,0 +1,89 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ getSymbols,
+ getSelectedFrame,
+ getCurrentThread,
+} from "../../selectors";
+
+// a is an ast location with start and end positions (line and column).
+// b is a single position (line and column).
+// This function tests to see if the b position
+// falls within the range given in a.
+function inHouseContainsPosition(a, b) {
+ const bColumn = b.column || 0;
+ const startsBefore =
+ a.start.line < b.line ||
+ (a.start.line === b.line && a.start.column <= bColumn);
+ const endsAfter =
+ a.end.line > b.line || (a.end.line === b.line && a.end.column >= bColumn);
+
+ return startsBefore && endsAfter;
+}
+
+export function highlightCalls(cx) {
+ return async function ({ dispatch, getState, parserWorker }) {
+ if (!cx) {
+ return null;
+ }
+
+ const frame = await getSelectedFrame(
+ getState(),
+ getCurrentThread(getState())
+ );
+
+ if (!frame || !parserWorker.isLocationSupported(frame.location)) {
+ return null;
+ }
+
+ const { thread } = cx;
+
+ const originalAstScopes = await parserWorker.getScopes(frame.location);
+ if (!originalAstScopes) {
+ return null;
+ }
+
+ const symbols = getSymbols(getState(), frame.location);
+
+ if (!symbols) {
+ return null;
+ }
+
+ if (!symbols.callExpressions) {
+ return null;
+ }
+
+ const localAstScope = originalAstScopes[0];
+ const allFunctionCalls = symbols.callExpressions;
+
+ const highlightedCalls = allFunctionCalls.filter(function (call) {
+ const containsStart = inHouseContainsPosition(
+ localAstScope,
+ call.location.start
+ );
+ const containsEnd = inHouseContainsPosition(
+ localAstScope,
+ call.location.end
+ );
+ return containsStart && containsEnd;
+ });
+
+ return dispatch({
+ type: "HIGHLIGHT_CALLS",
+ thread,
+ highlightedCalls,
+ });
+ };
+}
+
+export function unhighlightCalls(cx) {
+ return async function ({ dispatch, getState }) {
+ const { thread } = cx;
+ return dispatch({
+ type: "UNHIGHLIGHT_CALLS",
+ thread,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/index.js b/devtools/client/debugger/src/actions/pause/index.js
new file mode 100644
index 0000000000..be31894019
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/index.js
@@ -0,0 +1,33 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+/**
+ * Redux actions for the pause state
+ * @module actions/pause
+ */
+
+export {
+ selectThread,
+ stepIn,
+ stepOver,
+ stepOut,
+ resume,
+ restart,
+} from "./commands";
+export { fetchFrames } from "./fetchFrames";
+export { fetchScopes } from "./fetchScopes";
+export { paused } from "./paused";
+export { resumed } from "./resumed";
+export { continueToHere } from "./continueToHere";
+export { breakOnNext } from "./breakOnNext";
+export { resetBreakpointsPaneState } from "./resetBreakpointsPaneState";
+export { mapFrames } from "./mapFrames";
+export { mapDisplayNames } from "./mapDisplayNames";
+export { pauseOnExceptions } from "./pauseOnExceptions";
+export { selectFrame } from "./selectFrame";
+export { toggleSkipPausing, setSkipPausing } from "./skipPausing";
+export { toggleMapScopes } from "./mapScopes";
+export { setExpandedScope } from "./expandScopes";
+export { generateInlinePreview } from "./inlinePreview";
+export { highlightCalls, unhighlightCalls } from "./highlightCalls";
diff --git a/devtools/client/debugger/src/actions/pause/inlinePreview.js b/devtools/client/debugger/src/actions/pause/inlinePreview.js
new file mode 100644
index 0000000000..e3a4e614c0
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/inlinePreview.js
@@ -0,0 +1,244 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ getOriginalFrameScope,
+ getGeneratedFrameScope,
+ getInlinePreviews,
+ getSelectedLocation,
+} from "../../selectors";
+import { features } from "../../utils/prefs";
+import { validateThreadContext } from "../../utils/context";
+
+// We need to display all variables in the current functional scope so
+// include all data for block scopes until the first functional scope
+function getLocalScopeLevels(originalAstScopes) {
+ let levels = 0;
+ while (
+ originalAstScopes[levels] &&
+ originalAstScopes[levels].type === "block"
+ ) {
+ levels++;
+ }
+ return levels;
+}
+
+export function generateInlinePreview(cx, frame) {
+ return async function ({ dispatch, getState, parserWorker, client }) {
+ if (!frame || !features.inlinePreview) {
+ return null;
+ }
+
+ const { thread } = cx;
+
+ // Avoid regenerating inline previews when we already have preview data
+ if (getInlinePreviews(getState(), thread, frame.id)) {
+ return null;
+ }
+
+ const originalFrameScopes = getOriginalFrameScope(
+ getState(),
+ thread,
+ frame.location.sourceId,
+ frame.id
+ );
+
+ const generatedFrameScopes = getGeneratedFrameScope(
+ getState(),
+ thread,
+ frame.id
+ );
+
+ let scopes = originalFrameScopes?.scope || generatedFrameScopes?.scope;
+
+ if (!scopes || !scopes.bindings) {
+ return null;
+ }
+
+ // It's important to use selectedLocation, because we don't know
+ // if we'll be viewing the original or generated frame location
+ const selectedLocation = getSelectedLocation(getState());
+ if (!selectedLocation) {
+ return null;
+ }
+
+ if (!parserWorker.isLocationSupported(selectedLocation)) {
+ return null;
+ }
+
+ const originalAstScopes = await parserWorker.getScopes(selectedLocation);
+ validateThreadContext(getState(), cx);
+ if (!originalAstScopes) {
+ return null;
+ }
+
+ const allPreviews = [];
+ const pausedOnLine = selectedLocation.line;
+ const levels = getLocalScopeLevels(originalAstScopes);
+
+ for (
+ let curLevel = 0;
+ curLevel <= levels && scopes && scopes.bindings;
+ curLevel++
+ ) {
+ const bindings = { ...scopes.bindings.variables };
+ scopes.bindings.arguments.forEach(argument => {
+ Object.keys(argument).forEach(key => {
+ bindings[key] = argument[key];
+ });
+ });
+
+ const previewBindings = Object.keys(bindings).map(async name => {
+ // We want to show values of properties of objects only and not
+ // function calls on other data types like someArr.forEach etc..
+ let properties = null;
+ const objectGrip = bindings[name].value;
+ if (objectGrip.actor && objectGrip.class === "Object") {
+ properties = await client.loadObjectProperties(
+ {
+ name,
+ path: name,
+ contents: { value: objectGrip },
+ },
+ cx.thread
+ );
+ }
+
+ const previewsFromBindings = getBindingValues(
+ originalAstScopes,
+ pausedOnLine,
+ name,
+ bindings[name].value,
+ curLevel,
+ properties
+ );
+
+ allPreviews.push(...previewsFromBindings);
+ });
+ await Promise.all(previewBindings);
+
+ scopes = scopes.parent;
+ }
+
+ // Sort previews by line and column so they're displayed in the right order in the editor
+ allPreviews.sort((previewA, previewB) => {
+ if (previewA.line < previewB.line) {
+ return -1;
+ }
+ if (previewA.line > previewB.line) {
+ return 1;
+ }
+ // If we have the same line number
+ return previewA.column < previewB.column ? -1 : 1;
+ });
+
+ const previews = {};
+ for (const preview of allPreviews) {
+ const { line } = preview;
+ if (!previews[line]) {
+ previews[line] = [];
+ }
+ previews[line].push(preview);
+ }
+
+ return dispatch({
+ type: "ADD_INLINE_PREVIEW",
+ thread,
+ frame,
+ previews,
+ });
+ };
+}
+
+function getBindingValues(
+ originalAstScopes,
+ pausedOnLine,
+ name,
+ value,
+ curLevel,
+ properties
+) {
+ const previews = [];
+
+ const binding = originalAstScopes[curLevel]?.bindings[name];
+ if (!binding) {
+ return previews;
+ }
+
+ // Show a variable only once ( an object and it's child property are
+ // counted as different )
+ const identifiers = new Set();
+
+ // We start from end as we want to show values besides variable
+ // located nearest to the breakpoint
+ for (let i = binding.refs.length - 1; i >= 0; i--) {
+ const ref = binding.refs[i];
+ // Subtracting 1 from line as codemirror lines are 0 indexed
+ const line = ref.start.line - 1;
+ const column = ref.start.column;
+ // We don't want to render inline preview below the paused line
+ if (line >= pausedOnLine - 1) {
+ continue;
+ }
+
+ const { displayName, displayValue } = getExpressionNameAndValue(
+ name,
+ value,
+ ref,
+ properties
+ );
+
+ // Variable with same name exists, display value of current or
+ // closest to the current scope's variable
+ if (identifiers.has(displayName)) {
+ continue;
+ }
+ identifiers.add(displayName);
+
+ previews.push({
+ line,
+ column,
+ name: displayName,
+ value: displayValue,
+ });
+ }
+ return previews;
+}
+
+function getExpressionNameAndValue(
+ name,
+ value,
+ // TODO: Add data type to ref
+ ref,
+ properties
+) {
+ let displayName = name;
+ let displayValue = value;
+
+ // Only variables of type Object will have properties
+ if (properties) {
+ let { meta } = ref;
+ // Presence of meta property means expression contains child property
+ // reference eg: objName.propName
+ while (meta) {
+ // Initially properties will be an array, after that it will be an object
+ if (displayValue === value) {
+ const property = properties.find(prop => prop.name === meta.property);
+ displayValue = property?.contents.value;
+ displayName += `.${meta.property}`;
+ } else if (displayValue?.preview?.ownProperties) {
+ const { ownProperties } = displayValue.preview;
+ Object.keys(ownProperties).forEach(prop => {
+ if (prop === meta.property) {
+ displayValue = ownProperties[prop].value;
+ displayName += `.${meta.property}`;
+ }
+ });
+ }
+ meta = meta.parent;
+ }
+ }
+
+ return { displayName, displayValue };
+}
diff --git a/devtools/client/debugger/src/actions/pause/mapDisplayNames.js b/devtools/client/debugger/src/actions/pause/mapDisplayNames.js
new file mode 100644
index 0000000000..a7abbc36bd
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/mapDisplayNames.js
@@ -0,0 +1,49 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import { getFrames, getSymbols } from "../../selectors";
+
+import { findClosestFunction } from "../../utils/ast";
+
+function mapDisplayName(frame, { getState }) {
+ if (frame.isOriginal) {
+ return frame;
+ }
+
+ const symbols = getSymbols(getState(), frame.location);
+
+ if (!symbols || !symbols.functions) {
+ return frame;
+ }
+
+ const originalFunction = findClosestFunction(symbols, frame.location);
+
+ if (!originalFunction) {
+ return frame;
+ }
+
+ const originalDisplayName = originalFunction.name;
+ return { ...frame, originalDisplayName };
+}
+
+export function mapDisplayNames(cx) {
+ return function ({ dispatch, getState }) {
+ const frames = getFrames(getState(), cx.thread);
+
+ if (!frames) {
+ return;
+ }
+
+ const mappedFrames = frames.map(frame =>
+ mapDisplayName(frame, { getState })
+ );
+
+ dispatch({
+ type: "MAP_FRAME_DISPLAY_NAMES",
+ cx,
+ thread: cx.thread,
+ frames: mappedFrames,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/mapFrames.js b/devtools/client/debugger/src/actions/pause/mapFrames.js
new file mode 100644
index 0000000000..d677677505
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/mapFrames.js
@@ -0,0 +1,157 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ getFrames,
+ getBlackBoxRanges,
+ getSelectedFrame,
+} from "../../selectors";
+
+import { isFrameBlackBoxed } from "../../utils/source";
+
+import assert from "../../utils/assert";
+import { getOriginalLocation } from "../../utils/source-maps";
+import {
+ debuggerToSourceMapLocation,
+ sourceMapToDebuggerLocation,
+} from "../../utils/location";
+import { isGeneratedId } from "devtools/client/shared/source-map-loader/index";
+
+function getSelectedFrameId(state, thread, frames) {
+ let selectedFrame = getSelectedFrame(state, thread);
+ const blackboxedRanges = getBlackBoxRanges(state);
+
+ if (selectedFrame && !isFrameBlackBoxed(selectedFrame, blackboxedRanges)) {
+ return selectedFrame.id;
+ }
+
+ selectedFrame = frames.find(frame => {
+ return !isFrameBlackBoxed(frame, blackboxedRanges);
+ });
+ return selectedFrame?.id;
+}
+
+async function updateFrameLocation(frame, thunkArgs) {
+ if (frame.isOriginal) {
+ return Promise.resolve(frame);
+ }
+ const location = await getOriginalLocation(frame.location, thunkArgs, true);
+ return {
+ ...frame,
+ location,
+ generatedLocation: frame.generatedLocation || frame.location,
+ };
+}
+
+function updateFrameLocations(frames, thunkArgs) {
+ if (!frames || !frames.length) {
+ return Promise.resolve(frames);
+ }
+
+ return Promise.all(
+ frames.map(frame => updateFrameLocation(frame, thunkArgs))
+ );
+}
+
+function isWasmOriginalSourceFrame(frame, getState) {
+ if (isGeneratedId(frame.location.sourceId)) {
+ return false;
+ }
+
+ return Boolean(frame.generatedLocation?.source.isWasm);
+}
+
+async function expandFrames(frames, { getState, sourceMapLoader }) {
+ const result = [];
+ for (let i = 0; i < frames.length; ++i) {
+ const frame = frames[i];
+ if (frame.isOriginal || !isWasmOriginalSourceFrame(frame, getState)) {
+ result.push(frame);
+ continue;
+ }
+ const originalFrames = await sourceMapLoader.getOriginalStackFrames(
+ debuggerToSourceMapLocation(frame.generatedLocation)
+ );
+ if (!originalFrames) {
+ result.push(frame);
+ continue;
+ }
+
+ assert(!!originalFrames.length, "Expected at least one original frame");
+ // First entry has not specific location -- use one from original frame.
+ originalFrames[0] = {
+ ...originalFrames[0],
+ location: frame.location,
+ };
+
+ originalFrames.forEach((originalFrame, j) => {
+ if (!originalFrame.location) {
+ return;
+ }
+
+ // Keep outer most frame with true actor ID, and generate uniquie
+ // one for the nested frames.
+ const id = j == 0 ? frame.id : `${frame.id}-originalFrame${j}`;
+ result.push({
+ id,
+ displayName: originalFrame.displayName,
+ location: sourceMapToDebuggerLocation(
+ getState(),
+ originalFrame.location
+ ),
+ index: frame.index,
+ source: null,
+ thread: frame.thread,
+ scope: frame.scope,
+ this: frame.this,
+ isOriginal: true,
+ // More fields that will be added by the mapDisplayNames and
+ // updateFrameLocation.
+ generatedLocation: frame.generatedLocation,
+ originalDisplayName: originalFrame.displayName,
+ originalVariables: originalFrame.variables,
+ asyncCause: frame.asyncCause,
+ state: frame.state,
+ });
+ });
+ }
+ return result;
+}
+
+/**
+ * Map call stack frame locations and display names to originals.
+ * e.g.
+ * 1. When the debuggee pauses
+ * 2. When a source is pretty printed
+ * 3. When symbols are loaded
+ * @memberof actions/pause
+ * @static
+ */
+export function mapFrames(cx) {
+ return async function (thunkArgs) {
+ const { dispatch, getState } = thunkArgs;
+ const frames = getFrames(getState(), cx.thread);
+ if (!frames) {
+ return;
+ }
+
+ let mappedFrames = await updateFrameLocations(frames, thunkArgs);
+
+ mappedFrames = await expandFrames(mappedFrames, thunkArgs);
+
+ const selectedFrameId = getSelectedFrameId(
+ getState(),
+ cx.thread,
+ mappedFrames
+ );
+
+ dispatch({
+ type: "MAP_FRAMES",
+ cx,
+ thread: cx.thread,
+ frames: mappedFrames,
+ selectedFrameId,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/mapScopes.js b/devtools/client/debugger/src/actions/pause/mapScopes.js
new file mode 100644
index 0000000000..2a352dc578
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/mapScopes.js
@@ -0,0 +1,194 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ getSelectedFrameId,
+ getSettledSourceTextContent,
+ isMapScopesEnabled,
+ getSelectedFrame,
+ getSelectedGeneratedScope,
+ getSelectedOriginalScope,
+ getThreadContext,
+ getFirstSourceActorForGeneratedSource,
+} from "../../selectors";
+import {
+ loadOriginalSourceText,
+ loadGeneratedSourceText,
+} from "../sources/loadSourceText";
+import { PROMISE } from "../utils/middleware/promise";
+import assert from "../../utils/assert";
+
+import { log } from "../../utils/log";
+import { isGenerated } from "../../utils/source";
+
+import { buildMappedScopes } from "../../utils/pause/mapScopes";
+import { isFulfilled } from "../../utils/async-value";
+
+import { getMappedLocation } from "../../utils/source-maps";
+
+const expressionRegex = /\bfp\(\)/g;
+
+export async function buildOriginalScopes(
+ frame,
+ client,
+ cx,
+ frameId,
+ generatedScopes
+) {
+ if (!frame.originalVariables) {
+ throw new TypeError("(frame.originalVariables: XScopeVariables)");
+ }
+ const originalVariables = frame.originalVariables;
+ const frameBase = originalVariables.frameBase || "";
+
+ const inputs = [];
+ for (let i = 0; i < originalVariables.vars.length; i++) {
+ const { expr } = originalVariables.vars[i];
+ const expression = expr
+ ? expr.replace(expressionRegex, frameBase)
+ : "void 0";
+
+ inputs[i] = expression;
+ }
+
+ const results = await client.evaluateExpressions(inputs, {
+ frameId,
+ });
+
+ const variables = {};
+ for (let i = 0; i < originalVariables.vars.length; i++) {
+ const { name } = originalVariables.vars[i];
+ variables[name] = { value: results[i].result };
+ }
+
+ const bindings = {
+ arguments: [],
+ variables,
+ };
+
+ const { actor } = await generatedScopes;
+ const scope = {
+ type: "function",
+ scopeKind: "",
+ actor,
+ bindings,
+ parent: null,
+ function: null,
+ block: null,
+ };
+ return {
+ mappings: {},
+ scope,
+ };
+}
+
+export function toggleMapScopes() {
+ return async function ({ dispatch, getState }) {
+ if (isMapScopesEnabled(getState())) {
+ dispatch({ type: "TOGGLE_MAP_SCOPES", mapScopes: false });
+ return;
+ }
+
+ dispatch({ type: "TOGGLE_MAP_SCOPES", mapScopes: true });
+
+ const cx = getThreadContext(getState());
+
+ if (getSelectedOriginalScope(getState(), cx.thread)) {
+ return;
+ }
+
+ const scopes = getSelectedGeneratedScope(getState(), cx.thread);
+ const frame = getSelectedFrame(getState(), cx.thread);
+ if (!scopes || !frame) {
+ return;
+ }
+
+ dispatch(mapScopes(cx, Promise.resolve(scopes.scope), frame));
+ };
+}
+
+export function mapScopes(cx, scopes, frame) {
+ return async function (thunkArgs) {
+ const { dispatch, client, getState } = thunkArgs;
+ assert(cx.thread == frame.thread, "Thread mismatch");
+
+ await dispatch({
+ type: "MAP_SCOPES",
+ cx,
+ thread: cx.thread,
+ frame,
+ [PROMISE]: (async function () {
+ if (frame.isOriginal && frame.originalVariables) {
+ const frameId = getSelectedFrameId(getState(), cx.thread);
+ return buildOriginalScopes(frame, client, cx, frameId, scopes);
+ }
+
+ return dispatch(getMappedScopes(cx, scopes, frame));
+ })(),
+ });
+ };
+}
+
+export function getMappedScopes(cx, scopes, frame) {
+ return async function (thunkArgs) {
+ const { getState, dispatch } = thunkArgs;
+ const generatedSource = frame.generatedLocation.source;
+
+ const source = frame.location.source;
+
+ if (
+ !isMapScopesEnabled(getState()) ||
+ !source ||
+ !generatedSource ||
+ generatedSource.isWasm ||
+ source.isPrettyPrinted ||
+ isGenerated(source)
+ ) {
+ return null;
+ }
+
+ // Load source text for the original source
+ await dispatch(loadOriginalSourceText({ cx, source }));
+
+ const generatedSourceActor = getFirstSourceActorForGeneratedSource(
+ getState(),
+ generatedSource.id
+ );
+
+ // Also load source text for its corresponding generated source
+ await dispatch(
+ loadGeneratedSourceText({
+ cx,
+ sourceActor: generatedSourceActor,
+ })
+ );
+
+ try {
+ // load original source text content
+ const content = getSettledSourceTextContent(getState(), frame.location);
+
+ return await buildMappedScopes(
+ source,
+ content && isFulfilled(content)
+ ? content.value
+ : { type: "text", value: "", contentType: undefined },
+ frame,
+ await scopes,
+ thunkArgs
+ );
+ } catch (e) {
+ log(e);
+ return null;
+ }
+ };
+}
+
+export function getMappedScopesForLocation(location) {
+ return async function (thunkArgs) {
+ const { dispatch, getState } = thunkArgs;
+ const cx = getThreadContext(getState());
+ const mappedLocation = await getMappedLocation(location, thunkArgs);
+ return dispatch(getMappedScopes(cx, null, mappedLocation));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/moz.build b/devtools/client/debugger/src/actions/pause/moz.build
new file mode 100644
index 0000000000..54cf792166
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/moz.build
@@ -0,0 +1,27 @@
+# vim: set filetype=python:
+# 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 http://mozilla.org/MPL/2.0/.
+
+DIRS += []
+
+CompiledModules(
+ "breakOnNext.js",
+ "commands.js",
+ "continueToHere.js",
+ "expandScopes.js",
+ "fetchFrames.js",
+ "fetchScopes.js",
+ "highlightCalls.js",
+ "index.js",
+ "inlinePreview.js",
+ "mapDisplayNames.js",
+ "mapFrames.js",
+ "mapScopes.js",
+ "paused.js",
+ "pauseOnExceptions.js",
+ "resetBreakpointsPaneState.js",
+ "resumed.js",
+ "selectFrame.js",
+ "skipPausing.js",
+)
diff --git a/devtools/client/debugger/src/actions/pause/pauseOnExceptions.js b/devtools/client/debugger/src/actions/pause/pauseOnExceptions.js
new file mode 100644
index 0000000000..e7c04ded61
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/pauseOnExceptions.js
@@ -0,0 +1,34 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import { PROMISE } from "../utils/middleware/promise";
+import { recordEvent } from "../../utils/telemetry";
+
+/**
+ *
+ * @memberof actions/pause
+ * @static
+ */
+export function pauseOnExceptions(
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions
+) {
+ return ({ dispatch, getState, client }) => {
+ recordEvent("pause_on_exceptions", {
+ exceptions: shouldPauseOnExceptions,
+ // There's no "n" in the key below (#1463117)
+ ["caught_exceptio"]: shouldPauseOnCaughtExceptions,
+ });
+
+ return dispatch({
+ type: "PAUSE_ON_EXCEPTIONS",
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions,
+ [PROMISE]: client.pauseOnExceptions(
+ shouldPauseOnExceptions,
+ shouldPauseOnCaughtExceptions
+ ),
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/paused.js b/devtools/client/debugger/src/actions/pause/paused.js
new file mode 100644
index 0000000000..0e797035a5
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/paused.js
@@ -0,0 +1,73 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ getHiddenBreakpoint,
+ isEvaluatingExpression,
+ getSelectedFrame,
+ getThreadContext,
+} from "../../selectors";
+
+import { mapFrames, fetchFrames } from ".";
+import { removeBreakpoint } from "../breakpoints";
+import { evaluateExpressions } from "../expressions";
+import { selectLocation } from "../sources";
+import assert from "../../utils/assert";
+
+import { fetchScopes } from "./fetchScopes";
+
+/**
+ * Debugger has just paused
+ *
+ * @param {object} pauseInfo
+ * @memberof actions/pause
+ * @static
+ */
+export function paused(pauseInfo) {
+ return async function ({ dispatch, getState }) {
+ const { thread, frame, why } = pauseInfo;
+
+ dispatch({ type: "PAUSED", thread, why, frame });
+
+ // Get a context capturing the newly paused and selected thread.
+ const cx = getThreadContext(getState());
+ // Note that this is a rethorical assertion as threadcx.thread is updated by PAUSED action
+ assert(cx.thread == thread, "Thread mismatch");
+
+ // When we use "continue to here" feature we register an "hidden" breakpoint
+ // that should be removed on the next paused, even if we didn't hit it and
+ // paused for any other reason.
+ const hiddenBreakpoint = getHiddenBreakpoint(getState());
+ if (hiddenBreakpoint) {
+ dispatch(removeBreakpoint(cx, hiddenBreakpoint));
+ }
+
+ // The THREAD_STATE's "paused" resource only passes the top level stack frame,
+ // we dispatch the PAUSED action with it so that we can right away
+ // display it and update the UI to be paused.
+ // But we then fetch all the other frames:
+ await dispatch(fetchFrames(cx));
+ // And map them to original source locations.
+ // Note that this will wait for all related original sources to be loaded in the reducers.
+ // So this step may pause for a little while.
+ await dispatch(mapFrames(cx));
+
+ // If we paused on a particular frame, automatically select the related source
+ // and highlight the paused line
+ const selectedFrame = getSelectedFrame(getState(), thread);
+ if (selectedFrame) {
+ await dispatch(selectLocation(cx, selectedFrame.location));
+ }
+
+ // Fetch the previews for variables visible in the currently selected paused stackframe
+ await dispatch(fetchScopes(cx));
+
+ // Run after fetching scoping data so that it may make use of the sourcemap
+ // expression mappings for local variables.
+ const atException = why.type == "exception";
+ if (!atException || !isEvaluatingExpression(getState(), thread)) {
+ await dispatch(evaluateExpressions(cx));
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/resetBreakpointsPaneState.js b/devtools/client/debugger/src/actions/pause/resetBreakpointsPaneState.js
new file mode 100644
index 0000000000..a602c58896
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/resetBreakpointsPaneState.js
@@ -0,0 +1,18 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+/**
+ * Action for the breakpoints panel while paused.
+ *
+ * @memberof actions/pause
+ * @static
+ */
+export function resetBreakpointsPaneState(thread) {
+ return async ({ dispatch }) => {
+ dispatch({
+ type: "RESET_BREAKPOINTS_PANE_STATE",
+ thread,
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/resumed.js b/devtools/client/debugger/src/actions/pause/resumed.js
new file mode 100644
index 0000000000..323e9f0ff8
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/resumed.js
@@ -0,0 +1,28 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import { isStepping, getPauseReason, getThreadContext } from "../../selectors";
+import { evaluateExpressions } from "../expressions";
+import { inDebuggerEval } from "../../utils/pause";
+
+/**
+ * Debugger has just resumed
+ *
+ * @memberof actions/pause
+ * @static
+ */
+export function resumed(thread) {
+ return async ({ dispatch, client, getState }) => {
+ const why = getPauseReason(getState(), thread);
+ const wasPausedInEval = inDebuggerEval(why);
+ const wasStepping = isStepping(getState(), thread);
+
+ dispatch({ type: "RESUME", thread, wasStepping });
+
+ const cx = getThreadContext(getState());
+ if (!wasStepping && !wasPausedInEval && cx.thread == thread) {
+ await dispatch(evaluateExpressions(cx));
+ }
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/selectFrame.js b/devtools/client/debugger/src/actions/pause/selectFrame.js
new file mode 100644
index 0000000000..f97be42787
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/selectFrame.js
@@ -0,0 +1,39 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import { selectLocation } from "../sources";
+import { evaluateExpressions } from "../expressions";
+import { fetchScopes } from "./fetchScopes";
+import assert from "../../utils/assert";
+
+/**
+ * @memberof actions/pause
+ * @static
+ */
+export function selectFrame(cx, frame) {
+ return async ({ dispatch, getState }) => {
+ assert(cx.thread == frame.thread, "Thread mismatch");
+
+ // Frames that aren't on-stack do not support evalling and may not
+ // have live inspectable scopes, so we do not allow selecting them.
+ if (frame.state !== "on-stack") {
+ dispatch(selectLocation(cx, frame.location));
+ return;
+ }
+
+ dispatch({
+ type: "SELECT_FRAME",
+ cx,
+ thread: cx.thread,
+ frame,
+ });
+
+ // It's important that we wait for selectLocation to finish because
+ // we rely on the source being loaded and symbols fetched below.
+ await dispatch(selectLocation(cx, frame.location));
+
+ dispatch(evaluateExpressions(cx));
+ dispatch(fetchScopes(cx));
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/skipPausing.js b/devtools/client/debugger/src/actions/pause/skipPausing.js
new file mode 100644
index 0000000000..1ecdf33b76
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/skipPausing.js
@@ -0,0 +1,33 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import { getSkipPausing } from "../../selectors";
+
+/**
+ * @memberof actions/pause
+ * @static
+ */
+export function toggleSkipPausing() {
+ return async ({ dispatch, client, getState }) => {
+ const skipPausing = !getSkipPausing(getState());
+ await client.setSkipPausing(skipPausing);
+ dispatch({ type: "TOGGLE_SKIP_PAUSING", skipPausing });
+ };
+}
+
+/**
+ * @memberof actions/pause
+ * @static
+ */
+export function setSkipPausing(skipPausing) {
+ return async ({ dispatch, client, getState }) => {
+ const currentlySkipping = getSkipPausing(getState());
+ if (currentlySkipping === skipPausing) {
+ return;
+ }
+
+ await client.setSkipPausing(skipPausing);
+ dispatch({ type: "TOGGLE_SKIP_PAUSING", skipPausing });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap b/devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap
new file mode 100644
index 0000000000..55b8d3e724
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/__snapshots__/pauseOnExceptions.spec.js.snap
@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`pauseOnExceptions should track telemetry for pauseOnException changes 1`] = `
+Array [
+ Object {
+ "caught_exceptio": false,
+ "exceptions": true,
+ },
+]
+`;
diff --git a/devtools/client/debugger/src/actions/pause/tests/pause.spec.js b/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
new file mode 100644
index 0000000000..3a562ccfdd
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/pause.spec.js
@@ -0,0 +1,413 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ actions,
+ selectors,
+ createStore,
+ createSourceObject,
+ waitForState,
+ makeSource,
+ makeOriginalSource,
+ makeFrame,
+} from "../../../utils/test-head";
+
+import { makeWhyNormal } from "../../../utils/test-mockup";
+import { createLocation } from "../../../utils/location";
+
+const { isStepping } = selectors;
+
+let stepInResolve = null;
+const mockCommandClient = {
+ stepIn: () =>
+ new Promise(_resolve => {
+ stepInResolve = _resolve;
+ }),
+ stepOver: () => new Promise(_resolve => _resolve),
+ evaluate: async () => {},
+ evaluateExpressions: async () => [],
+ resume: async () => {},
+ getFrameScopes: async frame => frame.scope,
+ getFrames: async () => [],
+ setBreakpoint: () => new Promise(_resolve => {}),
+ sourceContents: ({ source }) => {
+ return new Promise((resolve, reject) => {
+ switch (source) {
+ case "foo1":
+ return resolve({
+ source: "function foo1() {\n return 5;\n}",
+ contentType: "text/javascript",
+ });
+ case "await":
+ return resolve({
+ source: "async function aWait() {\n await foo(); return 5;\n}",
+ contentType: "text/javascript",
+ });
+
+ case "foo":
+ return resolve({
+ source: "function foo() {\n return -5;\n}",
+ contentType: "text/javascript",
+ });
+ case "foo-original":
+ return resolve({
+ source: "\n\nfunction fooOriginal() {\n return -5;\n}",
+ contentType: "text/javascript",
+ });
+ case "foo-wasm":
+ return resolve({
+ source: { binary: new ArrayBuffer(0) },
+ contentType: "application/wasm",
+ });
+ case "foo-wasm/originalSource":
+ return resolve({
+ source: "fn fooBar() {}\nfn barZoo() { fooBar() }",
+ contentType: "text/rust",
+ });
+ }
+
+ return resolve();
+ });
+ },
+ getSourceActorBreakpointPositions: async () => ({}),
+ getSourceActorBreakableLines: async () => [],
+ actorID: "threadActorID",
+};
+
+const mockFrameId = "1";
+
+function createPauseInfo(
+ frameLocation = createLocation({
+ source: createSourceObject("foo1"),
+ line: 2,
+ }),
+ frameOpts = {}
+) {
+ const frames = [
+ makeFrame(
+ { id: mockFrameId, sourceId: frameLocation.sourceId },
+ {
+ location: frameLocation,
+ generatedLocation: frameLocation,
+ ...frameOpts,
+ }
+ ),
+ ];
+ return {
+ thread: "FakeThread",
+ frame: frames[0],
+ frames,
+ loadedObjects: [],
+ why: makeWhyNormal(),
+ };
+}
+
+describe("pause", () => {
+ describe("stepping", () => {
+ it("should set and clear the command", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ const mockPauseInfo = createPauseInfo();
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.paused(mockPauseInfo));
+ const cx = selectors.getThreadContext(getState());
+ const stepped = dispatch(actions.stepIn(cx));
+ expect(isStepping(getState(), "FakeThread")).toBeTruthy();
+ if (!stepInResolve) {
+ throw new Error("no stepInResolve");
+ }
+ await stepInResolve();
+ await stepped;
+ expect(isStepping(getState(), "FakeThread")).toBeFalsy();
+ });
+
+ it("should only step when paused", async () => {
+ const client = { stepIn: jest.fn() };
+ const { dispatch, cx } = createStore(client);
+
+ dispatch(actions.stepIn(cx));
+ expect(client.stepIn.mock.calls).toHaveLength(0);
+ });
+
+ it("should step when paused", async () => {
+ const { dispatch, getState } = createStore(mockCommandClient);
+ const mockPauseInfo = createPauseInfo();
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.paused(mockPauseInfo));
+ const cx = selectors.getThreadContext(getState());
+ dispatch(actions.stepIn(cx));
+ expect(isStepping(getState(), "FakeThread")).toBeTruthy();
+ });
+
+ it("getting frame scopes with bindings", async () => {
+ const client = { ...mockCommandClient };
+ const store = createStore(client, {});
+ const { dispatch, getState } = store;
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ const generatedLocation = createLocation({
+ source,
+ line: 1,
+ column: 0,
+ sourceActor: selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ ),
+ });
+ const mockPauseInfo = createPauseInfo(generatedLocation, {
+ scope: {
+ bindings: {
+ variables: { b: { value: {} } },
+ arguments: [{ a: { value: {} } }],
+ },
+ },
+ });
+
+ const { frames } = mockPauseInfo;
+ client.getFrames = async () => frames;
+ await dispatch(actions.newOriginalSources([makeOriginalSource(source)]));
+
+ await dispatch(actions.paused(mockPauseInfo));
+ expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
+ {
+ id: mockFrameId,
+ generatedLocation,
+ location: generatedLocation,
+ originalDisplayName: "foo",
+ scope: {
+ bindings: {
+ arguments: [{ a: { value: {} } }],
+ variables: { b: { value: {} } },
+ },
+ },
+ thread: "FakeThread",
+ },
+ ]);
+
+ expect(selectors.getFrameScopes(getState(), "FakeThread")).toEqual({
+ generated: {
+ 1: {
+ pending: false,
+ scope: {
+ bindings: {
+ arguments: [{ a: { value: {} } }],
+ variables: { b: { value: {} } },
+ },
+ },
+ },
+ },
+ mappings: { 1: undefined },
+ original: { 1: { pending: false, scope: undefined } },
+ });
+
+ expect(
+ selectors.getSelectedFrameBindings(getState(), "FakeThread")
+ ).toEqual(["b", "a"]);
+ });
+
+ it("maps frame locations and names to original source", async () => {
+ const sourceMapLoaderMock = {
+ getOriginalLocation: () => Promise.resolve(originalLocation),
+ getOriginalLocations: async items => items,
+ getOriginalSourceText: async () => ({
+ text: "\n\nfunction fooOriginal() {\n return -5;\n}",
+ contentType: "text/javascript",
+ }),
+ getGeneratedLocation: async location => location,
+ };
+
+ const client = { ...mockCommandClient };
+ const store = createStore(client, {}, sourceMapLoaderMock);
+ const { dispatch, getState } = store;
+
+ const originalSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo-original"))
+ );
+
+ const originalLocation = createLocation({
+ source: originalSource,
+ line: 3,
+ column: 0,
+ sourceActor: selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ originalSource.id
+ ),
+ });
+
+ const generatedSource = await dispatch(
+ actions.newGeneratedSource(makeSource("foo"))
+ );
+ const generatedLocation = createLocation({
+ source: generatedSource,
+ line: 1,
+ column: 0,
+ sourceActor: selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ generatedSource.id
+ ),
+ });
+ const mockPauseInfo = createPauseInfo(generatedLocation);
+
+ const { frames } = mockPauseInfo;
+ client.getFrames = async () => frames;
+
+ await dispatch(actions.paused(mockPauseInfo));
+ expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
+ {
+ id: mockFrameId,
+ generatedLocation,
+ location: originalLocation,
+ originalDisplayName: "fooOriginal",
+ scope: { bindings: { arguments: [], variables: {} } },
+ thread: "FakeThread",
+ },
+ ]);
+ });
+
+ it("maps frame to original frames", async () => {
+ const sourceMapLoaderMock = {
+ getOriginalStackFrames: loc => Promise.resolve(originStackFrames),
+ getOriginalLocation: () => Promise.resolve(originalLocation),
+ getOriginalLocations: async items => items,
+ getOriginalSourceText: async () => ({
+ text: "fn fooBar() {}\nfn barZoo() { fooBar() }",
+ contentType: "text/rust",
+ }),
+ getGeneratedRangesForOriginal: async () => [],
+ };
+
+ const client = { ...mockCommandClient };
+ const store = createStore(client, {}, sourceMapLoaderMock);
+ const { dispatch, getState } = store;
+
+ const generatedSource = await dispatch(
+ actions.newGeneratedSource(
+ makeSource("foo-wasm", { introductionType: "wasm" })
+ )
+ );
+
+ const generatedLocation = createLocation({
+ source: generatedSource,
+ line: 1,
+ column: 0,
+ sourceActor: selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ generatedSource.id
+ ),
+ });
+ const mockPauseInfo = createPauseInfo(generatedLocation);
+ const { frames } = mockPauseInfo;
+ client.getFrames = async () => frames;
+
+ const [originalSource] = await dispatch(
+ actions.newOriginalSources([makeOriginalSource(generatedSource)])
+ );
+
+ const originalLocation = createLocation({
+ source: originalSource,
+ line: 1,
+ column: 1,
+ sourceActor: selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ originalSource.id
+ ),
+ });
+ const originalLocation2 = createLocation({
+ source: originalSource,
+ line: 2,
+ column: 14,
+ sourceActor: selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ originalSource.id
+ ),
+ });
+
+ const originStackFrames = [
+ {
+ displayName: "fooBar",
+ thread: "FakeThread",
+ },
+ {
+ displayName: "barZoo",
+ location: originalLocation2,
+ thread: "FakeThread",
+ },
+ ];
+
+ await dispatch(actions.paused(mockPauseInfo));
+ expect(selectors.getFrames(getState(), "FakeThread")).toEqual([
+ {
+ asyncCause: undefined,
+ displayName: "fooBar",
+ generatedLocation,
+ id: "1",
+ index: undefined,
+ isOriginal: true,
+ location: originalLocation,
+ originalDisplayName: "fooBar",
+ originalVariables: undefined,
+ scope: { bindings: { arguments: [], variables: {} } },
+ source: null,
+ state: undefined,
+ this: undefined,
+ thread: "FakeThread",
+ },
+ {
+ asyncCause: undefined,
+ displayName: "barZoo",
+ generatedLocation,
+ id: "1-originalFrame1",
+ index: undefined,
+ isOriginal: true,
+ location: originalLocation2,
+ originalDisplayName: "barZoo",
+ originalVariables: undefined,
+ scope: { bindings: { arguments: [], variables: {} } },
+ source: null,
+ state: undefined,
+ this: undefined,
+ thread: "FakeThread",
+ },
+ ]);
+ });
+ });
+
+ describe("resumed", () => {
+ it("should not evaluate expression while stepping", async () => {
+ const client = { ...mockCommandClient, evaluateExpressions: jest.fn() };
+ const { dispatch, getState } = createStore(client);
+ const mockPauseInfo = createPauseInfo();
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.paused(mockPauseInfo));
+
+ const cx = selectors.getThreadContext(getState());
+ dispatch(actions.stepIn(cx));
+ await dispatch(actions.resumed(mockCommandClient.actorID));
+ expect(client.evaluateExpressions.mock.calls).toHaveLength(1);
+ });
+
+ it("resuming - will re-evaluate watch expressions", async () => {
+ const client = { ...mockCommandClient, evaluateExpressions: jest.fn() };
+ const store = createStore(client);
+ const { dispatch, getState, cx } = store;
+ const mockPauseInfo = createPauseInfo();
+
+ await dispatch(actions.newGeneratedSource(makeSource("foo1")));
+ await dispatch(actions.newGeneratedSource(makeSource("foo")));
+ await dispatch(actions.addExpression(cx, "foo"));
+ await waitForState(store, state => selectors.getExpression(state, "foo"));
+
+ client.evaluateExpressions.mockReturnValue(Promise.resolve(["YAY"]));
+ await dispatch(actions.paused(mockPauseInfo));
+
+ await dispatch(actions.resumed(mockCommandClient.actorID));
+ const expression = selectors.getExpression(getState(), "foo");
+ expect(expression && expression.value).toEqual("YAY");
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js b/devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js
new file mode 100644
index 0000000000..bc8d000697
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/pauseOnExceptions.spec.js
@@ -0,0 +1,24 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import {
+ actions,
+ createStore,
+ getTelemetryEvents,
+} from "../../../utils/test-head";
+
+import {
+ getShouldPauseOnExceptions,
+ getShouldPauseOnCaughtExceptions,
+} from "../../../selectors/pause";
+
+describe("pauseOnExceptions", () => {
+ it("should track telemetry for pauseOnException changes", async () => {
+ const { dispatch, getState } = createStore({ pauseOnExceptions: () => {} });
+ dispatch(actions.pauseOnExceptions(true, false));
+ expect(getTelemetryEvents("pause_on_exceptions")).toMatchSnapshot();
+ expect(getShouldPauseOnExceptions(getState())).toBe(true);
+ expect(getShouldPauseOnCaughtExceptions(getState())).toBe(false);
+ });
+});
diff --git a/devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js b/devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js
new file mode 100644
index 0000000000..83006c3089
--- /dev/null
+++ b/devtools/client/debugger/src/actions/pause/tests/skipPausing.spec.js
@@ -0,0 +1,18 @@
+/* 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 <http://mozilla.org/MPL/2.0/>. */
+
+import { actions, selectors, createStore } from "../../../utils/test-head";
+
+describe("sources - pretty print", () => {
+ it("returns a pretty source for a minified file", async () => {
+ const client = { setSkipPausing: jest.fn() };
+ const { dispatch, getState } = createStore(client);
+
+ await dispatch(actions.toggleSkipPausing());
+ expect(selectors.getSkipPausing(getState())).toBe(true);
+
+ await dispatch(actions.toggleSkipPausing());
+ expect(selectors.getSkipPausing(getState())).toBe(false);
+ });
+});