Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / devtools / client / webconsole / webconsole-wrapper.js
blobdfd0649d6f45f14f6611959f9e9ad98b507ebbb1
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
6 const {
7 createElement,
8 createFactory,
9 } = require("resource://devtools/client/shared/vendor/react.js");
10 const ReactDOM = require("resource://devtools/client/shared/vendor/react-dom.js");
11 const {
12 Provider,
13 createProvider,
14 } = require("resource://devtools/client/shared/vendor/react-redux.js");
16 const actions = require("resource://devtools/client/webconsole/actions/index.js");
17 const {
18 configureStore,
19 } = require("resource://devtools/client/webconsole/store.js");
21 const {
22 isPacketPrivate,
23 } = require("resource://devtools/client/webconsole/utils/messages.js");
24 const {
25 getMutableMessagesById,
26 getMessage,
27 getAllNetworkMessagesUpdateById,
28 } = require("resource://devtools/client/webconsole/selectors/messages.js");
30 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
31 const App = createFactory(
32 require("resource://devtools/client/webconsole/components/App.js")
34 const {
35 getAllFilters,
36 } = require("resource://devtools/client/webconsole/selectors/filters.js");
38 loader.lazyGetter(this, "AppErrorBoundary", () =>
39 createFactory(
40 require("resource://devtools/client/shared/components/AppErrorBoundary.js")
44 const {
45 setupServiceContainer,
46 } = require("resource://devtools/client/webconsole/service-container.js");
48 loader.lazyRequireGetter(
49 this,
50 "Constants",
51 "resource://devtools/client/webconsole/constants.js"
54 // Localized strings for (devtools/client/locales/en-US/startup.properties)
55 loader.lazyGetter(this, "L10N", function () {
56 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
57 return new LocalizationHelper("devtools/client/locales/startup.properties");
58 });
60 // Only Browser Console needs Fluent bundles at the moment
61 loader.lazyRequireGetter(
62 this,
63 "FluentL10n",
64 "resource://devtools/client/shared/fluent-l10n/fluent-l10n.js",
65 true
67 loader.lazyRequireGetter(
68 this,
69 "LocalizationProvider",
70 "resource://devtools/client/shared/vendor/fluent-react.js",
71 true
74 let store = null;
76 class WebConsoleWrapper {
77 /**
79 * @param {HTMLElement} parentNode
80 * @param {WebConsoleUI} webConsoleUI
81 * @param {Toolbox} toolbox
82 * @param {Document} document
85 constructor(parentNode, webConsoleUI, toolbox, document) {
86 EventEmitter.decorate(this);
88 this.parentNode = parentNode;
89 this.webConsoleUI = webConsoleUI;
90 this.toolbox = toolbox;
91 this.hud = this.webConsoleUI.hud;
92 this.document = document;
94 this.init = this.init.bind(this);
96 this.queuedMessageAdds = [];
97 this.queuedMessageUpdates = [];
98 this.queuedRequestUpdates = [];
99 this.throttledDispatchPromise = null;
101 this.telemetry = this.hud.telemetry;
104 #serviceContainer;
106 async init() {
107 const { webConsoleUI } = this;
109 let fluentBundles;
110 if (webConsoleUI.isBrowserConsole) {
111 const fluentL10n = new FluentL10n();
112 await fluentL10n.init(["devtools/client/toolbox.ftl"]);
113 fluentBundles = fluentL10n.getBundles();
116 return new Promise(resolve => {
117 store = configureStore(this.webConsoleUI, {
118 // We may not have access to the toolbox (e.g. in the browser console).
119 telemetry: this.telemetry,
120 thunkArgs: {
121 webConsoleUI,
122 hud: this.hud,
123 toolbox: this.toolbox,
124 commands: this.hud.commands,
128 const app = AppErrorBoundary(
130 componentName: "Console",
131 panel: L10N.getStr("ToolboxTabWebconsole.label"),
133 App({
134 serviceContainer: this.getServiceContainer(),
135 webConsoleUI,
136 onFirstMeaningfulPaint: resolve,
137 closeSplitConsole: this.closeSplitConsole.bind(this),
138 inputEnabled:
139 !webConsoleUI.isBrowserConsole ||
140 Services.prefs.getBoolPref("devtools.chrome.enabled"),
144 // Render the root Application component.
145 if (this.parentNode) {
146 const maybeLocalizedElement = fluentBundles
147 ? createElement(LocalizationProvider, { bundles: fluentBundles }, app)
148 : app;
150 this.body = ReactDOM.render(
151 createElement(
152 Provider,
153 { store },
154 createElement(
155 createProvider(this.hud.commands.targetCommand.storeId),
156 { store: this.hud.commands.targetCommand.store },
157 maybeLocalizedElement
160 this.parentNode
162 } else {
163 // If there's no parentNode, we are in a test. So we can resolve immediately.
164 resolve();
169 destroy() {
170 // This component can be instantiated from jest test, in which case we don't have
171 // a parentNode reference.
172 if (this.parentNode) {
173 ReactDOM.unmountComponentAtNode(this.parentNode);
178 * Query the reducer store for the current state of filtering
179 * a given type of message
181 * @param {String} filter
182 * Type of message to be filtered.
183 * @return {Boolean}
184 * True if this type of message should be displayed.
186 getFilterState(filter) {
187 return getAllFilters(this.getStore().getState())[filter];
190 dispatchMessageAdd(packet) {
191 this.batchedMessagesAdd([packet]);
194 dispatchMessagesAdd(messages) {
195 this.batchedMessagesAdd(messages);
198 dispatchNetworkMessagesDisable() {
199 const networkMessageIds = Object.keys(
200 getAllNetworkMessagesUpdateById(store.getState())
202 store.dispatch(actions.messagesDisable(networkMessageIds));
205 dispatchMessagesClear() {
206 // We might still have pending message additions and updates when the clear action is
207 // triggered, so we need to flush them to make sure we don't have unexpected behavior
208 // in the ConsoleOutput. *But* we want to keep any pending navigation request,
209 // as we want to keep displaying them even if we received a clear request.
210 function filter(l) {
211 return l.filter(update => update.isNavigationRequest);
213 this.queuedMessageAdds = filter(this.queuedMessageAdds);
214 this.queuedMessageUpdates = filter(this.queuedMessageUpdates);
215 this.queuedRequestUpdates = this.queuedRequestUpdates.filter(
216 update => update.data.isNavigationRequest
219 store?.dispatch(actions.messagesClear());
220 this.webConsoleUI.emitForTests("messages-cleared");
223 dispatchPrivateMessagesClear() {
224 // We might still have pending private message additions when the private messages
225 // clear action is triggered. We need to remove any private-window-issued packets from
226 // the queue so they won't appear in the output.
228 // For (network) message updates, we need to check both messages queue and the state
229 // since we can receive updates even if the message isn't rendered yet.
230 const messages = [...getMutableMessagesById(store.getState()).values()];
231 this.queuedMessageUpdates = this.queuedMessageUpdates.filter(
232 ({ actor }) => {
233 const queuedNetworkMessage = this.queuedMessageAdds.find(
234 p => p.actor === actor
236 if (queuedNetworkMessage && isPacketPrivate(queuedNetworkMessage)) {
237 return false;
240 const requestMessage = messages.find(
241 message => actor === message.actor
243 if (requestMessage && requestMessage.private === true) {
244 return false;
247 return true;
251 // For (network) requests updates, we can check only the state, since there must be a
252 // user interaction to get an update (i.e. the network message is displayed and thus
253 // in the state).
254 this.queuedRequestUpdates = this.queuedRequestUpdates.filter(({ id }) => {
255 const requestMessage = getMessage(store.getState(), id);
256 if (requestMessage && requestMessage.private === true) {
257 return false;
260 return true;
263 // Finally we clear the messages queue. This needs to be done here since we use it to
264 // clean the other queues.
265 this.queuedMessageAdds = this.queuedMessageAdds.filter(
266 p => !isPacketPrivate(p)
269 store.dispatch(actions.privateMessagesClear());
272 dispatchTargetMessagesRemove(targetFront) {
273 // We might still have pending packets in the queues from the target that we need to remove
274 // to prevent messages appearing in the output.
276 for (let i = this.queuedMessageUpdates.length - 1; i >= 0; i--) {
277 const packet = this.queuedMessageUpdates[i];
278 if (packet.targetFront == targetFront) {
279 this.queuedMessageUpdates.splice(i, 1);
283 for (let i = this.queuedRequestUpdates.length - 1; i >= 0; i--) {
284 const packet = this.queuedRequestUpdates[i];
285 if (packet.data.targetFront == targetFront) {
286 this.queuedRequestUpdates.splice(i, 1);
290 for (let i = this.queuedMessageAdds.length - 1; i >= 0; i--) {
291 const packet = this.queuedMessageAdds[i];
292 // Keep in sync with the check done in the reducer for the TARGET_MESSAGES_REMOVE action.
293 if (
294 packet.targetFront == targetFront &&
295 packet.type !== Constants.MESSAGE_TYPE.COMMAND &&
296 packet.type !== Constants.MESSAGE_TYPE.RESULT
298 this.queuedMessageAdds.splice(i, 1);
302 store.dispatch(actions.targetMessagesRemove(targetFront));
305 dispatchMessagesUpdate(messages) {
306 this.batchedMessagesUpdates(messages);
309 dispatchSidebarClose() {
310 store.dispatch(actions.sidebarClose());
313 dispatchSplitConsoleCloseButtonToggle() {
314 store.dispatch(
315 actions.splitConsoleCloseButtonToggle(
316 this.toolbox && this.toolbox.currentToolId !== "webconsole"
321 dispatchTabWillNavigate(packet) {
322 const { ui } = store.getState();
324 // For the browser console, we receive tab navigation
325 // when the original top level window we attached to is closed,
326 // but we don't want to reset console history and just switch to
327 // the next available window.
328 if (ui.persistLogs || this.webConsoleUI.isBrowserConsole) {
329 // Add a type in order for this event packet to be identified by
330 // utils/messages.js's `transformPacket`
331 packet.type = "will-navigate";
332 this.dispatchMessageAdd(packet);
333 } else {
334 this.dispatchMessagesClear();
335 store.dispatch({
336 type: Constants.WILL_NAVIGATE,
341 batchedMessagesUpdates(messages) {
342 if (messages.length) {
343 this.queuedMessageUpdates.push(...messages);
344 this.setTimeoutIfNeeded();
348 batchedRequestUpdates(message) {
349 this.queuedRequestUpdates.push(message);
350 return this.setTimeoutIfNeeded();
353 batchedMessagesAdd(messages) {
354 if (messages.length) {
355 this.queuedMessageAdds.push(...messages);
356 this.setTimeoutIfNeeded();
360 dispatchClearHistory() {
361 store.dispatch(actions.clearHistory());
366 * @param {String} expression: The expression to evaluate
368 dispatchEvaluateExpression(expression) {
369 store.dispatch(actions.evaluateExpression(expression));
372 dispatchUpdateInstantEvaluationResultForCurrentExpression() {
373 store.dispatch(actions.updateInstantEvaluationResultForCurrentExpression());
377 * Returns a Promise that resolves once any async dispatch is finally dispatched.
379 waitAsyncDispatches() {
380 if (!this.throttledDispatchPromise) {
381 return Promise.resolve();
383 // When closing the console during initialization,
384 // setTimeoutIfNeeded may never resolve its promise
385 // as window.setTimeout will be disabled on document destruction.
386 const onUnload = new Promise(r =>
387 window.addEventListener("unload", r, { once: true })
389 return Promise.race([this.throttledDispatchPromise, onUnload]);
392 setTimeoutIfNeeded() {
393 if (this.throttledDispatchPromise) {
394 return this.throttledDispatchPromise;
396 this.throttledDispatchPromise = new Promise(done => {
397 setTimeout(async () => {
398 this.throttledDispatchPromise = null;
400 if (!store) {
401 // The store is not initialized yet, we can call setTimeoutIfNeeded so the
402 // messages will be handled in the next timeout when the store is ready.
403 this.setTimeoutIfNeeded();
404 done();
405 return;
408 const { ui } = store.getState();
409 store.dispatch(
410 actions.messagesAdd(this.queuedMessageAdds, null, ui.persistLogs)
413 const { length } = this.queuedMessageAdds;
415 // This telemetry event is only useful when we have a toolbox so only
416 // send it when we have one.
417 if (this.toolbox) {
418 this.telemetry.addEventProperty(
419 this.toolbox,
420 "enter",
421 "webconsole",
422 null,
423 "message_count",
424 length
428 this.queuedMessageAdds = [];
430 if (this.queuedMessageUpdates.length) {
431 await store.dispatch(
432 actions.networkMessageUpdates(this.queuedMessageUpdates, null)
434 this.webConsoleUI.emitForTests("network-messages-updated");
435 this.queuedMessageUpdates = [];
437 if (this.queuedRequestUpdates.length) {
438 await store.dispatch(
439 actions.networkUpdateRequests(this.queuedRequestUpdates)
441 const updateCount = this.queuedRequestUpdates.length;
442 this.queuedRequestUpdates = [];
444 // Fire an event indicating that all data fetched from
445 // the backend has been received. This is based on
446 // 'FirefoxDataProvider.isQueuePayloadReady', see more
447 // comments in that method.
448 // (netmonitor/src/connector/firefox-data-provider).
449 // This event might be utilized in tests to find the right
450 // time when to finish.
452 this.webConsoleUI.emitForTests(
453 "network-request-payload-ready",
454 updateCount
457 done();
458 }, 50);
460 return this.throttledDispatchPromise;
463 getStore() {
464 return store;
467 getServiceContainer() {
468 if (!this.#serviceContainer) {
469 this.#serviceContainer = setupServiceContainer({
470 webConsoleUI: this.webConsoleUI,
471 toolbox: this.toolbox,
472 hud: this.hud,
473 webConsoleWrapper: this,
476 return this.#serviceContainer;
479 subscribeToStore(callback) {
480 store.subscribe(() => callback(store.getState()));
483 createElement(nodename) {
484 return this.document.createElement(nodename);
487 // Called by pushing close button.
488 closeSplitConsole() {
489 this.toolbox.closeSplitConsole();
492 toggleOriginalVariableMappingEvaluationNotification(show) {
493 store.dispatch(
494 actions.showEvaluationNotification(
495 show ? Constants.ORIGINAL_VARIABLE_MAPPING : ""
501 // Exports from this module
502 module.exports = WebConsoleWrapper;