diff options
Diffstat (limited to 'devtools/client/debugger/src/actions/tests')
23 files changed, 2179 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/actions/tests/__snapshots__/ast.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/ast.spec.js.snap new file mode 100644 index 0000000000..f9f1af276e --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/__snapshots__/ast.spec.js.snap @@ -0,0 +1,83 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ast setSymbols when the source is loaded should be able to set symbols 1`] = ` +Object { + "callExpressions": Array [], + "classes": Array [], + "comments": Array [], + "functions": Array [ + Object { + "identifier": Object { + "end": 13, + "loc": Object { + "end": Object { + "column": 13, + "line": 1, + }, + "identifierName": "base", + "start": Object { + "column": 9, + "line": 1, + }, + }, + "name": "base", + "start": 9, + "type": "Identifier", + }, + "index": 0, + "klass": null, + "location": Object { + "end": Object { + "column": 21, + "line": 1, + }, + "start": Object { + "column": 0, + "line": 1, + }, + }, + "name": "base", + "parameterNames": Array [ + "boo", + ], + }, + ], + "hasJsx": false, + "hasTypes": false, + "identifiers": Array [ + Object { + "expression": "base", + "location": Object { + "end": Object { + "column": 13, + "line": 1, + }, + "start": Object { + "column": 9, + "line": 1, + }, + }, + "name": "base", + }, + Object { + "expression": "boo", + "location": Object { + "end": Object { + "column": 17, + "line": 1, + }, + "start": Object { + "column": 14, + "line": 1, + }, + }, + "name": "boo", + }, + ], + "imports": Array [], + "literals": Array [], + "loading": false, + "memberExpressions": Array [], + "objectProperties": Array [], +} +`; 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..bb87128f1d --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap @@ -0,0 +1,112 @@ +// 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, + "sourceUrl": "http://localhost:8000/examples/bar.js", + }, + "location": Object { + "column": 2, + "line": 5, + "sourceId": "", + "sourceUrl": "http://localhost:8000/examples/bar.js", + }, + "options": Object { + "condition": null, + "hidden": false, + }, + }, +} +`; + +exports[`when adding breakpoints a corresponding pending breakpoint should be added 1`] = ` +Object { + "astLocation": Object { + "index": 0, + "name": undefined, + "offset": Object { + "column": 1, + "line": 5, + "sourceId": "foo.js", + "sourceUrl": "http://localhost:8000/examples/foo.js", + }, + }, + "disabled": false, + "generatedLocation": Object { + "column": 1, + "line": 5, + "sourceUrl": "http://localhost:8000/examples/foo.js", + }, + "location": Object { + "column": 1, + "line": 5, + "sourceUrl": "http://localhost:8000/examples/foo.js", + }, + "options": Object {}, +} +`; + +exports[`when adding breakpoints adding and deleting breakpoints add a corresponding pendingBreakpoint for each addition 1`] = ` +Object { + "astLocation": Object { + "index": 0, + "name": undefined, + "offset": Object { + "column": 0, + "line": 5, + "sourceId": "foo", + "sourceUrl": "http://localhost:8000/examples/foo", + }, + }, + "disabled": false, + "generatedLocation": Object { + "column": 0, + "line": 5, + "sourceUrl": "http://localhost:8000/examples/foo", + }, + "location": Object { + "column": 0, + "line": 5, + "sourceUrl": "http://localhost:8000/examples/foo", + }, + "options": Object {}, +} +`; + +exports[`when adding breakpoints adding and deleting breakpoints add a corresponding pendingBreakpoint for each addition 2`] = ` +Object { + "astLocation": Object { + "index": 0, + "name": undefined, + "offset": Object { + "column": 0, + "line": 5, + "sourceId": "foo2", + "sourceUrl": "http://localhost:8000/examples/foo2", + }, + }, + "disabled": false, + "generatedLocation": Object { + "column": 0, + "line": 5, + "sourceUrl": "http://localhost:8000/examples/foo2", + }, + "location": Object { + "column": 0, + "line": 5, + "sourceUrl": "http://localhost:8000/examples/foo2", + }, + "options": Object {}, +} +`; 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/__snapshots__/project-text-search.spec.js.snap b/devtools/client/debugger/src/actions/tests/__snapshots__/project-text-search.spec.js.snap new file mode 100644 index 0000000000..e7f0e40d64 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/__snapshots__/project-text-search.spec.js.snap @@ -0,0 +1,180 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`project text search should clear all the search results 1`] = ` +Array [ + Object { + "filepath": "http://localhost:8000/examples/foo1", + "matches": Array [ + Object { + "column": 9, + "line": 1, + "match": "foo", + "matchIndex": 9, + "sourceId": "foo1", + "type": "MATCH", + "value": "function foo1() {", + }, + Object { + "column": 8, + "line": 2, + "match": "foo", + "matchIndex": 8, + "sourceId": "foo1", + "type": "MATCH", + "value": " const foo = 5; return foo;", + }, + Object { + "column": 24, + "line": 2, + "match": "foo", + "matchIndex": 24, + "sourceId": "foo1", + "type": "MATCH", + "value": " const foo = 5; return foo;", + }, + ], + "sourceId": "foo1", + "type": "RESULT", + }, +] +`; + +exports[`project text search should clear all the search results 2`] = `Array []`; + +exports[`project text search should close project search 1`] = ` +Array [ + Object { + "filepath": "http://localhost:8000/examples/foo1", + "matches": Array [ + Object { + "column": 9, + "line": 1, + "match": "foo", + "matchIndex": 9, + "sourceId": "foo1", + "type": "MATCH", + "value": "function foo1() {", + }, + Object { + "column": 8, + "line": 2, + "match": "foo", + "matchIndex": 8, + "sourceId": "foo1", + "type": "MATCH", + "value": " const foo = 5; return foo;", + }, + Object { + "column": 24, + "line": 2, + "match": "foo", + "matchIndex": 24, + "sourceId": "foo1", + "type": "MATCH", + "value": " const foo = 5; return foo;", + }, + ], + "sourceId": "foo1", + "type": "RESULT", + }, +] +`; + +exports[`project text search should close project search 2`] = `Array []`; + +exports[`project text search should ignore sources with minified versions 1`] = ` +Array [ + Object { + "filepath": "http://localhost:8000/examples/bar:formatted", + "matches": Array [ + Object { + "column": 9, + "line": 1, + "match": "bla", + "matchIndex": 9, + "sourceId": "bar/originalSource-79d3ab91075b948b7044296e606a28c5", + "type": "MATCH", + "value": "function bla(x, y) {", + }, + ], + "sourceId": "bar/originalSource-79d3ab91075b948b7044296e606a28c5", + "type": "RESULT", + }, +] +`; + +exports[`project text search should search a specific source 1`] = ` +Array [ + Object { + "filepath": "http://localhost:8000/examples/bar", + "matches": Array [ + Object { + "column": 9, + "line": 1, + "match": "bla", + "matchIndex": 9, + "sourceId": "bar", + "type": "MATCH", + "value": "function bla(x, y) {", + }, + ], + "sourceId": "bar", + "type": "RESULT", + }, +] +`; + +exports[`project text search should search all the loaded sources based on the query 1`] = ` +Array [ + Object { + "filepath": "http://localhost:8000/examples/foo1", + "matches": Array [ + Object { + "column": 9, + "line": 1, + "match": "foo", + "matchIndex": 9, + "sourceId": "foo1", + "type": "MATCH", + "value": "function foo1() {", + }, + Object { + "column": 8, + "line": 2, + "match": "foo", + "matchIndex": 8, + "sourceId": "foo1", + "type": "MATCH", + "value": " const foo = 5; return foo;", + }, + Object { + "column": 24, + "line": 2, + "match": "foo", + "matchIndex": 24, + "sourceId": "foo1", + "type": "MATCH", + "value": " const foo = 5; return foo;", + }, + ], + "sourceId": "foo1", + "type": "RESULT", + }, + Object { + "filepath": "http://localhost:8000/examples/foo2", + "matches": Array [ + Object { + "column": 9, + "line": 1, + "match": "foo", + "matchIndex": 9, + "sourceId": "foo2", + "type": "MATCH", + "value": "function foo2(x, y) {", + }, + ], + "sourceId": "foo2", + "type": "RESULT", + }, +] +`; diff --git a/devtools/client/debugger/src/actions/tests/ast.spec.js b/devtools/client/debugger/src/actions/tests/ast.spec.js new file mode 100644 index 0000000000..9dde191e77 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/ast.spec.js @@ -0,0 +1,128 @@ +/* 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/>. */ + +// @flow + +import { + createStore, + selectors, + actions, + makeSource, + makeOriginalSource, + waitForState, +} from "../../utils/test-head"; + +import readFixture from "./helpers/readFixture"; +const { getSymbols, isSymbolsLoading, getFramework } = selectors; + +const mockCommandClient = { + sourceContents: async ({ source }) => ({ + source: sourceTexts[source], + contentType: "text/javascript", + }), + getFrameScopes: async () => {}, + evaluate: async expression => ({ result: evaluationResult[expression] }), + evaluateExpressions: async expressions => + expressions.map(expression => ({ result: evaluationResult[expression] })), + getSourceActorBreakpointPositions: async () => ({}), + getSourceActorBreakableLines: async () => [], +}; + +const sourceMaps = { + getOriginalSourceText: async id => ({ + id, + text: sourceTexts[id], + contentType: "text/javascript", + }), + getGeneratedRangesForOriginal: async () => [], + getOriginalLocations: async items => items, +}; + +const sourceTexts = { + "base.js": "function base(boo) {}", + "foo.js": "function base(boo) { return this.bazz; } outOfScope", + "reactComponent.js/originalSource": readFixture("reactComponent.js"), + "reactFuncComponent.js/originalSource": readFixture("reactFuncComponent.js"), +}; + +const evaluationResult = { + "this.bazz": { actor: "bazz", preview: {} }, + this: { actor: "this", preview: {} }, +}; + +describe("ast", () => { + describe("setSymbols", () => { + describe("when the source is loaded", () => { + it("should be able to set symbols", async () => { + const store = createStore(mockCommandClient); + const { dispatch, getState, cx } = store; + const base = await dispatch( + actions.newGeneratedSource(makeSource("base.js")) + ); + await dispatch(actions.loadSourceText({ cx, source: base })); + + const loadedSource = selectors.getSourceFromId(getState(), base.id); + await dispatch(actions.setSymbols({ cx, source: loadedSource })); + await waitForState(store, state => !isSymbolsLoading(state, base)); + + const baseSymbols = getSymbols(getState(), base); + expect(baseSymbols).toMatchSnapshot(); + }); + }); + + describe("when the source is not loaded", () => { + it("should return null", async () => { + const { getState, dispatch } = createStore(mockCommandClient); + const base = await dispatch( + actions.newGeneratedSource(makeSource("base.js")) + ); + + const baseSymbols = getSymbols(getState(), base); + expect(baseSymbols).toEqual(null); + }); + }); + + describe("when there is no source", () => { + it("should return null", async () => { + const { getState } = createStore(mockCommandClient); + const baseSymbols = getSymbols(getState()); + expect(baseSymbols).toEqual(null); + }); + }); + + describe("frameworks", () => { + it("should detect react components", async () => { + const store = createStore(mockCommandClient, {}, sourceMaps); + const { cx, dispatch, getState } = store; + + const genSource = await dispatch( + actions.newGeneratedSource(makeSource("reactComponent.js")) + ); + + const source = await dispatch( + actions.newOriginalSource(makeOriginalSource(genSource)) + ); + + await dispatch(actions.loadSourceText({ cx, source })); + const loadedSource = selectors.getSourceFromId(getState(), source.id); + await dispatch(actions.setSymbols({ cx, source: loadedSource })); + + expect(getFramework(getState(), source)).toBe("React"); + }); + + it("should not give false positive on non react components", async () => { + const store = createStore(mockCommandClient); + const { cx, dispatch, getState } = store; + const base = await dispatch( + actions.newGeneratedSource(makeSource("base.js")) + ); + await dispatch(actions.loadSourceText({ cx, source: base })); + await dispatch(actions.setSymbols({ cx, source: base })); + + expect(getFramework(getState(), base)).toBe(undefined); + }); + }); + }); +}); 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..0a1cffefe7 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/expressions.spec.js @@ -0,0 +1,186 @@ +/* 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/>. */ + +// @flow + +import { + actions, + selectors, + createStore, + makeSource, +} from "../../utils/test-head"; + +import { makeMockFrame } from "../../utils/test-mockup"; + +const mockThreadFront = { + evaluateInFrame: (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: any))); + 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/file-search.spec.js b/devtools/client/debugger/src/actions/tests/file-search.spec.js new file mode 100644 index 0000000000..06118edc8d --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/file-search.spec.js @@ -0,0 +1,66 @@ +/* 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/>. */ + +// @flow + +import { createStore, selectors, actions } from "../../utils/test-head"; + +const { + getFileSearchQuery, + getFileSearchModifiers, + getFileSearchResults, +} = selectors; + +describe("file text search", () => { + it("should update search results", () => { + const { dispatch, getState, cx } = createStore(); + expect(getFileSearchResults(getState())).toEqual({ + matches: [], + matchIndex: -1, + index: -1, + count: 0, + }); + + const matches = [ + { line: 1, ch: 3 }, + { line: 3, ch: 2 }, + ]; + dispatch(actions.updateSearchResults(cx, 2, 3, matches)); + + expect(getFileSearchResults(getState())).toEqual({ + count: 2, + index: 2, + matchIndex: 1, + matches, + }); + }); + + it("should update the file search query", () => { + const { dispatch, getState, cx } = createStore(); + let fileSearchQueryState = getFileSearchQuery(getState()); + expect(fileSearchQueryState).toBe(""); + dispatch(actions.setFileSearchQuery(cx, "foobar")); + fileSearchQueryState = getFileSearchQuery(getState()); + expect(fileSearchQueryState).toBe("foobar"); + }); + + it("should toggle a file search modifier", () => { + const { dispatch, getState, cx } = createStore(); + let fileSearchModState = getFileSearchModifiers(getState()); + expect(fileSearchModState.caseSensitive).toBe(false); + dispatch(actions.toggleFileSearchModifier(cx, "caseSensitive")); + fileSearchModState = getFileSearchModifiers(getState()); + expect(fileSearchModState.caseSensitive).toBe(true); + }); + + it("should toggle a file search query cleaning", () => { + const { dispatch, getState, cx } = createStore(); + dispatch(actions.setFileSearchQuery(cx, "foobar")); + let fileSearchQueryState = getFileSearchQuery(getState()); + expect(fileSearchQueryState).toBe("foobar"); + dispatch(actions.setFileSearchQuery(cx, "")); + fileSearchQueryState = getFileSearchQuery(getState()); + expect(fileSearchQueryState).toBe(""); + }); +}); 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..dd4f108d71 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/helpers/breakpoints.js @@ -0,0 +1,64 @@ +/* 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/>. */ + +// @flow + +export function mockPendingBreakpoint(overrides: Object = {}) { + const { sourceUrl, line, column, condition, disabled, hidden } = overrides; + return { + location: { + sourceId: "", + sourceUrl: sourceUrl || "http://localhost:8000/examples/bar.js", + line: line || 5, + column: column || 1, + }, + generatedLocation: { + 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: string, + line: number = 5, + column: number = 0 +) { + return { + id: "breakpoint", + originalText: "", + text: "", + location: { + sourceUrl: `http://localhost:8000/examples/${filename}`, + sourceId: `${filename}`, + line, + column, + }, + generatedLocation: { + 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..9980b721f9 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js @@ -0,0 +1,55 @@ +/* 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/>. */ + +// @flow + +import type { SourceActor } from "../../../types"; + +export function createSource(name: string, code?: string) { + 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, + }: SourceActor): Promise<{| source: any, contentType: ?string |}> { + return new Promise((resolve, reject) => { + if (sources.includes(source)) { + resolve(createSource(source)); + } + + reject(`unknown source: ${source}`); + }); + }, + setBreakpoint: async () => {}, + removeBreakpoint: (_id: string) => 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..0206514b00 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/helpers/readFixture.js @@ -0,0 +1,16 @@ +/* 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/>. */ + +// @flow + +import fs from "fs"; +import path from "path"; + +export default function readFixture(name: string) { + 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..cc8bd8300a --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/navigation.spec.js @@ -0,0 +1,108 @@ +/* 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/>. */ + +// @flow + +import { + createStore, + selectors, + actions, + makeSource, +} from "../../utils/test-head"; + +jest.mock("../../utils/editor"); + +const { + getActiveSearch, + getTextSearchQuery, + getTextSearchResults, + getTextSearchStatus, + getFileSearchQuery, + getFileSearchResults, +} = 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 closes project-search", async () => { + const { dispatch, getState, cx } = createStore(threadFront); + const mockQuery = "foo"; + + await dispatch(actions.newGeneratedSource(makeSource("foo1"))); + await dispatch(actions.searchSources(cx, mockQuery)); + + let results = getTextSearchResults(getState()); + expect(results).toHaveLength(1); + expect(selectors.getTextSearchQuery(getState())).toEqual("foo"); + expect(getTextSearchStatus(getState())).toEqual("DONE"); + + await dispatch(actions.willNavigate("will-navigate")); + + results = getTextSearchResults(getState()); + expect(results).toHaveLength(0); + expect(getTextSearchQuery(getState())).toEqual(""); + expect(getTextSearchStatus(getState())).toEqual("INITIAL"); + }); + + it("navigation removes activeSearch 'project' value", async () => { + const { dispatch, getState } = createStore(threadFront); + dispatch(actions.setActiveSearch("project")); + expect(getActiveSearch(getState())).toBe("project"); + + await dispatch(actions.willNavigate("will-navigate")); + expect(getActiveSearch(getState())).toBe(null); + }); + + it("navigation clears the file-search query", async () => { + const { dispatch, getState, cx } = createStore(threadFront); + + dispatch(actions.setFileSearchQuery(cx, "foobar")); + expect(getFileSearchQuery(getState())).toBe("foobar"); + + await dispatch(actions.willNavigate("will-navigate")); + + expect(getFileSearchQuery(getState())).toBe(""); + }); + + it("navigation clears the file-search results", async () => { + const { dispatch, getState, cx } = createStore(threadFront); + + const searchResults = [ + { line: 1, ch: 3 }, + { line: 3, ch: 2 }, + ]; + dispatch(actions.updateSearchResults(cx, 2, 3, searchResults)); + expect(getFileSearchResults(getState())).toEqual({ + count: 2, + index: 2, + matchIndex: 1, + matches: searchResults, + }); + + await dispatch(actions.willNavigate("will-navigate")); + + expect(getFileSearchResults(getState())).toEqual({ + count: 0, + index: -1, + matchIndex: -1, + matches: [], + }); + }); + + 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..f43686d199 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js @@ -0,0 +1,439 @@ +/* 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/>. */ + +// @flow + +// 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 id = makePendingLocationId(mockedPendingBreakpoint.location); + 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 sourceMaps from "devtools-source-map"; + +import { makePendingLocationId } from "../../utils/breakpoint"; +function mockClient(bpPos = {}) { + return { + ...mockCommandClient, + setSkipPausing: jest.fn(), + getSourceActorBreakpointPositions: async () => bpPos, + getSourceActorBreakableLines: async () => [], + }; +} + +function mockSourceMaps() { + return { + ...sourceMaps, + 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")) + ); + await dispatch(actions.newGeneratedSource(makeSource("foo.js"))); + await dispatch(actions.loadSourceText({ cx, source })); + + const bp = generateBreakpoint("foo.js", 5, 1); + const id = makePendingLocationId(bp.location); + + await dispatch(actions.addBreakpoint(cx, bp.location)); + const pendingBps = selectors.getPendingBreakpoints(getState()); + + expect(selectors.getPendingBreakpointList(getState())).toHaveLength(2); + expect(pendingBps[id]).toMatchSnapshot(); + }); + + 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, cx } = createStore( + mockClient({ "5": [0] }), + loadInitialState(), + mockSourceMaps() + ); + + await dispatch(actions.newGeneratedSource(makeSource("foo"))); + await dispatch(actions.newGeneratedSource(makeSource("foo2"))); + + const source1 = await dispatch( + actions.newGeneratedSource(makeSource("foo")) + ); + const source2 = await dispatch( + actions.newGeneratedSource(makeSource("foo2")) + ); + + await dispatch(actions.loadSourceText({ cx, source: source1 })); + await dispatch(actions.loadSourceText({ cx, source: source2 })); + + await dispatch(actions.addBreakpoint(cx, breakpoint1.location)); + await dispatch(actions.addBreakpoint(cx, breakpoint2.location)); + + const pendingBps = selectors.getPendingBreakpoints(getState()); + + // NOTE the sourceId should be `foo2/originalSource`, but is `foo2` + // because we do not have a real source map for `getOriginalLocation` + // to map. + expect(pendingBps[breakpointLocationId1]).toMatchSnapshot(); + expect(pendingBps[breakpointLocationId2]).toMatchSnapshot(); + }); + + it("hidden breakponts do not create pending bps", async () => { + const { dispatch, getState, cx } = createStore( + mockClient({ "5": [0] }), + loadInitialState(), + mockSourceMaps() + ); + + await dispatch(actions.newGeneratedSource(makeSource("foo"))); + const source = await dispatch( + actions.newGeneratedSource(makeSource("foo")) + ); + await dispatch(actions.loadSourceText({ cx, source })); + + await dispatch( + actions.addBreakpoint(cx, breakpoint1.location, { hidden: true }) + ); + const pendingBps = selectors.getPendingBreakpoints(getState()); + + expect(pendingBps[breakpointLocationId1]).toBeUndefined(); + }); + + it("remove a corresponding pending breakpoint when deleting", async () => { + const { dispatch, getState, cx } = createStore( + mockClient({ "5": [0] }), + loadInitialState(), + mockSourceMaps() + ); + + await dispatch(actions.newGeneratedSource(makeSource("foo"))); + await dispatch(actions.newGeneratedSource(makeSource("foo2"))); + + const source1 = await dispatch( + actions.newGeneratedSource(makeSource("foo")) + ); + const source2 = await dispatch( + actions.newGeneratedSource(makeSource("foo2")) + ); + + await dispatch(actions.loadSourceText({ cx, source: source1 })); + await dispatch(actions.loadSourceText({ cx, source: source2 })); + + await dispatch(actions.addBreakpoint(cx, breakpoint1.location)); + await dispatch(actions.addBreakpoint(cx, breakpoint2.location)); + await dispatch(actions.removeBreakpoint(cx, breakpoint1)); + + const pendingBps = selectors.getPendingBreakpoints(getState()); + expect(pendingBps.hasOwnProperty(breakpointLocationId1)).toBe(false); + expect(pendingBps.hasOwnProperty(breakpointLocationId2)).toBe(true); + }); + }); +}); + +describe("when changing an existing breakpoint", () => { + it("updates corresponding pendingBreakpoint", async () => { + const { dispatch, getState, cx } = createStore( + mockClient({ "5": [0] }), + loadInitialState(), + mockSourceMaps() + ); + const bp = generateBreakpoint("foo"); + const id = makePendingLocationId(bp.location); + + const source = await dispatch( + actions.newGeneratedSource(makeSource("foo")) + ); + await dispatch(actions.newGeneratedSource(makeSource("foo"))); + await dispatch(actions.loadSourceText({ cx, source })); + + await dispatch(actions.addBreakpoint(cx, bp.location)); + await dispatch( + actions.setBreakpointOptions(cx, bp.location, { condition: "2" }) + ); + const bps = selectors.getPendingBreakpoints(getState()); + const breakpoint = bps[id]; + expect(breakpoint.options.condition).toBe("2"); + }); + + it("if disabled, updates corresponding pendingBreakpoint", async () => { + const { dispatch, getState, cx } = createStore( + mockClient({ "5": [0] }), + loadInitialState(), + mockSourceMaps() + ); + const bp = generateBreakpoint("foo"); + const id = makePendingLocationId(bp.location); + + await dispatch(actions.newGeneratedSource(makeSource("foo"))); + + const source = await dispatch( + actions.newGeneratedSource(makeSource("foo")) + ); + await dispatch(actions.loadSourceText({ cx, source })); + + await dispatch(actions.addBreakpoint(cx, bp.location)); + await dispatch(actions.disableBreakpoint(cx, bp)); + const bps = selectors.getPendingBreakpoints(getState()); + const breakpoint = bps[id]; + expect(breakpoint.disabled).toBe(true); + }); + + it("does not delete the pre-existing pendingBreakpoint", 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")) + ); + await dispatch(actions.newGeneratedSource(makeSource("foo.js"))); + await dispatch(actions.loadSourceText({ cx, source })); + + const id = makePendingLocationId(bp.location); + + await dispatch(actions.addBreakpoint(cx, bp.location)); + await dispatch( + actions.setBreakpointOptions(cx, bp.location, { condition: "2" }) + ); + const bps = selectors.getPendingBreakpoints(getState()); + const breakpoint = bps[id]; + expect(breakpoint.options.condition).toBe("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); + + await dispatch(actions.newGeneratedSource(makeSource("bar.js"))); + + const source = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + await dispatch(actions.loadSourceText({ cx, source })); + 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")) + ); + await dispatch(actions.newGeneratedSource(makeSource("foo.js"))); + await dispatch(actions.loadSourceText({ cx, source })); + + 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; + + await dispatch(actions.newGeneratedSource(makeSource("bar.js"))); + const source = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + await dispatch(actions.loadSourceText({ cx, source })); + + await waitForState(store, state => { + const bps = selectors.getBreakpointsForSource(state, source.id); + return bps && Object.values(bps).length > 0; + }); + + const bp = selectors.getBreakpointForLocation(getState(), { + line: 5, + column: 2, + sourceUrl: source.url, + 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); + + await dispatch(actions.newGeneratedSource(makeSource("bar.js"))); + const source = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + await dispatch(actions.loadSourceText({ cx, source })); + + 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: sourceMaps.generatedToOriginalId(source.id, sourceURL), + url: sourceURL, + }, + ], + getOriginalSourceText: async () => ({ text: "" }), + getGeneratedLocation: async location => ({ + line: location.line, + column: location.column, + sourceId: location.sourceId, + }), + getOriginalLocation: async location => location, + getGeneratedRangesForOriginal: async () => [ + { start: { line: 0, column: 0 }, end: { line: 10, column: 10 } }, + ], + getOriginalLocations: async items => + items.map(item => ({ + ...item, + sourceId: sourceMaps.generatedToOriginalId(item.sourceId, sourceURL), + })), + }); + + const { getState, dispatch } = store; + + expect(selectors.getBreakpointCount(getState())).toEqual(0); + + await dispatch(actions.newGeneratedSource(makeSource("bar.js"))); + 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); + + await dispatch(actions.newGeneratedSource(makeSource("bar.js"))); + await dispatch(actions.newGeneratedSource(makeSource("foo.js"))); + const [source1, source2] = await dispatch( + actions.newGeneratedSources([makeSource("bar.js"), makeSource("foo.js")]) + ); + await dispatch(actions.loadSourceText({ cx, source: source1 })); + await dispatch(actions.loadSourceText({ cx, source: source2 })); + + 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..4cb2542adf --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/preview.spec.js @@ -0,0 +1,215 @@ +/* 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/>. */ + +// @flow + +import { + createStore, + selectors, + actions, + makeSource, + makeFrame, + waitForState, + waitATick, +} from "../../utils/test-head"; + +function waitForPreview(store, expression) { + return waitForState(store, state => { + const preview = selectors.getPreview(state); + return preview && preview.expression == expression; + }); +} + +function mockThreadFront(overrides) { + return { + evaluateInFrame: 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 } = store; + const base = await dispatch( + actions.newGeneratedSource(makeSource("base.js")) + ); + + await dispatch(actions.selectSource(cx, base.id)); + await waitForState(store, state => selectors.hasSymbols(state, base)); + + 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 base = await dispatch( + actions.newGeneratedSource(makeSource("base.js")) + ); + + await dispatch(actions.selectSource(cx, base.id)); + await waitForState(store, state => selectors.hasSymbols(state, base)); + 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; + + /* $FlowIgnore[not-a-function] this is guarantied to be initialized because + `new new Promise(foo)` calls foo synchronously */ + resolveFirst(); + waitForPreview(store, "firstSetPreview").then(() => { + fail = true; + }); + + // $FlowIgnore[not-a-function] same as above + 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; + + /* $FlowIgnore[not-a-function] this is guarantied to be initialized because + `new new Promise(foo)` calls foo synchronously */ + resolveSecond(); + await waitForPreview(store, "secondSetPreview"); + + // $FlowIgnore[not-a-function] same as above + 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/project-text-search.spec.js b/devtools/client/debugger/src/actions/tests/project-text-search.spec.js new file mode 100644 index 0000000000..e63714c58c --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/project-text-search.spec.js @@ -0,0 +1,161 @@ +/* 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/>. */ + +// @flow + +import { + actions, + createStore, + selectors, + makeSource, +} from "../../utils/test-head"; + +const { + getSource, + getTextSearchQuery, + getTextSearchResults, + getTextSearchStatus, +} = selectors; + +const sources = { + foo1: { + source: "function foo1() {\n const foo = 5; return foo;\n}", + contentType: "text/javascript", + }, + foo2: { + source: "function foo2(x, y) {\n return x + y;\n}", + contentType: "text/javascript", + }, + bar: { + source: "function bla(x, y) {\n const bar = 4; return 2;\n}", + contentType: "text/javascript", + }, + "bar:formatted": { + source: "function bla(x, y) {\n const bar = 4; return 2;\n}", + contentType: "text/javascript", + }, +}; + +const threadFront = { + sourceContents: async ({ source }) => sources[source], + getSourceActorBreakpointPositions: async () => ({}), + getSourceActorBreakableLines: async () => [], +}; + +describe("project text search", () => { + it("should add a project text search query", () => { + const { dispatch, getState, cx } = createStore(); + const mockQuery = "foo"; + + dispatch(actions.addSearchQuery(cx, mockQuery)); + + expect(getTextSearchQuery(getState())).toEqual(mockQuery); + }); + + it("should search all the loaded sources based on the query", async () => { + const { dispatch, getState, cx } = createStore(threadFront); + const mockQuery = "foo"; + + await dispatch(actions.newGeneratedSource(makeSource("foo1"))); + await dispatch(actions.newGeneratedSource(makeSource("foo2"))); + + await dispatch(actions.searchSources(cx, mockQuery)); + + const results = getTextSearchResults(getState()); + expect(results).toMatchSnapshot(); + }); + + it("should ignore sources with minified versions", async () => { + const mockMaps = { + getOriginalSourceText: async () => ({ + source: "function bla(x, y) {\n const bar = 4; return 2;\n}", + contentType: "text/javascript", + }), + applySourceMap: async () => {}, + getGeneratedRangesForOriginal: async () => [], + getOriginalLocations: async items => items, + getOriginalLocation: async loc => loc, + }; + + const { dispatch, getState, cx } = createStore(threadFront, {}, mockMaps); + + const source1 = await dispatch( + actions.newGeneratedSource(makeSource("bar")) + ); + await dispatch(actions.loadSourceText({ cx, source: source1 })); + + await dispatch(actions.togglePrettyPrint(cx, source1.id)); + + await dispatch(actions.searchSources(cx, "bla")); + + const results = getTextSearchResults(getState()); + expect(results).toMatchSnapshot(); + }); + + it("should search a specific source", async () => { + const { dispatch, getState, cx } = createStore(threadFront); + + const source = await dispatch( + actions.newGeneratedSource(makeSource("bar")) + ); + await dispatch(actions.loadSourceText({ cx, source })); + + dispatch(actions.addSearchQuery(cx, "bla")); + + const barSource = getSource(getState(), "bar"); + if (!barSource) { + throw new Error("no barSource"); + } + const sourceId = barSource.id; + + await dispatch(actions.searchSource(cx, sourceId, "bla"), "bla"); + + const results = getTextSearchResults(getState()); + + expect(results).toMatchSnapshot(); + expect(results).toHaveLength(1); + }); + + it("should clear all the search results", async () => { + const { dispatch, getState, cx } = createStore(threadFront); + const mockQuery = "foo"; + + await dispatch(actions.newGeneratedSource(makeSource("foo1"))); + await dispatch(actions.searchSources(cx, mockQuery)); + + expect(getTextSearchResults(getState())).toMatchSnapshot(); + + await dispatch(actions.clearSearchResults(cx)); + + expect(getTextSearchResults(getState())).toMatchSnapshot(); + }); + + it("should set the status properly", () => { + const { dispatch, getState, cx } = createStore(); + const mockStatus = "FETCHING"; + dispatch(actions.updateSearchStatus(cx, mockStatus)); + expect(getTextSearchStatus(getState())).toEqual(mockStatus); + }); + + it("should close project search", async () => { + const { dispatch, getState, cx } = createStore(threadFront); + const mockQuery = "foo"; + + await dispatch(actions.newGeneratedSource(makeSource("foo1"))); + await dispatch(actions.searchSources(cx, mockQuery)); + + expect(getTextSearchResults(getState())).toMatchSnapshot(); + + dispatch(actions.closeProjectSearch(cx)); + + expect(getTextSearchQuery(getState())).toEqual(""); + + const results = getTextSearchResults(getState()); + + expect(results).toMatchSnapshot(); + expect(results).toHaveLength(0); + const status = getTextSearchStatus(getState()); + expect(status).toEqual("INITIAL"); + }); +}); diff --git a/devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js b/devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js new file mode 100644 index 0000000000..b8d2fb2906 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js @@ -0,0 +1,95 @@ +/* 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/>. */ + +// @flow + +import { + createStore, + selectors, + actions, + makeSource, +} from "../../utils/test-head"; + +const { + getProjectDirectoryRoot, + getProjectDirectoryRootName, + getDisplayedSources, +} = selectors; + +describe("setProjectDirectoryRoot", () => { + it("should set domain directory as root", async () => { + const { dispatch, getState, cx } = createStore(); + dispatch(actions.setProjectDirectoryRoot(cx, "example.com", "foo")); + expect(getProjectDirectoryRoot(getState())).toBe("example.com"); + }); + + it("should set a directory as root directory", async () => { + const { dispatch, getState, cx } = createStore(); + dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/foo", "foo")); + expect(getProjectDirectoryRoot(getState())).toBe("/example.com/foo"); + }); + + it("should add to the directory ", () => { + const { dispatch, getState, cx } = createStore(); + dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/foo", "foo")); + dispatch(actions.setProjectDirectoryRoot(cx, "/foo/bar", "foo")); + expect(getProjectDirectoryRoot(getState())).toBe("/example.com/foo/bar"); + }); + + it("should update the directory ", () => { + const { dispatch, getState, cx } = createStore(); + dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/foo", "foo")); + dispatch(actions.clearProjectDirectoryRoot(cx)); + dispatch(actions.setProjectDirectoryRoot(cx, "/example.com/bar", "foo")); + expect(getProjectDirectoryRoot(getState())).toBe("/example.com/bar"); + }); + + it("should filter sources", async () => { + const store = createStore({ + getSourceActorBreakableLines: async () => [], + }); + const { dispatch, getState, cx } = store; + await dispatch(actions.newGeneratedSource(makeSource("js/scopes.js"))); + await dispatch(actions.newGeneratedSource(makeSource("lib/vendor.js"))); + + dispatch( + actions.setProjectDirectoryRoot(cx, "localhost:8000/examples/js", "foo") + ); + + const filteredSourcesByThread = getDisplayedSources(getState()); + const filteredSources = (Object.values( + filteredSourcesByThread.FakeThread + ): any)[0]; + + expect(filteredSources.url).toEqual( + "http://localhost:8000/examples/js/scopes.js" + ); + + expect(filteredSources.relativeUrl).toEqual("scopes.js"); + }); + + it("should update the child directory ", () => { + const { dispatch, getState, cx } = createStore({ + getSourceActorBreakableLines: async () => [], + }); + dispatch(actions.setProjectDirectoryRoot(cx, "example.com", "foo")); + dispatch(actions.setProjectDirectoryRoot(cx, "example.com/foo/bar", "foo")); + expect(getProjectDirectoryRoot(getState())).toBe("example.com/foo/bar"); + }); + + it("should update the child directory when domain name is Webpack://", () => { + const { dispatch, getState, cx } = createStore({ + getSourceActorBreakableLines: async () => [], + }); + dispatch(actions.setProjectDirectoryRoot(cx, "webpack://", "foo")); + dispatch(actions.setProjectDirectoryRoot(cx, "webpack:///app", "foo")); + expect(getProjectDirectoryRoot(getState())).toBe("webpack:///app"); + }); + + it("should set the name of the root directory", () => { + const { dispatch, getState, cx } = createStore(); + dispatch(actions.setProjectDirectoryRoot(cx, "foo", "example.com")); + expect(getProjectDirectoryRootName(getState())).toBe("example.com"); + }); +}); diff --git a/devtools/client/debugger/src/actions/tests/source-tree.spec.js b/devtools/client/debugger/src/actions/tests/source-tree.spec.js new file mode 100644 index 0000000000..fcbd56da33 --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/source-tree.spec.js @@ -0,0 +1,19 @@ +/* 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/>. */ + +// @flow + +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..210cc0373a --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/tabs.spec.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/>. */ + +// @flow + +import { + actions, + selectors, + createStore, + makeSource, +} from "../../utils/test-head"; +const { getSelectedSource, getSourceTabs } = selectors; + +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, { sourceId: "foo.js", 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")) + ); + await dispatch(actions.newGeneratedSource(makeSource("bar.js"))); + await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 })); + await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", 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, { sourceId: "foo.js", 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); + + await dispatch(actions.newGeneratedSource(makeSource("foo.js"))); + const barSource = await dispatch( + actions.newGeneratedSource(makeSource("bar.js")) + ); + await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 })); + await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", 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); + + await dispatch(actions.newGeneratedSource(makeSource("foo.js"))); + await dispatch(actions.newGeneratedSource(makeSource("bar.js"))); + await dispatch(actions.newGeneratedSource(makeSource("bazz.js"))); + await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 })); + await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 })); + await dispatch( + actions.selectLocation(cx, { sourceId: "bazz.js", 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); + + await dispatch(actions.newGeneratedSource(makeSource("foo.js"))); + await dispatch(actions.newGeneratedSource(makeSource("bar.js"))); + await dispatch(actions.newGeneratedSource(makeSource("bazz.js"))); + await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 })); + await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", line: 1 })); + await dispatch( + actions.selectLocation(cx, { sourceId: "bazz.js", 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); + + await dispatch(actions.newGeneratedSource(makeSource("foo.js"))); + await dispatch(actions.newGeneratedSource(makeSource("bar.js"))); + await dispatch(actions.selectLocation(cx, { sourceId: "foo.js", line: 1 })); + await dispatch(actions.selectLocation(cx, { sourceId: "bar.js", 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..e0cce0225a --- /dev/null +++ b/devtools/client/debugger/src/actions/tests/ui.spec.js @@ -0,0 +1,75 @@ +/* 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/>. */ + +// @flow + +import { createStore, selectors, actions } from "../../utils/test-head"; + +const { + getActiveSearch, + getFrameworkGroupingState, + getPaneCollapse, + getHighlightedLineRange, +} = selectors; + +describe("ui", () => { + it("should toggle the visible state of project search", () => { + const { dispatch, getState } = createStore(); + expect(getActiveSearch(getState())).toBe(null); + dispatch(actions.setActiveSearch("project")); + expect(getActiveSearch(getState())).toBe("project"); + }); + + it("should close project search", () => { + const { dispatch, getState } = createStore(); + expect(getActiveSearch(getState())).toBe(null); + dispatch(actions.setActiveSearch("project")); + dispatch(actions.closeActiveSearch()); + expect(getActiveSearch(getState())).toBe(null); + }); + + 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", () => { + const { dispatch, getState } = createStore(); + const range = { start: 3, end: 5, sourceId: "2" }; + dispatch(actions.highlightLineRange(range)); + expect(getHighlightedLineRange(getState())).toEqual(range); + }); + + it("should clear highlight lines", () => { + const { dispatch, getState } = createStore(); + const range = { start: 3, end: 5, sourceId: "2" }; + dispatch(actions.highlightLineRange(range)); + dispatch(actions.clearHighlightLineRange()); + expect(getHighlightedLineRange(getState())).toEqual({}); + }); +}); |