Bug 1550001 - Provide event listener breakpoint UI r=loganfsmyth

Differential Revision: https://phabricator.services.mozilla.com/D30986

--HG--
extra : moz-landing-system : lando
This commit is contained in:
David Walsh 2019-05-20 17:23:58 +00:00
parent eef685d1fd
commit bc1c818a3b
13 changed files with 647 additions and 130 deletions

View File

@ -4,29 +4,88 @@
// @flow
import { uniq, remove } from "lodash";
import { asyncStore } from "../utils/prefs";
import {
getActiveEventListeners,
getEventListenerExpanded,
} from "../selectors";
import type { ThunkArgs } from "./types";
import type { EventListenerBreakpoints } from "../types";
export function addEventListeners(events: EventListenerBreakpoints) {
return async ({ dispatch, client }: ThunkArgs) => {
await dispatch({
type: "ADD_EVENT_LISTENERS",
events,
async function updateBreakpoints(dispatch, client, newEvents: string[]) {
dispatch({ type: "UPDATE_EVENT_LISTENERS", active: newEvents });
const current = await asyncStore.eventListenerBreakpoints;
asyncStore.eventListenerBreakpoints = {
...current,
active: newEvents,
};
client.setEventListenerBreakpoints(newEvents);
}
async function updateExpanded(dispatch, newExpanded: string[]) {
dispatch({
type: "UPDATE_EVENT_LISTENER_EXPANDED",
expanded: newExpanded,
});
const newList = await asyncStore.eventListenerBreakpoints;
client.setEventListenerBreakpoints(newList);
const current = await asyncStore.eventListenerBreakpoints;
asyncStore.eventListenerBreakpoints = {
...current,
expanded: newExpanded,
};
}
export function removeEventListeners(events: EventListenerBreakpoints) {
return async ({ dispatch, client }: ThunkArgs) => {
await dispatch({
type: "REMOVE_EVENT_LISTENERS",
events,
});
const newList = await asyncStore.eventListenerBreakpoints;
client.setEventListenerBreakpoints(newList);
export function addEventListenerBreakpoints(eventsToAdd: string[]) {
return async ({ dispatch, client, getState }: ThunkArgs) => {
const activeListenerBreakpoints = await getActiveEventListeners(getState());
const newEvents = uniq([...eventsToAdd, ...activeListenerBreakpoints]);
updateBreakpoints(dispatch, client, newEvents);
};
}
export function removeEventListenerBreakpoints(eventsToRemove: string[]) {
return async ({ dispatch, client, getState }: ThunkArgs) => {
const activeListenerBreakpoints = await getActiveEventListeners(getState());
const newEvents = remove(
activeListenerBreakpoints,
event => !eventsToRemove.includes(event)
);
updateBreakpoints(dispatch, client, newEvents);
};
}
export function addEventListenerExpanded(category: string) {
return async ({ dispatch, getState }: ThunkArgs) => {
const expanded = await getEventListenerExpanded(getState());
const newExpanded = uniq([...expanded, category]);
await updateExpanded(dispatch, newExpanded);
};
}
export function removeEventListenerExpanded(category: string) {
return async ({ dispatch, getState }: ThunkArgs) => {
const expanded = await getEventListenerExpanded(getState());
const newExpanded = expanded.filter(expand => expand != category);
updateExpanded(dispatch, newExpanded);
};
}
export function getEventListenerBreakpointTypes() {
return async ({ dispatch, client }: ThunkArgs) => {
const categories = await client.getEventListenerBreakpointTypes();
dispatch({ type: "RECEIVE_EVENT_LISTENER_TYPES", categories });
};
}

View File

@ -159,6 +159,30 @@ export type {
export type { panelPositionType } from "./UIAction";
export type { ASTAction } from "./ASTAction";
type ActiveEventListener = string;
type EventListenerEvent = { name: string, id: ActiveEventListener };
type EventListenerCategory = { name: string, events: EventListenerEvent[] };
export type EventListenerActiveList = ActiveEventListener[];
export type EventListenerCategoryList = EventListenerCategory[];
export type EventListenerExpandedList = string[];
export type EventListenerAction =
| {|
+type: "UPDATE_EVENT_LISTENERS",
+active: EventListenerActiveList,
|}
| {|
+type: "RECEIVE_EVENT_LISTENER_TYPES",
+categories: EventListenerCategoryList,
|}
| {|
+type: "UPDATE_EVENT_LISTENER_EXPANDED",
+expanded: EventListenerExpandedList,
|};
/**
* Actions: Source, Breakpoint, and Navigation
*
@ -180,4 +204,5 @@ export type Action =
| FileTextSearchAction
| ProjectTextSearchAction
| DebuggeeAction
| SourceTreeAction;
| SourceTreeAction
| EventListenerAction;

View File

@ -47,6 +47,9 @@ export async function onConnect(connection: any, actions: Object) {
skipBreakpoints: prefs.skipPausing,
});
// Retrieve possible event listener breakpoints
actions.getEventListenerBreakpointTypes();
// In Firefox, we need to initially request all of the sources. This
// usually fires off individual `newSource` notifications as the
// debugger finds them, but there may be existing sources already in

View File

@ -16,7 +16,6 @@ import type {
BreakpointLocation,
BreakpointOptions,
PendingLocation,
EventListenerBreakpoints,
Frame,
FrameId,
GeneratedSourceData,
@ -36,6 +35,8 @@ import type {
SourcesPacket,
} from "./types";
import type { EventListenerCategoryList } from "../../actions/types";
let workerClients: Object;
let threadClient: ThreadClient;
let tabTarget: TabTarget;
@ -361,8 +362,17 @@ function interrupt(thread: string): Promise<*> {
return lookupThreadClient(thread).interrupt();
}
function setEventListenerBreakpoints(eventTypes: EventListenerBreakpoints) {
// TODO: Figure out what sendpoint we want to hit
async function setEventListenerBreakpoints(ids: string[]) {
await threadClient.setActiveEventBreakpoints(ids);
await forEachWorkerThread(thread => thread.setActiveEventBreakpoints(ids));
}
// eslint-disable-next-line
async function getEventListenerBreakpointTypes(): Promise<
EventListenerCategoryList
> {
const { value } = await threadClient.getAvailableEventBreakpoints();
return value;
}
function pauseGrip(thread: string, func: Function): ObjectClient {
@ -525,6 +535,7 @@ const clientCommands = {
sendPacket,
setSkipPausing,
setEventListenerBreakpoints,
getEventListenerBreakpointTypes,
waitForWorkers,
detachWorkers,
hasWasmSupport,

View File

@ -25,6 +25,8 @@ import type {
Range,
} from "../../types";
import type { EventListenerCategoryList } from "../../actions/types";
type URL = string;
/**
@ -369,7 +371,10 @@ export type ThreadClient = {
actor: ActorId,
request: (payload: Object) => Promise<*>,
url: string,
setEventListenerBreakpoints: (string[]) => void,
setActiveEventBreakpoints: (string[]) => void,
getAvailableEventBreakpoints: () => Promise<{|
value: EventListenerCategoryList,
|}>,
skipBreakpoints: boolean => Promise<{| skip: boolean |}>,
};

View File

@ -15,6 +15,7 @@ import {
bootstrapStore,
bootstrapWorkers,
} from "../utils/bootstrap";
import { initialBreakpointsState } from "../reducers/breakpoints";
import type { Panel } from "./firefox/types";
@ -47,7 +48,12 @@ async function loadInitialState() {
const breakpoints = initialBreakpointsState(xhrBreakpoints);
return { pendingBreakpoints, tabs, breakpoints, eventListenerBreakpoints };
return {
pendingBreakpoints,
tabs,
breakpoints,
eventListenerBreakpoints,
};
}
function getClient(connection: any) {

View File

@ -9,141 +9,121 @@ import classnames from "classnames";
import { connect } from "../../utils/connect";
import actions from "../../actions";
import { getActiveEventListeners } from "../../selectors";
import {
getActiveEventListeners,
getEventListenerBreakpointTypes,
getEventListenerExpanded,
} from "../../selectors";
import AccessibleImage from "../shared/AccessibleImage";
import type { EventListenerBreakpoints } from "../../types";
import type {
EventListenerActiveList,
EventListenerCategoryList,
EventListenerExpandedList,
} from "../../actions/types";
import "./EventListeners.css";
const CATEGORIES = {
Mouse: ["click", "mouseover", "dblclick"],
Keyboard: ["keyup", "keydown"],
};
type Props = {
addEventListeners: typeof actions.addEventListeners,
removeEventListeners: typeof actions.removeEventListeners,
activeEventListeners: EventListenerBreakpoints,
categories: EventListenerCategoryList,
expandedCategories: EventListenerExpandedList,
activeEventListeners: EventListenerActiveList,
addEventListeners: typeof actions.addEventListenerBreakpoints,
removeEventListeners: typeof actions.removeEventListenerBreakpoints,
addEventListenerExpanded: typeof actions.addEventListenerExpanded,
removeEventListenerExpanded: typeof actions.removeEventListenerExpanded,
};
type State = {
expandedCategories: string[],
};
function getKey(category: string, eventType: string) {
return `${category}:${eventType}`;
}
class EventListeners extends Component<Props, State> {
constructor(props) {
super(props);
this.state = {
expandedCategories: [],
};
}
onCategoryToggle(category, event) {
const { expandedCategories } = this.state;
class EventListeners extends Component<Props> {
onCategoryToggle(category) {
const {
expandedCategories,
removeEventListenerExpanded,
addEventListenerExpanded,
} = this.props;
if (expandedCategories.includes(category)) {
this.setState({
expandedCategories: expandedCategories.filter(
eventCategory => eventCategory !== category
),
});
removeEventListenerExpanded(category);
} else {
this.setState({
expandedCategories: [...expandedCategories, category],
});
addEventListenerExpanded(category);
}
}
onCategoryClick(category, isChecked) {
const { addEventListeners, removeEventListeners } = this.props;
const events = CATEGORIES[category].map(eventType =>
getKey(category, eventType)
);
const eventsIds = category.events.map(event => event.id);
if (isChecked) {
addEventListeners(events);
addEventListeners(eventsIds);
} else {
removeEventListeners(events);
removeEventListeners(eventsIds);
}
}
onEventTypeClick(eventType, isChecked) {
onEventTypeClick(eventId, isChecked) {
const { addEventListeners, removeEventListeners } = this.props;
if (isChecked) {
addEventListeners([eventType]);
addEventListeners([eventId]);
} else {
removeEventListeners([eventType]);
removeEventListeners([eventId]);
}
}
renderCategoryHeading(category) {
const { expandedCategories } = this.state;
const { activeEventListeners } = this.props;
const { activeEventListeners, expandedCategories } = this.props;
const { events } = category;
const eventTypes = CATEGORIES[category];
const expanded = expandedCategories.includes(category);
const checked = eventTypes.every(eventType =>
activeEventListeners.includes(getKey(category, eventType))
);
const expanded = expandedCategories.includes(category.name);
const checked = events.every(({ id }) => activeEventListeners.includes(id));
const indeterminate =
!checked &&
eventTypes.some(eventType =>
activeEventListeners.includes(getKey(category, eventType))
);
!checked && events.some(({ id }) => activeEventListeners.includes(id));
return (
<div className="event-listener-header">
<button
className="event-listener-expand"
onClick={e => this.onCategoryToggle(category, e)}
onClick={() => this.onCategoryToggle(category.name)}
>
<AccessibleImage className={classnames("arrow", { expanded })} />
</button>
<label className="event-listener-label">
<input
type="checkbox"
value={category}
value={category.name}
onChange={e => this.onCategoryClick(category, e.target.checked)}
checked={checked}
ref={el => el && (el.indeterminate = indeterminate)}
/>
<span className="event-listener-category">{category}</span>
<span className="event-listener-category">{category.name}</span>
</label>
</div>
);
}
renderCategoryListing(category) {
const { activeEventListeners } = this.props;
const { expandedCategories } = this.state;
const { activeEventListeners, expandedCategories } = this.props;
const expanded = expandedCategories.includes(category);
const expanded = expandedCategories.includes(category.name);
if (!expanded) {
return null;
}
return (
<ul>
{CATEGORIES[category].map(eventType => {
const key = getKey(category, eventType);
{category.events.map(event => {
return (
<li className="event-listener-event" key={key}>
<li className="event-listener-event" key={event.id}>
<label className="event-listener-label">
<input
type="checkbox"
value={key}
onChange={e => this.onEventTypeClick(key, e.target.checked)}
checked={activeEventListeners.includes(key)}
value={event.id}
onChange={e =>
this.onEventTypeClick(event.id, e.target.checked)
}
checked={activeEventListeners.includes(event.id)}
/>
<span className="event-listener-name">{eventType}</span>
<span className="event-listener-name">{event.name}</span>
</label>
</li>
);
@ -153,12 +133,14 @@ class EventListeners extends Component<Props, State> {
}
render() {
const { categories } = this.props;
return (
<div className="event-listeners-content">
<ul className="event-listeners-list">
{Object.keys(CATEGORIES).map(category => {
{categories.map((category, index) => {
return (
<li className="event-listener-group" key={category}>
<li className="event-listener-group" key={index}>
{this.renderCategoryHeading(category)}
{this.renderCategoryListing(category)}
</li>
@ -170,14 +152,20 @@ class EventListeners extends Component<Props, State> {
}
}
const mapStateToProps = state => ({
const mapStateToProps = state => {
return {
activeEventListeners: getActiveEventListeners(state),
});
categories: getEventListenerBreakpointTypes(state),
expandedCategories: getEventListenerExpanded(state),
};
};
export default connect(
mapStateToProps,
{
addEventListeners: actions.addEventListeners,
removeEventListeners: actions.removeEventListeners,
addEventListeners: actions.addEventListenerBreakpoints,
removeEventListeners: actions.removeEventListenerBreakpoints,
addEventListenerExpanded: actions.addEventListenerExpanded,
removeEventListenerExpanded: actions.removeEventListenerExpanded,
}
)(EventListeners);

View File

@ -0,0 +1,82 @@
/* 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 React from "react";
import { shallow } from "enzyme";
import EventListeners from "../EventListeners";
function getCategories() {
return [
{
name: "Category 1",
events: [
{ name: "Subcategory 1", id: "category1.subcategory1" },
{ name: "Subcategory 2", id: "category1.subcategory2" },
],
},
{
name: "Category 2",
events: [
{ name: "Subcategory 3", id: "category2.subcategory1" },
{ name: "Subcategory 4", id: "category2.subcategory2" },
],
},
];
}
function generateDefaults(overrides = {}) {
const defaults = {
activeEventListeners: [],
expandedCategories: [],
categories: [],
};
return { ...defaults, ...overrides };
}
function render(overrides = {}) {
const props = generateDefaults(overrides);
// $FlowIgnore
const component = shallow(<EventListeners.WrappedComponent {...props} />);
return { component, props };
}
describe("EventListeners", () => {
it("should render", async () => {
const { component } = render();
expect(component).toMatchSnapshot();
});
it("should render categories appropriately", async () => {
const props = {
...generateDefaults(),
categories: getCategories(),
};
const { component } = render(props);
expect(component).toMatchSnapshot();
});
it("should render expanded categories appropriately", async () => {
const props = {
...generateDefaults(),
categories: getCategories(),
expandedCategories: ["Category 2"],
};
const { component } = render(props);
expect(component).toMatchSnapshot();
});
it("should render checked subcategories appropriately", async () => {
const props = {
...generateDefaults(),
categories: getCategories(),
activeEventListeners: ["category1.subcategory2"],
expandedCategories: ["Category 1"],
};
const { component } = render(props);
expect(component).toMatchSnapshot();
});
});

View File

@ -0,0 +1,320 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EventListeners should render 1`] = `
<div
className="event-listeners-content"
>
<ul
className="event-listeners-list"
/>
</div>
`;
exports[`EventListeners should render categories appropriately 1`] = `
<div
className="event-listeners-content"
>
<ul
className="event-listeners-list"
>
<li
className="event-listener-group"
key="0"
>
<div
className="event-listener-header"
>
<button
className="event-listener-expand"
onClick={[Function]}
>
<AccessibleImage
className="arrow"
/>
</button>
<label
className="event-listener-label"
>
<input
checked={false}
onChange={[Function]}
type="checkbox"
value="Category 1"
/>
<span
className="event-listener-category"
>
Category 1
</span>
</label>
</div>
</li>
<li
className="event-listener-group"
key="1"
>
<div
className="event-listener-header"
>
<button
className="event-listener-expand"
onClick={[Function]}
>
<AccessibleImage
className="arrow"
/>
</button>
<label
className="event-listener-label"
>
<input
checked={false}
onChange={[Function]}
type="checkbox"
value="Category 2"
/>
<span
className="event-listener-category"
>
Category 2
</span>
</label>
</div>
</li>
</ul>
</div>
`;
exports[`EventListeners should render checked subcategories appropriately 1`] = `
<div
className="event-listeners-content"
>
<ul
className="event-listeners-list"
>
<li
className="event-listener-group"
key="0"
>
<div
className="event-listener-header"
>
<button
className="event-listener-expand"
onClick={[Function]}
>
<AccessibleImage
className="arrow expanded"
/>
</button>
<label
className="event-listener-label"
>
<input
checked={false}
onChange={[Function]}
type="checkbox"
value="Category 1"
/>
<span
className="event-listener-category"
>
Category 1
</span>
</label>
</div>
<ul>
<li
className="event-listener-event"
key="category1.subcategory1"
>
<label
className="event-listener-label"
>
<input
checked={false}
onChange={[Function]}
type="checkbox"
value="category1.subcategory1"
/>
<span
className="event-listener-name"
>
Subcategory 1
</span>
</label>
</li>
<li
className="event-listener-event"
key="category1.subcategory2"
>
<label
className="event-listener-label"
>
<input
checked={true}
onChange={[Function]}
type="checkbox"
value="category1.subcategory2"
/>
<span
className="event-listener-name"
>
Subcategory 2
</span>
</label>
</li>
</ul>
</li>
<li
className="event-listener-group"
key="1"
>
<div
className="event-listener-header"
>
<button
className="event-listener-expand"
onClick={[Function]}
>
<AccessibleImage
className="arrow"
/>
</button>
<label
className="event-listener-label"
>
<input
checked={false}
onChange={[Function]}
type="checkbox"
value="Category 2"
/>
<span
className="event-listener-category"
>
Category 2
</span>
</label>
</div>
</li>
</ul>
</div>
`;
exports[`EventListeners should render expanded categories appropriately 1`] = `
<div
className="event-listeners-content"
>
<ul
className="event-listeners-list"
>
<li
className="event-listener-group"
key="0"
>
<div
className="event-listener-header"
>
<button
className="event-listener-expand"
onClick={[Function]}
>
<AccessibleImage
className="arrow"
/>
</button>
<label
className="event-listener-label"
>
<input
checked={false}
onChange={[Function]}
type="checkbox"
value="Category 1"
/>
<span
className="event-listener-category"
>
Category 1
</span>
</label>
</div>
</li>
<li
className="event-listener-group"
key="1"
>
<div
className="event-listener-header"
>
<button
className="event-listener-expand"
onClick={[Function]}
>
<AccessibleImage
className="arrow expanded"
/>
</button>
<label
className="event-listener-label"
>
<input
checked={false}
onChange={[Function]}
type="checkbox"
value="Category 2"
/>
<span
className="event-listener-category"
>
Category 2
</span>
</label>
</div>
<ul>
<li
className="event-listener-event"
key="category2.subcategory1"
>
<label
className="event-listener-label"
>
<input
checked={false}
onChange={[Function]}
type="checkbox"
value="category2.subcategory1"
/>
<span
className="event-listener-name"
>
Subcategory 3
</span>
</label>
</li>
<li
className="event-listener-event"
key="category2.subcategory2"
>
<label
className="event-listener-label"
>
<input
checked={false}
onChange={[Function]}
type="checkbox"
value="category2.subcategory2"
/>
<span
className="event-listener-name"
>
Subcategory 4
</span>
</label>
</li>
</ul>
</li>
</ul>
</div>
`;

View File

@ -4,45 +4,61 @@
// @flow
import { uniq } from "lodash";
import type { State } from "./types";
import type {
EventListenerAction,
EventListenerActiveList,
EventListenerCategoryList,
EventListenerExpandedList,
} from "../actions/types";
import { asyncStore } from "../utils/prefs";
import type { EventListenerBreakpoints } from "../types";
export type EventListenersState = {
active: EventListenerActiveList,
categories: EventListenerCategoryList,
expanded: EventListenerExpandedList,
};
type OuterState = { eventListenerBreakpoints: EventListenerBreakpoints };
export function initialEventListenerState(): EventListenersState {
return {
active: [],
categories: [],
expanded: [],
};
}
function update(state: EventListenerBreakpoints = [], action: any) {
function update(
state: EventListenersState = initialEventListenerState(),
action: EventListenerAction
) {
switch (action.type) {
case "ADD_EVENT_LISTENERS":
return updateEventTypes("add", state, action.events);
case "UPDATE_EVENT_LISTENERS":
return { ...state, active: action.active };
case "REMOVE_EVENT_LISTENERS":
return updateEventTypes("remove", state, action.events);
case "RECEIVE_EVENT_LISTENER_TYPES":
return { ...state, categories: action.categories };
case "UPDATE_EVENT_LISTENER_EXPANDED":
return { ...state, expanded: action.expanded };
default:
return state;
}
}
function updateEventTypes(
addOrRemove: string,
currentEvents: EventListenerBreakpoints,
events: EventListenerBreakpoints
): EventListenerBreakpoints {
let newEventListeners;
if (addOrRemove === "add") {
newEventListeners = uniq([...currentEvents, ...events]);
} else {
newEventListeners = currentEvents.filter(event => !events.includes(event));
export function getActiveEventListeners(state: State): EventListenerActiveList {
return state.eventListenerBreakpoints.active;
}
asyncStore.eventListenerBreakpoints = newEventListeners;
return newEventListeners;
export function getEventListenerBreakpointTypes(
state: State
): EventListenerCategoryList {
return state.eventListenerBreakpoints.categories;
}
export function getActiveEventListeners(state: OuterState) {
return state.eventListenerBreakpoints;
export function getEventListenerExpanded(
state: State
): EventListenerExpandedList {
return state.eventListenerBreakpoints.expanded;
}
export default update;

View File

@ -24,11 +24,13 @@ import type { SourceActorsState } from "./source-actors";
import type { TabList } from "./tabs";
import type { UIState } from "./ui";
import type { QuickOpenState } from "./quick-open";
import type { EventListenersState } from "./event-listeners";
export type State = {
ast: ASTState,
breakpoints: BreakpointsState,
expressions: Record<ExpressionState>,
eventListenerBreakpoints: EventListenersState,
debuggee: DebuggeeState,
fileSearch: Record<FileSearchState>,
pause: PauseState,

View File

@ -12,7 +12,7 @@ import { asyncStoreHelper } from "./asyncStoreHelper";
// Schema version to bump when the async store format has changed incompatibly
// and old stores should be cleared. This needs to match the prefs schema
// version in devtools/client/preferences/debugger.js.
const prefsSchemaVersion = "1.0.9";
const prefsSchemaVersion = "1.0.10";
const pref = Services.pref;
if (isDevelopment()) {
@ -134,7 +134,7 @@ export const asyncStore = asyncStoreHelper("debugger", {
pendingBreakpoints: ["pending-breakpoints", {}],
tabs: ["tabs", []],
xhrBreakpoints: ["xhr-breakpoints", []],
eventListenerBreakpoints: ["event-listener-breakpoints", []],
eventListenerBreakpoints: ["event-listener-breakpoints", undefined],
});
export function verifyPrefSchema() {
@ -143,6 +143,7 @@ export function verifyPrefSchema() {
asyncStore.pendingBreakpoints = {};
asyncStore.tabs = [];
asyncStore.xhrBreakpoints = [];
asyncStore.eventListenerBreakpoints = undefined;
prefs.debuggerPrefsSchemaVersion = prefsSchemaVersion;
}
}

View File

@ -20,7 +20,7 @@ pref("devtools.debugger.workers", false);
// The default Debugger UI settings
// This schema version needs to match that in devtools/client/debugger/src/utils/prefs.js.
pref("devtools.debugger.prefs-schema-version", "1.0.9");
pref("devtools.debugger.prefs-schema-version", "1.0.10");
pref("devtools.debugger.ui.panes-workers-and-sources-width", 200);
pref("devtools.debugger.ui.panes-instruments-width", 300);
pref("devtools.debugger.ui.panes-visible-on-startup", false);
@ -46,7 +46,6 @@ pref("devtools.debugger.tabsBlackBoxed", "[]");
pref("devtools.debugger.pending-selected-location", "{}");
pref("devtools.debugger.pending-breakpoints", "{}");
pref("devtools.debugger.expressions", "[]");
pref("devtools.debugger.event-listener-breakpoints", "[]");
pref("devtools.debugger.file-search-case-sensitive", false);
pref("devtools.debugger.file-search-whole-word", false);
pref("devtools.debugger.file-search-regex-match", false);