Backed out 2 changesets (bug 1943998) for causing wd failures @ phases.py CLOSED...
[gecko.git] / devtools / client / webconsole / webconsole-ui.js
blob3a7db02941506f729641e720de5970ca6ab573f3
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/. */
5 "use strict";
7 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
8 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
9 const {
10 l10n,
11 } = require("resource://devtools/client/webconsole/utils/messages.js");
13 const { BrowserLoader } = ChromeUtils.importESModule(
14 "resource://devtools/shared/loader/browser-loader.sys.mjs"
16 const {
17 getAdHocFrontOrPrimitiveGrip,
18 } = require("resource://devtools/client/fronts/object.js");
20 const {
21 PREFS,
22 FILTERS,
23 } = require("resource://devtools/client/webconsole/constants.js");
25 const FirefoxDataProvider = require("resource://devtools/client/netmonitor/src/connector/firefox-data-provider.js");
27 const lazy = {};
28 ChromeUtils.defineESModuleGetters(lazy, {
29 AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
30 });
32 loader.lazyRequireGetter(
33 this,
34 "START_IGNORE_ACTION",
35 "resource://devtools/client/shared/redux/middleware/ignore.js",
36 true
38 const ZoomKeys = require("resource://devtools/client/shared/zoom-keys.js");
39 loader.lazyRequireGetter(
40 this,
41 "TRACER_LOG_METHODS",
42 "resource://devtools/shared/specs/tracer.js",
43 true
46 const PREF_SIDEBAR_ENABLED = "devtools.webconsole.sidebarToggle";
47 const PREF_BROWSERTOOLBOX_SCOPE = "devtools.browsertoolbox.scope";
49 /**
50 * A WebConsoleUI instance is an interactive console initialized *per target*
51 * that displays console log data as well as provides an interactive terminal to
52 * manipulate the target's document content.
54 * The WebConsoleUI is responsible for the actual Web Console UI
55 * implementation.
57 class WebConsoleUI {
59 * @param {WebConsole} hud: The WebConsole owner object.
61 constructor(hud) {
62 this.hud = hud;
63 this.hudId = this.hud.hudId;
64 this.isBrowserConsole = this.hud.isBrowserConsole;
66 this.isBrowserToolboxConsole =
67 this.hud.commands.descriptorFront.isBrowserProcessDescriptor &&
68 !this.isBrowserConsole;
70 this.window = this.hud.iframeWindow;
72 this._onPanelSelected = this._onPanelSelected.bind(this);
73 this._onChangeSplitConsoleState =
74 this._onChangeSplitConsoleState.bind(this);
75 this._onTargetAvailable = this._onTargetAvailable.bind(this);
76 this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
77 this._onResourceAvailable = this._onResourceAvailable.bind(this);
78 this._onNetworkResourceUpdated = this._onNetworkResourceUpdated.bind(this);
79 this._onScopePrefChanged = this._onScopePrefChanged.bind(this);
80 this._onShowConsoleEvaluation = this._onShowConsoleEvaluation.bind(this);
82 if (this.isBrowserConsole) {
83 Services.prefs.addObserver(
84 PREF_BROWSERTOOLBOX_SCOPE,
85 this._onScopePrefChanged
89 EventEmitter.decorate(this);
92 /**
93 * Initialize the WebConsoleUI instance.
94 * @return object
95 * A promise object that resolves once the frame is ready to use.
97 init() {
98 if (this._initializer) {
99 return this._initializer;
102 this._initializer = (async () => {
103 this._initUI();
105 if (this.isBrowserConsole) {
106 // Bug 1605763:
107 // TargetCommand.startListening will start fetching additional targets
108 // and may overload the Browser Console with loads of targets and resources.
109 // We can call it from here, as `_attachTargets` is called after the UI is initialized.
110 // Bug 1642599:
111 // TargetCommand.startListening has to be called before:
112 // - `_attachTargets`, in order to set TargetCommand.watcherFront which is used by ResourceWatcher.watchResources.
113 // - `ConsoleCommands`, in order to set TargetCommand.targetFront which is wrapped by hud.currentTarget
114 await this.hud.commands.targetCommand.startListening();
115 if (this._destroyed) {
116 return;
120 await this.wrapper.init();
121 if (this._destroyed) {
122 return;
125 // Bug 1605763: It's important to call _attachTargets once the UI is initialized, as
126 // it may overload the Browser Console with many updates.
127 // It is also important to do it only after the wrapper is initialized,
128 // otherwise its `store` will be null while we already call a few dispatch methods
129 // from onResourceAvailable
130 await this._attachTargets();
131 if (this._destroyed) {
132 return;
135 // `_attachTargets` will process resources and throttle some actions
136 // Wait for these actions to be dispatched before reporting that the
137 // console is initialized. Otherwise `showToolbox` will resolve before
138 // all already existing console messages are displayed.
139 await this.wrapper.waitAsyncDispatches();
140 this._initNotifications();
141 })();
143 return this._initializer;
146 destroy() {
147 if (this._destroyed) {
148 return;
151 this._destroyed = true;
153 this.React = this.ReactDOM = this.FrameView = null;
155 if (this.wrapper) {
156 this.wrapper.getStore()?.dispatch(START_IGNORE_ACTION);
157 this.wrapper.destroy();
160 if (this.jsterm) {
161 this.jsterm.destroy();
162 this.jsterm = null;
165 const { toolbox } = this.hud;
166 if (toolbox) {
167 toolbox.off("webconsole-selected", this._onPanelSelected);
168 toolbox.off("split-console", this._onChangeSplitConsoleState);
169 toolbox.off("select", this._onChangeSplitConsoleState);
170 toolbox.off(
171 "show-original-variable-mapping-warnings",
172 this._onShowConsoleEvaluation
176 if (this.isBrowserConsole) {
177 Services.prefs.removeObserver(
178 PREF_BROWSERTOOLBOX_SCOPE,
179 this._onScopePrefChanged
183 // Stop listening for targets
184 this.hud.commands.targetCommand.unwatchTargets({
185 types: this.hud.commands.targetCommand.ALL_TYPES,
186 onAvailable: this._onTargetAvailable,
187 onDestroyed: this._onTargetDestroyed,
190 const resourceCommand = this.hud.resourceCommand;
191 if (this._watchedResources) {
192 resourceCommand.unwatchResources(this._watchedResources, {
193 onAvailable: this._onResourceAvailable,
197 this.stopWatchingNetworkResources();
199 if (this.networkDataProvider) {
200 this.networkDataProvider.destroy();
201 this.networkDataProvider = null;
204 // Nullify `hud` last as it nullify also target which is used on destroy
205 this.window = this.hud = this.wrapper = null;
209 * Clear the Web Console output.
211 * This method emits the "messages-cleared" notification.
213 * @param boolean clearStorage
214 * True if you want to clear the console messages storage associated to
215 * this Web Console.
216 * @param object event
217 * If the event exists, calls preventDefault on it.
219 async clearOutput(clearStorage, event) {
220 if (event) {
221 event.preventDefault();
223 if (this.wrapper) {
224 this.wrapper.dispatchMessagesClear();
227 if (clearStorage) {
228 await this.clearMessagesCache();
230 this.emitForTests("messages-cleared");
233 async clearMessagesCache() {
234 if (this._destroyed) {
235 return;
238 // This can be called during console destruction and getAllFronts would reject in such case.
239 try {
240 const consoleFronts = await this.hud.commands.targetCommand.getAllFronts(
241 this.hud.commands.targetCommand.ALL_TYPES,
242 "console"
244 const promises = [];
245 for (const consoleFront of consoleFronts) {
246 promises.push(consoleFront.clearMessagesCacheAsync());
248 await Promise.all(promises);
249 this.emitForTests("messages-cache-cleared");
250 } catch (e) {
251 console.warn("Exception in clearMessagesCache", e);
256 * Remove all of the private messages from the Web Console output.
258 * This method emits the "private-messages-cleared" notification.
260 clearPrivateMessages() {
261 if (this._destroyed) {
262 return;
265 this.wrapper.dispatchPrivateMessagesClear();
266 this.emitForTests("private-messages-cleared");
269 inspectObjectActor(objectActor) {
270 const { targetFront } = this.hud.commands.targetCommand;
271 this.wrapper.dispatchMessageAdd(
273 helperResult: {
274 type: "inspectObject",
275 object:
276 objectActor && objectActor.getGrip
277 ? objectActor
278 : getAdHocFrontOrPrimitiveGrip(objectActor, targetFront),
281 true
283 return this.wrapper;
286 disableAllNetworkMessages() {
287 if (this._destroyed) {
288 return;
290 this.wrapper.dispatchNetworkMessagesDisable();
293 getPanelWindow() {
294 return this.window;
297 logWarningAboutReplacedAPI() {
298 return this.hud.currentTarget.logWarningInPage(
299 l10n.getStr("ConsoleAPIDisabled"),
300 "ConsoleAPIDisabled"
305 * Connect to the server using the remote debugging protocol.
307 * @private
308 * @return object
309 * A promise object that is resolved/reject based on the proxies connections.
311 async _attachTargets() {
312 const { commands, resourceCommand } = this.hud;
313 this.networkDataProvider = new FirefoxDataProvider({
314 commands,
315 actions: {
316 updateRequest: (id, data) =>
317 this.wrapper.batchedRequestUpdates({ id, data }),
319 owner: this,
322 // Listen for all target types, including:
323 // - frames, in order to get the parent process target
324 // which is considered as a frame rather than a process.
325 // - workers, for similar reason. When we open a toolbox
326 // for just a worker, the top level target is a worker target.
327 // - processes, as we want to spawn additional proxies for them.
328 await commands.targetCommand.watchTargets({
329 types: this.hud.commands.targetCommand.ALL_TYPES,
330 onAvailable: this._onTargetAvailable,
331 onDestroyed: this._onTargetDestroyed,
334 this._watchedResources = [
335 resourceCommand.TYPES.CONSOLE_MESSAGE,
336 resourceCommand.TYPES.ERROR_MESSAGE,
337 resourceCommand.TYPES.PLATFORM_MESSAGE,
338 resourceCommand.TYPES.DOCUMENT_EVENT,
339 resourceCommand.TYPES.LAST_PRIVATE_CONTEXT_EXIT,
340 resourceCommand.TYPES.JSTRACER_TRACE,
341 resourceCommand.TYPES.JSTRACER_STATE,
344 // CSS Warnings are only enabled when the user explicitely requested to show them
345 // as it can slow down page load.
346 const shouldShowCssWarnings = this.wrapper.getFilterState(FILTERS.CSS);
347 if (shouldShowCssWarnings) {
348 this._watchedResources.push(resourceCommand.TYPES.CSS_MESSAGE);
351 await resourceCommand.watchResources(this._watchedResources, {
352 onAvailable: this._onResourceAvailable,
355 if (this.isBrowserConsole || this.isBrowserToolboxConsole) {
356 const shouldEnableNetworkMonitoring = Services.prefs.getBoolPref(
357 PREFS.UI.ENABLE_NETWORK_MONITORING
359 if (shouldEnableNetworkMonitoring) {
360 await this.startWatchingNetworkResources();
361 } else {
362 await this.stopWatchingNetworkResources();
364 } else {
365 // We should always watch for network resources in the webconsole
366 await this.startWatchingNetworkResources();
370 async startWatchingNetworkResources() {
371 const { commands, resourceCommand } = this.hud;
372 await resourceCommand.watchResources(
374 resourceCommand.TYPES.NETWORK_EVENT,
375 resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
378 onAvailable: this._onResourceAvailable,
379 onUpdated: this._onNetworkResourceUpdated,
383 // When opening a worker toolbox from about:debugging,
384 // we do not instantiate any Watcher actor yet and would throw here.
385 // But even once we do, we wouldn't support network inspection anyway.
386 if (commands.targetCommand.hasTargetWatcherSupport()) {
387 const networkFront = await commands.watcherFront.getNetworkParentActor();
388 // There is no way to view response bodies from the Browser Console, so do
389 // not waste the memory.
390 const saveBodies =
391 !this.isBrowserConsole &&
392 Services.prefs.getBoolPref(
393 "devtools.netmonitor.saveRequestAndResponseBodies"
395 await networkFront.setSaveRequestAndResponseBodies(saveBodies);
399 async stopWatchingNetworkResources() {
400 if (this._destroyed) {
401 return;
404 await this.hud.resourceCommand.unwatchResources(
406 this.hud.resourceCommand.TYPES.NETWORK_EVENT,
407 this.hud.resourceCommand.TYPES.NETWORK_EVENT_STACKTRACE,
410 onAvailable: this._onResourceAvailable,
411 onUpdated: this._onNetworkResourceUpdated,
416 handleDocumentEvent(resource) {
417 // Only consider top level document, and ignore remote iframes top document
418 if (!resource.targetFront.isTopLevel) {
419 return;
422 if (resource.name == "will-navigate") {
423 this.handleWillNavigate({
424 timeStamp: resource.time,
425 url: resource.newURI,
427 } else if (resource.name == "dom-complete") {
428 this.handleNavigated({
429 hasNativeConsoleAPI: resource.hasNativeConsoleAPI,
432 // For now, ignore all other DOCUMENT_EVENT's.
436 * Handler for when the page is done loading.
438 * @param Boolean hasNativeConsoleAPI
439 * True if the `console` object is the native one and hasn't been overloaded by a custom
440 * object by the page itself.
442 async handleNavigated({ hasNativeConsoleAPI }) {
443 // Updates instant evaluation on page navigation
444 this.wrapper.dispatchUpdateInstantEvaluationResultForCurrentExpression();
446 // Wait for completion of any async dispatch before notifying that the console
447 // is fully updated after a page reload
448 await this.wrapper.waitAsyncDispatches();
450 if (!hasNativeConsoleAPI) {
451 this.logWarningAboutReplacedAPI();
454 this.emit("reloaded");
457 handleWillNavigate({ timeStamp, url }) {
458 this.wrapper.dispatchTabWillNavigate({ timeStamp, url });
462 * Called when the CSS Warning filter is enabled, in order to start observing for them in the backend.
464 async watchCssMessages() {
465 const { resourceCommand } = this.hud;
466 if (this._watchedResources.includes(resourceCommand.TYPES.CSS_MESSAGE)) {
467 return;
469 await resourceCommand.watchResources([resourceCommand.TYPES.CSS_MESSAGE], {
470 onAvailable: this._onResourceAvailable,
472 this._watchedResources.push(resourceCommand.TYPES.CSS_MESSAGE);
475 // eslint-disable-next-line complexity
476 _onResourceAvailable(resources) {
477 if (this._destroyed) {
478 return;
481 const { logMethod } = this.hud.commands.tracerCommand.getTracingOptions();
483 const messages = [];
484 for (const resource of resources) {
485 const { TYPES } = this.hud.resourceCommand;
486 if (resource.resourceType === TYPES.DOCUMENT_EVENT) {
487 this.handleDocumentEvent(resource);
488 continue;
490 if (resource.resourceType == TYPES.LAST_PRIVATE_CONTEXT_EXIT) {
491 // Private messages only need to be removed from the output in Browser Console/Browser Toolbox
492 // (but in theory this resource should only be send from parent process watchers)
493 if (this.isBrowserConsole || this.isBrowserToolboxConsole) {
494 this.clearPrivateMessages();
496 continue;
498 // Ignore messages forwarded from content processes if we're in fission browser toolbox.
499 if (
500 !this.wrapper ||
501 ((resource.resourceType === TYPES.ERROR_MESSAGE ||
502 resource.resourceType === TYPES.CSS_MESSAGE) &&
503 resource.pageError?.isForwardedFromContentProcess &&
504 (this.isBrowserToolboxConsole || this.isBrowserConsole))
506 continue;
509 // Don't show messages emitted from a private window before the Browser Console was
510 // opened to avoid leaking data from past usage of the browser (e.g. content message
511 // from now closed private tabs)
512 if (
513 (this.isBrowserToolboxConsole || this.isBrowserConsole) &&
514 resource.isAlreadyExistingResource &&
515 (resource.pageError?.private || resource.private)
517 continue;
520 if (
521 resource.resourceType === TYPES.JSTRACER_TRACE &&
522 logMethod != TRACER_LOG_METHODS.CONSOLE
524 continue;
526 if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) {
527 this.networkDataProvider?.onStackTraceAvailable(resource);
528 continue;
531 if (resource.resourceType === TYPES.NETWORK_EVENT) {
532 this.networkDataProvider?.onNetworkResourceAvailable(resource);
534 messages.push(resource);
536 this.wrapper.dispatchMessagesAdd(messages);
539 _onNetworkResourceUpdated(updates) {
540 if (this._destroyed) {
541 return;
544 const messageUpdates = [];
545 for (const { resource } of updates) {
546 if (
547 resource.resourceType == this.hud.resourceCommand.TYPES.NETWORK_EVENT
549 this.networkDataProvider?.onNetworkResourceUpdated(resource);
550 messageUpdates.push(resource);
553 this.wrapper.dispatchMessagesUpdate(messageUpdates);
557 * Called any time a new target is available.
558 * i.e. it was already existing or has just been created.
560 * @private
562 async _onTargetAvailable() {
563 // onTargetAvailable is a mandatory argument for watchTargets,
564 // we still define it solely for being able to use onTargetDestroyed.
567 _onTargetDestroyed({ targetFront, isModeSwitching }) {
568 // Don't try to do anything if the WebConsole is being destroyed
569 if (this._destroyed) {
570 return;
573 // We only want to remove messages from a target destroyed when we're switching mode
574 // in the Browser Console/Browser Toolbox Console.
575 // For regular cases, we want to keep the message history (the output will still be
576 // cleared when the top level target navigates, if "Persist Logs" isn't true, via handleWillNavigate)
577 if (isModeSwitching) {
578 this.wrapper.dispatchTargetMessagesRemove(targetFront);
582 _initUI() {
583 this.document = this.window.document;
584 this.rootElement = this.document.documentElement;
586 this.outputNode = this.document.getElementById("app-wrapper");
588 const { toolbox } = this.hud;
590 // Initialize module loader and load all the WebConsoleWrapper. The entire code-base
591 // doesn't need any extra privileges and runs entirely in content scope.
592 const WebConsoleWrapper = BrowserLoader({
593 baseURI: "resource://devtools/client/webconsole/",
594 window: this.window,
595 }).require("resource://devtools/client/webconsole/webconsole-wrapper.js");
597 this.wrapper = new WebConsoleWrapper(
598 this.outputNode,
599 this,
600 toolbox,
601 this.document
604 this._initShortcuts();
605 this._initOutputSyntaxHighlighting();
607 if (toolbox) {
608 toolbox.on("webconsole-selected", this._onPanelSelected);
609 toolbox.on("split-console", this._onChangeSplitConsoleState);
610 toolbox.on("select", this._onChangeSplitConsoleState);
614 _initOutputSyntaxHighlighting() {
615 // Given a DOM node, we syntax highlight identically to how the input field
616 // looks. See https://codemirror.net/demo/runmode.html;
617 const syntaxHighlightNode = node => {
618 const editor = this.jsterm && this.jsterm.editor;
619 if (node && editor) {
620 node.classList.add("cm-s-mozilla");
621 editor.CodeMirror.runMode(
622 node.textContent,
623 "application/javascript",
624 node
629 // Use a Custom Element to handle syntax highlighting to avoid
630 // dealing with refs or innerHTML from React.
631 const win = this.window;
632 win.customElements.define(
633 "syntax-highlighted",
634 class extends win.HTMLElement {
635 connectedCallback() {
636 if (!this.connected) {
637 this.connected = true;
638 syntaxHighlightNode(this);
640 // Highlight Again when the innerText changes
641 // We remove the listener before running codemirror mode and add
642 // it again to capture text changes
643 this.observer = new win.MutationObserver((mutations, observer) => {
644 observer.disconnect();
645 syntaxHighlightNode(this);
646 observer.observe(this, { childList: true });
649 this.observer.observe(this, { childList: true });
656 _initNotifications() {
657 if (this.hud.toolbox) {
658 this.wrapper.toggleOriginalVariableMappingEvaluationNotification(
659 !!this.hud.toolbox
660 .getPanel("jsdebugger")
661 ?.shouldShowOriginalVariableMappingWarnings()
663 this.hud.toolbox.on(
664 "show-original-variable-mapping-warnings",
665 this._onShowConsoleEvaluation
670 _initShortcuts() {
671 const shortcuts = new KeyShortcuts({
672 window: this.window,
675 let clearShortcut;
676 if (lazy.AppConstants.platform === "macosx") {
677 const alternativaClearShortcut = l10n.getStr(
678 "webconsole.clear.alternativeKeyOSX"
680 shortcuts.on(alternativaClearShortcut, event =>
681 this.clearOutput(true, event)
683 clearShortcut = l10n.getStr("webconsole.clear.keyOSX");
684 } else {
685 clearShortcut = l10n.getStr("webconsole.clear.key");
688 shortcuts.on(clearShortcut, event => this.clearOutput(true, event));
690 if (this.isBrowserConsole) {
691 // Make sure keyboard shortcuts work immediately after opening
692 // the Browser Console (Bug 1461366).
693 this.window.focus();
694 shortcuts.on(
695 l10n.getStr("webconsole.close.key"),
696 this.window.close.bind(this.window)
699 ZoomKeys.register(this.window, shortcuts);
701 /* This is the same as DevelopmentHelpers.quickRestart, but it runs in all
702 * builds (even official). This allows a user to do a restart + session restore
703 * with Ctrl+Shift+J (open Browser Console) and then Ctrl+Alt+R (restart).
705 shortcuts.on("CmdOrCtrl+Alt+R", () => {
706 this.hud.commands.targetCommand.reloadTopLevelTarget();
708 } else if (Services.prefs.getBoolPref(PREF_SIDEBAR_ENABLED)) {
709 shortcuts.on("Esc", () => {
710 this.wrapper.dispatchSidebarClose();
711 if (this.jsterm) {
712 this.jsterm.focus();
719 * Sets the focus to JavaScript input field when the web console tab is
720 * selected or when there is a split console present.
721 * @private
723 _onPanelSelected() {
724 // We can only focus when we have the jsterm reference. This is fine because if the
725 // jsterm is not mounted yet, it will be focused in JSTerm's componentDidMount.
726 if (this.jsterm) {
727 this.jsterm.focus();
731 _onChangeSplitConsoleState() {
732 this.wrapper.dispatchSplitConsoleCloseButtonToggle();
735 _onScopePrefChanged() {
736 if (this.isBrowserConsole) {
737 this.hud.updateWindowTitle();
741 _onShowConsoleEvaluation(isOriginalVariableMappingEnabled) {
742 this.wrapper.toggleOriginalVariableMappingEvaluationNotification(
743 isOriginalVariableMappingEnabled
747 getInputCursor() {
748 return this.jsterm && this.jsterm.getSelectionStart();
751 getJsTermTooltipAnchor() {
752 return this.outputNode.querySelector(".CodeMirror-cursor");
755 attachRef(id, node) {
756 this[id] = node;
759 getSelectedNodeActorID() {
760 const inspectorSelection = this.hud.getInspectorSelection();
761 return inspectorSelection?.nodeFront?.actorID;
765 exports.WebConsoleUI = WebConsoleUI;