diff --git a/.babel/transform-mc.js b/.babel/transform-mc.js index eb682ae148..7274d26567 100644 --- a/.babel/transform-mc.js +++ b/.babel/transform-mc.js @@ -36,6 +36,11 @@ const VENDORS = [ "Svg" ]; +const moduleMapping = { + Telemetry: "devtools/client/shared/telemetry", + asyncStorage: "devtools/shared/async-storage" +}; + /* * Updates devtools-modules imports such as * `import { Telemetry } from "devtools-modules"` @@ -49,11 +54,11 @@ function updateDevtoolsModulesImport(path, t) { for (let i = 0; i < specifiers.length; i++) { let specifier = specifiers[i]; - - if (specifier.local.name === "Telemetry") { + const localName = specifier.local.name; + if (localName in moduleMapping) { const newImport = t.importDeclaration( [t.importDefaultSpecifier(specifier.local)], - t.stringLiteral("devtools/client/shared/telemetry") + t.stringLiteral(moduleMapping[localName]) ); if (specifiers.length > 1) { diff --git a/configs/mozilla-central-mappings.js b/configs/mozilla-central-mappings.js index 21d50790a7..92e9f9701b 100644 --- a/configs/mozilla-central-mappings.js +++ b/configs/mozilla-central-mappings.js @@ -25,7 +25,6 @@ module.exports = Object.assign( "react-redux": "devtools/client/shared/vendor/react-redux", redux: "devtools/client/shared/vendor/redux", "prop-types": "devtools/client/shared/vendor/react-prop-types", - "devtools-modules/src/utils/telemetry": "devtools/client/shared/telemetry", "devtools-services": "Services", "wasmparser/dist/WasmParser": "devtools/client/shared/vendor/WasmParser", "wasmparser/dist/WasmDis": "devtools/client/shared/vendor/WasmDis" diff --git a/docs/dbg.md b/docs/dbg.md index 1171ab8d69..eb31efb61c 100644 --- a/docs/dbg.md +++ b/docs/dbg.md @@ -61,6 +61,16 @@ dbg.prefs.pauseOnExceptions = true; dbg.prefs.pauseOnExceptions; // true ``` +### prefs + +`dbg.asyncStore` references the async store helper. You can use `dbg.asyncStore` to see or change the state of any item. + +```js +dbg.asyncStore.pendingBreakpoints; // Promise(false) +dbg.asyncStore.pendingBreakpoints = true; +dbg.asyncStore.pendingBreakpoints; // Promise(true) +``` + ### features `dbg.features` references the debugger's feature flags. You can use `dbg.features` to see or change the state of any flag. diff --git a/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap b/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap index 68bd889dc8..c382fe03d2 100644 --- a/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap +++ b/src/actions/tests/__snapshots__/pending-breakpoints.spec.js.snap @@ -2,24 +2,26 @@ exports[`initializing when pending breakpoints exist in prefs syncs pending breakpoints 1`] = ` Object { - "astLocation": Object { - "name": undefined, - "offset": Object { + "http://localhost:8000/examples/bar.js:5:": Object { + "astLocation": Object { + "name": undefined, + "offset": Object { + "line": 5, + }, + }, + "condition": null, + "disabled": false, + "generatedLocation": Object { + "column": undefined, "line": 5, + "sourceUrl": "http://localhost:8000/examples/bar.js", + }, + "hidden": false, + "location": Object { + "column": undefined, + "line": 5, + "sourceUrl": "http://localhost:8000/examples/bar.js", }, - }, - "condition": null, - "disabled": false, - "generatedLocation": Object { - "column": undefined, - "line": 5, - "sourceUrl": "http://localhost:8000/examples/bar.js", - }, - "hidden": false, - "location": Object { - "column": undefined, - "line": 5, - "sourceUrl": "http://localhost:8000/examples/bar.js", }, } `; diff --git a/src/actions/tests/pending-breakpoints.spec.js b/src/actions/tests/pending-breakpoints.spec.js index d20c4e9457..3f6b24401c 100644 --- a/src/actions/tests/pending-breakpoints.spec.js +++ b/src/actions/tests/pending-breakpoints.spec.js @@ -13,11 +13,21 @@ import { simpleMockThreadClient } from "./helpers/threadClient.js"; -import { prefs } from "../../utils/prefs"; +import { asyncStore } from "../../utils/prefs"; + +function loadInitialState(opts = {}) { + const mockedPendingBreakpoint = mockPendingBreakpoint(opts); + const id = makePendingLocationId(mockedPendingBreakpoint.location); + asyncStore.pendingBreakpoints = { [id]: mockedPendingBreakpoint }; + + return { pendingBreakpoints: asyncStore.pendingBreakpoints }; +} jest.mock("../../utils/prefs", () => ({ prefs: { - expressions: [], + expressions: [] + }, + asyncStore: { pendingBreakpoints: {} }, features: { @@ -39,15 +49,11 @@ import { import { makePendingLocationId } from "../../utils/breakpoint"; describe("when adding breakpoints", () => { - const mockedPendingBreakpoint = mockPendingBreakpoint(); - - beforeEach(() => { - const id = makePendingLocationId(mockedPendingBreakpoint.location); - prefs.pendingBreakpoints = { [id]: mockedPendingBreakpoint }; - }); - it("a corresponding pending breakpoint should be added", async () => { - const { dispatch, getState } = createStore(simpleMockThreadClient); + const { dispatch, getState } = createStore( + simpleMockThreadClient, + loadInitialState() + ); await dispatch(actions.newSource(makeSource("foo.js"))); await dispatch(actions.loadSourceText(makeSource("foo.js"))); @@ -76,7 +82,10 @@ describe("when adding breakpoints", () => { }); it("add a corresponding pendingBreakpoint for each addition", async () => { - const { dispatch, getState } = createStore(simpleMockThreadClient); + const { dispatch, getState } = createStore( + simpleMockThreadClient, + loadInitialState() + ); await dispatch(actions.newSource(makeSource("foo"))); await dispatch(actions.newSource(makeSource("foo2"))); @@ -93,7 +102,10 @@ describe("when adding breakpoints", () => { }); it("hidden breakponts do not create pending bps", async () => { - const { dispatch, getState } = createStore(simpleMockThreadClient); + const { dispatch, getState } = createStore( + simpleMockThreadClient, + loadInitialState() + ); await dispatch(actions.newSource(makeSource("foo"))); await dispatch(actions.loadSourceText(makeSource("foo"))); @@ -107,7 +119,10 @@ describe("when adding breakpoints", () => { }); it("remove a corresponding pending breakpoint when deleting", async () => { - const { dispatch, getState } = createStore(simpleMockThreadClient); + const { dispatch, getState } = createStore( + simpleMockThreadClient, + loadInitialState() + ); await dispatch(actions.newSource(makeSource("foo"))); await dispatch(actions.newSource(makeSource("foo2"))); await dispatch(actions.loadSourceText(makeSource("foo"))); @@ -125,15 +140,11 @@ describe("when adding breakpoints", () => { }); describe("when changing an existing breakpoint", () => { - const mockedPendingBreakpoint = mockPendingBreakpoint(); - - beforeEach(() => { - const id = makePendingLocationId(mockedPendingBreakpoint.location); - prefs.pendingBreakpoints = { [id]: mockedPendingBreakpoint }; - }); - it("updates corresponding pendingBreakpoint", async () => { - const { dispatch, getState } = createStore(simpleMockThreadClient); + const { dispatch, getState } = createStore( + simpleMockThreadClient, + loadInitialState() + ); const bp = generateBreakpoint("foo"); const id = makePendingLocationId(bp.location); await dispatch(actions.newSource(makeSource("foo"))); @@ -149,7 +160,10 @@ describe("when changing an existing breakpoint", () => { }); it("if disabled, updates corresponding pendingBreakpoint", async () => { - const { dispatch, getState } = createStore(simpleMockThreadClient); + const { dispatch, getState } = createStore( + simpleMockThreadClient, + loadInitialState() + ); const bp = generateBreakpoint("foo"); const id = makePendingLocationId(bp.location); @@ -164,7 +178,10 @@ describe("when changing an existing breakpoint", () => { }); it("does not delete the pre-existing pendingBreakpoint", async () => { - const { dispatch, getState } = createStore(simpleMockThreadClient); + const { dispatch, getState } = createStore( + simpleMockThreadClient, + loadInitialState() + ); const bp = generateBreakpoint("foo.js"); const source = makeSource("foo.js"); await dispatch(actions.newSource(source)); @@ -183,23 +200,20 @@ describe("when changing an existing breakpoint", () => { }); describe("initializing when pending breakpoints exist in prefs", () => { - const mockedPendingBreakpoint = mockPendingBreakpoint(); - - beforeEach(() => { - const id = makePendingLocationId(mockedPendingBreakpoint.location); - prefs.pendingBreakpoints = { [id]: mockedPendingBreakpoint }; - }); - it("syncs pending breakpoints", async () => { - const { getState } = createStore(simpleMockThreadClient); - const id = makePendingLocationId(mockedPendingBreakpoint.location); + const { getState } = createStore( + simpleMockThreadClient, + loadInitialState() + ); const bps = selectors.getPendingBreakpoints(getState()); - const bp = bps[id]; - expect(bp).toMatchSnapshot(); + expect(bps).toMatchSnapshot(); }); it("re-adding breakpoints update existing pending breakpoints", async () => { - const { dispatch, getState } = createStore(simpleMockThreadClient); + const { dispatch, getState } = createStore( + simpleMockThreadClient, + loadInitialState() + ); const bar = generateBreakpoint("bar.js"); await dispatch(actions.newSource(makeSource("bar.js"))); await dispatch(actions.loadSourceText(makeSource("bar.js"))); @@ -211,7 +225,10 @@ describe("initializing when pending breakpoints exist in prefs", () => { }); it("adding bps doesn't remove existing pending breakpoints", async () => { - const { dispatch, getState } = createStore(simpleMockThreadClient); + const { dispatch, getState } = createStore( + simpleMockThreadClient, + loadInitialState() + ); const bp = generateBreakpoint("foo.js"); await dispatch(actions.newSource(makeSource("foo.js"))); @@ -225,46 +242,32 @@ describe("initializing when pending breakpoints exist in prefs", () => { }); describe("initializing with disabled pending breakpoints in prefs", () => { - const mockedPendingBreakpoint = mockPendingBreakpoint({ disabled: true }); - - beforeEach(() => { - const id = makePendingLocationId(mockedPendingBreakpoint.location); - prefs.pendingBreakpoints = { [id]: mockedPendingBreakpoint }; - }); - it("syncs breakpoints with pending breakpoints", async () => { - const expectedLocation = { - ...mockedPendingBreakpoint.location, - sourceId: "bar.js" - }; + const store = createStore( + simpleMockThreadClient, + loadInitialState({ disabled: true }) + ); - const store = createStore(simpleMockThreadClient); const { getState, dispatch } = store; const source = makeSource("bar.js"); - await dispatch(actions.newSource(source)); + await dispatch(actions.newSource(source)); await dispatch(actions.loadSourceText(makeSource("bar.js"))); - await waitForState(store, state => - selectors.getBreakpoint(state, expectedLocation) - ); + await waitForState(store, state => { + const bps = selectors.getBreakpointsForSource(state, "bar.js"); + return bps && bps.size > 0; + }); - const bp = selectors.getBreakpoint(getState(), expectedLocation); - expect(bp.location).toEqual(expectedLocation); - expect(bp.disabled).toEqual(mockedPendingBreakpoint.disabled); + const bp = selectors.getBreakpointForLine(getState(), "bar.js", 5); + expect(bp.location.sourceId).toEqual("bar.js"); + expect(bp.disabled).toEqual(true); }); }); describe("adding sources", () => { - const mockedPendingBreakpoint = mockPendingBreakpoint(); - - beforeEach(() => { - const id = makePendingLocationId(mockedPendingBreakpoint.location); - prefs.pendingBreakpoints = { [id]: mockedPendingBreakpoint }; - }); - it("corresponding breakpoints are added for a single source", async () => { - const store = createStore(simpleMockThreadClient); + const store = createStore(simpleMockThreadClient, loadInitialState()); const { getState, dispatch } = store; let bps = selectors.getBreakpoints(getState()); @@ -284,7 +287,7 @@ describe("adding sources", () => { }); it("add corresponding breakpoints for multiple sources", async () => { - const store = createStore(simpleMockThreadClient); + const store = createStore(simpleMockThreadClient, loadInitialState()); const { getState, dispatch } = store; let bps = selectors.getBreakpoints(getState()); @@ -307,13 +310,6 @@ describe("adding sources", () => { }); describe("invalid breakpoint location", () => { - const mockedPendingBreakpoint = mockPendingBreakpoint(); - - beforeEach(() => { - const id = makePendingLocationId(mockedPendingBreakpoint.location); - prefs.pendingBreakpoints = { [id]: mockedPendingBreakpoint }; - }); - it("a corrected corresponding pending breakpoint is added", async () => { // setup const bp = generateBreakpoint("foo.js"); diff --git a/src/client/index.js b/src/client/index.js index b4f50f1360..9c45cf5da8 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -6,7 +6,7 @@ import * as firefox from "./firefox"; -import { prefs } from "../utils/prefs"; +import { prefs, asyncStore } from "../utils/prefs"; import { setupHelper } from "../utils/dbg"; import { @@ -25,6 +25,11 @@ function loadFromPrefs(actions: Object) { } } +async function loadInitialState() { + const pendingBreakpoints = await asyncStore.pendingBreakpoints; + return { pendingBreakpoints }; +} + export async function onConnect( connection: Object, { services, toolboxActions }: Object @@ -35,10 +40,15 @@ export async function onConnect( } const commands = firefox.clientCommands; - const { store, actions, selectors } = bootstrapStore(commands, { - services, - toolboxActions - }); + const initialState = await loadInitialState(); + const { store, actions, selectors } = bootstrapStore( + commands, + { + services, + toolboxActions + }, + initialState + ); const workers = bootstrapWorkers(); await firefox.onConnect(connection, actions); diff --git a/src/reducers/pending-breakpoints.js b/src/reducers/pending-breakpoints.js index 78c65b3c42..215b41ab6e 100644 --- a/src/reducers/pending-breakpoints.js +++ b/src/reducers/pending-breakpoints.js @@ -14,21 +14,12 @@ import { makePendingLocationId } from "../utils/breakpoint"; -import { prefs } from "../utils/prefs"; - import type { PendingBreakpoint } from "../types"; import type { Action, DonePromiseAction } from "../actions/types"; export type PendingBreakpointsState = { [string]: PendingBreakpoint }; -export function initialPendingBreakpointsState(): PendingBreakpointsState { - return restorePendingBreakpoints(); -} - -function update( - state: PendingBreakpointsState = initialPendingBreakpointsState(), - action: Action -) { +function update(state: PendingBreakpointsState = {}, action: Action) { switch (action.type) { case "ADD_BREAKPOINT": { return addBreakpoint(state, action); @@ -161,8 +152,4 @@ export function getPendingBreakpointsForSource( ); } -function restorePendingBreakpoints() { - return { ...prefs.pendingBreakpoints }; -} - export default update; diff --git a/src/test/mochitest/browser_dbg-sourcemapped-breakpoint-console.js b/src/test/mochitest/browser_dbg-sourcemapped-breakpoint-console.js index d54bdebf34..396c9d95d4 100644 --- a/src/test/mochitest/browser_dbg-sourcemapped-breakpoint-console.js +++ b/src/test/mochitest/browser_dbg-sourcemapped-breakpoint-console.js @@ -47,7 +47,6 @@ add_task(async function() { await pushPref("devtools.debugger.features.map-scopes", true); const dbg = await initDebugger("doc-sourcemapped.html"); - await evalInConsoleAtPoint(dbg, "webpack3-babel6", "eval-maps", { line: 14, column: 4 }, [ "one === 1", "two === 4", diff --git a/src/test/mochitest/helpers.js b/src/test/mochitest/helpers.js index af93ecc7f3..e508d7335a 100644 --- a/src/test/mochitest/helpers.js +++ b/src/test/mochitest/helpers.js @@ -8,6 +8,7 @@ var { Toolbox } = require("devtools/client/framework/toolbox"); var { Task } = require("devtools/shared/task"); +var asyncStorage = require("devtools/shared/async-storage"); const sourceUtils = { isLoaded: source => source.loadedState === "loaded" @@ -481,6 +482,7 @@ function createDebuggerContext(toolbox) { * Clear all the debugger related preferences. */ function clearDebuggerPreferences() { + asyncStorage.clear() Services.prefs.clearUserPref("devtools.debugger.pause-on-exceptions"); Services.prefs.clearUserPref("devtools.debugger.pause-on-caught-exceptions"); Services.prefs.clearUserPref("devtools.debugger.ignore-caught-exceptions"); diff --git a/src/utils/asyncStoreHelper.js b/src/utils/asyncStoreHelper.js new file mode 100644 index 0000000000..9e793a7bd5 --- /dev/null +++ b/src/utils/asyncStoreHelper.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 . */ + +// @flow + +import { asyncStorage } from "devtools-modules"; + +/* + * asyncStoreHelper wraps asyncStorage so that it is easy to define project + * specific properties. It is similar to PrefsHelper. + * + * e.g. + * const asyncStore = asyncStoreHelper("r", {a: "_a"}) + * asyncStore.a // => asyncStorage.getItem("r._a") + * asyncStore.a = 2 // => asyncStorage.setItem("r._a", 2) + */ +export function asyncStoreHelper(root: string, mappings: Object) { + let store: any = {}; + + function getMappingKey(key) { + return Array.isArray(mappings[key]) ? mappings[key][0] : mappings[key]; + } + + function getMappingDefaultValue(key) { + return Array.isArray(mappings[key]) ? mappings[key][1] : null; + } + + Object.keys(mappings).map(key => + Object.defineProperty(store, key, { + async get() { + const value = await asyncStorage.getItem( + `${root}.${getMappingKey(key)}` + ); + return value || getMappingDefaultValue(key); + }, + set(value) { + return asyncStorage.setItem(`${root}.${getMappingKey(key)}`, value); + } + }) + ); + + store = new Proxy(store, { + set: function(target, property, value, receiver) { + if (!mappings.hasOwnProperty(property)) { + throw new Error(`AsyncStore: ${property} is not defined in mappings`); + } + + Reflect.set(...arguments); + return true; + } + }); + + return (store: { [$Keys]: any }); +} diff --git a/src/utils/bootstrap.js b/src/utils/bootstrap.js index 4f53fc5197..3861e821a5 100644 --- a/src/utils/bootstrap.js +++ b/src/utils/bootstrap.js @@ -19,7 +19,7 @@ import configureStore from "../actions/utils/create-store"; import reducers from "../reducers"; import * as selectors from "../selectors"; import App from "../components/App"; -import { prefs } from "./prefs"; +import { asyncStore } from "./prefs"; function renderPanel(component, store) { const root = document.createElement("div"); @@ -37,7 +37,11 @@ function renderPanel(component, store) { ); } -export function bootstrapStore(client: any, { services, toolboxActions }: any) { +export function bootstrapStore( + client: any, + { services, toolboxActions }: any, + initialState: Object +) { const createStore = configureStore({ log: isTesting(), timing: isDevelopment(), @@ -46,7 +50,7 @@ export function bootstrapStore(client: any, { services, toolboxActions }: any) { } }); - const store = createStore(combineReducers(reducers)); + const store = createStore(combineReducers(reducers), initialState); store.subscribe(() => updatePrefs(store.getState())); const actions = bindActionCreators( @@ -101,6 +105,6 @@ function updatePrefs(state: any) { previousPendingBreakpoints && currentPendingBreakpoints !== previousPendingBreakpoints ) { - prefs.pendingBreakpoints = currentPendingBreakpoints; + asyncStore.pendingBreakpoints = currentPendingBreakpoints; } } diff --git a/src/utils/dbg.js b/src/utils/dbg.js index 9743907c2f..882f65bbd7 100644 --- a/src/utils/dbg.js +++ b/src/utils/dbg.js @@ -5,7 +5,7 @@ // @flow import * as timings from "./timings"; -import { prefs, features } from "./prefs"; +import { prefs, asyncStore, features } from "./prefs"; import { isDevelopment, isTesting } from "devtools-environment"; import { formatPausePoints } from "./pause/pausePoints"; @@ -61,6 +61,7 @@ export function setupHelper(obj: Object) { ...obj, selectors, prefs, + asyncStore, features, timings, getCM, diff --git a/src/utils/prefs.js b/src/utils/prefs.js index 7dead8941c..f6efc6198c 100644 --- a/src/utils/prefs.js +++ b/src/utils/prefs.js @@ -4,9 +4,10 @@ // @flow -const { isDevelopment } = require("devtools-environment"); -const { PrefsHelper } = require("devtools-modules"); -const Services = require("devtools-services"); +import { PrefsHelper } from "devtools-modules"; +import { isDevelopment } from "devtools-environment"; +import Services from "devtools-services"; +import { asyncStoreHelper } from "./asyncStoreHelper"; const prefsSchemaVersion = "1.0.3"; @@ -110,6 +111,10 @@ export const features = new PrefsHelper("devtools.debugger.features", { componentPane: ["Bool", "component-pane"] }); +export const asyncStore = asyncStoreHelper("debugger", { + pendingBreakpoints: ["pending-breakpoints", {}] +}); + if (prefs.debuggerPrefsSchemaVersion !== prefsSchemaVersion) { // clear pending Breakpoints prefs.pendingBreakpoints = {}; diff --git a/src/utils/tests/asyncStoreHelper.spec.js b/src/utils/tests/asyncStoreHelper.spec.js new file mode 100644 index 0000000000..9a158f104c --- /dev/null +++ b/src/utils/tests/asyncStoreHelper.spec.js @@ -0,0 +1,44 @@ +/* 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 . */ + +import { asyncStoreHelper } from "../asyncStoreHelper"; +import { asyncStorage } from "devtools-modules"; + +function mockAsyncStorage() { + const store = {}; + jest.spyOn(asyncStorage, "getItem"); + jest.spyOn(asyncStorage, "setItem"); + + asyncStorage.getItem.mockImplementation(key => store[key]); + asyncStorage.setItem.mockImplementation((key, value) => (store[key] = value)); +} + +describe("asycStoreHelper", () => { + it("can get and set values", async () => { + mockAsyncStorage(); + const asyncStore = asyncStoreHelper("root", { a: "_a" }); + asyncStore.a = 3; + expect(await asyncStore.a).toEqual(3); + }); + + it("supports default values", async () => { + mockAsyncStorage(); + const asyncStore = asyncStoreHelper("root", { a: ["_a", {}] }); + expect(await asyncStore.a).toEqual({}); + }); + + it("undefined default value", async () => { + mockAsyncStorage(); + const asyncStore = asyncStoreHelper("root", { a: "_a" }); + expect(await asyncStore.a).toEqual(null); + }); + + it("setting an undefined mapping", async () => { + mockAsyncStorage(); + const asyncStore = asyncStoreHelper("root", { a: "_a" }); + expect(() => { + asyncStore.b = 3; + }).toThrow("AsyncStore: b is not defined"); + }); +});