mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-26 06:11:37 +00:00
Bug 1177891 - Introduce redux-style UI architecture in the debugger and refactor event listeners to use it. r=fitzgen
--HG-- rename : browser/devtools/debugger/views/event-listeners-view.js => browser/devtools/debugger/content/views/event-listeners-view.js
This commit is contained in:
parent
bfebe66af1
commit
6c6400c0ee
7
browser/devtools/debugger/content/constants.js
Normal file
7
browser/devtools/debugger/content/constants.js
Normal file
@ -0,0 +1,7 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
exports.UPDATE_EVENT_BREAKPOINTS = 'UPDATE_EVENT_BREAKPOINTS';
|
||||
exports.FETCH_EVENT_LISTENERS = 'FETCH_EVENT_LISTENERS';
|
9
browser/devtools/debugger/content/globalActions.js
Normal file
9
browser/devtools/debugger/content/globalActions.js
Normal file
@ -0,0 +1,9 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const constants = require('./constants');
|
||||
|
||||
// No global actions right now, but I'm sure there will be soon.
|
||||
module.exports = {};
|
147
browser/devtools/debugger/content/stores/event-listeners.js
Normal file
147
browser/devtools/debugger/content/stores/event-listeners.js
Normal file
@ -0,0 +1,147 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const constants = require('../constants');
|
||||
const promise = require('promise');
|
||||
const { rdpInvoke, asPaused } = require('../utils');
|
||||
const { reportException } = require("devtools/toolkit/DevToolsUtils");
|
||||
|
||||
const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
|
||||
|
||||
const initialState = {
|
||||
activeEventNames: [],
|
||||
listeners: [],
|
||||
fetchingListeners: false,
|
||||
};
|
||||
|
||||
function update(state = initialState, action, emitChange) {
|
||||
switch(action.type) {
|
||||
case constants.UPDATE_EVENT_BREAKPOINTS:
|
||||
state.activeEventNames = action.eventNames;
|
||||
emitChange('activeEventNames', state.activeEventNames);
|
||||
break;
|
||||
case constants.FETCH_EVENT_LISTENERS:
|
||||
if (action.status === "begin") {
|
||||
state.fetchingListeners = true;
|
||||
}
|
||||
else if (action.status === "done") {
|
||||
state.fetchingListeners = false;
|
||||
state.listeners = action.listeners;
|
||||
emitChange('listeners', state.listeners);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
function fetchEventListeners() {
|
||||
return (dispatch, getState) => {
|
||||
// Make sure we're not sending a batch of closely repeated requests.
|
||||
// This can easily happen whenever new sources are fetched.
|
||||
setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
|
||||
// In case there is still a request of listeners going on (it
|
||||
// takes several RDP round trips right now), make sure we wait
|
||||
// on a currently running request
|
||||
if (getState().eventListeners.fetchingListeners) {
|
||||
dispatch({
|
||||
type: services.WAIT_UNTIL,
|
||||
predicate: action => (
|
||||
action.type === constants.FETCH_EVENT_LISTENERS &&
|
||||
action.status === "done"
|
||||
),
|
||||
run: dispatch => dispatch(fetchEventListeners())
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: constants.FETCH_EVENT_LISTENERS,
|
||||
status: "begin"
|
||||
});
|
||||
|
||||
asPaused(gThreadClient, _getListeners).then(listeners => {
|
||||
// Notify that event listeners were fetched and shown in the view,
|
||||
// and callback to resume the active thread if necessary.
|
||||
window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
|
||||
dispatch({
|
||||
type: constants.FETCH_EVENT_LISTENERS,
|
||||
status: "done",
|
||||
listeners: listeners
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const _getListeners = Task.async(function*() {
|
||||
const response = yield rdpInvoke(gThreadClient, gThreadClient.eventListeners);
|
||||
|
||||
// Make sure all the listeners are sorted by the event type, since
|
||||
// they're not guaranteed to be clustered together.
|
||||
response.listeners.sort((a, b) => a.type > b.type ? 1 : -1);
|
||||
|
||||
// Add all the listeners in the debugger view event linsteners container.
|
||||
let fetchedDefinitions = new Map();
|
||||
let listeners = [];
|
||||
for (let listener of response.listeners) {
|
||||
let definitionSite;
|
||||
if (fetchedDefinitions.has(listener.function.actor)) {
|
||||
definitionSite = fetchedDefinitions.get(listener.function.actor);
|
||||
} else if (listener.function.class == "Function") {
|
||||
definitionSite = yield _getDefinitionSite(listener.function);
|
||||
if (!definitionSite) {
|
||||
// We don't know where this listener comes from so don't show it in
|
||||
// the UI as breaking on it doesn't work (bug 942899).
|
||||
continue;
|
||||
}
|
||||
|
||||
fetchedDefinitions.set(listener.function.actor, definitionSite);
|
||||
}
|
||||
listener.function.url = definitionSite;
|
||||
listeners.push(listener);
|
||||
}
|
||||
fetchedDefinitions.clear();
|
||||
|
||||
return listeners;
|
||||
});
|
||||
|
||||
const _getDefinitionSite = Task.async(function*(aFunction) {
|
||||
const grip = gThreadClient.pauseGrip(aFunction);
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = yield rdpInvoke(grip, grip.getDefinitionSite);
|
||||
}
|
||||
catch(e) {
|
||||
// Don't make this error fatal, because it would break the entire events pane.
|
||||
reportException("_getDefinitionSite", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.source.url;
|
||||
});
|
||||
|
||||
function updateEventBreakpoints(eventNames) {
|
||||
return dispatch => {
|
||||
setNamedTimeout("event-breakpoints-update", 0, () => {
|
||||
gThreadClient.pauseOnDOMEvents(eventNames, function() {
|
||||
// Notify that event breakpoints were added/removed on the server.
|
||||
window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
|
||||
dispatch({
|
||||
type: constants.UPDATE_EVENT_BREAKPOINTS,
|
||||
eventNames: eventNames
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
update: update,
|
||||
actions: { updateEventBreakpoints, fetchEventListeners }
|
||||
}
|
8
browser/devtools/debugger/content/stores/index.js
Normal file
8
browser/devtools/debugger/content/stores/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const eventListeners = require('./event-listeners');
|
||||
|
||||
module.exports = { eventListeners };
|
45
browser/devtools/debugger/content/utils.js
Normal file
45
browser/devtools/debugger/content/utils.js
Normal file
@ -0,0 +1,45 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const { promiseInvoke } = require("devtools/async-utils");
|
||||
const { reportException } = require("devtools/toolkit/DevToolsUtils");
|
||||
|
||||
function rdpInvoke(client, method, ...args) {
|
||||
return promiseInvoke(client, method, ...args)
|
||||
.then((packet) => {
|
||||
let { error, message } = packet;
|
||||
if (error) {
|
||||
throw new Error(error + ": " + message);
|
||||
}
|
||||
|
||||
return packet;
|
||||
});
|
||||
}
|
||||
|
||||
function asPaused(client, func) {
|
||||
if (client.state != "paused") {
|
||||
return Task.spawn(function*() {
|
||||
yield rdpInvoke(client, client.interrupt);
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = yield func();
|
||||
}
|
||||
catch(e) {
|
||||
// Try to put the debugger back in a working state by resuming
|
||||
// it
|
||||
yield rdpInvoke(client, client.resume);
|
||||
throw e;
|
||||
}
|
||||
|
||||
yield rdpInvoke(client, client.resume);
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
return func();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { rdpInvoke, asPaused };
|
@ -3,14 +3,24 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
const actions = require('../stores/event-listeners').actions;
|
||||
const bindActionCreators = require('devtools/shared/fluxify/bindActionCreators');
|
||||
|
||||
/**
|
||||
* Functions handling the event listeners UI.
|
||||
*/
|
||||
function EventListenersView(DebuggerController) {
|
||||
function EventListenersView(dispatcher, DebuggerController) {
|
||||
dumpn("EventListenersView was instantiated");
|
||||
|
||||
this.actions = bindActionCreators(actions, dispatcher.dispatch);
|
||||
this.getState = () => dispatcher.getState().eventListeners;
|
||||
|
||||
this.Breakpoints = DebuggerController.Breakpoints;
|
||||
|
||||
dispatcher.onChange({
|
||||
"eventListeners": { "listeners": this.renderListeners }
|
||||
}, this);
|
||||
|
||||
this._onCheck = this._onCheck.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
@ -47,6 +57,15 @@ EventListenersView.prototype = Heritage.extend(WidgetMethods, {
|
||||
this.widget.removeEventListener("click", this._onClick, false);
|
||||
},
|
||||
|
||||
renderListeners: function(listeners) {
|
||||
listeners.forEach(listener => {
|
||||
this.addListener(listener, { staged: true });
|
||||
});
|
||||
|
||||
// Flushes all the prepared events into the event listeners container.
|
||||
this.commit();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds an event to this event listeners container.
|
||||
*
|
||||
@ -142,12 +161,12 @@ EventListenersView.prototype = Heritage.extend(WidgetMethods, {
|
||||
}
|
||||
|
||||
// Create the element node for the event listener item.
|
||||
let itemView = this._createItemView(type, selector, url);
|
||||
const itemView = this._createItemView(type, selector, url);
|
||||
|
||||
// Event breakpoints survive target navigations. Make sure the newly
|
||||
// inserted event item is correctly checked.
|
||||
let checkboxState =
|
||||
this.Breakpoints.DOM.activeEventNames.indexOf(type) != -1;
|
||||
const activeEventNames = this.getState().activeEventNames;
|
||||
const checkboxState = activeEventNames.indexOf(type) != -1;
|
||||
|
||||
// Append an event listener item to this container.
|
||||
this.push([itemView.container], {
|
||||
@ -241,7 +260,8 @@ EventListenersView.prototype = Heritage.extend(WidgetMethods, {
|
||||
_onCheck: function({ detail: { description, checked }, target }) {
|
||||
if (description == "item") {
|
||||
this.getItemForElement(target).attachment.checkboxState = checked;
|
||||
this.Breakpoints.DOM.scheduleEventBreakpointsUpdate();
|
||||
|
||||
this.actions.updateEventBreakpoints(this.getCheckedEvents());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -271,4 +291,4 @@ EventListenersView.prototype = Heritage.extend(WidgetMethods, {
|
||||
_inNativeCodeString: ""
|
||||
});
|
||||
|
||||
DebuggerView.EventListeners = new EventListenersView(DebuggerController);
|
||||
module.exports = EventListenersView;
|
@ -11,7 +11,6 @@ const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
|
||||
const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "XStringBundle"];
|
||||
const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
|
||||
const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
|
||||
const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
|
||||
const FRAME_STEP_CLEAR_DELAY = 100; // ms
|
||||
const CALL_STACK_PAGE_SIZE = 25; // frames
|
||||
|
||||
@ -103,9 +102,11 @@ Cu.import("resource:///modules/devtools/VariablesView.jsm");
|
||||
Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
|
||||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
|
||||
const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
Cu.import("resource:///modules/devtools/shared/browser-loader.js");
|
||||
const require = BrowserLoader("resource:///modules/devtools/debugger/", this).require;
|
||||
|
||||
const {TargetFactory} = require("devtools/framework/target");
|
||||
const {Toolbox} = require("devtools/framework/toolbox")
|
||||
const {Toolbox} = require("devtools/framework/toolbox");
|
||||
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
|
||||
const promise = require("devtools/toolkit/deprecated-sync-thenables");
|
||||
const Editor = require("devtools/sourceeditor/editor");
|
||||
@ -320,7 +321,9 @@ let DebuggerController = {
|
||||
this.SourceScripts.connect();
|
||||
|
||||
if (aThreadClient.paused) {
|
||||
aThreadClient.resume(this._ensureResumptionOrder);
|
||||
aThreadClient.resume(res => {
|
||||
this._ensureResumptionOrder(res)
|
||||
});
|
||||
}
|
||||
|
||||
deferred.resolve();
|
||||
@ -1274,7 +1277,7 @@ SourceScripts.prototype = {
|
||||
|
||||
// Make sure the events listeners are up to date.
|
||||
if (DebuggerView.instrumentsPaneTab == "events-tab") {
|
||||
DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
|
||||
dispatcher.dispatch(actions.fetchEventListeners());
|
||||
}
|
||||
|
||||
// Signal that a new source has been added.
|
||||
@ -1534,151 +1537,6 @@ SourceScripts.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles breaking on event listeners in the currently debugged target.
|
||||
*/
|
||||
function EventListeners() {
|
||||
}
|
||||
|
||||
EventListeners.prototype = {
|
||||
/**
|
||||
* A list of event names on which the debuggee will automatically pause
|
||||
* when invoked.
|
||||
*/
|
||||
activeEventNames: [],
|
||||
|
||||
/**
|
||||
* Updates the list of events types with listeners that, when invoked,
|
||||
* will automatically pause the debuggee. The respective events are
|
||||
* retrieved from the UI.
|
||||
*/
|
||||
scheduleEventBreakpointsUpdate: function() {
|
||||
// Make sure we're not sending a batch of closely repeated requests.
|
||||
// This can easily happen when toggling all events of a certain type.
|
||||
setNamedTimeout("event-breakpoints-update", 0, () => {
|
||||
this.activeEventNames = DebuggerView.EventListeners.getCheckedEvents();
|
||||
gThreadClient.pauseOnDOMEvents(this.activeEventNames);
|
||||
|
||||
// Notify that event breakpoints were added/removed on the server.
|
||||
window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Schedules fetching the currently attached event listeners from the debugee.
|
||||
*/
|
||||
scheduleEventListenersFetch: function() {
|
||||
// Make sure we're not sending a batch of closely repeated requests.
|
||||
// This can easily happen whenever new sources are fetched.
|
||||
setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
|
||||
if (gThreadClient.state != "paused") {
|
||||
gThreadClient.interrupt(() => this._getListeners(() => gThreadClient.resume()));
|
||||
} else {
|
||||
this._getListeners();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* A semaphore that is used to ensure only a single protocol request for event
|
||||
* listeners will be ongoing at any given time.
|
||||
*/
|
||||
_parsingListeners: false,
|
||||
|
||||
/**
|
||||
* A flag the indicates whether a new request to fetch updated event listeners
|
||||
* has arrived, while another one was in progress.
|
||||
*/
|
||||
_eventListenersUpdateNeeded: false,
|
||||
|
||||
/**
|
||||
* Fetches the currently attached event listeners from the debugee.
|
||||
* The thread client state is assumed to be "paused".
|
||||
*
|
||||
* @param function aCallback
|
||||
* Invoked once the event listeners are fetched and displayed.
|
||||
*/
|
||||
_getListeners: function(aCallback) {
|
||||
// Don't make a new request if one is still ongoing, but schedule one for
|
||||
// later.
|
||||
if (this._parsingListeners) {
|
||||
this._eventListenersUpdateNeeded = true;
|
||||
return;
|
||||
}
|
||||
this._parsingListeners = true;
|
||||
gThreadClient.eventListeners(Task.async(function*(aResponse) {
|
||||
if (aResponse.error) {
|
||||
throw "Error getting event listeners: " + aResponse.message;
|
||||
}
|
||||
|
||||
// Make sure all the listeners are sorted by the event type, since
|
||||
// they're not guaranteed to be clustered together.
|
||||
aResponse.listeners.sort((a, b) => a.type > b.type ? 1 : -1);
|
||||
|
||||
// Add all the listeners in the debugger view event linsteners container.
|
||||
let fetchedDefinitions = new Map();
|
||||
for (let listener of aResponse.listeners) {
|
||||
let definitionSite;
|
||||
if (fetchedDefinitions.has(listener.function.actor)) {
|
||||
definitionSite = fetchedDefinitions.get(listener.function.actor);
|
||||
} else if (listener.function.class == "Function") {
|
||||
definitionSite = yield this._getDefinitionSite(listener.function);
|
||||
if (!definitionSite) {
|
||||
// We don't know where this listener comes from so don't show it in
|
||||
// the UI as breaking on it doesn't work (bug 942899).
|
||||
continue;
|
||||
}
|
||||
|
||||
fetchedDefinitions.set(listener.function.actor, definitionSite);
|
||||
}
|
||||
listener.function.url = definitionSite;
|
||||
DebuggerView.EventListeners.addListener(listener, { staged: true });
|
||||
}
|
||||
fetchedDefinitions.clear();
|
||||
|
||||
// Flushes all the prepared events into the event listeners container.
|
||||
DebuggerView.EventListeners.commit();
|
||||
|
||||
// Now that we are done, schedule a new update if necessary.
|
||||
this._parsingListeners = false;
|
||||
if (this._eventListenersUpdateNeeded) {
|
||||
this._eventListenersUpdateNeeded = false;
|
||||
this.scheduleEventListenersFetch();
|
||||
}
|
||||
|
||||
// Notify that event listeners were fetched and shown in the view,
|
||||
// and callback to resume the active thread if necessary.
|
||||
window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
aCallback && aCallback();
|
||||
}.bind(this)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a function's source-mapped definiton site.
|
||||
*
|
||||
* @param object aFunction
|
||||
* The grip of the function to get the definition site for.
|
||||
* @return object
|
||||
* A promise that is resolved with the function's owner source url.
|
||||
*/
|
||||
_getDefinitionSite: function(aFunction) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
gThreadClient.pauseGrip(aFunction).getDefinitionSite(aResponse => {
|
||||
if (aResponse.error) {
|
||||
// Don't make this error fatal, because it would break the entire events pane.
|
||||
const msg = "Error getting function definition site: " + aResponse.message;
|
||||
DevToolsUtils.reportException("_getDefinitionSite", msg);
|
||||
deferred.resolve(null);
|
||||
} else {
|
||||
deferred.resolve(aResponse.source.url);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles all the breakpoints in the current debugger.
|
||||
*/
|
||||
@ -2205,7 +2063,6 @@ DebuggerController.ThreadState = new ThreadState();
|
||||
DebuggerController.StackFrames = new StackFrames();
|
||||
DebuggerController.SourceScripts = new SourceScripts();
|
||||
DebuggerController.Breakpoints = new Breakpoints();
|
||||
DebuggerController.Breakpoints.DOM = new EventListeners();
|
||||
|
||||
/**
|
||||
* Export some properties to the global scope for easier access.
|
||||
|
@ -33,6 +33,17 @@ const TOOLBAR_ORDER_POPUP_POSITION = "topcenter bottomleft";
|
||||
const PROMISE_DEBUGGER_URL =
|
||||
"chrome://browser/content/devtools/promisedebugger/promise-debugger.xhtml";
|
||||
|
||||
const createDispatcher = require('devtools/shared/create-dispatcher')();
|
||||
const stores = require('./content/stores/index');
|
||||
const dispatcher = createDispatcher(stores);
|
||||
const waitUntilService = require('devtools/shared/fluxify/waitUntilService');
|
||||
const services = {
|
||||
WAIT_UNTIL: waitUntilService.name
|
||||
};
|
||||
|
||||
const EventListenersView = require('./content/views/event-listeners-view');
|
||||
const actions = require('./content/stores/event-listeners').actions;
|
||||
|
||||
/**
|
||||
* Object defining the debugger view components.
|
||||
*/
|
||||
@ -598,7 +609,7 @@ let DebuggerView = {
|
||||
*/
|
||||
_onInstrumentsPaneTabSelect: function() {
|
||||
if (this._instrumentsPane.selectedTab.id == "events-tab") {
|
||||
DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
|
||||
dispatcher.dispatch(actions.fetchEventListeners());
|
||||
}
|
||||
},
|
||||
|
||||
@ -859,3 +870,5 @@ ResultsPanelContainer.prototype = Heritage.extend(WidgetMethods, {
|
||||
left: 0,
|
||||
top: 0
|
||||
});
|
||||
|
||||
DebuggerView.EventListeners = new EventListenersView(dispatcher, DebuggerController);
|
||||
|
@ -8,4 +8,19 @@ EXTRA_JS_MODULES.devtools.debugger += [
|
||||
'panel.js'
|
||||
]
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
|
||||
EXTRA_JS_MODULES.devtools.debugger.content += [
|
||||
'content/constants.js',
|
||||
'content/globalActions.js',
|
||||
'content/utils.js'
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.devtools.debugger.content.views += [
|
||||
'content/views/event-listeners-view.js'
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.devtools.debugger.content.stores += [
|
||||
'content/stores/event-listeners.js',
|
||||
'content/stores/index.js'
|
||||
]
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/mochitest/browser.ini']
|
||||
|
@ -13,6 +13,8 @@ function test() {
|
||||
let gDebugger = aPanel.panelWin;
|
||||
let gView = gDebugger.DebuggerView;
|
||||
let gEvents = gView.EventListeners;
|
||||
let gDispatcher = gDebugger.dispatcher;
|
||||
let constants = gDebugger.require('./content/constants');
|
||||
|
||||
Task.spawn(function*() {
|
||||
yield waitForSourceShown(aPanel, ".html");
|
||||
@ -24,7 +26,7 @@ function test() {
|
||||
|
||||
function testFetchOnFocus() {
|
||||
return Task.spawn(function*() {
|
||||
let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
|
||||
|
||||
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
|
||||
is(gView.instrumentsPaneHidden, false,
|
||||
@ -43,7 +45,7 @@ function test() {
|
||||
|
||||
function testFetchOnReloadWhenFocused() {
|
||||
return Task.spawn(function*() {
|
||||
let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
|
||||
|
||||
let reloading = once(gDebugger.gTarget, "will-navigate");
|
||||
let reloaded = waitForSourcesAfterReload();
|
||||
|
@ -12,11 +12,13 @@ function test() {
|
||||
let gDebugger = aPanel.panelWin;
|
||||
let gView = gDebugger.DebuggerView;
|
||||
let gEvents = gView.EventListeners;
|
||||
let gDispatcher = gDebugger.dispatcher;
|
||||
let constants = gDebugger.require('./content/constants');
|
||||
|
||||
Task.spawn(function*() {
|
||||
yield waitForSourceShown(aPanel, ".html");
|
||||
|
||||
let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
|
||||
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
|
||||
yield fetched;
|
||||
|
||||
@ -44,7 +46,7 @@ function test() {
|
||||
is(gEvents.getCheckedEvents().toString(), "",
|
||||
"The getCheckedEvents() method returns the correct stuff.");
|
||||
|
||||
yield ensureThreadClientState(aPanel, "resumed");
|
||||
yield ensureThreadClientState(aPanel, "attached");
|
||||
yield closeDebuggerAndFinish(aPanel);
|
||||
});
|
||||
|
||||
|
@ -14,12 +14,14 @@ function test() {
|
||||
let gView = gDebugger.DebuggerView;
|
||||
let gController = gDebugger.DebuggerController
|
||||
let gEvents = gView.EventListeners;
|
||||
let gBreakpoints = gController.Breakpoints;
|
||||
let gDispatcher = gDebugger.dispatcher;
|
||||
let getState = gDispatcher.getState;
|
||||
let constants = gDebugger.require('./content/constants');
|
||||
|
||||
Task.spawn(function*() {
|
||||
yield waitForSourceShown(aPanel, ".html");
|
||||
|
||||
let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
|
||||
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
|
||||
yield fetched;
|
||||
|
||||
@ -32,7 +34,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "");
|
||||
|
||||
let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
let updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
|
||||
yield updated;
|
||||
|
||||
@ -45,7 +47,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "change");
|
||||
|
||||
updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
|
||||
yield updated;
|
||||
|
||||
@ -58,7 +60,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "");
|
||||
|
||||
yield ensureThreadClientState(aPanel, "resumed");
|
||||
yield ensureThreadClientState(aPanel, "attached");
|
||||
yield closeDebuggerAndFinish(aPanel);
|
||||
});
|
||||
|
||||
@ -90,7 +92,7 @@ function test() {
|
||||
"The getAllEvents() method returns the correct stuff.");
|
||||
is(gEvents.getCheckedEvents().toString(), checked,
|
||||
"The getCheckedEvents() method returns the correct stuff.");
|
||||
is(gBreakpoints.DOM.activeEventNames.toString(), checked,
|
||||
is(getState().eventListeners.activeEventNames.toString(), checked,
|
||||
"The correct event names are listed as being active breakpoints.");
|
||||
}
|
||||
});
|
||||
|
@ -15,12 +15,14 @@ function test() {
|
||||
let gView = gDebugger.DebuggerView;
|
||||
let gController = gDebugger.DebuggerController
|
||||
let gEvents = gView.EventListeners;
|
||||
let gBreakpoints = gController.Breakpoints;
|
||||
let gDispatcher = gDebugger.dispatcher;
|
||||
let getState = gDispatcher.getState;
|
||||
let constants = gDebugger.require('./content/constants');
|
||||
|
||||
Task.spawn(function*() {
|
||||
yield waitForSourceShown(aPanel, ".html");
|
||||
|
||||
let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
|
||||
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
|
||||
yield fetched;
|
||||
|
||||
@ -33,7 +35,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "");
|
||||
|
||||
let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
let updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("interactionEvents"), gDebugger);
|
||||
yield updated;
|
||||
|
||||
@ -46,7 +48,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "change");
|
||||
|
||||
updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("interactionEvents"), gDebugger);
|
||||
yield updated;
|
||||
|
||||
@ -59,7 +61,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "");
|
||||
|
||||
updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("keyboardEvents"), gDebugger);
|
||||
yield updated;
|
||||
|
||||
@ -72,7 +74,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "keydown,keyup");
|
||||
|
||||
updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getGroupCheckboxNode("keyboardEvents"), gDebugger);
|
||||
yield updated;
|
||||
|
||||
@ -85,7 +87,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "");
|
||||
|
||||
yield ensureThreadClientState(aPanel, "resumed");
|
||||
yield ensureThreadClientState(aPanel, "attached");
|
||||
yield closeDebuggerAndFinish(aPanel);
|
||||
});
|
||||
|
||||
@ -117,7 +119,7 @@ function test() {
|
||||
"The getAllEvents() method returns the correct stuff.");
|
||||
is(gEvents.getCheckedEvents().toString(), checked,
|
||||
"The getCheckedEvents() method returns the correct stuff.");
|
||||
is(gBreakpoints.DOM.activeEventNames.toString(), checked,
|
||||
is(getState().eventListeners.activeEventNames.toString(), checked,
|
||||
"The correct event names are listed as being active breakpoints.");
|
||||
}
|
||||
});
|
||||
|
@ -15,11 +15,14 @@ function test() {
|
||||
let gController = gDebugger.DebuggerController
|
||||
let gEvents = gView.EventListeners;
|
||||
let gBreakpoints = gController.Breakpoints;
|
||||
let gDispatcher = gDebugger.dispatcher;
|
||||
let getState = gDispatcher.getState;
|
||||
let constants = gDebugger.require('./content/constants');
|
||||
|
||||
Task.spawn(function*() {
|
||||
yield waitForSourceShown(aPanel, ".html");
|
||||
|
||||
let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
|
||||
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
|
||||
yield fetched;
|
||||
|
||||
@ -32,7 +35,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "");
|
||||
|
||||
let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
let updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(2), gDebugger);
|
||||
@ -47,7 +50,8 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "change,click,keydown");
|
||||
|
||||
yield reloadActiveTab(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
reload(aPanel);
|
||||
yield afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
|
||||
|
||||
testEventItem(0, true);
|
||||
testEventItem(1, true);
|
||||
@ -58,7 +62,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "change,click,keydown");
|
||||
|
||||
updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(0), gDebugger);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(2), gDebugger);
|
||||
@ -73,7 +77,8 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "");
|
||||
|
||||
yield reloadActiveTab(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
reload(aPanel);
|
||||
yield afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
|
||||
|
||||
testEventItem(0, false);
|
||||
testEventItem(1, false);
|
||||
@ -84,7 +89,7 @@ function test() {
|
||||
testEventGroup("mouseEvents", false);
|
||||
testEventArrays("change,click,keydown,keyup", "");
|
||||
|
||||
yield ensureThreadClientState(aPanel, "resumed");
|
||||
yield ensureThreadClientState(aPanel, "attached");
|
||||
yield closeDebuggerAndFinish(aPanel);
|
||||
});
|
||||
|
||||
@ -115,9 +120,9 @@ function test() {
|
||||
is(gEvents.getAllEvents().toString(), all,
|
||||
"The getAllEvents() method returns the correct stuff.");
|
||||
is(gEvents.getCheckedEvents().toString(), checked,
|
||||
"The getCheckedEvents() method returns the correct stuff.");
|
||||
is(gBreakpoints.DOM.activeEventNames.toString(), checked,
|
||||
"The correct event names are listed as being active breakpoints.");
|
||||
"The getCheckedEvents() method returns the correct stuff.");
|
||||
is(getState().eventListeners.activeEventNames.toString(), checked,
|
||||
"The correct event names are listed as being active breakpoints.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -13,25 +13,28 @@ function test() {
|
||||
let gDebugger = aPanel.panelWin;
|
||||
let gView = gDebugger.DebuggerView;
|
||||
let gEvents = gView.EventListeners;
|
||||
let gDispatcher = gDebugger.dispatcher;
|
||||
let getState = gDispatcher.getState;
|
||||
let constants = gDebugger.require('./content/constants');
|
||||
|
||||
Task.spawn(function*() {
|
||||
yield waitForSourceShown(aPanel, ".html");
|
||||
yield callInTab(gTab, "addBodyClickEventListener");
|
||||
|
||||
let fetched = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
|
||||
gView.toggleInstrumentsPane({ visible: true, animated: false }, 1);
|
||||
yield fetched;
|
||||
yield ensureThreadClientState(aPanel, "resumed");
|
||||
yield ensureThreadClientState(aPanel, "attached");
|
||||
|
||||
is(gView.instrumentsPaneHidden, false,
|
||||
"The instruments pane should be visible.");
|
||||
is(gView.instrumentsPaneTab, "events-tab",
|
||||
"The events tab should be selected.");
|
||||
|
||||
let updated = waitForDebuggerEvents(aPanel, gDebugger.EVENTS.EVENT_BREAKPOINTS_UPDATED);
|
||||
let updated = afterDispatch(gDispatcher, constants.UPDATE_EVENT_BREAKPOINTS);
|
||||
EventUtils.sendMouseEvent({ type: "click" }, getItemCheckboxNode(1), gDebugger);
|
||||
yield updated;
|
||||
yield ensureThreadClientState(aPanel, "resumed");
|
||||
yield ensureThreadClientState(aPanel, "attached");
|
||||
|
||||
let paused = waitForCaretAndScopes(aPanel, 48);
|
||||
generateMouseClickInTab(gTab, "content.document.body");
|
||||
|
@ -31,10 +31,13 @@ add_task(function* () {
|
||||
|
||||
let [,, panel, win] = yield initDebugger(tab);
|
||||
let gDebugger = panel.panelWin;
|
||||
let fetched = waitForDebuggerEvents(panel, gDebugger.EVENTS.EVENT_LISTENERS_FETCHED);
|
||||
let gDispatcher = gDebugger.dispatcher;
|
||||
let constants = gDebugger.require('./content/constants');
|
||||
let eventListeners = gDebugger.require('./content/stores/event-listeners');
|
||||
let fetched = afterDispatch(gDispatcher, constants.FETCH_EVENT_LISTENERS);
|
||||
|
||||
info("Scheduling event listener fetch.");
|
||||
gDebugger.DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
|
||||
gDispatcher.dispatch(eventListeners.actions.fetchEventListeners());
|
||||
|
||||
info("Waiting for updated event listeners to arrive.");
|
||||
yield fetched;
|
||||
|
@ -459,10 +459,14 @@ function ensureThreadClientState(aPanel, aState) {
|
||||
}
|
||||
}
|
||||
|
||||
function navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) {
|
||||
let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat);
|
||||
function reload(aPanel, aUrl) {
|
||||
let activeTab = aPanel.panelWin.DebuggerController._target.activeTab;
|
||||
aUrl ? activeTab.navigateTo(aUrl) : activeTab.reload();
|
||||
}
|
||||
|
||||
function navigateActiveTabTo(aPanel, aUrl, aWaitForEventName, aEventRepeat) {
|
||||
let finished = waitForDebuggerEvents(aPanel, aWaitForEventName, aEventRepeat);
|
||||
reload(aPanel, aUrl);
|
||||
return finished;
|
||||
}
|
||||
|
||||
@ -1196,7 +1200,10 @@ function afterDispatch(dispatcher, type) {
|
||||
// internal name here so tests aren't forced to always pass it
|
||||
// in
|
||||
type: "@@service/waitUntil",
|
||||
predicate: action => action.type === type,
|
||||
predicate: action => (
|
||||
action.type === type &&
|
||||
action.status ? action.status === "done" : true
|
||||
),
|
||||
run: resolve
|
||||
});
|
||||
});
|
||||
|
@ -72,7 +72,6 @@ browser.jar:
|
||||
content/browser/devtools/debugger/sources-view.js (debugger/views/sources-view.js)
|
||||
content/browser/devtools/debugger/variable-bubble-view.js (debugger/views/variable-bubble-view.js)
|
||||
content/browser/devtools/debugger/watch-expressions-view.js (debugger/views/watch-expressions-view.js)
|
||||
content/browser/devtools/debugger/event-listeners-view.js (debugger/views/event-listeners-view.js)
|
||||
content/browser/devtools/debugger/global-search-view.js (debugger/views/global-search-view.js)
|
||||
content/browser/devtools/debugger/toolbar-view.js (debugger/views/toolbar-view.js)
|
||||
content/browser/devtools/debugger/options-view.js (debugger/views/options-view.js)
|
||||
|
90
browser/devtools/shared/browser-loader.js
Normal file
90
browser/devtools/shared/browser-loader.js
Normal file
@ -0,0 +1,90 @@
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
const loaders = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
|
||||
const devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
|
||||
const { joinURI } = devtools.require("devtools/toolkit/path");
|
||||
let appConstants;
|
||||
|
||||
// Some of the services that the system module requires is not
|
||||
// available in xpcshell tests. This is ok, we can easily polyfill the
|
||||
// values that we need.
|
||||
try {
|
||||
const system = devtools.require("devtools/toolkit/shared/system");
|
||||
appConstants = system.constants;
|
||||
}
|
||||
catch(e) {
|
||||
// We are in a testing environment most likely. There isn't much
|
||||
// risk to this defaulting to true because the dev version of React
|
||||
// will be loaded if this is true, and that file doesn't get built
|
||||
// into the release version of Firefox, so this will only work with
|
||||
// dev environments.
|
||||
appConstants = {
|
||||
DEBUG_JS_MODULES: true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Create a loader to be used in a browser environment. This evaluates
|
||||
* modules in their own environment, but sets window (the normal
|
||||
* global object) as the sandbox prototype, so when a variable is not
|
||||
* defined it checks `window` before throwing an error. This makes all
|
||||
* browser APIs available to modules by default, like a normal browser
|
||||
* environment, but modules are still evaluated in their own scope.
|
||||
*
|
||||
* Another very important feature of this loader is that it *only*
|
||||
* deals with modules loaded from under `baseURI`. Anything loaded
|
||||
* outside of that path will still be loaded from the devtools loader,
|
||||
* so all system modules are still shared and cached across instances.
|
||||
* An exception to this is anything under
|
||||
* `browser/devtools/shared/content`, which is where shared libraries
|
||||
* live that should be evaluated in a browser environment.
|
||||
*
|
||||
* @param string baseURI
|
||||
* Base path to load modules from.
|
||||
* @param Object window
|
||||
* The window instance to evaluate modules within
|
||||
* @return Object
|
||||
* An object with two properties:
|
||||
* - loader: the Loader instance
|
||||
* - require: a function to require modules with
|
||||
*/
|
||||
function BrowserLoader(baseURI, window) {
|
||||
const loaderOptions = devtools.require('@loader/options');
|
||||
|
||||
let dynamicPaths = {};
|
||||
if (appConstants.DEBUG_JS_MODULES) {
|
||||
// Load in the dev version of React
|
||||
dynamicPaths["devtools/shared/content/react"] =
|
||||
"resource:///modules/devtools/shared/content/react-dev.js";
|
||||
}
|
||||
|
||||
const opts = {
|
||||
id: "browser-loader",
|
||||
sharedGlobal: true,
|
||||
sandboxPrototype: window,
|
||||
paths: Object.assign({}, loaderOptions.paths, dynamicPaths),
|
||||
invisibleToDebugger: loaderOptions.invisibleToDebugger,
|
||||
require: (id, require) => {
|
||||
const uri = require.resolve(id);
|
||||
if (!uri.startsWith(baseURI) &&
|
||||
!uri.startsWith("resource:///modules/devtools/shared/content")) {
|
||||
return devtools.require(uri);
|
||||
}
|
||||
return require(uri);
|
||||
}
|
||||
};
|
||||
|
||||
// The main.js file does not have to actually exist. It just
|
||||
// represents the base environment, so requires will be relative to
|
||||
// that path when used outside of modules.
|
||||
const mainModule = loaders.Module(baseURI, joinURI(baseURI, "main.js"));
|
||||
const mainLoader = loaders.Loader(opts);
|
||||
|
||||
return {
|
||||
loader: mainLoader,
|
||||
require: loaders.Require(mainLoader, mainModule)
|
||||
};
|
||||
}
|
||||
|
||||
EXPORTED_SYMBOLS = ["BrowserLoader"];
|
31
browser/devtools/shared/create-dispatcher.js
Normal file
31
browser/devtools/shared/create-dispatcher.js
Normal file
@ -0,0 +1,31 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const fluxify = require('./fluxify/dispatcher');
|
||||
const thunkMiddleware = require('./fluxify/thunkMiddleware');
|
||||
const logMiddleware = require('./fluxify/logMiddleware');
|
||||
const waitUntilService = require('./fluxify/waitUntilService')
|
||||
const { compose } = require('devtools/toolkit/DevToolsUtils');
|
||||
|
||||
/**
|
||||
* This creates a dispatcher with all the standard middleware in place
|
||||
* that all code requires. It can also be optionally configured in
|
||||
* various ways, such as logging and recording.
|
||||
*
|
||||
* @param {object} opts - boolean configuration flags
|
||||
* - log: log all dispatched actions to console
|
||||
*/
|
||||
module.exports = (opts={}) => {
|
||||
const middleware = [
|
||||
thunkMiddleware,
|
||||
waitUntilService.service
|
||||
];
|
||||
|
||||
if (opts.log) {
|
||||
middleware.push(logMiddleware);
|
||||
}
|
||||
|
||||
return fluxify.applyMiddleware(...middleware)(fluxify.createDispatcher);
|
||||
}
|
28
browser/devtools/shared/fluxify/bindActionCreators.js
Normal file
28
browser/devtools/shared/fluxify/bindActionCreators.js
Normal file
@ -0,0 +1,28 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
function bindActionCreator(actionCreator, dispatch) {
|
||||
return (...args) => dispatch(actionCreator(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps action creator functions into a function that automatically
|
||||
* dispatches the created action with `dispatch`. Normally action
|
||||
* creators simply return actions, but wrapped functions will
|
||||
* automatically dispatch.
|
||||
*
|
||||
* @param {object} actionCreators
|
||||
* An object of action creator functions (names as keys)
|
||||
* @param {function} dispatch
|
||||
*/
|
||||
function bindActionCreators(actionCreators, dispatch) {
|
||||
let actions = {};
|
||||
for (let k of Object.keys(actionCreators)) {
|
||||
actions[k] = bindActionCreator(actionCreators[k], dispatch);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
module.exports = bindActionCreators;
|
247
browser/devtools/shared/fluxify/dispatcher.js
Normal file
247
browser/devtools/shared/fluxify/dispatcher.js
Normal file
@ -0,0 +1,247 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const { entries, compose } = require("devtools/toolkit/DevToolsUtils");
|
||||
|
||||
/**
|
||||
* A store creator that creates a dispatch function that runs the
|
||||
* provided middlewares before actually dispatching. This allows
|
||||
* simple middleware to augment the kinds of actions that can
|
||||
* be dispatched. This would be used like this:
|
||||
* `createDispatcher = applyMiddleware([fooMiddleware, ...])(createDispatcher)`
|
||||
*
|
||||
* Middlewares are simple functions that are provided `dispatch` and
|
||||
* `getState` functions. They create functions that accept actions and
|
||||
* can re-dispatch them in any way they want. A common scenario is
|
||||
* asynchronously dispatching multiple actions. Here is a full
|
||||
* middleware:
|
||||
*
|
||||
* function thunkMiddleware({ dispatch, getState }) {
|
||||
* return next => action => {
|
||||
* return typeof action === 'function' ?
|
||||
* action(dispatch, getState) :
|
||||
* next(action);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* `next` is essentially a `dispatch` function, but it calls the next
|
||||
* middelware in the chain (or the real `dispatch` function). Using
|
||||
* this middleware, you can return a function that gives you a
|
||||
* dispatch function:
|
||||
*
|
||||
* function actionCreator(timeout) {
|
||||
* return (dispatch, getState) => {
|
||||
* dispatch({ type: TIMEOUT, status: "start" });
|
||||
* setTimeout(() => dispatch({ type: TIMEOUT, status: "end" }),
|
||||
* timeout);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
function applyMiddleware(...middlewares) {
|
||||
return next => stores => {
|
||||
const dispatcher = next(stores);
|
||||
let dispatch = dispatcher.dispatch;
|
||||
|
||||
const api = {
|
||||
getState: dispatcher.getState,
|
||||
dispatch: action => dispatch(action)
|
||||
};
|
||||
const chain = middlewares.map(middleware => middleware(api));
|
||||
dispatch = compose(...chain)(dispatcher.dispatch);
|
||||
|
||||
return Object.assign({}, dispatcher, { dispatch: dispatch });
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The heart of the system. This creates a dispatcher which is the
|
||||
* interface between views and stores. Views can use a dispatcher
|
||||
* instance to dispatch actions, which know nothing about the stores.
|
||||
* Actions are broadcasted to all registered stores, and stores can
|
||||
* handle the action and update their state. The dispatcher gives
|
||||
* stores an `emitChange` function, which signifies that a piece of
|
||||
* state has changed. The dispatcher will notify all views that are
|
||||
* listening to that piece of state, registered with `onChange`.
|
||||
*
|
||||
* Views generally are stateless, pure components (eventually React
|
||||
* components). They simply take state and render it.
|
||||
*
|
||||
* Stores make up the entire app state, and are all grouped together
|
||||
* into a single app state atom, returned by the dispatcher's
|
||||
* `getState` function. The shape of the app state is determined by
|
||||
* the `stores` object passed in to the dispatcher, so if
|
||||
* `{ foo: fooStore }` was passed to `createDispatcher` the app state
|
||||
* would be `{ foo: fooState }`
|
||||
*
|
||||
* Actions are just JavaScript object with a `type` property and any
|
||||
* other fields pertinent to the action. Action creators should
|
||||
* generally be used to create actions, which are just functions that
|
||||
* return the action object. Additionally, the `bindActionCreators`
|
||||
* module provides a function for automatically binding action
|
||||
* creators to a dispatch function, so calling them automatically
|
||||
* dispatches. For example:
|
||||
*
|
||||
* ```js
|
||||
* // Manually dispatch
|
||||
* dispatcher.dispatch({ type: constants.ADD_ITEM, item: item });
|
||||
* // Using an action creator
|
||||
* dispatcher.dispatch(addItem(item));
|
||||
*
|
||||
* // Using an action creator bound to dispatch
|
||||
* actions = bindActionCreators({ addItem: addItem });
|
||||
* actions.addItem(item);
|
||||
* ```
|
||||
*
|
||||
* Our system expects stores to exist as an `update` function. You
|
||||
* should define an update function in a module, and optionally
|
||||
* any action creators that are useful to go along with it. Here is
|
||||
* an example store file:
|
||||
*
|
||||
* ```js
|
||||
* const initialState = { items: [] };
|
||||
* function update(state = initialState, action, emitChange) {
|
||||
* if (action.type === constants.ADD_ITEM) {
|
||||
* state.items.push(action.item);
|
||||
* emitChange("items", state.items);
|
||||
* }
|
||||
*
|
||||
* return state;
|
||||
* }
|
||||
*
|
||||
* function addItem(item) {
|
||||
* return {
|
||||
* type: constants.ADD_ITEM,
|
||||
* item: item
|
||||
* };
|
||||
* }
|
||||
*
|
||||
* module.exports = {
|
||||
* update: update,
|
||||
* actions: { addItem }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Lastly, "constants" are simple strings that specify action names.
|
||||
* Usually the entire set of available action types are specified in
|
||||
* a `constants.js` file, so they are available globally. Use
|
||||
* variables that are the same name as the string, for example
|
||||
* `const ADD_ITEM = "ADD_ITEM"`.
|
||||
*
|
||||
* This entire system was inspired by Redux, which hopefully we will
|
||||
* eventually use once things get cleaned up enough. You should read
|
||||
* its docs, and keep in mind that it calls stores "reducers" and the
|
||||
* dispatcher instance is called a single store.
|
||||
* http://rackt.github.io/redux/
|
||||
*/
|
||||
function createDispatcher(stores) {
|
||||
const state = {};
|
||||
const listeners = {};
|
||||
let enqueuedChanges = [];
|
||||
let isDispatching = false;
|
||||
|
||||
// Validate the stores to make sure they have the right shape,
|
||||
// and accumulate the initial state
|
||||
entries(stores).forEach(([name, store]) => {
|
||||
if (!store || typeof store.update !== "function") {
|
||||
throw new Error("Error creating dispatcher: store \"" + name +
|
||||
"\" does not have an `update` function");
|
||||
}
|
||||
|
||||
state[name] = store.update(undefined, {});
|
||||
});
|
||||
|
||||
function getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
function emitChange(storeName, dataName, payload) {
|
||||
enqueuedChanges.push([storeName, dataName, payload]);
|
||||
}
|
||||
|
||||
function onChange(paths, view) {
|
||||
entries(paths).forEach(([storeName, data]) => {
|
||||
if (!stores[storeName]) {
|
||||
throw new Error("Error adding onChange handler to store: store " +
|
||||
"\"" + storeName + "\" does not exist");
|
||||
}
|
||||
|
||||
if (!listeners[storeName]) {
|
||||
listeners[storeName] = [];
|
||||
}
|
||||
|
||||
if (typeof data == 'function') {
|
||||
listeners[storeName].push(data.bind(view));
|
||||
}
|
||||
else {
|
||||
entries(data).forEach(([watchedName, handler]) => {
|
||||
listeners[storeName].push((payload, dataName, storeName) => {
|
||||
if (dataName === watchedName) {
|
||||
handler.call(view, payload, dataName, storeName);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any enqueued state changes from the dispatch cycle. Listeners
|
||||
* are not immediately notified of changes, only after dispatching
|
||||
* is completed, to ensure that all state is consistent (in the case
|
||||
* of multiple stores changes at once).
|
||||
*/
|
||||
function flushChanges() {
|
||||
enqueuedChanges.forEach(([storeName, dataName, payload]) => {
|
||||
if (listeners[storeName]) {
|
||||
listeners[storeName].forEach(listener => {
|
||||
listener(payload, dataName, storeName);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
enqueuedChanges = [];
|
||||
}
|
||||
|
||||
function dispatch(action) {
|
||||
if (isDispatching) {
|
||||
throw new Error('Cannot dispatch in the middle of a dispatch');
|
||||
}
|
||||
if (!action.type) {
|
||||
throw new Error(
|
||||
'action type is null, ' +
|
||||
'did you make a typo when publishing this action? ' +
|
||||
JSON.stringify(action, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
isDispatching = true;
|
||||
try {
|
||||
entries(stores).forEach(([name, store]) => {
|
||||
state[name] = store.update(
|
||||
state[name],
|
||||
action,
|
||||
emitChange.bind(null, name)
|
||||
);
|
||||
});
|
||||
}
|
||||
finally {
|
||||
isDispatching = false;
|
||||
}
|
||||
|
||||
flushChanges();
|
||||
}
|
||||
|
||||
return {
|
||||
getState,
|
||||
dispatch,
|
||||
onChange
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createDispatcher: createDispatcher,
|
||||
applyMiddleware: applyMiddleware
|
||||
};
|
17
browser/devtools/shared/fluxify/logMiddleware.js
Normal file
17
browser/devtools/shared/fluxify/logMiddleware.js
Normal file
@ -0,0 +1,17 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* A middleware that logs all actions coming through the system
|
||||
* to the console.
|
||||
*/
|
||||
function logMiddleware({ dispatch, getState }) {
|
||||
return next => action => {
|
||||
console.log('[DISPATCH]', JSON.stringify(action));
|
||||
next(action);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = logMiddleware;
|
15
browser/devtools/shared/fluxify/moz.build
Normal file
15
browser/devtools/shared/fluxify/moz.build
Normal file
@ -0,0 +1,15 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
EXTRA_JS_MODULES.devtools.shared.fluxify += [
|
||||
'bindActionCreators.js',
|
||||
'dispatcher.js',
|
||||
'logMiddleware.js',
|
||||
'thunkMiddleware.js',
|
||||
'waitUntilService.js'
|
||||
]
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
|
28
browser/devtools/shared/fluxify/test/unit/head.js
Normal file
28
browser/devtools/shared/fluxify/test/unit/head.js
Normal file
@ -0,0 +1,28 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
const Cr = Components.results;
|
||||
const CC = Components.Constructor;
|
||||
|
||||
const { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
const createDispatcher = require('devtools/shared/create-dispatcher')({ log: true });
|
||||
const waitUntilService = require('devtools/shared/fluxify/waitUntilService');
|
||||
const services = {
|
||||
WAIT_UNTIL: waitUntilService.name
|
||||
};
|
||||
|
||||
const Services = require("Services");
|
||||
const { waitForTick, waitForTime } = require("devtools/toolkit/DevToolsUtils");
|
||||
|
||||
let loadSubScript = Cc[
|
||||
'@mozilla.org/moz/jssubscript-loader;1'
|
||||
].getService(Ci.mozIJSSubScriptLoader).loadSubScript;
|
||||
|
||||
function getFileUrl(name, allowMissing=false) {
|
||||
let file = do_get_file(name, allowMissing);
|
||||
return Services.io.newFileURI(file).spec;
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// This file should be loaded with `loadSubScript` because it contains
|
||||
// stateful stores that need a new instance per test.
|
||||
|
||||
const NumberStore = {
|
||||
update: (state = 1, action, emitChange) => {
|
||||
switch(action.type) {
|
||||
case constants.ADD_NUMBER: {
|
||||
const newState = state + action.value;
|
||||
emitChange('number', newState);
|
||||
return newState;
|
||||
}
|
||||
case constants.DOUBLE_NUMBER: {
|
||||
const newState = state * 2;
|
||||
emitChange('number', newState);
|
||||
return newState;
|
||||
}}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
constants: {
|
||||
ADD_NUMBER: 'ADD_NUMBER',
|
||||
DOUBLE_NUMBER: 'DOUBLE_NUMBER'
|
||||
}
|
||||
};
|
||||
|
||||
const itemsInitialState = {
|
||||
list: []
|
||||
};
|
||||
const ItemStore = {
|
||||
update: (state = itemsInitialState, action, emitChange) => {
|
||||
switch(action.type) {
|
||||
case constants.ADD_ITEM:
|
||||
state.list.push(action.item);
|
||||
emitChange('list', state.list);
|
||||
return state;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
constants: {
|
||||
ADD_ITEM: 'ADD_ITEM'
|
||||
}
|
||||
}
|
101
browser/devtools/shared/fluxify/test/unit/test_dispatcher.js
Normal file
101
browser/devtools/shared/fluxify/test/unit/test_dispatcher.js
Normal file
@ -0,0 +1,101 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
loadSubScript(getFileUrl("stores-for-testing.js"));
|
||||
const constants = Object.assign(
|
||||
{},
|
||||
NumberStore.constants,
|
||||
ItemStore.constants
|
||||
);
|
||||
const stores = { number: NumberStore,
|
||||
items: ItemStore };
|
||||
|
||||
function addNumber(num) {
|
||||
return {
|
||||
type: constants.ADD_NUMBER,
|
||||
value: num
|
||||
}
|
||||
}
|
||||
|
||||
function addItem(item) {
|
||||
return {
|
||||
type: constants.ADD_ITEM,
|
||||
item: item
|
||||
};
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
function run_test() {
|
||||
testInitialValue();
|
||||
testDispatch();
|
||||
testEmitChange();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function testInitialValue() {
|
||||
do_print("Testing initial value");
|
||||
const dispatcher = createDispatcher(stores);
|
||||
equal(dispatcher.getState().number, 1);
|
||||
}
|
||||
|
||||
function testDispatch() {
|
||||
do_print("Testing dispatch");
|
||||
|
||||
const dispatcher = createDispatcher(stores);
|
||||
dispatcher.dispatch(addNumber(5));
|
||||
equal(dispatcher.getState().number, 6);
|
||||
|
||||
dispatcher.dispatch(addNumber(2));
|
||||
equal(dispatcher.getState().number, 8);
|
||||
|
||||
// It should ignore unknown action types
|
||||
dispatcher.dispatch({ type: "FOO" });
|
||||
equal(dispatcher.getState().number, 8);
|
||||
}
|
||||
|
||||
function testEmitChange() {
|
||||
do_print("Testing change emittters");
|
||||
const dispatcher = createDispatcher(stores);
|
||||
let listenerRan = false;
|
||||
|
||||
const numberView = {
|
||||
x: 3,
|
||||
renderNumber: function(num) {
|
||||
ok(this.x, 3, "listener ran in context of view");
|
||||
ok(num, 10);
|
||||
listenerRan = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Views can listen to changes in state by specifying which part of
|
||||
// the state to listen to and giving a handler function. The
|
||||
// function will be run with the view as `this`.
|
||||
dispatcher.onChange({
|
||||
"number": numberView.renderNumber
|
||||
}, numberView);
|
||||
|
||||
dispatcher.dispatch(addNumber(9));
|
||||
ok(listenerRan, "number listener actually ran");
|
||||
listenerRan = false;
|
||||
|
||||
const itemsView = {
|
||||
renderList: function(items) {
|
||||
ok(items.length, 1);
|
||||
ok(items[0].name = "james");
|
||||
listenerRan = true;
|
||||
}
|
||||
}
|
||||
|
||||
// You can listen to deeper sections of the state by nesting objects
|
||||
// to specify the path to that state. You can do this 1 level deep;
|
||||
// you cannot arbitrarily nest state listeners.
|
||||
dispatcher.onChange({
|
||||
"items": {
|
||||
"list": itemsView.renderList
|
||||
}
|
||||
}, itemsView);
|
||||
|
||||
dispatcher.dispatch(addItem({ name: "james" }));
|
||||
ok(listenerRan, "items listener actually ran");
|
||||
}
|
114
browser/devtools/shared/fluxify/test/unit/test_middlewares.js
Normal file
114
browser/devtools/shared/fluxify/test/unit/test_middlewares.js
Normal file
@ -0,0 +1,114 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
loadSubScript(getFileUrl('stores-for-testing.js'));
|
||||
const constants = NumberStore.constants;
|
||||
const stores = { number: NumberStore };
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function* testThunkDispatch() {
|
||||
do_print("Testing thunk dispatch");
|
||||
// The thunk middleware allows you to return a function from an
|
||||
// action creator which takes `dispatch` and `getState` functions as
|
||||
// arguments. This allows the action creator to fire multiple
|
||||
// actions manually with `dispatch`, possibly asynchronously.
|
||||
|
||||
function addNumberLater(num) {
|
||||
return dispatch => {
|
||||
// Just do it in the next tick, no need to wait too long
|
||||
waitForTick().then(() => {
|
||||
dispatch({
|
||||
type: constants.ADD_NUMBER,
|
||||
value: num
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function addNumber(num) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: constants.ADD_NUMBER,
|
||||
value: getState().number > 10 ? (num * 2) : num
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const dispatcher = createDispatcher(stores);
|
||||
equal(dispatcher.getState().number, 1);
|
||||
dispatcher.dispatch(addNumberLater(5));
|
||||
equal(dispatcher.getState().number, 1, "state should not have changed");
|
||||
yield waitForTick();
|
||||
equal(dispatcher.getState().number, 6, "state should have changed");
|
||||
|
||||
dispatcher.dispatch(addNumber(5));
|
||||
equal(dispatcher.getState().number, 11);
|
||||
dispatcher.dispatch(addNumber(2));
|
||||
// 2 * 2 should have actually been added because the state is
|
||||
// greater than 10 (the action creator changes the value based on
|
||||
// the current state)
|
||||
equal(dispatcher.getState().number, 15);
|
||||
});
|
||||
|
||||
add_task(function* testWaitUntilService() {
|
||||
do_print("Testing waitUntil service");
|
||||
// The waitUntil service allows you to queue functions to be run at a
|
||||
// later time, depending on a predicate. As actions comes through
|
||||
// the system, you predicate will be called with each action. Once
|
||||
// your predicate returns true, the queued function will be run and
|
||||
// removed from the pending queue.
|
||||
|
||||
function addWhenDoubled(num) {
|
||||
return {
|
||||
type: services.WAIT_UNTIL,
|
||||
predicate: action => action.type === constants.DOUBLE_NUMBER,
|
||||
run: (dispatch, getState, action) => {
|
||||
ok(action.type, constants.DOUBLE_NUMBER);
|
||||
ok(getState(), 10);
|
||||
|
||||
dispatch({
|
||||
type: constants.ADD_NUMBER,
|
||||
value: 2
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function addWhenGreaterThan(threshold, num) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: services.WAIT_UNTIL,
|
||||
predicate: () => getState().number > threshold,
|
||||
run: () => {
|
||||
dispatch({
|
||||
type: constants.ADD_NUMBER,
|
||||
value: num
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const dispatcher = createDispatcher(stores);
|
||||
|
||||
// Add a pending action that adds 2 after the number is doubled
|
||||
equal(dispatcher.getState().number, 1);
|
||||
dispatcher.dispatch(addWhenDoubled(2));
|
||||
equal(dispatcher.getState().number, 1);
|
||||
dispatcher.dispatch({ type: constants.DOUBLE_NUMBER });
|
||||
// Note how the pending function we added ran synchronously. It
|
||||
// should have added 2 after doubling 1, so 1 * 2 + 2 = 4
|
||||
equal(dispatcher.getState().number, 4);
|
||||
|
||||
// Add a pending action that adds 5 once the number is greater than 10
|
||||
dispatcher.dispatch(addWhenGreaterThan(10, 5));
|
||||
equal(dispatcher.getState().number, 4);
|
||||
dispatcher.dispatch({ type: constants.ADD_NUMBER, value: 10 });
|
||||
// Again, the pending function we added ran synchronously. It should
|
||||
// have added 5 more after 10 was added, since the number was
|
||||
// greater than 10.
|
||||
equal(dispatcher.getState().number, 19);
|
||||
});
|
12
browser/devtools/shared/fluxify/test/unit/xpcshell.ini
Normal file
12
browser/devtools/shared/fluxify/test/unit/xpcshell.ini
Normal file
@ -0,0 +1,12 @@
|
||||
[DEFAULT]
|
||||
tags = devtools
|
||||
head = head.js
|
||||
tail =
|
||||
firefox-appdir = browser
|
||||
skip-if = toolkit == 'android' || toolkit == 'gonk'
|
||||
|
||||
support-files =
|
||||
stores-for-testing.js
|
||||
|
||||
[test_dispatcher.js]
|
||||
[test_middlewares.js]
|
20
browser/devtools/shared/fluxify/thunkMiddleware.js
Normal file
20
browser/devtools/shared/fluxify/thunkMiddleware.js
Normal file
@ -0,0 +1,20 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* A middleware that allows thunks (functions) to be dispatched.
|
||||
* If it's a thunk, it is called with `dispatch` and `getState`,
|
||||
* allowing the action to create multiple actions (most likely
|
||||
* asynchronously).
|
||||
*/
|
||||
function thunkMiddleware({ dispatch, getState }) {
|
||||
return next => action => {
|
||||
return typeof action === "function"
|
||||
? action(dispatch, getState)
|
||||
: next(action);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = thunkMiddleware;
|
69
browser/devtools/shared/fluxify/waitUntilService.js
Normal file
69
browser/devtools/shared/fluxify/waitUntilService.js
Normal file
@ -0,0 +1,69 @@
|
||||
/* 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/. */
|
||||
"use strict";
|
||||
|
||||
const NAME = "@@service/waitUntil";
|
||||
|
||||
/**
|
||||
* A middleware which acts like a service, because it is stateful
|
||||
* and "long-running" in the background. It provides the ability
|
||||
* for actions to install a function to be run once when a specific
|
||||
* condition is met by an action coming through the system. Think of
|
||||
* it as a thunk that blocks until the condition is met. Example:
|
||||
*
|
||||
* ```js
|
||||
* const services = { WAIT_UNTIL: require('waitUntilService').name };
|
||||
*
|
||||
* { type: services.WAIT_UNTIL,
|
||||
* predicate: action => action.type === constants.ADD_ITEM,
|
||||
* run: (dispatch, getState, action) => {
|
||||
* // Do anything here. You only need to accept the arguments
|
||||
* // if you need them. `action` is the action that satisfied
|
||||
* // the predicate.
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function waitUntilService({ dispatch, getState }) {
|
||||
let pending = [];
|
||||
|
||||
function checkPending(action) {
|
||||
let readyRequests = [];
|
||||
let stillPending = [];
|
||||
|
||||
// Find the pending requests whose predicates are satisfied with
|
||||
// this action. Wait to run the requests until after we update the
|
||||
// pending queue because the request handler may synchronously
|
||||
// dispatch again and run this service (that use case is
|
||||
// completely valid).
|
||||
for (let request of pending) {
|
||||
if (request.predicate(action)) {
|
||||
readyRequests.push(request);
|
||||
}
|
||||
else {
|
||||
stillPending.push(request);
|
||||
}
|
||||
}
|
||||
|
||||
pending = stillPending;
|
||||
for (let request of readyRequests) {
|
||||
request.run(dispatch, getState, action);
|
||||
}
|
||||
}
|
||||
|
||||
return next => action => {
|
||||
if (action.type === NAME) {
|
||||
pending.push(action);
|
||||
}
|
||||
else {
|
||||
next(action);
|
||||
checkPending(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
service: waitUntilService,
|
||||
name: NAME
|
||||
};
|
@ -31,6 +31,8 @@ EXTRA_JS_MODULES.devtools += [
|
||||
|
||||
EXTRA_JS_MODULES.devtools.shared += [
|
||||
'autocomplete-popup.js',
|
||||
'browser-loader.js',
|
||||
'create-dispatcher.js',
|
||||
'devices.js',
|
||||
'doorhanger.js',
|
||||
'frame-script-utils.js',
|
||||
@ -72,3 +74,5 @@ EXTRA_JS_MODULES.devtools.shared.widgets += [
|
||||
'widgets/Tooltip.js',
|
||||
'widgets/TreeWidget.js',
|
||||
]
|
||||
|
||||
DIRS += ['fluxify']
|
||||
|
@ -116,6 +116,38 @@ exports.zip = function zip(a, b) {
|
||||
return pairs;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Converts an object into an array with 2-element arrays as key/value
|
||||
* pairs of the object. `{ foo: 1, bar: 2}` would become
|
||||
* `[[foo, 1], [bar 2]]` (order not guaranteed);
|
||||
*
|
||||
* @param object obj
|
||||
* @returns array
|
||||
*/
|
||||
exports.entries = function entries(obj) {
|
||||
return Object.keys(obj).map(k => [k, obj[k]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Composes the given functions into a single function, which will
|
||||
* apply the results of each function right-to-left, starting with
|
||||
* applying the given arguments to the right-most function.
|
||||
* `compose(foo, bar, baz)` === `args => foo(bar(baz(args)`
|
||||
*
|
||||
* @param ...function funcs
|
||||
* @returns function
|
||||
*/
|
||||
exports.compose = function compose(...funcs) {
|
||||
return (...args) => {
|
||||
const initialValue = funcs[funcs.length - 1].apply(null, args);
|
||||
const leftFuncs = funcs.slice(0, -1);
|
||||
return leftFuncs.reduceRight((composed, f) => f(composed),
|
||||
initialValue);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Waits for the next tick in the event loop to execute a callback.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user