summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/actions/tests
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/actions/tests')
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/ast.spec.js.snap83
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/expressions.spec.js.snap11
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap112
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/preview.spec.js.snap3
-rw-r--r--devtools/client/debugger/src/actions/tests/__snapshots__/project-text-search.spec.js.snap180
-rw-r--r--devtools/client/debugger/src/actions/tests/ast.spec.js128
-rw-r--r--devtools/client/debugger/src/actions/tests/expressions.spec.js186
-rw-r--r--devtools/client/debugger/src/actions/tests/file-search.spec.js66
-rw-r--r--devtools/client/debugger/src/actions/tests/fixtures/immutable.js2
-rw-r--r--devtools/client/debugger/src/actions/tests/fixtures/reactComponent.js7
-rw-r--r--devtools/client/debugger/src/actions/tests/fixtures/reactFuncComponent.js5
-rw-r--r--devtools/client/debugger/src/actions/tests/fixtures/scopes.js11
-rw-r--r--devtools/client/debugger/src/actions/tests/helpers/breakpoints.js64
-rw-r--r--devtools/client/debugger/src/actions/tests/helpers/mockCommandClient.js55
-rw-r--r--devtools/client/debugger/src/actions/tests/helpers/readFixture.js16
-rw-r--r--devtools/client/debugger/src/actions/tests/navigation.spec.js108
-rw-r--r--devtools/client/debugger/src/actions/tests/pending-breakpoints.spec.js439
-rw-r--r--devtools/client/debugger/src/actions/tests/preview.spec.js215
-rw-r--r--devtools/client/debugger/src/actions/tests/project-text-search.spec.js161
-rw-r--r--devtools/client/debugger/src/actions/tests/setProjectDirectoryRoot.spec.js95
-rw-r--r--devtools/client/debugger/src/actions/tests/source-tree.spec.js19
-rw-r--r--devtools/client/debugger/src/actions/tests/tabs.spec.js138
-rw-r--r--devtools/client/debugger/src/actions/tests/ui.spec.js75
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({});
+ });
+});