summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions/breakpoints
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/debugger/src/actions/breakpoints
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/debugger/src/actions/breakpoints')
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js273
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/index.js426
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/modify.js382
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/moz.build13
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js138
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap173
-rw-r--r--devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js521
7 files changed, 1926 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
new file mode 100644
index 0000000000..263b476364
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/breakpointPositions.js
@@ -0,0 +1,273 @@
+/* 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 {
+ isOriginalId,
+ isGeneratedId,
+ originalToGeneratedId,
+} from "devtools/client/shared/source-map-loader/index";
+
+import {
+ getSource,
+ getSourceFromId,
+ getBreakpointPositionsForSource,
+ getSourceActorsForSource,
+} from "../../selectors";
+
+import { makeBreakpointId } from "../../utils/breakpoint";
+import { memoizeableAction } from "../../utils/memoizableAction";
+import { fulfilled } from "../../utils/async-value";
+import {
+ debuggerToSourceMapLocation,
+ sourceMapToDebuggerLocation,
+ createLocation,
+} from "../../utils/location";
+
+async function mapLocations(generatedLocations, { getState, sourceMapLoader }) {
+ if (!generatedLocations.length) {
+ return [];
+ }
+
+ const originalLocations = await sourceMapLoader.getOriginalLocations(
+ generatedLocations.map(debuggerToSourceMapLocation)
+ );
+ return originalLocations.map((location, index) => ({
+ // If location is null, this particular location doesn't map to any original source.
+ location: location
+ ? sourceMapToDebuggerLocation(getState(), location)
+ : generatedLocations[index],
+ generatedLocation: generatedLocations[index],
+ }));
+}
+
+// Filter out positions, that are not in the original source Id
+function filterBySource(positions, sourceId) {
+ if (!isOriginalId(sourceId)) {
+ return positions;
+ }
+ return positions.filter(position => position.location.sourceId == sourceId);
+}
+
+/**
+ * Merge positions that refer to duplicated positions.
+ * Some sourcemaped positions might refer to the exact same source/line/column triple.
+ *
+ * @param {Array<{location, generatedLocation}>} positions: List of possible breakable positions
+ * @returns {Array<{location, generatedLocation}>} A new, filtered array.
+ */
+function filterByUniqLocation(positions) {
+ const handledBreakpointIds = new Set();
+ return positions.filter(({ location }) => {
+ const breakpointId = makeBreakpointId(location);
+ if (handledBreakpointIds.has(breakpointId)) {
+ return false;
+ }
+
+ handledBreakpointIds.add(breakpointId);
+ return true;
+ });
+}
+
+function convertToList(results, source) {
+ const positions = [];
+
+ for (const line in results) {
+ for (const column of results[line]) {
+ positions.push(
+ createLocation({
+ line: Number(line),
+ column,
+ source,
+ sourceUrl: source.url,
+ })
+ );
+ }
+ }
+
+ return positions;
+}
+
+function groupByLine(results, sourceId, line) {
+ const isOriginal = isOriginalId(sourceId);
+ const positions = {};
+
+ // Ensure that we have an entry for the line fetched
+ if (typeof line === "number") {
+ positions[line] = [];
+ }
+
+ for (const result of results) {
+ const location = isOriginal ? result.location : result.generatedLocation;
+
+ if (!positions[location.line]) {
+ positions[location.line] = [];
+ }
+
+ positions[location.line].push(result);
+ }
+
+ return positions;
+}
+
+async function _setBreakpointPositions(cx, location, thunkArgs) {
+ const { client, dispatch, getState, sourceMapLoader } = thunkArgs;
+ const results = {};
+ let generatedSource = location.source;
+ if (isOriginalId(location.sourceId)) {
+ const ranges = await sourceMapLoader.getGeneratedRangesForOriginal(
+ location.sourceId,
+ true
+ );
+ const generatedSourceId = originalToGeneratedId(location.sourceId);
+ generatedSource = getSourceFromId(getState(), generatedSourceId);
+
+ // Note: While looping here may not look ideal, in the vast majority of
+ // cases, the number of ranges here should be very small, and is quite
+ // likely to only be a single range.
+ for (const range of ranges) {
+ // Wrap infinite end positions to the next line to keep things simple
+ // and because we know we don't care about the end-line whitespace
+ // in this case.
+ if (range.end.column === Infinity) {
+ range.end = {
+ line: range.end.line + 1,
+ column: 0,
+ };
+ }
+
+ const actorBps = await Promise.all(
+ getSourceActorsForSource(getState(), generatedSourceId).map(actor =>
+ client.getSourceActorBreakpointPositions(actor, range)
+ )
+ );
+
+ for (const actorPositions of actorBps) {
+ for (const rangeLine of Object.keys(actorPositions)) {
+ let columns = actorPositions[parseInt(rangeLine, 10)];
+ const existing = results[rangeLine];
+ if (existing) {
+ columns = [...new Set([...existing, ...columns])];
+ }
+
+ results[rangeLine] = columns;
+ }
+ }
+ }
+ } else {
+ const { line } = location;
+ if (typeof line !== "number") {
+ throw new Error("Line is required for generated sources");
+ }
+
+ const actorColumns = await Promise.all(
+ getSourceActorsForSource(getState(), location.sourceId).map(
+ async actor => {
+ const positions = await client.getSourceActorBreakpointPositions(
+ actor,
+ {
+ start: { line: line, column: 0 },
+ end: { line: line + 1, column: 0 },
+ }
+ );
+ return positions[line] || [];
+ }
+ )
+ );
+
+ for (const columns of actorColumns) {
+ results[line] = (results[line] || []).concat(columns);
+ }
+ }
+
+ let positions = convertToList(results, generatedSource);
+ positions = await mapLocations(positions, thunkArgs);
+
+ positions = filterBySource(positions, location.sourceId);
+ positions = filterByUniqLocation(positions);
+ positions = groupByLine(positions, location.sourceId, location.line);
+
+ const source = getSource(getState(), location.sourceId);
+ // NOTE: it's possible that the source was removed during a navigation
+ if (!source) {
+ return;
+ }
+
+ dispatch({
+ type: "ADD_BREAKPOINT_POSITIONS",
+ cx,
+ source,
+ positions,
+ });
+}
+
+function generatedSourceActorKey(state, sourceId) {
+ const generatedSource = getSource(
+ state,
+ isOriginalId(sourceId) ? originalToGeneratedId(sourceId) : sourceId
+ );
+ const actors = generatedSource
+ ? getSourceActorsForSource(state, generatedSource.id).map(
+ ({ actor }) => actor
+ )
+ : [];
+ return [sourceId, ...actors].join(":");
+}
+
+/**
+ * This method will force retrieving the breakable positions for a given source, on a given line.
+ * If this data has already been computed, it will returned the cached data.
+ *
+ * For original sources, this will query the SourceMap worker.
+ * For generated sources, this will query the DevTools server and the related source actors.
+ *
+ * @param Object options
+ * Dictionary object with many arguments:
+ * @param String options.sourceId
+ * The source we want to fetch breakable positions
+ * @param Number options.line
+ * The line we want to know which columns are breakable.
+ * (note that this seems to be optional for original sources)
+ * @return Array<Object>
+ * The list of all breakable positions, each object of this array will be like this:
+ * {
+ * line: Number
+ * column: Number
+ * sourceId: String
+ * sourceUrl: String
+ * }
+ */
+export const setBreakpointPositions = memoizeableAction(
+ "setBreakpointPositions",
+ {
+ getValue: ({ location }, { getState }) => {
+ const positions = getBreakpointPositionsForSource(
+ getState(),
+ location.sourceId
+ );
+ if (!positions) {
+ return null;
+ }
+
+ if (
+ isGeneratedId(location.sourceId) &&
+ location.line &&
+ !positions[location.line]
+ ) {
+ // We always return the full position dataset, but if a given line is
+ // not available, we treat the whole set as loading.
+ return null;
+ }
+
+ return fulfilled(positions);
+ },
+ createKey({ location }, { getState }) {
+ const key = generatedSourceActorKey(getState(), location.sourceId);
+ return isGeneratedId(location.sourceId) && location.line
+ ? `${key}-${location.line}`
+ : key;
+ },
+ action: async ({ cx, location }, thunkArgs) =>
+ _setBreakpointPositions(cx, location, thunkArgs),
+ }
+);
diff --git a/devtools/client/debugger/src/actions/breakpoints/index.js b/devtools/client/debugger/src/actions/breakpoints/index.js
new file mode 100644
index 0000000000..d188af05dc
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/index.js
@@ -0,0 +1,426 @@
+/* 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 breakpoints
+ * @module actions/breakpoints
+ */
+
+import { PROMISE } from "../utils/middleware/promise";
+import { asyncStore } from "../../utils/prefs";
+import { createLocation } from "../../utils/location";
+import {
+ getBreakpointsList,
+ getXHRBreakpoints,
+ getSelectedSource,
+ getBreakpointAtLocation,
+ getBreakpointsForSource,
+ getBreakpointsAtLine,
+} from "../../selectors";
+import { createXHRBreakpoint } from "../../utils/breakpoint";
+import {
+ addBreakpoint,
+ removeBreakpoint,
+ enableBreakpoint,
+ disableBreakpoint,
+} from "./modify";
+import { getOriginalLocation } from "../../utils/source-maps";
+
+import { isOriginalId } from "devtools/client/shared/source-map-loader/index";
+// this will need to be changed so that addCLientBreakpoint is removed
+
+export * from "./breakpointPositions";
+export * from "./modify";
+export * from "./syncBreakpoint";
+
+export function addHiddenBreakpoint(cx, location) {
+ return ({ dispatch }) => {
+ return dispatch(addBreakpoint(cx, location, { hidden: true }));
+ };
+}
+
+/**
+ * Disable all breakpoints in a source
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function disableBreakpointsInSource(cx, source) {
+ return async ({ dispatch, getState, client }) => {
+ const breakpoints = getBreakpointsForSource(getState(), source.id);
+ for (const breakpoint of breakpoints) {
+ if (!breakpoint.disabled) {
+ dispatch(disableBreakpoint(cx, breakpoint));
+ }
+ }
+ };
+}
+
+/**
+ * Enable all breakpoints in a source
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function enableBreakpointsInSource(cx, source) {
+ return async ({ dispatch, getState, client }) => {
+ const breakpoints = getBreakpointsForSource(getState(), source.id);
+ for (const breakpoint of breakpoints) {
+ if (breakpoint.disabled) {
+ dispatch(enableBreakpoint(cx, breakpoint));
+ }
+ }
+ };
+}
+
+/**
+ * Toggle All Breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function toggleAllBreakpoints(cx, shouldDisableBreakpoints) {
+ return async ({ dispatch, getState, client }) => {
+ const breakpoints = getBreakpointsList(getState());
+
+ for (const breakpoint of breakpoints) {
+ if (shouldDisableBreakpoints) {
+ dispatch(disableBreakpoint(cx, breakpoint));
+ } else {
+ dispatch(enableBreakpoint(cx, breakpoint));
+ }
+ }
+ };
+}
+
+/**
+ * Toggle Breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function toggleBreakpoints(cx, shouldDisableBreakpoints, breakpoints) {
+ return async ({ dispatch }) => {
+ const promises = breakpoints.map(breakpoint =>
+ shouldDisableBreakpoints
+ ? dispatch(disableBreakpoint(cx, breakpoint))
+ : dispatch(enableBreakpoint(cx, breakpoint))
+ );
+
+ await Promise.all(promises);
+ };
+}
+
+export function toggleBreakpointsAtLine(cx, shouldDisableBreakpoints, line) {
+ return async ({ dispatch, getState }) => {
+ const breakpoints = getBreakpointsAtLine(getState(), line);
+ return dispatch(
+ toggleBreakpoints(cx, shouldDisableBreakpoints, breakpoints)
+ );
+ };
+}
+
+/**
+ * Removes all breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeAllBreakpoints(cx) {
+ return async ({ dispatch, getState }) => {
+ const breakpointList = getBreakpointsList(getState());
+
+ await Promise.all(
+ breakpointList.map(bp => dispatch(removeBreakpoint(cx, bp)))
+ );
+ dispatch({ type: "CLEAR_BREAKPOINTS" });
+ };
+}
+
+/**
+ * Removes breakpoints
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeBreakpoints(cx, breakpoints) {
+ return async ({ dispatch }) => {
+ return Promise.all(
+ breakpoints.map(bp => dispatch(removeBreakpoint(cx, bp)))
+ );
+ };
+}
+
+/**
+ * Removes all breakpoints in a source
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeBreakpointsInSource(cx, source) {
+ return async ({ dispatch, getState, client }) => {
+ const breakpoints = getBreakpointsForSource(getState(), source.id);
+ for (const breakpoint of breakpoints) {
+ dispatch(removeBreakpoint(cx, breakpoint));
+ }
+ };
+}
+
+/**
+ * Update the original location information of breakpoints.
+
+/*
+ * Update breakpoints for a source that just got pretty printed.
+ * This method maps the breakpoints currently set only against the
+ * non-pretty-printed (generated) source to the related pretty-printed
+ * (original) source by querying the SourceMap service.
+ *
+ * @param {Objeect} cx
+ * @param {String} sourceId - the generated source id
+ */
+export function updateBreakpointsForNewPrettyPrintedSource(cx, sourceId) {
+ return async thunkArgs => {
+ const { dispatch, getState } = thunkArgs;
+ if (isOriginalId(sourceId)) {
+ console.error("Can't update breakpoints on original sources");
+ return;
+ }
+ const breakpoints = getBreakpointsForSource(getState(), sourceId);
+ // Remap the breakpoints with the original location information from
+ // the pretty-printed source.
+ const newBreakpoints = await Promise.all(
+ breakpoints.map(async breakpoint => {
+ const location = await getOriginalLocation(
+ breakpoint.generatedLocation,
+ thunkArgs
+ );
+ return { ...breakpoint, location };
+ })
+ );
+
+ // Normally old breakpoints will be clobbered if we re-add them, but when
+ // remapping we have changed the source maps and the old breakpoints will
+ // have different locations than the new ones. Manually remove the
+ // old breakpoints before adding the new ones.
+ for (const bp of breakpoints) {
+ dispatch(removeBreakpoint(cx, bp));
+ }
+
+ for (const bp of newBreakpoints) {
+ await dispatch(addBreakpoint(cx, bp.location, bp.options, bp.disabled));
+ }
+ };
+}
+
+export function toggleBreakpointAtLine(cx, line) {
+ return ({ dispatch, getState }) => {
+ const state = getState();
+ const selectedSource = getSelectedSource(state);
+
+ if (!selectedSource) {
+ return null;
+ }
+
+ const bp = getBreakpointAtLocation(state, { line, column: undefined });
+ if (bp) {
+ return dispatch(removeBreakpoint(cx, bp));
+ }
+ return dispatch(
+ addBreakpoint(
+ cx,
+ createLocation({
+ source: selectedSource,
+ sourceUrl: selectedSource.url,
+ line,
+ })
+ )
+ );
+ };
+}
+
+export function addBreakpointAtLine(
+ cx,
+ line,
+ shouldLog = false,
+ disabled = false
+) {
+ return ({ dispatch, getState }) => {
+ const state = getState();
+ const source = getSelectedSource(state);
+
+ if (!source) {
+ return null;
+ }
+ const breakpointLocation = createLocation({
+ source,
+ sourceUrl: source.url,
+ column: undefined,
+ line,
+ });
+
+ const options = {};
+ if (shouldLog) {
+ options.logValue = "displayName";
+ }
+
+ return dispatch(addBreakpoint(cx, breakpointLocation, options, disabled));
+ };
+}
+
+export function removeBreakpointsAtLine(cx, sourceId, line) {
+ return ({ dispatch, getState }) => {
+ const breakpointsAtLine = getBreakpointsForSource(
+ getState(),
+ sourceId,
+ line
+ );
+ return dispatch(removeBreakpoints(cx, breakpointsAtLine));
+ };
+}
+
+export function disableBreakpointsAtLine(cx, sourceId, line) {
+ return ({ dispatch, getState }) => {
+ const breakpointsAtLine = getBreakpointsForSource(
+ getState(),
+ sourceId,
+ line
+ );
+ return dispatch(toggleBreakpoints(cx, true, breakpointsAtLine));
+ };
+}
+
+export function enableBreakpointsAtLine(cx, sourceId, line) {
+ return ({ dispatch, getState }) => {
+ const breakpointsAtLine = getBreakpointsForSource(
+ getState(),
+ sourceId,
+ line
+ );
+ return dispatch(toggleBreakpoints(cx, false, breakpointsAtLine));
+ };
+}
+
+export function toggleDisabledBreakpoint(cx, breakpoint) {
+ return ({ dispatch, getState }) => {
+ if (!breakpoint.disabled) {
+ return dispatch(disableBreakpoint(cx, breakpoint));
+ }
+ return dispatch(enableBreakpoint(cx, breakpoint));
+ };
+}
+
+export function enableXHRBreakpoint(index, bp) {
+ return ({ dispatch, getState, client }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = bp || xhrBreakpoints[index];
+ const enabledBreakpoint = {
+ ...breakpoint,
+ disabled: false,
+ };
+
+ return dispatch({
+ type: "ENABLE_XHR_BREAKPOINT",
+ breakpoint: enabledBreakpoint,
+ index,
+ [PROMISE]: client.setXHRBreakpoint(breakpoint.path, breakpoint.method),
+ });
+ };
+}
+
+export function disableXHRBreakpoint(index, bp) {
+ return ({ dispatch, getState, client }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = bp || xhrBreakpoints[index];
+ const disabledBreakpoint = {
+ ...breakpoint,
+ disabled: true,
+ };
+
+ return dispatch({
+ type: "DISABLE_XHR_BREAKPOINT",
+ breakpoint: disabledBreakpoint,
+ index,
+ [PROMISE]: client.removeXHRBreakpoint(breakpoint.path, breakpoint.method),
+ });
+ };
+}
+
+export function updateXHRBreakpoint(index, path, method) {
+ return ({ dispatch, getState, client }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = xhrBreakpoints[index];
+
+ const updatedBreakpoint = {
+ ...breakpoint,
+ path,
+ method,
+ text: L10N.getFormatStr("xhrBreakpoints.item.label", path),
+ };
+
+ return dispatch({
+ type: "UPDATE_XHR_BREAKPOINT",
+ breakpoint: updatedBreakpoint,
+ index,
+ [PROMISE]: Promise.all([
+ client.removeXHRBreakpoint(breakpoint.path, breakpoint.method),
+ client.setXHRBreakpoint(path, method),
+ ]),
+ });
+ };
+}
+export function togglePauseOnAny() {
+ return ({ dispatch, getState }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const index = xhrBreakpoints.findIndex(({ path }) => path.length === 0);
+ if (index < 0) {
+ return dispatch(setXHRBreakpoint("", "ANY"));
+ }
+
+ const bp = xhrBreakpoints[index];
+ if (bp.disabled) {
+ return dispatch(enableXHRBreakpoint(index, bp));
+ }
+
+ return dispatch(disableXHRBreakpoint(index, bp));
+ };
+}
+
+export function setXHRBreakpoint(path, method) {
+ return ({ dispatch, getState, client }) => {
+ const breakpoint = createXHRBreakpoint(path, method);
+
+ return dispatch({
+ type: "SET_XHR_BREAKPOINT",
+ breakpoint,
+ [PROMISE]: client.setXHRBreakpoint(path, method),
+ });
+ };
+}
+
+export function removeAllXHRBreakpoints() {
+ return async ({ dispatch, getState, client }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const promises = xhrBreakpoints.map(breakpoint =>
+ client.removeXHRBreakpoint(breakpoint.path, breakpoint.method)
+ );
+ await dispatch({
+ type: "CLEAR_XHR_BREAKPOINTS",
+ [PROMISE]: Promise.all(promises),
+ });
+ asyncStore.xhrBreakpoints = [];
+ };
+}
+
+export function removeXHRBreakpoint(index) {
+ return ({ dispatch, getState, client }) => {
+ const xhrBreakpoints = getXHRBreakpoints(getState());
+ const breakpoint = xhrBreakpoints[index];
+ return dispatch({
+ type: "REMOVE_XHR_BREAKPOINT",
+ breakpoint,
+ index,
+ [PROMISE]: client.removeXHRBreakpoint(breakpoint.path, breakpoint.method),
+ });
+ };
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/modify.js b/devtools/client/debugger/src/actions/breakpoints/modify.js
new file mode 100644
index 0000000000..4576a61e27
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/modify.js
@@ -0,0 +1,382 @@
+/* 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 { createBreakpoint } from "../../client/firefox/create";
+import {
+ makeBreakpointServerLocation,
+ makeBreakpointId,
+} from "../../utils/breakpoint";
+import {
+ getBreakpoint,
+ getBreakpointPositionsForLocation,
+ getFirstBreakpointPosition,
+ getSettledSourceTextContent,
+ getBreakpointsList,
+ getPendingBreakpointList,
+ isMapScopesEnabled,
+ getBlackBoxRanges,
+ isSourceMapIgnoreListEnabled,
+ isSourceOnSourceMapIgnoreList,
+} from "../../selectors";
+
+import { setBreakpointPositions } from "./breakpointPositions";
+import { setSkipPausing } from "../pause/skipPausing";
+
+import { PROMISE } from "../utils/middleware/promise";
+import { recordEvent } from "../../utils/telemetry";
+import { comparePosition } from "../../utils/location";
+import { getTextAtPosition, isLineBlackboxed } from "../../utils/source";
+import { getMappedScopesForLocation } from "../pause/mapScopes";
+import { validateNavigateContext } from "../../utils/context";
+
+// This file has the primitive operations used to modify individual breakpoints
+// and keep them in sync with the breakpoints installed on server threads. These
+// are collected here to make it easier to preserve the following invariant:
+//
+// Breakpoints are included in reducer state if they are disabled or requests
+// have been dispatched to set them in all server threads.
+//
+// To maintain this property, updates to the reducer and installed breakpoints
+// must happen with no intervening await. Using await allows other operations to
+// modify the breakpoint state in the interim and potentially cause breakpoint
+// state to go out of sync.
+//
+// The reducer is optimistically updated when users set or remove a breakpoint,
+// but it might take a little while before the breakpoints have been set or
+// removed in each thread. Once all outstanding requests sent to a thread have
+// been processed, the reducer and server threads will be in sync.
+//
+// There is another exception to the above invariant when first connecting to
+// the server: breakpoints have been installed on all generated locations in the
+// pending breakpoints, but no breakpoints have been added to the reducer. When
+// a matching source appears, either the server breakpoint will be removed or a
+// breakpoint will be added to the reducer, to restore the above invariant.
+// See syncBreakpoint.js for more.
+
+async function clientSetBreakpoint(
+ client,
+ cx,
+ { getState, dispatch },
+ breakpoint
+) {
+ const breakpointServerLocation = makeBreakpointServerLocation(
+ getState(),
+ breakpoint.generatedLocation
+ );
+ const shouldMapBreakpointExpressions =
+ isMapScopesEnabled(getState()) &&
+ breakpoint.location.source.isOriginal &&
+ (breakpoint.options.logValue || breakpoint.options.condition);
+
+ if (shouldMapBreakpointExpressions) {
+ breakpoint = await dispatch(updateBreakpointSourceMapping(cx, breakpoint));
+ }
+ return client.setBreakpoint(breakpointServerLocation, breakpoint.options);
+}
+
+function clientRemoveBreakpoint(client, state, generatedLocation) {
+ const breakpointServerLocation = makeBreakpointServerLocation(
+ state,
+ generatedLocation
+ );
+ return client.removeBreakpoint(breakpointServerLocation);
+}
+
+export function enableBreakpoint(cx, initialBreakpoint) {
+ return thunkArgs => {
+ const { dispatch, getState, client } = thunkArgs;
+ const state = getState();
+ const breakpoint = getBreakpoint(state, initialBreakpoint.location);
+ const blackboxedRanges = getBlackBoxRanges(state);
+ const isSourceOnIgnoreList =
+ isSourceMapIgnoreListEnabled(state) &&
+ isSourceOnSourceMapIgnoreList(state, breakpoint.location.source);
+ if (
+ !breakpoint ||
+ !breakpoint.disabled ||
+ isLineBlackboxed(
+ blackboxedRanges[breakpoint.location.source.url],
+ breakpoint.location.line,
+ isSourceOnIgnoreList
+ )
+ ) {
+ return null;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ cx,
+ breakpoint: createBreakpoint({ ...breakpoint, disabled: false }),
+ [PROMISE]: clientSetBreakpoint(client, cx, thunkArgs, breakpoint),
+ });
+ };
+}
+
+export function addBreakpoint(
+ cx,
+ initialLocation,
+ options = {},
+ disabled,
+ shouldCancel = () => false
+) {
+ return async thunkArgs => {
+ const { dispatch, getState, client } = thunkArgs;
+ recordEvent("add_breakpoint");
+
+ await dispatch(
+ setBreakpointPositions({
+ cx,
+ location: initialLocation,
+ })
+ );
+
+ const position = initialLocation.column
+ ? getBreakpointPositionsForLocation(getState(), initialLocation)
+ : getFirstBreakpointPosition(getState(), initialLocation);
+
+ // No position is found if the `initialLocation` is on a non-breakable line or
+ // the line no longer exists.
+ if (!position) {
+ return null;
+ }
+
+ const { location, generatedLocation } = position;
+
+ if (!location.source || !generatedLocation.source) {
+ return null;
+ }
+
+ const originalContent = getSettledSourceTextContent(getState(), location);
+ const originalText = getTextAtPosition(
+ location.source.id,
+ originalContent,
+ location
+ );
+
+ const content = getSettledSourceTextContent(getState(), generatedLocation);
+ const text = getTextAtPosition(
+ generatedLocation.source.id,
+ content,
+ generatedLocation
+ );
+
+ const id = makeBreakpointId(location);
+ const breakpoint = createBreakpoint({
+ id,
+ disabled,
+ options,
+ location,
+ generatedLocation,
+ text,
+ originalText,
+ });
+
+ if (shouldCancel()) {
+ return null;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ cx,
+ breakpoint,
+ // If we just clobbered an enabled breakpoint with a disabled one, we need
+ // to remove any installed breakpoint in the server.
+ [PROMISE]: disabled
+ ? clientRemoveBreakpoint(client, getState(), generatedLocation)
+ : clientSetBreakpoint(client, cx, thunkArgs, breakpoint),
+ });
+ };
+}
+
+/**
+ * Remove a single breakpoint
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function removeBreakpoint(cx, initialBreakpoint) {
+ return ({ dispatch, getState, client }) => {
+ recordEvent("remove_breakpoint");
+
+ const breakpoint = getBreakpoint(getState(), initialBreakpoint.location);
+ if (!breakpoint) {
+ return null;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "REMOVE_BREAKPOINT",
+ cx,
+ breakpoint,
+ // If the breakpoint is disabled then it is not installed in the server.
+ [PROMISE]: breakpoint.disabled
+ ? Promise.resolve()
+ : clientRemoveBreakpoint(
+ client,
+ getState(),
+ breakpoint.generatedLocation
+ ),
+ });
+ };
+}
+
+/**
+ * Remove all installed, pending, and client breakpoints associated with a
+ * target generated location.
+ *
+ * @param {Object} target
+ * Location object where to remove breakpoints.
+ */
+export function removeBreakpointAtGeneratedLocation(cx, target) {
+ return ({ dispatch, getState, client }) => {
+ // remove breakpoint from the server
+ const onBreakpointRemoved = clientRemoveBreakpoint(
+ client,
+ getState(),
+ target
+ );
+ // Remove any breakpoints matching the generated location.
+ const breakpoints = getBreakpointsList(getState());
+ for (const breakpoint of breakpoints) {
+ const { generatedLocation } = breakpoint;
+ if (
+ generatedLocation.sourceId == target.sourceId &&
+ comparePosition(generatedLocation, target)
+ ) {
+ dispatch({
+ type: "REMOVE_BREAKPOINT",
+ cx,
+ breakpoint,
+ [PROMISE]: onBreakpointRemoved,
+ });
+ }
+ }
+
+ // Remove any remaining pending breakpoints matching the generated location.
+ const pending = getPendingBreakpointList(getState());
+ for (const pendingBreakpoint of pending) {
+ const { generatedLocation } = pendingBreakpoint;
+ if (
+ generatedLocation.sourceUrl == target.sourceUrl &&
+ comparePosition(generatedLocation, target)
+ ) {
+ dispatch({
+ type: "REMOVE_PENDING_BREAKPOINT",
+ cx,
+ pendingBreakpoint,
+ });
+ }
+ }
+ return onBreakpointRemoved;
+ };
+}
+
+/**
+ * Disable a single breakpoint
+ *
+ * @memberof actions/breakpoints
+ * @static
+ */
+export function disableBreakpoint(cx, initialBreakpoint) {
+ return ({ dispatch, getState, client }) => {
+ const breakpoint = getBreakpoint(getState(), initialBreakpoint.location);
+ if (!breakpoint || breakpoint.disabled) {
+ return null;
+ }
+
+ dispatch(setSkipPausing(false));
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ cx,
+ breakpoint: createBreakpoint({ ...breakpoint, disabled: true }),
+ [PROMISE]: clientRemoveBreakpoint(
+ client,
+ getState(),
+ breakpoint.generatedLocation
+ ),
+ });
+ };
+}
+
+/**
+ * Update the options of a breakpoint.
+ *
+ * @throws {Error} "not implemented"
+ * @memberof actions/breakpoints
+ * @static
+ * @param {SourceLocation} location
+ * @see DebuggerController.Breakpoints.addBreakpoint
+ * @param {Object} options
+ * Any options to set on the breakpoint
+ */
+export function setBreakpointOptions(cx, location, options = {}) {
+ return thunkArgs => {
+ const { dispatch, getState, client } = thunkArgs;
+ let breakpoint = getBreakpoint(getState(), location);
+ if (!breakpoint) {
+ return dispatch(addBreakpoint(cx, location, options));
+ }
+
+ // Note: setting a breakpoint's options implicitly enables it.
+ breakpoint = createBreakpoint({ ...breakpoint, disabled: false, options });
+
+ return dispatch({
+ type: "SET_BREAKPOINT",
+ cx,
+ breakpoint,
+ [PROMISE]: clientSetBreakpoint(client, cx, thunkArgs, breakpoint),
+ });
+ };
+}
+
+async function updateExpression(parserWorker, mappings, originalExpression) {
+ const mapped = await parserWorker.mapExpression(
+ originalExpression,
+ mappings,
+ [],
+ false,
+ false
+ );
+ if (!mapped) {
+ return originalExpression;
+ }
+ if (!originalExpression.trimEnd().endsWith(";")) {
+ return mapped.expression.replace(/;$/, "");
+ }
+ return mapped.expression;
+}
+
+function updateBreakpointSourceMapping(cx, breakpoint) {
+ return async ({ getState, dispatch, parserWorker }) => {
+ const options = { ...breakpoint.options };
+
+ const mappedScopes = await dispatch(
+ getMappedScopesForLocation(breakpoint.location)
+ );
+ if (!mappedScopes) {
+ return breakpoint;
+ }
+ const { mappings } = mappedScopes;
+
+ if (options.condition) {
+ options.condition = await updateExpression(
+ parserWorker,
+ mappings,
+ options.condition
+ );
+ }
+ if (options.logValue) {
+ options.logValue = await updateExpression(
+ parserWorker,
+ mappings,
+ options.logValue
+ );
+ }
+
+ validateNavigateContext(getState(), cx);
+ return { ...breakpoint, options };
+ };
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/moz.build b/devtools/client/debugger/src/actions/breakpoints/moz.build
new file mode 100644
index 0000000000..65910c4ef2
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/moz.build
@@ -0,0 +1,13 @@
+# 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(
+ "breakpointPositions.js",
+ "index.js",
+ "modify.js",
+ "syncBreakpoint.js",
+)
diff --git a/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js b/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js
new file mode 100644
index 0000000000..b52c0ddfb1
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/syncBreakpoint.js
@@ -0,0 +1,138 @@
+/* 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 { setBreakpointPositions } from "./breakpointPositions";
+import {
+ findPosition,
+ makeBreakpointServerLocation,
+} from "../../utils/breakpoint";
+
+import { comparePosition, createLocation } from "../../utils/location";
+
+import {
+ originalToGeneratedId,
+ isOriginalId,
+} from "devtools/client/shared/source-map-loader/index";
+import { getSource } from "../../selectors";
+import { addBreakpoint, removeBreakpointAtGeneratedLocation } from ".";
+
+async function findBreakpointPosition(cx, { getState, dispatch }, location) {
+ const positions = await dispatch(setBreakpointPositions({ cx, location }));
+
+ const position = findPosition(positions, location);
+ return position;
+}
+
+// Breakpoint syncing occurs when a source is found that matches either the
+// original or generated URL of a pending breakpoint. A new breakpoint is
+// constructed that might have a different original and/or generated location,
+// if the original source has changed since the pending breakpoint was created.
+// There are a couple subtle aspects to syncing:
+//
+// - We handle both the original and generated source because there is no
+// guarantee that seeing the generated source means we will also see the
+// original source. When connecting, a breakpoint will be installed in the
+// client for the generated location in the pending breakpoint, and we need
+// to make sure that either a breakpoint is added to the reducer or that this
+// client breakpoint is deleted.
+//
+// - If we see both the original and generated sources and the source mapping
+// has changed, we need to make sure that only a single breakpoint is added
+// to the reducer for the new location corresponding to the original location
+// in the pending breakpoint.
+export function syncPendingBreakpoint(cx, sourceId, pendingBreakpoint) {
+ return async thunkArgs => {
+ const { getState, client, dispatch } = thunkArgs;
+
+ const source = getSource(getState(), sourceId);
+
+ const generatedSourceId = isOriginalId(sourceId)
+ ? originalToGeneratedId(sourceId)
+ : sourceId;
+
+ const generatedSource = getSource(getState(), generatedSourceId);
+
+ if (!source || !generatedSource) {
+ return null;
+ }
+
+ // /!\ Pending breakpoint locations come only with sourceUrl, line and column attributes.
+ // We have to map it to a specific source object and avoid trying to query its non-existent 'source' attribute.
+ const { location, generatedLocation } = pendingBreakpoint;
+ const isPendingBreakpointWithSourceMap =
+ location.sourceUrl != generatedLocation.sourceUrl;
+ const sourceGeneratedLocation = createLocation({
+ ...generatedLocation,
+ source: generatedSource,
+ });
+
+ if (source == generatedSource && isPendingBreakpointWithSourceMap) {
+ // We are handling the generated source and the pending breakpoint has a
+ // source mapping. Supply a cancellation callback that will abort the
+ // breakpoint if the original source was synced to a different location,
+ // in which case the client breakpoint has been removed.
+ const breakpointServerLocation = makeBreakpointServerLocation(
+ getState(),
+ sourceGeneratedLocation
+ );
+ return dispatch(
+ addBreakpoint(
+ cx,
+ sourceGeneratedLocation,
+ pendingBreakpoint.options,
+ pendingBreakpoint.disabled,
+ () => !client.hasBreakpoint(breakpointServerLocation)
+ )
+ );
+ }
+
+ const originalLocation = createLocation({
+ ...location,
+ source,
+ });
+
+ const newPosition = await findBreakpointPosition(
+ cx,
+ thunkArgs,
+ originalLocation
+ );
+
+ const newGeneratedLocation = newPosition?.generatedLocation;
+ if (!newGeneratedLocation) {
+ // We couldn't find a new mapping for the breakpoint. If there is a source
+ // mapping, remove any breakpoints for the generated location, as if the
+ // breakpoint moved. If the old generated location still maps to an
+ // original location then we don't want to add a breakpoint for it.
+ if (isPendingBreakpointWithSourceMap) {
+ dispatch(
+ removeBreakpointAtGeneratedLocation(cx, sourceGeneratedLocation)
+ );
+ }
+ return null;
+ }
+
+ const isSameLocation = comparePosition(
+ generatedLocation,
+ newGeneratedLocation
+ );
+
+ // If the new generated location has changed from that in the pending
+ // breakpoint, remove any breakpoint associated with the old generated
+ // location.
+ if (!isSameLocation) {
+ dispatch(
+ removeBreakpointAtGeneratedLocation(cx, sourceGeneratedLocation)
+ );
+ }
+
+ return dispatch(
+ addBreakpoint(
+ cx,
+ newGeneratedLocation,
+ pendingBreakpoint.options,
+ pendingBreakpoint.disabled
+ )
+ );
+ };
+}
diff --git a/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap b/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap
new file mode 100644
index 0000000000..c18c3593d9
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/__snapshots__/breakpoints.spec.js.snap
@@ -0,0 +1,173 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`breakpoints should add a breakpoint 1`] = `
+Array [
+ Object {
+ "breakpoints": Array [
+ Object {
+ "disabled": false,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 2,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ "sourceActor": null,
+ "sourceActorId": undefined,
+ "sourceId": "a",
+ "sourceUrl": "http://localhost:8000/examples/a",
+ },
+ "id": "a:2:1",
+ "location": Object {
+ "column": 1,
+ "line": 2,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ "sourceActor": null,
+ "sourceActorId": undefined,
+ "sourceId": "a",
+ "sourceUrl": "http://localhost:8000/examples/a",
+ },
+ "options": Object {},
+ "originalText": "return a",
+ "text": "return a",
+ "thread": undefined,
+ },
+ ],
+ "filename": "a",
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ },
+]
+`;
+
+exports[`breakpoints should not show a breakpoint that does not have text 1`] = `Array []`;
+
+exports[`breakpoints should show a disabled breakpoint that does not have text 1`] = `
+Array [
+ Object {
+ "breakpoints": Array [
+ Object {
+ "disabled": true,
+ "generatedLocation": Object {
+ "column": 1,
+ "line": 5,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ "sourceActor": null,
+ "sourceActorId": undefined,
+ "sourceId": "a",
+ "sourceUrl": "http://localhost:8000/examples/a",
+ },
+ "id": "a:5:1",
+ "location": Object {
+ "column": 1,
+ "line": 5,
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ "sourceActor": null,
+ "sourceActorId": undefined,
+ "sourceId": "a",
+ "sourceUrl": "http://localhost:8000/examples/a",
+ },
+ "options": Object {},
+ "originalText": "",
+ "text": "",
+ "thread": undefined,
+ },
+ ],
+ "filename": "a",
+ "source": Object {
+ "displayURL": Object {
+ "fileExtension": "",
+ "filename": "a",
+ "group": "localhost:8000",
+ "path": "/examples/a",
+ "search": "",
+ },
+ "extensionName": null,
+ "id": "a",
+ "isExtension": false,
+ "isHTML": false,
+ "isOriginal": false,
+ "isPrettyPrinted": false,
+ "isWasm": false,
+ "url": "http://localhost:8000/examples/a",
+ },
+ },
+]
+`;
diff --git a/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js b/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
new file mode 100644
index 0000000000..558d2400a8
--- /dev/null
+++ b/devtools/client/debugger/src/actions/breakpoints/tests/breakpoints.spec.js
@@ -0,0 +1,521 @@
+/* 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 {
+ createStore,
+ selectors,
+ actions,
+ makeSource,
+ getTelemetryEvents,
+} from "../../../utils/test-head";
+
+import { mockCommandClient } from "../../tests/helpers/mockCommandClient";
+import { createLocation } from "../../../utils/location";
+
+jest.mock("../../../utils/prefs", () => ({
+ prefs: {
+ expressions: [],
+ },
+ asyncStore: {
+ pendingBreakpoints: {},
+ },
+ features: {
+ inlinePreview: true,
+ },
+}));
+
+function mockClient(positionsResponse = {}) {
+ return {
+ ...mockCommandClient,
+ setSkipPausing: jest.fn(),
+ getSourceActorBreakpointPositions: async () => positionsResponse,
+ getSourceActorBreakableLines: async () => [],
+ };
+}
+
+describe("breakpoints", () => {
+ it("should add a breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ 2: [1] }));
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ const loc1 = createLocation({
+ source,
+ line: 2,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ });
+ await dispatch(
+ actions.selectLocation(
+ cx,
+ createLocation({
+ source,
+ line: 1,
+ column: 1,
+ })
+ )
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+ expect(getTelemetryEvents("add_breakpoint")).toHaveLength(1);
+
+ const bpSources = selectors.getBreakpointSources(getState());
+ expect(bpSources).toMatchSnapshot();
+ });
+
+ it("should not show a breakpoint that does not have text", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ 5: [1] }));
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ const loc1 = createLocation({
+ source,
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ });
+ await dispatch(
+ actions.selectLocation(
+ cx,
+ createLocation({
+ source,
+ line: 1,
+ column: 1,
+ })
+ )
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+ expect(selectors.getBreakpointSources(getState())).toMatchSnapshot();
+ });
+
+ it("should show a disabled breakpoint that does not have text", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ 5: [1] }));
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ const loc1 = createLocation({
+ source,
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ });
+ await dispatch(
+ actions.selectLocation(
+ cx,
+ createLocation({
+ source,
+ line: 1,
+ column: 1,
+ })
+ )
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ const breakpoint = selectors.getBreakpoint(getState(), loc1);
+ if (!breakpoint) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(cx, breakpoint));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+ expect(selectors.getBreakpointSources(getState())).toMatchSnapshot();
+ });
+
+ it("should not re-add a breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ 5: [1] }));
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ const loc1 = createLocation({
+ source,
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ });
+ await dispatch(
+ actions.selectLocation(
+ cx,
+ createLocation({
+ source,
+ line: 1,
+ column: 1,
+ })
+ )
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.location).toEqual(loc1);
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+
+ it("should remove a breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ 5: [1], 6: [2] })
+ );
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+
+ const bSource = await dispatch(actions.newGeneratedSource(makeSource("b")));
+
+ const loc1 = createLocation({
+ source: aSource,
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ });
+
+ const loc2 = createLocation({
+ source: bSource,
+ line: 6,
+ column: 2,
+ sourceUrl: "http://localhost:8000/examples/b",
+ });
+ const bSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ bSource.id
+ );
+
+ await dispatch(
+ actions.loadGeneratedSourceText({
+ cx,
+ sourceActor: bSourceActor,
+ })
+ );
+
+ await dispatch(
+ actions.selectLocation(
+ cx,
+ createLocation({
+ source: aSource,
+ line: 1,
+ column: 1,
+ })
+ )
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ await dispatch(actions.addBreakpoint(cx, loc2));
+
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ if (!bp) {
+ throw new Error("no bp");
+ }
+ await dispatch(actions.removeBreakpoint(cx, bp));
+
+ expect(selectors.getBreakpointCount(getState())).toEqual(1);
+ });
+
+ it("should disable a breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ 5: [1], 6: [2] })
+ );
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ const aSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ aSource.id
+ );
+ await dispatch(
+ actions.loadGeneratedSourceText({
+ cx,
+ sourceActor: aSourceActor,
+ })
+ );
+
+ const bSource = await dispatch(actions.newGeneratedSource(makeSource("b")));
+ const bSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ bSource.id
+ );
+ await dispatch(
+ actions.loadGeneratedSourceText({
+ cx,
+ sourceActor: bSourceActor,
+ })
+ );
+
+ const loc1 = createLocation({
+ source: aSource,
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ });
+
+ const loc2 = createLocation({
+ source: bSource,
+ line: 6,
+ column: 2,
+ sourceUrl: "http://localhost:8000/examples/b",
+ });
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ await dispatch(actions.addBreakpoint(cx, loc2));
+
+ const breakpoint = selectors.getBreakpoint(getState(), loc1);
+ if (!breakpoint) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(cx, breakpoint));
+
+ const bp = selectors.getBreakpoint(getState(), loc1);
+ expect(bp && bp.disabled).toBe(true);
+ });
+
+ it("should enable breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ 5: [1], 6: [2] })
+ );
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ const loc = createLocation({
+ source: aSource,
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ });
+ const aSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ aSource.id
+ );
+ await dispatch(
+ actions.loadGeneratedSourceText({
+ cx,
+ sourceActor: aSourceActor,
+ })
+ );
+
+ await dispatch(actions.addBreakpoint(cx, loc));
+ let bp = selectors.getBreakpoint(getState(), loc);
+ if (!bp) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(cx, bp));
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ if (!bp) {
+ throw new Error("no breakpoint");
+ }
+
+ expect(bp && bp.disabled).toBe(true);
+
+ await dispatch(actions.enableBreakpoint(cx, bp));
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && !bp.disabled).toBe(true);
+ });
+
+ it("should toggle all the breakpoints", async () => {
+ const { dispatch, getState, cx } = createStore(
+ mockClient({ 5: [1], 6: [2] })
+ );
+
+ const aSource = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ const aSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ aSource.id
+ );
+ await dispatch(
+ actions.loadGeneratedSourceText({
+ cx,
+ sourceActor: aSourceActor,
+ })
+ );
+
+ const bSource = await dispatch(actions.newGeneratedSource(makeSource("b")));
+ const bSourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ bSource.id
+ );
+ await dispatch(
+ actions.loadGeneratedSourceText({
+ cx,
+ sourceActor: bSourceActor,
+ })
+ );
+
+ const loc1 = createLocation({
+ source: aSource,
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ });
+
+ const loc2 = createLocation({
+ source: bSource,
+ line: 6,
+ column: 2,
+ sourceUrl: "http://localhost:8000/examples/b",
+ });
+
+ await dispatch(actions.addBreakpoint(cx, loc1));
+ await dispatch(actions.addBreakpoint(cx, loc2));
+
+ await dispatch(actions.toggleAllBreakpoints(cx, true));
+
+ let bp1 = selectors.getBreakpoint(getState(), loc1);
+ let bp2 = selectors.getBreakpoint(getState(), loc2);
+
+ expect(bp1 && bp1.disabled).toBe(true);
+ expect(bp2 && bp2.disabled).toBe(true);
+
+ await dispatch(actions.toggleAllBreakpoints(cx, false));
+
+ bp1 = selectors.getBreakpoint(getState(), loc1);
+ bp2 = selectors.getBreakpoint(getState(), loc2);
+ expect(bp1 && bp1.disabled).toBe(false);
+ expect(bp2 && bp2.disabled).toBe(false);
+ });
+
+ it("should toggle a breakpoint at a location", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ 5: [1] }));
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+ const loc = createLocation({ source, line: 5, column: 1 });
+ const getBp = () => selectors.getBreakpoint(getState(), loc);
+ await dispatch(actions.selectLocation(cx, loc));
+
+ await dispatch(actions.toggleBreakpointAtLine(cx, 5));
+ const bp = getBp();
+ expect(bp && !bp.disabled).toBe(true);
+
+ await dispatch(actions.toggleBreakpointAtLine(cx, 5));
+ expect(getBp()).toBe(undefined);
+ });
+
+ it("should disable/enable a breakpoint at a location", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ 5: [1] }));
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("foo1"))
+ );
+ const location = createLocation({ source, line: 5, column: 1 });
+ const getBp = () => selectors.getBreakpoint(getState(), location);
+ await dispatch(
+ actions.selectLocation(cx, createLocation({ source, line: 1 }))
+ );
+
+ await dispatch(actions.toggleBreakpointAtLine(cx, 5));
+ let bp = getBp();
+ expect(bp && !bp.disabled).toBe(true);
+ bp = getBp();
+ if (!bp) {
+ throw new Error("no bp");
+ }
+ await dispatch(actions.toggleDisabledBreakpoint(cx, bp));
+ bp = getBp();
+ expect(bp && bp.disabled).toBe(true);
+ });
+
+ it("should set the breakpoint condition", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ 5: [1] }));
+
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ const loc = createLocation({
+ source,
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ });
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ await dispatch(actions.loadGeneratedSourceText({ cx, sourceActor }));
+
+ await dispatch(actions.addBreakpoint(cx, loc));
+
+ let bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && bp.options.condition).toBe(undefined);
+
+ await dispatch(
+ actions.setBreakpointOptions(cx, loc, {
+ condition: "const foo = 0",
+ getTextForLine: () => {},
+ })
+ );
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && bp.options.condition).toBe("const foo = 0");
+ });
+
+ it("should set the condition and enable a breakpoint", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ 5: [1] }));
+
+ const source = await dispatch(actions.newGeneratedSource(makeSource("a")));
+ const loc = createLocation({
+ source,
+ line: 5,
+ column: 1,
+ sourceUrl: "http://localhost:8000/examples/a",
+ });
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ await dispatch(actions.loadGeneratedSourceText({ cx, sourceActor }));
+
+ await dispatch(actions.addBreakpoint(cx, loc));
+ let bp = selectors.getBreakpoint(getState(), loc);
+ if (!bp) {
+ throw new Error("no breakpoint");
+ }
+
+ await dispatch(actions.disableBreakpoint(cx, bp));
+
+ bp = selectors.getBreakpoint(getState(), loc);
+ expect(bp && bp.options.condition).toBe(undefined);
+
+ await dispatch(
+ actions.setBreakpointOptions(cx, loc, {
+ condition: "const foo = 0",
+ getTextForLine: () => {},
+ })
+ );
+ const newBreakpoint = selectors.getBreakpoint(getState(), loc);
+ expect(newBreakpoint && !newBreakpoint.disabled).toBe(true);
+ expect(newBreakpoint && newBreakpoint.options.condition).toBe(
+ "const foo = 0"
+ );
+ });
+
+ it("should remove the pretty-printed breakpoint that was added", async () => {
+ const { dispatch, getState, cx } = createStore(mockClient({ 1: [0] }));
+
+ const source = await dispatch(
+ actions.newGeneratedSource(makeSource("a.js"))
+ );
+ const loc = createLocation({
+ source,
+ line: 1,
+ column: 0,
+ sourceUrl: "http://localhost:8000/examples/a.js",
+ });
+ const sourceActor = selectors.getFirstSourceActorForGeneratedSource(
+ getState(),
+ source.id
+ );
+ await dispatch(actions.loadGeneratedSourceText({ cx, sourceActor }));
+
+ await dispatch(actions.addBreakpoint(cx, loc));
+ await dispatch(actions.togglePrettyPrint(cx, "a.js"));
+
+ const breakpoint = selectors.getBreakpointsList(getState())[0];
+
+ await dispatch(actions.removeBreakpoint(cx, breakpoint));
+
+ const breakpointList = selectors.getPendingBreakpointList(getState());
+ expect(breakpointList.length).toBe(0);
+ });
+});