diff options
Diffstat (limited to 'devtools/client/debugger/src/actions/tests')
17 files changed, 1241 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap new file mode 100644 index 0000000000..f27eb26f50 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`expressions should get the autocomplete matches for the input 1`] = ` +Array [ + "toLocaleString", + "toSource", + "toString", + "toolbar", + "top", +] +`; diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap new file mode 100644 index 0000000000..741e45d6d8 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`initializing when pending breakpoints exist in prefs syncs pending breakpoints 1`] = ` +Object { + "http://localhost:8000/examples/bar.js:5:2": Object { + "astLocation": Object { + "index": 0, + "name": undefined, + "offset": Object { + "line": 5, + }, + }, + "disabled": false, + "generatedLocation": Object { + "column": 2, + "line": 5, + "source": Object { + "id": "", + "url": "http://localhost:8000/examples/bar.js", + }, + "sourceActor": null, + "sourceActorId": undefined, + "sourceId": "", + "sourceUrl": "http://localhost:8000/examples/bar.js", + }, + "location": Object { + "column": 2, + "line": 5, + "source": Object { + "id": "", + "url": "http://localhost:8000/examples/bar.js", + }, + "sourceActor": null, + "sourceActorId": undefined, + "sourceId": "", + "sourceUrl": "http://localhost:8000/examples/bar.js", + }, + "options": Object { + "condition": null, + "hidden": false, + }, + }, +} +`; diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/preview.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/preview.spec.js.snap new file mode 100644 index 0000000000..026bfe4a89 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/__snapshots__/preview.spec.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`preview should generate previews 1`] = `null`; diff --git a/devtools/client/debugger/src/actions/tests/expressions.spec.js b/devtools/client/debugger/src/actions/tests/expressions.spec.js new file mode 100644 index 0000000000..48b06ebd1a --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/expressions.spec.js @@ -0,0 +1,184 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { + actions, + selectors, + createStore, + makeSource, +} from "../../utils/test-head"; + +import { makeMockFrame } from "../../utils/test-mockup"; + +const mockThreadFront = { + evaluate: (script, { frameId }) => + new Promise((resolve, reject) => { + if (!frameId) { + resolve("bla"); + } else { + resolve("boo"); + } + }), + evaluateExpressions: (inputs, { frameId }) => + Promise.all( + inputs.map( + input => + new Promise((resolve, reject) => { + if (!frameId) { + resolve("bla"); + } else { + resolve("boo"); + } + }) + ) + ), + getFrameScopes: async () => {}, + getFrames: async () => [], + sourceContents: () => ({ source: "", contentType: "text/javascript" }), + getSourceActorBreakpointPositions: async () => ({}), + getSourceActorBreakableLines: async () => [], + autocomplete: () => { + return new Promise(resolve => { + resolve({ + from: "foo", + matches: ["toLocaleString", "toSource", "toString", "toolbar", "top"], + matchProp: "to", + }); + }); + }, +}; + +describe("expressions", () => { + it("should add an expression", async () => { + const { dispatch, getState, cx } = createStore(mockThreadFront); + + await dispatch(actions.addExpression(cx, "foo")); + expect(selectors.getExpressions(getState())).toHaveLength(1); + }); + + it("should not add empty expressions", () => { + const { dispatch, getState, cx } = createStore(mockThreadFront); + + dispatch(actions.addExpression(cx, undefined)); + dispatch(actions.addExpression(cx, "")); + expect(selectors.getExpressions(getState())).toHaveLength(0); + }); + + it("should not add invalid expressions", async () => { + const { dispatch, getState, cx } = createStore(mockThreadFront); + await dispatch(actions.addExpression(cx, "foo#")); + const state = getState(); + expect(selectors.getExpressions(state)).toHaveLength(0); + expect(selectors.getExpressionError(state)).toBe(true); + }); + + it("should update an expression", async () => { + const { dispatch, getState, cx } = createStore(mockThreadFront); + + await dispatch(actions.addExpression(cx, "foo")); + const expression = selectors.getExpression(getState(), "foo"); + if (!expression) { + throw new Error("expression must exist"); + } + + await dispatch(actions.updateExpression(cx, "bar", expression)); + const bar = selectors.getExpression(getState(), "bar"); + + expect(bar && bar.input).toBe("bar"); + }); + + it("should not update an expression w/ invalid code", async () => { + const { dispatch, getState, cx } = createStore(mockThreadFront); + + await dispatch(actions.addExpression(cx, "foo")); + const expression = selectors.getExpression(getState(), "foo"); + if (!expression) { + throw new Error("expression must exist"); + } + await dispatch(actions.updateExpression(cx, "#bar", expression)); + expect(selectors.getExpression(getState(), "bar")).toBeUndefined(); + }); + + it("should delete an expression", async () => { + const { dispatch, getState, cx } = createStore(mockThreadFront); + + await dispatch(actions.addExpression(cx, "foo")); + await dispatch(actions.addExpression(cx, "bar")); + expect(selectors.getExpressions(getState())).toHaveLength(2); + + const expression = selectors.getExpression(getState(), "foo"); + + if (!expression) { + throw new Error("expression must exist"); + } + + const bar = selectors.getExpression(getState(), "bar"); + dispatch(actions.deleteExpression(expression)); + expect(selectors.getExpressions(getState())).toHaveLength(1); + expect(bar && bar.input).toBe("bar"); + }); + + it("should evaluate expressions global scope", async () => { + const { dispatch, getState, cx } = createStore(mockThreadFront); + await dispatch(actions.addExpression(cx, "foo")); + await dispatch(actions.addExpression(cx, "bar")); + + let foo = selectors.getExpression(getState(), "foo"); + let bar = selectors.getExpression(getState(), "bar"); + expect(foo && foo.value).toBe("bla"); + expect(bar && bar.value).toBe("bla"); + + await dispatch(actions.evaluateExpressions(cx)); + foo = selectors.getExpression(getState(), "foo"); + bar = selectors.getExpression(getState(), "bar"); + expect(foo && foo.value).toBe("bla"); + expect(bar && bar.value).toBe("bla"); + }); + + it("should evaluate expressions in specific scope", async () => { + const { dispatch, getState } = createStore(mockThreadFront); + await createFrames(getState, dispatch); + + const cx = selectors.getThreadContext(getState()); + await dispatch(actions.newGeneratedSource(makeSource("source"))); + await dispatch(actions.addExpression(cx, "foo")); + await dispatch(actions.addExpression(cx, "bar")); + + let foo = selectors.getExpression(getState(), "foo"); + let bar = selectors.getExpression(getState(), "bar"); + expect(foo && foo.value).toBe("boo"); + expect(bar && bar.value).toBe("boo"); + + await dispatch(actions.evaluateExpressions(cx)); + foo = selectors.getExpression(getState(), "foo"); + bar = selectors.getExpression(getState(), "bar"); + expect(foo && foo.value).toBe("boo"); + expect(bar && bar.value).toBe("boo"); + }); + + it("should get the autocomplete matches for the input", async () => { + const { cx, dispatch, getState } = createStore(mockThreadFront); + await dispatch(actions.autocomplete(cx, "to", 2)); + expect(selectors.getAutocompleteMatchset(getState())).toMatchSnapshot(); + }); +}); + +async function createFrames(getState, dispatch) { + const frame = makeMockFrame(); + await dispatch(actions.newGeneratedSource(makeSource("example.js"))); + await dispatch(actions.newGeneratedSource(makeSource("source"))); + + await dispatch( + actions.paused({ + thread: "FakeThread", + frame, + frames: [frame], + why: { type: "just because" }, + }) + ); + + await dispatch( + actions.selectFrame(selectors.getThreadContext(getState()), frame) + ); +} diff --git a/devtools/client/debugger/src/actions/tests/fixtures/immutable.js b/devtools/client/debugger/src/actions/tests/fixtures/immutable.js new file mode 100644 index 0000000000..e8ac7fb233 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/fixtures/immutable.js @@ -0,0 +1,2 @@ + +const m = Immutable.Map({a: 2}) diff --git a/devtools/client/debugger/src/actions/tests/fixtures/reactComponent.js b/devtools/client/debugger/src/actions/tests/fixtures/reactComponent.js new file mode 100644 index 0000000000..526c852d99 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/fixtures/reactComponent.js @@ -0,0 +1,7 @@ +import React, { Component } from "react"; + +class FixtureComponent extends Component { + render() { + return null; + } +} diff --git a/devtools/client/debugger/src/actions/tests/fixtures/reactFuncComponent.js b/devtools/client/debugger/src/actions/tests/fixtures/reactFuncComponent.js new file mode 100644 index 0000000000..3103161fe0 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/fixtures/reactFuncComponent.js @@ -0,0 +1,5 @@ +import React, { Component } from "react"; + +export default FixtureComponent = (props) => { + return <div>props.a</div>; +} diff --git a/devtools/client/debugger/src/actions/tests/fixtures/scopes.js b/devtools/client/debugger/src/actions/tests/fixtures/scopes.js new file mode 100644 index 0000000000..3a38097f5e --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/fixtures/scopes.js @@ -0,0 +1,11 @@ +// Program Scope + +function outer() { + function inner() { + const x = 1; + } + + const declaration = function() { + const x = 1; + }; +} diff --git a/devtools/client/debugger/src/actions/tests/helpers/breakpoints.js b/devtools/client/debugger/src/actions/tests/helpers/breakpoints.js new file mode 100644 index 0000000000..e0279e5fc1 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/helpers/breakpoints.js @@ -0,0 +1,77 @@ +/* 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 { createLocation } from "../../../utils/location"; + +export function mockPendingBreakpoint(overrides = {}) { + const { sourceUrl, line, column, condition, disabled, hidden } = overrides; + return { + location: createLocation({ + source: { + id: "", + url: sourceUrl || "http://localhost:8000/examples/bar.js", + }, + sourceId: "", + sourceUrl: sourceUrl || "http://localhost:8000/examples/bar.js", + line: line || 5, + column: column || 1, + }), + generatedLocation: createLocation({ + source: { + id: "", + url: sourceUrl || "http://localhost:8000/examples/bar.js", + }, + sourceId: "", + sourceUrl: sourceUrl || "http://localhost:8000/examples/bar.js", + line: line || 5, + column: column || 1, + }), + astLocation: { + name: undefined, + offset: { + line: line || 5, + }, + index: 0, + }, + options: { + condition: condition || null, + hidden: hidden || false, + }, + disabled: disabled || false, + }; +} + +export function generateBreakpoint(filename, line = 5, column = 0) { + return { + id: "breakpoint", + originalText: "", + text: "", + location: createLocation({ + source: { + url: `http://localhost:8000/examples/${filename}`, + id: filename, + }, + sourceUrl: `http://localhost:8000/examples/${filename}`, + sourceId: filename, + line, + column, + }), + generatedLocation: createLocation({ + source: { + url: `http://localhost:8000/examples/${filename}`, + id: filename, + }, + sourceUrl: `http://localhost:8000/examples/${filename}`, + sourceId: filename, + line, + column, + }), + astLocation: undefined, + options: { + condition: "", + hidden: false, + }, + disabled: false, + }; +} diff --git a/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js b/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js new file mode 100644 index 0000000000..38dd55c274 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +export function createSource(name, code) { + name = name.replace(/\..*$/, ""); + return { + source: code || `function ${name}() {\n return ${name} \n}`, + contentType: "text/javascript", + }; +} + +const sources = [ + "a", + "b", + "foo", + "bar", + "foo1", + "foo2", + "a.js", + "baz.js", + "foobar.js", + "barfoo.js", + "foo.js", + "bar.js", + "base.js", + "bazz.js", + "jquery.js", +]; + +export const mockCommandClient = { + sourceContents: function ({ source }) { + return new Promise((resolve, reject) => { + if (sources.includes(source)) { + resolve(createSource(source)); + } + + reject(`unknown source: ${source}`); + }); + }, + setBreakpoint: async () => {}, + removeBreakpoint: _id => Promise.resolve(), + threadFront: async () => {}, + getFrameScopes: async () => {}, + getFrames: async () => [], + evaluateExpressions: async () => {}, + getSourceActorBreakpointPositions: async () => ({}), + getSourceActorBreakableLines: async () => [], +}; diff --git a/devtools/client/debugger/src/actions/tests/helpers/readFixture.js b/devtools/client/debugger/src/actions/tests/helpers/readFixture.js new file mode 100644 index 0000000000..6c23641226 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/helpers/readFixture.js @@ -0,0 +1,14 @@ +/* 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 fs from "fs"; +import path from "path"; + +export default function readFixture(name) { + const text = fs.readFileSync( + path.join(__dirname, `../fixtures/${name}`), + "utf8" + ); + return text; +} diff --git a/devtools/client/debugger/src/actions/tests/navigation.spec.js b/devtools/client/debugger/src/actions/tests/navigation.spec.js new file mode 100644 index 0000000000..e2572fde80 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/navigation.spec.js @@ -0,0 +1,29 @@ +/* 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 } from "../../utils/test-head"; + +jest.mock("../../utils/editor"); + +const { getActiveSearch } = selectors; + +const threadFront = { + sourceContents: async () => ({ + source: "function foo1() {\n const foo = 5; return foo;\n}", + contentType: "text/javascript", + }), + getSourceActorBreakpointPositions: async () => ({}), + getSourceActorBreakableLines: async () => [], +}; + +describe("navigation", () => { + it("navigation removes activeSearch 'file' value", async () => { + const { dispatch, getState } = createStore(threadFront); + dispatch(actions.setActiveSearch("file")); + expect(getActiveSearch(getState())).toBe("file"); + + await dispatch(actions.willNavigate("will-navigate")); + expect(getActiveSearch(getState())).toBe(null); + }); +}); diff --git a/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js b/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js new file mode 100644 index 0000000000..2baa17a79f --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js @@ -0,0 +1,294 @@ +/* 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/>. */ + +// TODO: we would like to mock this in the local tests +import { + generateBreakpoint, + mockPendingBreakpoint, +} from "./helpers/breakpoints.js"; + +import { mockCommandClient } from "./helpers/mockCommandClient"; +import { asyncStore } from "../../utils/prefs"; + +function loadInitialState(opts = {}) { + const mockedPendingBreakpoint = mockPendingBreakpoint({ ...opts, column: 2 }); + const l = mockedPendingBreakpoint.location; + const id = `${l.sourceUrl}:${l.line}:${l.column}`; + asyncStore.pendingBreakpoints = { [id]: mockedPendingBreakpoint }; + + return { pendingBreakpoints: asyncStore.pendingBreakpoints }; +} + +jest.mock("../../utils/prefs", () => ({ + prefs: { + clientSourceMapsEnabled: true, + expressions: [], + }, + asyncStore: { + pendingBreakpoints: {}, + }, + clear: jest.fn(), + features: { + inlinePreview: true, + }, +})); + +import { + createStore, + selectors, + actions, + makeSource, + makeSourceURL, + waitForState, +} from "../../utils/test-head"; + +import sourceMapLoader from "devtools/client/shared/source-map-loader/index"; + +function mockClient(bpPos = {}) { + return { + ...mockCommandClient, + setSkipPausing: jest.fn(), + getSourceActorBreakpointPositions: async () => bpPos, + getSourceActorBreakableLines: async () => [], + }; +} + +function mockSourceMaps() { + return { + ...sourceMapLoader, + getOriginalSourceText: async id => ({ + id, + text: "", + contentType: "text/javascript", + }), + getGeneratedRangesForOriginal: async () => [ + { start: { line: 0, column: 0 }, end: { line: 10, column: 10 } }, + ], + getOriginalLocations: async items => items, + }; +} + +describe("when adding breakpoints", () => { + it("a corresponding pending breakpoint should be added", async () => { + const { dispatch, getState, cx } = createStore( + mockClient({ 5: [1] }), + loadInitialState(), + mockSourceMaps() + ); + + const source = await dispatch( + actions.newGeneratedSource(makeSource("foo.js")) + ); + const sourceActor = selectors.getFirstSourceActorForGeneratedSource( + getState(), + source.id + ); + + await dispatch(actions.loadGeneratedSourceText({ cx, sourceActor })); + + const bp = generateBreakpoint("foo.js", 5, 1); + + await dispatch(actions.addBreakpoint(cx, bp.location)); + + expect(selectors.getPendingBreakpointList(getState())).toHaveLength(2); + }); +}); + +describe("initializing when pending breakpoints exist in prefs", () => { + it("syncs pending breakpoints", async () => { + const { getState } = createStore( + mockClient({ 5: [0] }), + loadInitialState(), + mockSourceMaps() + ); + const bps = selectors.getPendingBreakpoints(getState()); + expect(bps).toMatchSnapshot(); + }); + + it("re-adding breakpoints update existing pending breakpoints", async () => { + const { dispatch, getState, cx } = createStore( + mockClient({ 5: [1, 2] }), + loadInitialState(), + mockSourceMaps() + ); + const bar = generateBreakpoint("bar.js", 5, 1); + + const source = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + const sourceActor = selectors.getFirstSourceActorForGeneratedSource( + getState(), + source.id + ); + + await dispatch(actions.loadGeneratedSourceText({ cx, sourceActor })); + await dispatch(actions.addBreakpoint(cx, bar.location)); + + const bps = selectors.getPendingBreakpointList(getState()); + expect(bps).toHaveLength(2); + }); + + it("adding bps doesn't remove existing pending breakpoints", async () => { + const { dispatch, getState, cx } = createStore( + mockClient({ 5: [0] }), + loadInitialState(), + mockSourceMaps() + ); + const bp = generateBreakpoint("foo.js"); + + const source = await dispatch( + actions.newGeneratedSource(makeSource("foo.js")) + ); + const sourceActor = selectors.getFirstSourceActorForGeneratedSource( + getState(), + source.id + ); + + await dispatch(actions.loadGeneratedSourceText({ cx, sourceActor })); + + await dispatch(actions.addBreakpoint(cx, bp.location)); + + const bps = selectors.getPendingBreakpointList(getState()); + expect(bps).toHaveLength(2); + }); +}); + +describe("initializing with disabled pending breakpoints in prefs", () => { + it("syncs breakpoints with pending breakpoints", async () => { + const store = createStore( + mockClient({ 5: [2] }), + loadInitialState({ disabled: true }), + mockSourceMaps() + ); + + const { getState, dispatch, cx } = store; + + const source = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + const sourceActor = selectors.getFirstSourceActorForGeneratedSource( + getState(), + source.id + ); + + await dispatch(actions.loadGeneratedSourceText({ cx, sourceActor })); + + await waitForState(store, state => { + const bps = selectors.getBreakpointsForSource(state, source.id); + return bps && !!Object.values(bps).length; + }); + + const bp = selectors.getBreakpointsList(getState()).find(({ location }) => { + return ( + location.line == 5 && + location.column == 2 && + location.sourceId == source.id + ); + }); + + if (!bp) { + throw new Error("no bp"); + } + expect(bp.location.sourceId).toEqual(source.id); + expect(bp.disabled).toEqual(true); + }); +}); + +describe("adding sources", () => { + it("corresponding breakpoints are added for a single source", async () => { + const store = createStore( + mockClient({ 5: [2] }), + loadInitialState({ disabled: true }), + mockSourceMaps() + ); + const { getState, dispatch, cx } = store; + + expect(selectors.getBreakpointCount(getState())).toEqual(0); + + const source = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + const sourceActor = selectors.getFirstSourceActorForGeneratedSource( + getState(), + source.id + ); + + await dispatch(actions.loadGeneratedSourceText({ cx, sourceActor })); + + await waitForState(store, state => selectors.getBreakpointCount(state) > 0); + + expect(selectors.getBreakpointCount(getState())).toEqual(1); + }); + + it("corresponding breakpoints are added to the original source", async () => { + const sourceURL = makeSourceURL("bar.js"); + const store = createStore(mockClient({ 5: [2] }), loadInitialState(), { + getOriginalURLs: async source => [ + { + id: sourceMapLoader.generatedToOriginalId(source.id, sourceURL), + url: sourceURL, + }, + ], + getOriginalSourceText: async () => ({ text: "" }), + getGeneratedLocation: async location => location, + getOriginalLocation: async location => location, + getGeneratedRangesForOriginal: async () => [ + { start: { line: 0, column: 0 }, end: { line: 10, column: 10 } }, + ], + getOriginalLocations: async items => + items.map(item => ({ + ...item, + sourceId: sourceMapLoader.generatedToOriginalId( + item.sourceId, + sourceURL + ), + })), + }); + + const { getState, dispatch } = store; + + expect(selectors.getBreakpointCount(getState())).toEqual(0); + + await dispatch( + actions.newGeneratedSource(makeSource("bar.js", { sourceMapURL: "foo" })) + ); + + await waitForState(store, state => selectors.getBreakpointCount(state) > 0); + + expect(selectors.getBreakpointCount(getState())).toEqual(1); + }); + + it("add corresponding breakpoints for multiple sources", async () => { + const store = createStore( + mockClient({ 5: [2] }), + loadInitialState({ disabled: true }), + mockSourceMaps() + ); + const { getState, dispatch, cx } = store; + + expect(selectors.getBreakpointCount(getState())).toEqual(0); + + const [source1, source2] = await dispatch( + actions.newGeneratedSources([makeSource("bar.js"), makeSource("foo.js")]) + ); + const sourceActor1 = selectors.getFirstSourceActorForGeneratedSource( + getState(), + source1.id + ); + const sourceActor2 = selectors.getFirstSourceActorForGeneratedSource( + getState(), + source2.id + ); + + await dispatch( + actions.loadGeneratedSourceText({ cx, sourceActor: sourceActor1 }) + ); + await dispatch( + actions.loadGeneratedSourceText({ cx, sourceActor: sourceActor2 }) + ); + + await waitForState(store, state => selectors.getBreakpointCount(state) > 0); + expect(selectors.getBreakpointCount(getState())).toEqual(1); + }); +}); diff --git a/devtools/client/debugger/src/actions/tests/preview.spec.js b/devtools/client/debugger/src/actions/tests/preview.spec.js new file mode 100644 index 0000000000..3b6c9c23ac --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/preview.spec.js @@ -0,0 +1,217 @@ +/* eslint max-nested-callbacks: ["error", 6] */ +/* 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, + makeFrame, + waitForState, + waitATick, +} from "../../utils/test-head"; +import { createLocation } from "../../utils/location"; + +function waitForPreview(store, expression) { + return waitForState(store, state => { + const preview = selectors.getPreview(state); + return preview && preview.expression == expression; + }); +} + +function mockThreadFront(overrides) { + return { + evaluate: async () => ({ result: {} }), + getFrameScopes: async () => {}, + getFrames: async () => [], + sourceContents: async () => ({ + source: "", + contentType: "text/javascript", + }), + getSourceActorBreakpointPositions: async () => ({}), + getSourceActorBreakableLines: async () => [], + evaluateExpressions: async () => [], + loadObjectProperties: async () => ({}), + ...overrides, + }; +} + +function dispatchSetPreview(dispatch, context, expression, target) { + return dispatch( + actions.setPreview( + context, + expression, + { + start: { url: "foo.js", line: 1, column: 2 }, + end: { url: "foo.js", line: 1, column: 5 }, + }, + { line: 2, column: 3 }, + target.getBoundingClientRect(), + target + ) + ); +} + +async function pause(store, client) { + const { dispatch, cx, getState } = store; + const source = makeSource("base.js"); + const base = await dispatch(actions.newGeneratedSource(source)); + const sourceActor = selectors.getFirstSourceActorForGeneratedSource( + getState(), + base.id + ); + + await dispatch(actions.selectSource(cx, base, sourceActor)); + const location = createLocation({ source: base, sourceActor }); + await waitForState(store, state => selectors.getSymbols(state, location)); + + const { thread } = cx; + const frames = [makeFrame({ id: "frame1", sourceId: base.id, thread })]; + client.getFrames = async () => frames; + + await dispatch( + actions.paused({ + thread, + frame: frames[0], + loadedObjects: [], + why: { type: "debuggerStatement" }, + }) + ); +} + +describe("preview", () => { + it("should generate previews", async () => { + const store = createStore(mockThreadFront()); + const { dispatch, getState, cx } = store; + const source = makeSource("base.js"); + const base = await dispatch(actions.newGeneratedSource(source)); + + await dispatch(actions.selectSource(cx, base)); + const sourceActor = selectors.getFirstSourceActorForGeneratedSource( + getState(), + base.id + ); + const location = createLocation({ source: base, sourceActor }); + + await waitForState(store, state => selectors.getSymbols(state, location)); + const frames = [makeFrame({ id: "f1", sourceId: base.id })]; + + await dispatch( + actions.paused({ + thread: store.cx.thread, + frame: frames[0], + frames, + loadedObjects: [], + why: { type: "debuggerStatement" }, + }) + ); + + const newCx = selectors.getContext(getState()); + const firstTarget = document.createElement("div"); + + dispatchSetPreview(dispatch, newCx, "foo", firstTarget); + + expect(selectors.getPreview(getState())).toMatchSnapshot(); + }); + + // When a 2nd setPreview is called before a 1st setPreview dispatches + // and the 2nd setPreview has not dispatched yet, + // the first setPreview should not finish dispatching + it("queued previews (w/ the 1st finishing first)", async () => { + let resolveFirst, resolveSecond; + const promises = [ + new Promise(resolve => { + resolveFirst = resolve; + }), + new Promise(resolve => { + resolveSecond = resolve; + }), + ]; + + const client = mockThreadFront({ + loadObjectProperties: () => promises.shift(), + }); + const store = createStore(client); + + const { dispatch, getState } = store; + await pause(store, client); + + const newCx = selectors.getContext(getState()); + const firstTarget = document.createElement("div"); + const secondTarget = document.createElement("div"); + + // Start the dispatch of the first setPreview. At this point, it will not + // finish execution until we resolve the firstSetPreview + dispatchSetPreview(dispatch, newCx, "firstSetPreview", firstTarget); + + // Start the dispatch of the second setPreview. At this point, it will not + // finish execution until we resolve the secondSetPreview + dispatchSetPreview(dispatch, newCx, "secondSetPreview", secondTarget); + + let fail = false; + + resolveFirst(); + waitForPreview(store, "firstSetPreview").then(() => { + fail = true; + }); + + resolveSecond(); + await waitForPreview(store, "secondSetPreview"); + expect(fail).toEqual(false); + + const preview = selectors.getPreview(getState()); + expect(preview && preview.expression).toEqual("secondSetPreview"); + }); + + // When a 2nd setPreview is called before a 1st setPreview dispatches + // and the 2nd setPreview has dispatched, + // the first setPreview should not finish dispatching + it("queued previews (w/ the 2nd finishing first)", async () => { + let resolveFirst, resolveSecond; + const promises = [ + new Promise(resolve => { + resolveFirst = resolve; + }), + new Promise(resolve => { + resolveSecond = resolve; + }), + ]; + + const client = mockThreadFront({ + loadObjectProperties: () => promises.shift(), + }); + const store = createStore(client); + + const { dispatch, getState } = store; + await pause(store, client); + + const cx = selectors.getThreadContext(getState()); + const firstTarget = document.createElement("div"); + const secondTarget = document.createElement("div"); + + // Start the dispatch of the first setPreview. At this point, it will not + // finish execution until we resolve the firstSetPreview + dispatchSetPreview(dispatch, cx, "firstSetPreview", firstTarget); + + // Start the dispatch of the second setPreview. At this point, it will not + // finish execution until we resolve the secondSetPreview + dispatchSetPreview(dispatch, cx, "secondSetPreview", secondTarget); + + let fail = false; + + resolveSecond(); + await waitForPreview(store, "secondSetPreview"); + + resolveFirst(); + waitForPreview(store, "firstSetPreview").then(() => { + fail = true; + }); + + await waitATick(() => expect(fail).toEqual(false)); + + const preview = selectors.getPreview(getState()); + expect(preview && preview.expression).toEqual("secondSetPreview"); + }); +}); diff --git a/devtools/client/debugger/src/actions/tests/sources-tree.spec.js b/devtools/client/debugger/src/actions/tests/sources-tree.spec.js new file mode 100644 index 0000000000..916b2d015b --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/sources-tree.spec.js @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { actions, selectors, createStore } from "../../utils/test-head"; +const { getExpandedState } = selectors; + +describe("source tree", () => { + it("should set the expanded state", () => { + const { dispatch, getState } = createStore(); + const expandedState = new Set(["foo", "bar"]); + + expect(getExpandedState(getState())).toEqual(new Set([])); + dispatch(actions.setExpandedState(expandedState)); + expect(getExpandedState(getState())).toEqual(expandedState); + }); +}); diff --git a/devtools/client/debugger/src/actions/tests/tabs.spec.js b/devtools/client/debugger/src/actions/tests/tabs.spec.js new file mode 100644 index 0000000000..419e5b7e60 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/tabs.spec.js @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +import { + actions, + selectors, + createStore, + makeSource, +} from "../../utils/test-head"; +const { getSelectedSource, getSourceTabs } = selectors; +import { createLocation } from "../../utils/location"; + +import { mockCommandClient } from "./helpers/mockCommandClient"; + +describe("closing tabs", () => { + it("closing a tab", async () => { + const { dispatch, getState, cx } = createStore(mockCommandClient); + + const fooSource = await dispatch( + actions.newGeneratedSource(makeSource("foo.js")) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: fooSource, line: 1 })) + ); + dispatch(actions.closeTab(cx, fooSource)); + + expect(getSelectedSource(getState())).toBe(undefined); + expect(getSourceTabs(getState())).toHaveLength(0); + }); + + it("closing the inactive tab", async () => { + const { dispatch, getState, cx } = createStore(mockCommandClient); + + const fooSource = await dispatch( + actions.newGeneratedSource(makeSource("foo.js")) + ); + const barSource = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: fooSource, line: 1 })) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: barSource, line: 1 })) + ); + dispatch(actions.closeTab(cx, fooSource)); + + const selected = getSelectedSource(getState()); + expect(selected && selected.id).toBe("bar.js"); + expect(getSourceTabs(getState())).toHaveLength(1); + }); + + it("closing the only tab", async () => { + const { dispatch, getState, cx } = createStore(mockCommandClient); + + const fooSource = await dispatch( + actions.newGeneratedSource(makeSource("foo.js")) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: fooSource, line: 1 })) + ); + dispatch(actions.closeTab(cx, fooSource)); + + expect(getSelectedSource(getState())).toBe(undefined); + expect(getSourceTabs(getState())).toHaveLength(0); + }); + + it("closing the active tab", async () => { + const { dispatch, getState, cx } = createStore(mockCommandClient); + + const fooSource = await dispatch( + actions.newGeneratedSource(makeSource("foo.js")) + ); + const barSource = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: fooSource, line: 1 })) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: barSource, line: 1 })) + ); + await dispatch(actions.closeTab(cx, barSource)); + + const selected = getSelectedSource(getState()); + expect(selected && selected.id).toBe("foo.js"); + expect(getSourceTabs(getState())).toHaveLength(1); + }); + + it("closing many inactive tabs", async () => { + const { dispatch, getState, cx } = createStore(mockCommandClient); + + const fooSource = await dispatch( + actions.newGeneratedSource(makeSource("foo.js")) + ); + const barSource = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + const bazzSource = await dispatch( + actions.newGeneratedSource(makeSource("bazz.js")) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: fooSource, line: 1 })) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: barSource, line: 1 })) + ); + await dispatch( + actions.selectLocation( + cx, + createLocation({ source: bazzSource, line: 1 }) + ) + ); + + const tabs = [ + "http://localhost:8000/examples/foo.js", + "http://localhost:8000/examples/bar.js", + ]; + dispatch(actions.closeTabs(cx, tabs)); + + const selected = getSelectedSource(getState()); + expect(selected && selected.id).toBe("bazz.js"); + expect(getSourceTabs(getState())).toHaveLength(1); + }); + + it("closing many tabs including the active tab", async () => { + const { dispatch, getState, cx } = createStore(mockCommandClient); + + const fooSource = await dispatch( + actions.newGeneratedSource(makeSource("foo.js")) + ); + const barSource = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + const bazzSource = await dispatch( + actions.newGeneratedSource(makeSource("bazz.js")) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: fooSource, line: 1 })) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: barSource, line: 1 })) + ); + await dispatch( + actions.selectLocation( + cx, + createLocation({ source: bazzSource, line: 1 }) + ) + ); + const tabs = [ + "http://localhost:8000/examples/bar.js", + "http://localhost:8000/examples/bazz.js", + ]; + await dispatch(actions.closeTabs(cx, tabs)); + + const selected = getSelectedSource(getState()); + expect(selected && selected.id).toBe("foo.js"); + expect(getSourceTabs(getState())).toHaveLength(1); + }); + + it("closing all the tabs", async () => { + const { dispatch, getState, cx } = createStore(mockCommandClient); + + const fooSource = await dispatch( + actions.newGeneratedSource(makeSource("foo.js")) + ); + const barSource = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: fooSource, line: 1 })) + ); + await dispatch( + actions.selectLocation(cx, createLocation({ source: barSource, line: 1 })) + ); + await dispatch( + actions.closeTabs(cx, [ + "http://localhost:8000/examples/foo.js", + "http://localhost:8000/examples/bar.js", + ]) + ); + + expect(getSelectedSource(getState())).toBe(undefined); + expect(getSourceTabs(getState())).toHaveLength(0); + }); +}); diff --git a/devtools/client/debugger/src/actions/tests/ui.spec.js b/devtools/client/debugger/src/actions/tests/ui.spec.js new file mode 100644 index 0000000000..0e13681a12 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/ui.spec.js @@ -0,0 +1,90 @@ +/* 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, +} from "../../utils/test-head"; +import { createLocation } from "../../utils/location"; +import { mockCommandClient } from "./helpers/mockCommandClient"; + +const { + getActiveSearch, + getFrameworkGroupingState, + getPaneCollapse, + getHighlightedLineRangeForSelectedSource, +} = selectors; + +describe("ui", () => { + it("should toggle the visible state of file search", () => { + const { dispatch, getState } = createStore(); + expect(getActiveSearch(getState())).toBe(null); + dispatch(actions.setActiveSearch("file")); + expect(getActiveSearch(getState())).toBe("file"); + }); + + it("should close file search", () => { + const { dispatch, getState } = createStore(); + expect(getActiveSearch(getState())).toBe(null); + dispatch(actions.setActiveSearch("file")); + dispatch(actions.closeActiveSearch()); + expect(getActiveSearch(getState())).toBe(null); + }); + + it("should toggle the collapse state of a pane", () => { + const { dispatch, getState } = createStore(); + expect(getPaneCollapse(getState(), "start")).toBe(false); + dispatch(actions.togglePaneCollapse("start", true)); + expect(getPaneCollapse(getState(), "start")).toBe(true); + }); + + it("should toggle the collapsed state of frameworks in the callstack", () => { + const { dispatch, getState } = createStore(); + const currentState = getFrameworkGroupingState(getState()); + dispatch(actions.toggleFrameworkGrouping(!currentState)); + expect(getFrameworkGroupingState(getState())).toBe(!currentState); + }); + + it("should highlight lines", async () => { + const { dispatch, getState } = createStore(mockCommandClient); + const base = await dispatch( + actions.newGeneratedSource(makeSource("base.js")) + ); + const sourceActor = selectors.getFirstSourceActorForGeneratedSource( + getState(), + base.id + ); + const cx = selectors.getThreadContext(getState()); + //await dispatch(actions.selectSource(cx, base, sourceActor)); + const location = createLocation({ + source: base, + line: 1, + sourceActor, + }); + await dispatch(actions.selectLocation(cx, location)); + + const range = { start: 3, end: 5, sourceId: base.id }; + dispatch(actions.highlightLineRange(range)); + expect(getHighlightedLineRangeForSelectedSource(getState())).toEqual(range); + }); + + it("should clear highlight lines", async () => { + const { dispatch, getState } = createStore(mockCommandClient); + const base = await dispatch( + actions.newGeneratedSource(makeSource("base.js")) + ); + const sourceActor = selectors.getFirstSourceActorForGeneratedSource( + getState(), + base.id + ); + const cx = selectors.getThreadContext(getState()); + await dispatch(actions.selectSource(cx, base, sourceActor)); + const range = { start: 3, end: 5, sourceId: "2" }; + dispatch(actions.highlightLineRange(range)); + dispatch(actions.clearHighlightLineRange()); + expect(getHighlightedLineRangeForSelectedSource(getState())).toEqual(null); + }); +}); |