diff --git a/circle.yml b/circle.yml index 399e55a4cd..d545af95e9 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,6 @@ machine: node: - version: 7.0 + version: 7.7.2 services: - docker environment: @@ -20,7 +20,7 @@ test: - mkdir -p $CIRCLE_TEST_REPORTS/mocha - mkdir -p $CIRCLE_TEST_REPORTS/flow-coverage - mkdir -p $CIRCLE_TEST_REPORTS/jest-coverage - - node ./node_modules/.bin/jest --testResultsProcessor jest-junit-reporter --coverage --coverageDirectory=$CIRCLE_TEST_REPORTS/jest-coverage + - jest -i --testResultsProcessor jest-junit-reporter --coverage --coverageDirectory=$CIRCLE_TEST_REPORTS/jest-coverage - yarn license-check - yarn lint-css - yarn lint-js diff --git a/flow-typed/debugger-html.js b/flow-typed/debugger-html.js index f9acdc3391..2e1beb4473 100644 --- a/flow-typed/debugger-html.js +++ b/flow-typed/debugger-html.js @@ -64,6 +64,20 @@ declare module "debugger-html" { actualLocation: Location }; + /** + * PendingBreakpoint + * + * @memberof types + * @static + */ + declare type PendingBreakpoint = { + location: Location, + loading: boolean, + disabled: boolean, + text: string, + condition: ?string + }; + /** * Frame ID * diff --git a/package.json b/package.json index f7d59c1be5..fd9e3c6f74 100644 --- a/package.json +++ b/package.json @@ -155,6 +155,7 @@ ], "testPathIgnorePatterns": [ "/node_modules/", + "/helpers/", "/test/", "/utils/tests/fixtures/" ], diff --git a/src/actions/breakpoints.js b/src/actions/breakpoints.js index 1a35b93080..f020542816 100644 --- a/src/actions/breakpoints.js +++ b/src/actions/breakpoints.js @@ -83,7 +83,6 @@ export function addBreakpoint( if (!text) { text = getTextForLine ? getTextForLine(actualLocation.line) : ""; } - return { id, actualLocation, text, hitCount }; })() }); @@ -110,7 +109,7 @@ export function removeBreakpoint(location: Location) { return _removeOrDisableBreakpoint(location); } -function _removeOrDisableBreakpoint(location, isDisabled) { +function _removeOrDisableBreakpoint(location, isDisabled = false) { return ({ dispatch, getState, client }: ThunkArgs) => { let bp = getBreakpoint(getState(), location); if (!bp) { diff --git a/src/actions/sources.js b/src/actions/sources.js index c084404472..87ebcc9db6 100644 --- a/src/actions/sources.js +++ b/src/actions/sources.js @@ -46,29 +46,38 @@ function checkSelectedSource(state, dispatch, source) { } } -function checkPendingBreakpoints(state, dispatch, source) { - const pendingBreakpoints = getPendingBreakpoints(state); - - if (pendingBreakpoints) { - pendingBreakpoints.forEach(pendingBreakpoint => { - const { - location: { line, sourceUrl, column }, - condition - } = pendingBreakpoint; - const sameSource = sourceUrl && sourceUrl == source.url; - - const location = { sourceId: source.id, sourceUrl, line, column }; +async function checkPendingBreakpoint( + state, + dispatch, + pendingBreakpoint, + source +) { + const { + location: { line, sourceUrl, column }, + condition + } = pendingBreakpoint; + const sameSource = sourceUrl && sourceUrl === source.url; + const location = { sourceId: source.id, sourceUrl, line, column }; + const bp = getBreakpoint(state, location); + + if (sameSource && !bp) { + if (location.column && isEnabled("columnBreakpoints")) { + await dispatch(addBreakpoint(location, { condition })); + } else { + await dispatch(addBreakpoint(location, { condition })); + } + } +} - const bp = getBreakpoint(state, location); +async function checkPendingBreakpoints(state, dispatch, source) { + const pendingBreakpoints = getPendingBreakpoints(state); + if (!pendingBreakpoints) { + return; + } - if (sameSource && !bp) { - if (location.column && isEnabled("columnBreakpoints")) { - dispatch(addBreakpoint(location, { condition })); - } else { - dispatch(addBreakpoint(location, { condition })); - } - } - }); + const pendingBreakpointsArray = pendingBreakpoints.valueSeq().toJS(); + for (let pendingBreakpoint of pendingBreakpointsArray) { + await checkPendingBreakpoint(state, dispatch, pendingBreakpoint, source); } } @@ -78,23 +87,27 @@ function checkPendingBreakpoints(state, dispatch, source) { * @static */ export function newSource(source: Source) { - return ({ dispatch, getState }: ThunkArgs) => { + return async ({ dispatch, getState }: ThunkArgs) => { + dispatch({ type: constants.ADD_SOURCE, source }); + if (prefs.clientSourceMapsEnabled) { - dispatch(loadSourceMap(source)); + await dispatch(loadSourceMap(source)); } - dispatch({ type: constants.ADD_SOURCE, source }); - checkSelectedSource(getState(), dispatch, source); - checkPendingBreakpoints(getState(), dispatch, source); + await checkPendingBreakpoints(getState(), dispatch, source); }; } export function newSources(sources: Source[]) { - return ({ dispatch, getState }: ThunkArgs) => { - sources - .filter(source => !getSource(getState(), source.id)) - .forEach(source => dispatch(newSource(source))); + return async ({ dispatch, getState }: ThunkArgs) => { + const filteredSources = sources.filter( + source => !getSource(getState(), source.id) + ); + + for (let source of filteredSources) { + await dispatch(newSource(source)); + } }; } diff --git a/src/actions/tests/breakpoints.js b/src/actions/tests/breakpoints.js index 5fa16bfc1d..532a789294 100644 --- a/src/actions/tests/breakpoints.js +++ b/src/actions/tests/breakpoints.js @@ -1,34 +1,54 @@ -import { createStore, selectors, actions } from "../../utils/test-head"; import expect from "expect.js"; -const simpleMockThreadClient = { - setBreakpoint: (location, condition) => { - return new Promise((resolve, reject) => { - resolve({ id: "hi", actualLocation: location }); - }); - }, - - removeBreakpoint: id => { - return new Promise((resolve, reject) => { - resolve({ status: "done" }); - }); - }, - - setBreakpointCondition: (id, location, condition, noSliding) => { - return new Promise((resolve, reject) => { - resolve({ sourceId: "a", line: 5 }); - }); - } -}; +import { createStore, selectors, actions } from "../../utils/test-head"; +import { + simulateCorrectThreadClient, + simpleMockThreadClient +} from "./helpers/breakpoints.js"; describe("breakpoints", () => { it("should add a breakpoint", async () => { const { dispatch, getState } = createStore(simpleMockThreadClient); + const loc1 = { sourceId: "a", line: 5 }; - await dispatch(actions.addBreakpoint({ sourceId: "a", line: 5 })); - await dispatch(actions.addBreakpoint({ sourceId: "b", line: 6 })); + await dispatch(actions.addBreakpoint(loc1)); + const bps = selectors.getBreakpoints(getState()); + const bp = selectors.getBreakpoint(getState(), loc1); + expect(bps.size).to.be(1); + expect(bp.location).to.eql(loc1); + }); - expect(selectors.getBreakpoints(getState()).size).to.be(2); + it("should not re-add a breakpoint", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + const loc1 = { sourceId: "a", line: 5 }; + + await dispatch(actions.addBreakpoint(loc1)); + let bps = selectors.getBreakpoints(getState()); + const bp = selectors.getBreakpoint(getState(), loc1); + expect(bps.size).to.be(1); + expect(bp.location).to.eql(loc1); + + await dispatch(actions.addBreakpoint(loc1)); + bps = selectors.getBreakpoints(getState()); + expect(bps.size).to.be(1); + }); + + describe("adding a breakpoint to an invalid location", async () => { + it("adds only one breakpoint with a corrected location", async () => { + const invalidLocation = { sourceId: "a", line: 5 }; + const { + correctedThreadClient, + correctedLocation + } = simulateCorrectThreadClient(2, invalidLocation); + const { dispatch, getState } = createStore(correctedThreadClient); + + await dispatch(actions.addBreakpoint(invalidLocation)); + const state = getState(); + const bps = selectors.getBreakpoints(state); + const bp = selectors.getBreakpoint(state, correctedLocation); + expect(bps.size).to.be(1); + expect(bp.location).to.eql(correctedLocation); + }); }); it("should remove a breakpoint", async () => { diff --git a/src/actions/tests/helpers/breakpoints.js b/src/actions/tests/helpers/breakpoints.js new file mode 100644 index 0000000000..f524f2905e --- /dev/null +++ b/src/actions/tests/helpers/breakpoints.js @@ -0,0 +1,79 @@ +import { makeLocationId } from "../../../reducers/breakpoints"; + +export const theMockedPendingBreakpoint = { + location: { + sourceUrl: "http://localhost:8000/examples/bar.js", + line: 5, + column: undefined + }, + condition: "3", + disabled: false +}; + +export function generateCorrectedBreakpoint(breakpoint, correctedLocation) { + return Object.assign({}, breakpoint, { location: correctedLocation }); +} + +function generateCorrectingThreadClient(offset = 0) { + return { + setBreakpoint: (location, condition) => { + const actualLocation = Object.assign({}, location, { + line: location.line + offset + }); + + return Promise.resolve({ + id: makeLocationId(location), + actualLocation, + condition + }); + } + }; +} + +/* in some cases, a breakpoint may be added, but the source will respond + * with a different breakpoint location. This is due to the breakpoint being + * added between functions, or somewhere that doesnt make sense. This function + * simulates that behavior. + * */ +export function simulateCorrectThreadClient(offset, location) { + const correctedThreadClient = generateCorrectingThreadClient(offset); + const offsetLine = { line: location.line + offset }; + const correctedLocation = Object.assign({}, location, offsetLine); + return { correctedThreadClient, correctedLocation }; +} + +export function generateBreakpoint(filename) { + return { + location: { + sourceUrl: `http://localhost:8000/examples/${filename}`, + sourceId: filename, + line: 5 + }, + condition: null, + disabled: false + }; +} + +export function generatePendingBreakpoint(breakpoint) { + const { + location: { sourceUrl, line, column }, + condition, + disabled + } = breakpoint; + + return { + location: { sourceUrl, line, column }, + condition, + disabled + }; +} + +export const simpleMockThreadClient = { + setBreakpoint: (location, _condition) => + Promise.resolve({ id: "hi", actualLocation: location }), + + removeBreakpoint: _id => Promise.resolve({ status: "done" }), + + setBreakpointCondition: (_id, _location, _condition, _noSliding) => + Promise.resolve({ sourceId: "a", line: 5 }) +}; diff --git a/src/actions/tests/pending-breakpoints.js b/src/actions/tests/pending-breakpoints.js new file mode 100644 index 0000000000..2f4582633a --- /dev/null +++ b/src/actions/tests/pending-breakpoints.js @@ -0,0 +1,206 @@ +// TODO: we would like to mock this in the local tests +import { + generatePendingBreakpoint, + generateBreakpoint, + theMockedPendingBreakpoint, + simulateCorrectThreadClient, + generateCorrectedBreakpoint, + simpleMockThreadClient +} from "./helpers/breakpoints.js"; + +jest.mock("../../utils/prefs", () => ({ + prefs: { + expressions: [], + pendingBreakpoints: { + "http://localhost:8000/examples/bar.js:5:": { + location: { + sourceUrl: "http://localhost:8000/examples/bar.js", + line: 5, + column: undefined + }, + condition: "3", + disabled: false + } + } + } +})); + +import { + createStore, + selectors, + actions, + makeSource +} from "../../utils/test-head"; +import { makePendingLocationId } from "../../reducers/breakpoints"; +import expect from "expect.js"; + +describe("when adding breakpoints", () => { + it("a corresponding pending breakpoint should be added", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + const bp = generateBreakpoint("foo"); + const id = makePendingLocationId(bp.location); + + await dispatch(actions.addBreakpoint(bp.location)); + const pendingBps = selectors.getPendingBreakpoints(getState()); + expect(pendingBps.size).to.be(2); + expect(pendingBps.get(id)).to.eql(generatePendingBreakpoint(bp)); + }); + + describe("adding and deleting breakpoints", () => { + let breakpoint1; + let breakpoint2; + let breakpointLocationId1; + let breakpointLocationId2; + + beforeEach(() => { + breakpoint1 = generateBreakpoint("foo"); + breakpoint2 = generateBreakpoint("foo2"); + breakpointLocationId1 = makePendingLocationId(breakpoint1.location); + breakpointLocationId2 = makePendingLocationId(breakpoint2.location); + }); + + it("add a corresponding pendingBreakpoint for each addition", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + await dispatch(actions.addBreakpoint(breakpoint1.location)); + await dispatch(actions.addBreakpoint(breakpoint2.location)); + + const pendingBps = selectors.getPendingBreakpoints(getState()); + expect(pendingBps.get(breakpointLocationId1)).to.eql( + generatePendingBreakpoint(breakpoint1) + ); + expect(pendingBps.get(breakpointLocationId2)).to.eql( + generatePendingBreakpoint(breakpoint2) + ); + }); + + it("remove a corresponding pending breakpoint when deleting", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + await dispatch(actions.addBreakpoint(breakpoint1.location)); + await dispatch(actions.addBreakpoint(breakpoint2.location)); + await dispatch(actions.removeBreakpoint(breakpoint1.location)); + + const pendingBps = selectors.getPendingBreakpoints(getState()); + expect(pendingBps.has(breakpointLocationId1)).not.to.be(true); + expect(pendingBps.has(breakpointLocationId2)).to.be(true); + }); + }); +}); + +describe("when changing an existing breakpoint", () => { + it("updates corresponding pendingBreakpoint", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + const bp = generateBreakpoint("foo"); + const id = makePendingLocationId(bp.location); + + await dispatch(actions.addBreakpoint(bp.location)); + await dispatch( + actions.setBreakpointCondition(bp.location, { condition: "2" }) + ); + const bps = selectors.getPendingBreakpoints(getState()); + const breakpoint = bps.get(id); + expect(breakpoint.condition).to.be("2"); + }); + + it("if disabled, updates corresponding pendingBreakpoint", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + const bp = generateBreakpoint("foo"); + const id = makePendingLocationId(bp.location); + + await dispatch(actions.addBreakpoint(bp.location)); + await dispatch(actions.disableBreakpoint(bp.location)); + const bps = selectors.getPendingBreakpoints(getState()); + const breakpoint = bps.get(id); + expect(breakpoint.disabled).to.be(true); + }); + + it("does not delete the pre-existing pendingBreakpoint", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + const bp = generateBreakpoint("foo"); + const id = makePendingLocationId(bp.location); + + await dispatch(actions.addBreakpoint(bp.location)); + await dispatch( + actions.setBreakpointCondition(bp.location, { condition: "2" }) + ); + const bps = selectors.getPendingBreakpoints(getState()); + const breakpoint = bps.get(id); + expect(breakpoint.condition).to.be("2"); + }); +}); + +describe("initializing when pending breakpoints exist in perfs", () => { + it("syncs pending breakpoints", async () => { + const { getState } = createStore(simpleMockThreadClient); + const id = makePendingLocationId(theMockedPendingBreakpoint.location); + const bps = selectors.getPendingBreakpoints(getState()); + const bp = bps.get(id); + expect(bp).to.eql(generatePendingBreakpoint(theMockedPendingBreakpoint)); + }); + + it("readding breakpoints update existing pending breakpoints", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + const bar = generateBreakpoint("bar.js"); + + await dispatch(actions.addBreakpoint(bar.location)); + + const bps = selectors.getPendingBreakpoints(getState()); + expect(bps.size).to.be(1); + }); + + it("adding bps doesn't remove existing pending breakpoints", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + const bp = generateBreakpoint("foo.js"); + + await dispatch(actions.addBreakpoint(bp.location)); + + const bps = selectors.getPendingBreakpoints(getState()); + expect(bps.size).to.be(2); + }); +}); + +describe("adding sources", () => { + it("corresponding breakpoints are added for a single source", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + + let bps = selectors.getBreakpoints(getState()); + expect(bps.size).to.be(0); + + const source = makeSource("bar.js"); + await dispatch(actions.newSource(source)); + bps = selectors.getBreakpoints(getState()); + expect(bps.size).to.be(1); + }); + + it("add corresponding breakpoints for multiple sources", async () => { + const { dispatch, getState } = createStore(simpleMockThreadClient); + + let bps = selectors.getBreakpoints(getState()); + expect(bps.size).to.be(0); + + const source1 = makeSource("bar.js"); + const source2 = makeSource("foo.js"); + await dispatch(actions.newSources([source1, source2])); + bps = selectors.getBreakpoints(getState()); + expect(bps.size).to.be(1); + }); +}); + +describe("invalid breakpoint location", () => { + it("a corrected corresponding pending breakpoint is added", async () => { + // setup + const bp = generateBreakpoint("foo"); + const { + correctedThreadClient, + correctedLocation + } = simulateCorrectThreadClient(2, bp.location); + const { dispatch, getState } = createStore(correctedThreadClient); + const slidBp = generateCorrectedBreakpoint(bp, correctedLocation); + const correctedPendingId = makePendingLocationId(correctedLocation); + + // test + await dispatch(actions.addBreakpoint(bp.location)); + const pendingBps = selectors.getPendingBreakpoints(getState()); + const pendingBp = pendingBps.get(correctedPendingId); + expect(pendingBp).to.eql(generatePendingBreakpoint(slidBp)); + }); +}); diff --git a/src/actions/tests/sources.js b/src/actions/tests/sources.js index 7faae59e86..132ae83eec 100644 --- a/src/actions/tests/sources.js +++ b/src/actions/tests/sources.js @@ -39,10 +39,10 @@ const threadClient = { process.on("unhandledRejection", (reason, p) => {}); describe("sources", () => { - it("should add sources to state", () => { + it("should add sources to state", async () => { const { dispatch, getState } = createStore(); - dispatch(actions.newSource(makeSource("base.js"))); - dispatch(actions.newSource(makeSource("jquery.js"))); + await dispatch(actions.newSource(makeSource("base.js"))); + await dispatch(actions.newSource(makeSource("jquery.js"))); expect(getSources(getState()).size).to.equal(2); const base = getSource(getState(), "base.js"); @@ -51,29 +51,29 @@ describe("sources", () => { expect(jquery.get("id")).to.equal("jquery.js"); }); - it("should select a source", () => { + it("should select a source", async () => { // Note that we pass an empty client in because the action checks // if it exists. const { dispatch, getState } = createStore(threadClient); - dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); dispatch(actions.selectSource("foo.js")); expect(getSelectedSource(getState()).get("id")).to.equal("foo.js"); }); - it("should automatically select a pending source", () => { + it("should automatically select a pending source", async () => { const { dispatch, getState } = createStore(threadClient); const baseSource = makeSource("base.js"); dispatch(actions.selectSourceURL(baseSource.url)); expect(getSelectedSource(getState())).to.be(undefined); - dispatch(actions.newSource(baseSource)); + await dispatch(actions.newSource(baseSource)); expect(getSelectedSource(getState()).get("url")).to.be(baseSource.url); }); - it("should open a tab for the source", () => { + it("should open a tab for the source", async () => { const { dispatch, getState } = createStore(threadClient); - dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); dispatch(actions.selectSource("foo.js")); const tabs = getSourceTabs(getState()); @@ -81,11 +81,11 @@ describe("sources", () => { expect(tabs.get(0)).to.equal("http://localhost:8000/examples/foo.js"); }); - it("should select previous tab on tab closed", () => { + it("should select previous tab on tab closed", async () => { const { dispatch, getState } = createStore(threadClient); - dispatch(actions.newSource(makeSource("foo.js"))); - dispatch(actions.newSource(makeSource("bar.js"))); - dispatch(actions.newSource(makeSource("baz.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("bar.js"))); + await dispatch(actions.newSource(makeSource("baz.js"))); dispatch(actions.selectSource("foo.js")); dispatch(actions.selectSource("bar.js")); dispatch(actions.selectSource("baz.js")); @@ -94,11 +94,11 @@ describe("sources", () => { expect(getSourceTabs(getState()).size).to.be(2); }); - it("should select next tab on tab closed if no previous tab", () => { + it("should select next tab on tab closed if no previous tab", async () => { const { dispatch, getState } = createStore(threadClient); - dispatch(actions.newSource(makeSource("foo.js"))); - dispatch(actions.newSource(makeSource("bar.js"))); - dispatch(actions.newSource(makeSource("baz.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("bar.js"))); + await dispatch(actions.newSource(makeSource("baz.js"))); dispatch(actions.selectSource("foo.js")); dispatch(actions.selectSource("bar.js")); dispatch(actions.selectSource("baz.js")); @@ -151,9 +151,9 @@ describe("sources", () => { }); describe("closing tabs", () => { - it("closing a tab", () => { + it("closing a tab", async () => { const { dispatch, getState } = createStore(threadClient); - dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); dispatch(actions.selectSource("foo.js")); dispatch(actions.closeTab("http://localhost:8000/examples/foo.js")); @@ -161,10 +161,10 @@ describe("closing tabs", () => { expect(getSourceTabs(getState()).size).to.be(0); }); - it("closing the inactive tab", () => { + it("closing the inactive tab", async () => { const { dispatch, getState } = createStore(threadClient); - dispatch(actions.newSource(makeSource("foo.js"))); - dispatch(actions.newSource(makeSource("bar.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("bar.js"))); dispatch(actions.selectSource("foo.js")); dispatch(actions.selectSource("bar.js")); dispatch(actions.closeTab("http://localhost:8000/examples/foo.js")); @@ -173,9 +173,9 @@ describe("closing tabs", () => { expect(getSourceTabs(getState()).size).to.be(1); }); - it("closing the only tab", () => { + it("closing the only tab", async () => { const { dispatch, getState } = createStore(threadClient); - dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); dispatch(actions.selectSource("foo.js")); dispatch(actions.closeTab("http://localhost:8000/examples/foo.js")); @@ -183,10 +183,10 @@ describe("closing tabs", () => { expect(getSourceTabs(getState()).size).to.be(0); }); - it("closing the active tab", () => { + it("closing the active tab", async () => { const { dispatch, getState } = createStore(threadClient); - dispatch(actions.newSource(makeSource("foo.js"))); - dispatch(actions.newSource(makeSource("bar.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("bar.js"))); dispatch(actions.selectSource("foo.js")); dispatch(actions.selectSource("bar.js")); dispatch(actions.closeTab("http://localhost:8000/examples/bar.js")); @@ -195,11 +195,11 @@ describe("closing tabs", () => { expect(getSourceTabs(getState()).size).to.be(1); }); - it("closing many inactive tabs", () => { + it("closing many inactive tabs", async () => { const { dispatch, getState } = createStore({}); - dispatch(actions.newSource(makeSource("foo.js"))); - dispatch(actions.newSource(makeSource("bar.js"))); - dispatch(actions.newSource(makeSource("bazz.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("bar.js"))); + await dispatch(actions.newSource(makeSource("bazz.js"))); dispatch(actions.selectSource("foo.js")); dispatch(actions.selectSource("bar.js")); dispatch(actions.selectSource("bazz.js")); @@ -214,11 +214,11 @@ describe("closing tabs", () => { expect(getSourceTabs(getState()).size).to.be(1); }); - it("closing many tabs including the active tab", () => { + it("closing many tabs including the active tab", async () => { const { dispatch, getState } = createStore({}); - dispatch(actions.newSource(makeSource("foo.js"))); - dispatch(actions.newSource(makeSource("bar.js"))); - dispatch(actions.newSource(makeSource("bazz.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("bar.js"))); + await dispatch(actions.newSource(makeSource("bazz.js"))); dispatch(actions.selectSource("foo.js")); dispatch(actions.selectSource("bar.js")); dispatch(actions.selectSource("bazz.js")); @@ -233,10 +233,10 @@ describe("closing tabs", () => { expect(getSourceTabs(getState()).size).to.be(1); }); - it("closing all the tabs", () => { + it("closing all the tabs", async () => { const { dispatch, getState } = createStore({}); - dispatch(actions.newSource(makeSource("foo.js"))); - dispatch(actions.newSource(makeSource("bar.js"))); + await dispatch(actions.newSource(makeSource("foo.js"))); + await dispatch(actions.newSource(makeSource("bar.js"))); dispatch(actions.selectSource("foo.js")); dispatch(actions.selectSource("bar.js")); dispatch( diff --git a/src/client/firefox/commands.js b/src/client/firefox/commands.js index 7960da5cef..78218543bc 100644 --- a/src/client/firefox/commands.js +++ b/src/client/firefox/commands.js @@ -97,6 +97,7 @@ function onNewBreakpoint( ): BreakpointResult { const bpClient = res[1]; let actualLocation = res[0].actualLocation; + bpClients[bpClient.actor] = bpClient; // Firefox only returns `actualLocation` if it actually changed, @@ -105,6 +106,7 @@ function onNewBreakpoint( actualLocation = actualLocation ? { sourceId: actualLocation.source.actor, + sourceUrl: location.sourceUrl, line: actualLocation.line, column: actualLocation.column } diff --git a/src/reducers/breakpoints.js b/src/reducers/breakpoints.js index 4a191480c0..7854aa06a9 100644 --- a/src/reducers/breakpoints.js +++ b/src/reducers/breakpoints.js @@ -14,13 +14,13 @@ import * as I from "immutable"; import makeRecord from "../utils/makeRecord"; import { prefs } from "../utils/prefs"; -import type { Breakpoint, Location } from "../types"; +import type { Breakpoint, PendingBreakpoint, Location } from "../types"; import type { Action } from "../actions/types"; import type { Record } from "../utils/makeRecord"; export type BreakpointsState = { breakpoints: I.Map, - pendingBreakpoints: ?I.Map, + pendingBreakpoints: I.Map, breakpointsDisabled: false }; @@ -51,9 +51,16 @@ function locationMoved(location, newLocation) { } export function makeLocationId(location: Location) { - let { sourceId, line, column } = location; - column = column || ""; - return `${sourceId}:${line}:${column}`; + const { sourceId, line, column } = location; + const columnString = column || ""; + return `${sourceId}:${line}:${columnString}`; +} + +export function makePendingLocationId(location: Location) { + const { sourceUrl, line, column } = location; + const sourceUrlString = sourceUrl || ""; + const columnString = column || ""; + return `${sourceUrlString}:${line}:${columnString}`; } function allBreakpointsDisabled(state) { @@ -64,14 +71,12 @@ function update(state: Record = State(), action: Action) { switch (action.type) { case "ADD_BREAKPOINT": { const newState = addBreakpoint(state, action); - if (newState) { - setPendingBreakpoints(newState); - } + setPendingBreakpoints(newState); return newState; } case "REMOVE_BREAKPOINT": { - const newState = removeBreakpoint(state, action); + const newState = removeOrDisableBreakpoint(state, action); setPendingBreakpoints(newState); return newState; } @@ -87,31 +92,9 @@ function update(state: Record = State(), action: Action) { } case "SET_BREAKPOINT_CONDITION": { - const id = makeLocationId(action.breakpoint.location); - - if (action.status === "start") { - const bp = state.breakpoints.get(id); - return state.setIn( - ["breakpoints", id], - updateObj(bp, { - loading: true, - condition: action.condition - }) - ); - } else if (action.status === "done") { - const bp = state.breakpoints.get(id); - return state.setIn( - ["breakpoints", id], - updateObj(bp, { - id: action.value.id, - loading: false - }) - ); - } else if (action.status === "error") { - return state.deleteIn(["breakpoints", id]); - } - - break; + const newState = setCondition(state, action); + setPendingBreakpoints(newState); + return newState; } } @@ -120,11 +103,10 @@ function update(state: Record = State(), action: Action) { function addBreakpoint(state, action) { const id = makeLocationId(action.breakpoint.location); - if (action.status === "start") { let bp = state.breakpoints.get(id) || action.breakpoint; - return state + const updatedState = state .setIn( ["breakpoints", id], updateObj(bp, { @@ -137,6 +119,8 @@ function addBreakpoint(state, action) { }) ) .set("breakpointsDisabled", false); + + return updatedState; } if (action.status === "done") { @@ -146,19 +130,13 @@ function addBreakpoint(state, action) { // If the breakpoint moved, update the map if (locationMoved(location, actualLocation)) { - state = state.deleteIn(["breakpoints", id]); - - const movedId = makeLocationId(actualLocation); - const currentBp = - state.breakpoints.get(movedId) || fromJS(action.breakpoint); - const newBp = updateObj(currentBp, { location: actualLocation }); - state = state.setIn(["breakpoints", movedId], newBp); + state = slideBreakpoint(state, action); location = actualLocation; } const locationId = makeLocationId(location); const bp = state.breakpoints.get(locationId); - return state.setIn( + const updatedState = state.setIn( ["breakpoints", locationId], updateObj(bp, { id: breakpointId, @@ -167,6 +145,8 @@ function addBreakpoint(state, action) { text: text }) ); + + return updatePendingBreakpoint(updatedState, bp); } if (action.status === "error") { @@ -175,56 +155,107 @@ function addBreakpoint(state, action) { } } -function removeBreakpoint(state, action) { - if (action.status === "done") { - const id = makeLocationId(action.breakpoint.location); +function disableBreakpoint(state, id) { + const bp = state.breakpoints.get(id); + const breakpoint = updateObj(bp, { + loading: false, + disabled: true + }); + const updatedState = state.setIn(["breakpoints", id], breakpoint); + return updatePendingBreakpoint(updatedState, breakpoint); +} - if (action.disabled) { - const bp = state.breakpoints.get(id); - const updatedState = state.setIn( - ["breakpoints", id], - updateObj(bp, { - loading: false, - disabled: true - }) - ); +function deleteBreakpoint(state, id, pendingId) { + return state + .deleteIn(["breakpoints", id]) + .deleteIn(["pendingBreakpoints", pendingId]); +} - return updatedState.set( - "breakpointsDisabled", - allBreakpointsDisabled(updatedState) - ); - } +function removeOrDisableBreakpoint(state, action) { + if (action.status != "done") { + return state; + } - const updatedState = state.deleteIn(["breakpoints", id]); + const id = makeLocationId(action.breakpoint.location); + const pendingId = makePendingLocationId(action.breakpoint.location); + + const updatedState = action.disabled + ? disableBreakpoint(state, id) + : deleteBreakpoint(state, id, pendingId); + + return updatedState.set( + "breakpointsDisabled", + allBreakpointsDisabled(updatedState) + ); +} + +function setCondition(state, action) { + const id = makeLocationId(action.breakpoint.location); - return updatedState.set( - "breakpointsDisabled", - allBreakpointsDisabled(updatedState) + if (action.status === "start") { + const bp = state.breakpoints.get(id); + return state.setIn( + ["breakpoints", id], + updateObj(bp, { + loading: true, + condition: action.condition + }) ); } - return state; + if (action.status === "done") { + const bp = state.breakpoints.get(id); + const updatedBreakpoint = updateObj(bp, { + id: action.value.id, + loading: false + }); + + let updatedState = state.setIn(["breakpoints", id], updatedBreakpoint); + + return updatePendingBreakpoint(updatedState, updatedBreakpoint); + } + + if (action.status === "error") { + return state.deleteIn(["breakpoints", id]); + } } -function makePendingBreakpoint(bp: any) { +function slideBreakpoint(state, action) { + const { actualLocation, id } = action.value; + const { breakpoint } = action; + const currentBp = state.breakpoints.get(id) || fromJS(breakpoint); + + const locationId = makeLocationId(breakpoint.location); + const movedLocationId = makeLocationId(actualLocation); + const updatedState = state.deleteIn(["breakpoints", locationId]); + + return updatedState.setIn( + ["breakpoints", movedLocationId], + updateObj(currentBp, { location: actualLocation }) + ); +} + +export function makePendingBreakpoint(bp: any) { const { location: { sourceUrl, line, column }, condition, disabled } = bp; const location = { sourceUrl, line, column }; return { condition, disabled, location }; } -function filterByNotLoading(bp: any): boolean { - return !bp.loading; +function setPendingBreakpoints(state: any) { + prefs.pendingBreakpoints = state.pendingBreakpoints; } -function setPendingBreakpoints(state) { - prefs.pendingBreakpoints = Object.values(state.get("breakpoints").toJS()) - .filter(filterByNotLoading) - .map(makePendingBreakpoint); +function updatePendingBreakpoint(state, breakpoint) { + const id = makePendingLocationId(breakpoint.location); + return state.setIn( + ["pendingBreakpoints", id], + makePendingBreakpoint(breakpoint) + ); } function restorePendingBreakpoints() { - return prefs.pendingBreakpoints; + return I.Map(prefs.pendingBreakpoints); } // Selectors diff --git a/src/test/mochitest/browser_dbg-sources.js b/src/test/mochitest/browser_dbg-sources.js index 39b6ed6c82..ed70bd86f1 100644 --- a/src/test/mochitest/browser_dbg-sources.js +++ b/src/test/mochitest/browser_dbg-sources.js @@ -11,11 +11,11 @@ function* waitForSourceCount(dbg, i) { }); } -add_task(function* () { +add_task(function*() { const dbg = yield initDebugger("doc-sources.html"); const { selectors: { getSelectedSource }, getState } = dbg; - yield waitForSources(dbg, "simple1"); + yield waitForSources(dbg, "simple1", "simple2", "nested-source", "long.js"); // Expand nodes and make sure more sources appear. is(findAllElements(dbg, "sourceNodes").length, 2); @@ -27,15 +27,21 @@ add_task(function* () { is(findAllElements(dbg, "sourceNodes").length, 8); // Select a source. - ok(!findElementWithSelector(dbg, ".sources-list .focused"), - "Source is not focused"); + ok( + !findElementWithSelector(dbg, ".sources-list .focused"), + "Source is not focused" + ); const selected = waitForDispatch(dbg, "SELECT_SOURCE"); clickElement(dbg, "sourceNode", 4); yield selected; - ok(findElementWithSelector(dbg, ".sources-list .focused"), - "Source is focused"); - ok(getSelectedSource(getState()).get("url").includes("nested-source.js"), - "The right source is selected"); + ok( + findElementWithSelector(dbg, ".sources-list .focused"), + "Source is focused" + ); + ok( + getSelectedSource(getState()).get("url").includes("nested-source.js"), + "The right source is selected" + ); // Make sure new sources appear in the list. ContentTask.spawn(gBrowser.selectedBrowser, null, function() { @@ -45,16 +51,20 @@ add_task(function* () { }); yield waitForSourceCount(dbg, 9); - is(findElement(dbg, "sourceNode", 7).textContent, - "math.min.js", - "The dynamic script exists"); + is( + findElement(dbg, "sourceNode", 7).textContent, + "math.min.js", + "The dynamic script exists" + ); // Make sure named eval sources appear in the list. ContentTask.spawn(gBrowser.selectedBrowser, null, function() { content.eval("window.evaledFunc = function() {} //# sourceURL=evaled.js"); }); yield waitForSourceCount(dbg, 11); - is(findElement(dbg, "sourceNode", 2).textContent, - "evaled.js", - "The eval script exists"); + is( + findElement(dbg, "sourceNode", 2).textContent, + "evaled.js", + "The eval script exists" + ); }); diff --git a/src/test/tests-setup.js b/src/test/tests-setup.js index 2803211773..0704341bfa 100644 --- a/src/test/tests-setup.js +++ b/src/test/tests-setup.js @@ -1,5 +1,4 @@ global.Worker = require("workerjs"); - global.L10N = { getStr: () => {} }; const path = require("path"); diff --git a/src/types.js b/src/types.js index dc9faccfd0..664e490afa 100644 --- a/src/types.js +++ b/src/types.js @@ -30,6 +30,7 @@ export type Mode = export type { Breakpoint, + PendingBreakpoint, Frame, Grip, LoadedObject,