Bug 1514815 - Keep console scrolled to bottom when rendering SmartTrace; r=bgrins.
☠☠ backed out by ec5a6528e1e1 ☠ ☠
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Wed, 19 Dec 2018 21:06:40 +0000
changeset 451512 d822219ea9bc8105d783d5fc994f1b830c57a9f2
parent 451511 209bdfcecdc941d62894968be9c65b6e3a3934d3
child 451513 cf28e3669e474b6e26ce8d0a804819c2ec66df3a
push id75076
push usernchevobbe@mozilla.com
push dateThu, 20 Dec 2018 12:16:38 +0000
treeherderautoland@d822219ea9bc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1514815
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1514815 - Keep console scrolled to bottom when rendering SmartTrace; r=bgrins. Depends on D14999 Differential Revision: https://phabricator.services.mozilla.com/D15010
devtools/client/webconsole/components/ConsoleOutput.js
devtools/client/webconsole/components/GripMessageBody.js
devtools/client/webconsole/components/Message.js
devtools/client/webconsole/components/message-types/ConsoleApiCall.js
devtools/client/webconsole/components/message-types/ConsoleCommand.js
devtools/client/webconsole/components/message-types/EvaluationResult.js
devtools/client/webconsole/components/message-types/PageError.js
devtools/client/webconsole/test/mochitest/browser_webconsole_scroll.js
devtools/client/webconsole/utils/object-inspector.js
--- a/devtools/client/webconsole/components/ConsoleOutput.js
+++ b/devtools/client/webconsole/components/ConsoleOutput.js
@@ -62,16 +62,17 @@ class ConsoleOutput extends Component {
       onFirstMeaningfulPaint: PropTypes.func.isRequired,
       pausedExecutionPoint: PropTypes.any,
     };
   }
 
   constructor(props) {
     super(props);
     this.onContextMenu = this.onContextMenu.bind(this);
+    this.maybeScrollToBottom = this.maybeScrollToBottom.bind(this);
   }
 
   componentDidMount() {
     scrollToBottom(this.outputNode);
     this.props.serviceContainer.attachRefToHud("outputScroller", this.outputNode);
 
     // Waiting for the next paint.
     new Promise(res => requestAnimationFrame(res))
@@ -120,16 +121,20 @@ class ConsoleOutput extends Component {
         nextProps.initialized &&
         isScrolledToBottom(lastChild, outputNode)
       ) ||
       (isNewMessageEvaluationResult) ||
       (visibleMessagesDelta > 0 && isScrolledToBottom(lastChild, outputNode));
   }
 
   componentDidUpdate() {
+    this.maybeScrollToBottom();
+  }
+
+  maybeScrollToBottom() {
     if (this.shouldScrollBottom) {
       scrollToBottom(this.outputNode);
     }
   }
 
   onContextMenu(e) {
     this.props.serviceContainer.openContextMenu(e);
     e.stopPropagation();
@@ -172,16 +177,17 @@ class ConsoleOutput extends Component {
       tableData: messagesTableData.get(messageId),
       timestampsVisible,
       repeat: messagesRepeat[messageId],
       networkMessageUpdate: networkMessagesUpdate[messageId],
       networkMessageActiveTabId,
       pausedExecutionPoint,
       getMessage: () => messages.get(messageId),
       isPaused: pausedMessage && pausedMessage.id == messageId,
+      maybeScrollToBottom: this.maybeScrollToBottom,
     }));
 
     return (
       dom.div({
         className: "webconsole-output",
         onContextMenu: this.onContextMenu,
         ref: node => {
           this.outputNode = node;
--- a/devtools/client/webconsole/components/GripMessageBody.js
+++ b/devtools/client/webconsole/components/GripMessageBody.js
@@ -31,41 +31,44 @@ GripMessageBody.propTypes = {
     hudProxy: PropTypes.object.isRequired,
     onViewSourceInDebugger: PropTypes.func.isRequired,
   }),
   userProvidedStyle: PropTypes.string,
   useQuotes: PropTypes.bool,
   escapeWhitespace: PropTypes.bool,
   type: PropTypes.string,
   helperType: PropTypes.string,
+  maybeScrollToBottom: PropTypes.func,
 };
 
 GripMessageBody.defaultProps = {
   mode: MODE.LONG,
 };
 
 function GripMessageBody(props) {
   const {
     grip,
     userProvidedStyle,
     serviceContainer,
     useQuotes,
     escapeWhitespace,
     mode = MODE.LONG,
     dispatch,
+    maybeScrollToBottom,
   } = props;
 
   let styleObject;
   if (userProvidedStyle && userProvidedStyle !== "") {
     styleObject = cleanupStyle(userProvidedStyle, serviceContainer.createElement);
   }
 
   const objectInspectorProps = {
     autoExpandDepth: shouldAutoExpandObjectInspector(props) ? 1 : 0,
     mode,
+    maybeScrollToBottom,
     // TODO: we disable focus since the tabbing trail is a bit weird in the output (e.g.
     // location links are not focused). Let's remove the property below when we found and
     // fixed the issue (See Bug 1456060).
     focusable: false,
     onCmdCtrlClick: (node, { depth, event, focused, expanded }) => {
       const value = utils.node.getValue(node);
       if (value) {
         dispatch(actions.showObjectInSidebar(value));
--- a/devtools/client/webconsole/components/Message.js
+++ b/devtools/client/webconsole/components/Message.js
@@ -65,16 +65,17 @@ class Message extends Component {
         openLink: PropTypes.func.isRequired,
         sourceMapService: PropTypes.any,
       }),
       notes: PropTypes.arrayOf(PropTypes.shape({
         messageBody: PropTypes.string.isRequired,
         frame: PropTypes.any,
       })),
       isPaused: PropTypes.bool,
+      maybeScrollToBottom: PropTypes.func,
     };
   }
 
   static get defaultProps() {
     return {
       indent: 0,
     };
   }
@@ -205,16 +206,17 @@ class Message extends Component {
         },
         SmartTrace({
           stacktrace,
           onViewSourceInDebugger: serviceContainer.onViewSourceInDebugger
             || serviceContainer.onViewSource,
           onViewSourceInScratchpad: serviceContainer.onViewSourceInScratchpad
             || serviceContainer.onViewSource,
           onViewSource: serviceContainer.onViewSource,
+          onReady: this.props.maybeScrollToBottom,
           sourceMapService: serviceContainer.sourceMapService,
         }),
       );
     }
 
     // If there is an expandable part, make it collapsible.
     let collapse = null;
     if (collapsible) {
--- a/devtools/client/webconsole/components/message-types/ConsoleApiCall.js
+++ b/devtools/client/webconsole/components/message-types/ConsoleApiCall.js
@@ -19,16 +19,17 @@ const Message = createFactory(require("d
 ConsoleApiCall.displayName = "ConsoleApiCall";
 
 ConsoleApiCall.propTypes = {
   dispatch: PropTypes.func.isRequired,
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
   serviceContainer: PropTypes.object.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
+  maybeScrollToBottom: PropTypes.func,
 };
 
 ConsoleApiCall.defaultProps = {
   open: false,
 };
 
 function ConsoleApiCall(props) {
   const {
@@ -36,16 +37,17 @@ function ConsoleApiCall(props) {
     message,
     open,
     tableData,
     serviceContainer,
     timestampsVisible,
     repeat,
     pausedExecutionPoint,
     isPaused,
+    maybeScrollToBottom,
   } = props;
   const {
     id: messageId,
     executionPoint,
     indent,
     source,
     type,
     level,
@@ -61,16 +63,17 @@ function ConsoleApiCall(props) {
   let messageBody;
   const messageBodyConfig = {
     dispatch,
     messageId,
     parameters,
     userProvidedStyles,
     serviceContainer,
     type,
+    maybeScrollToBottom,
   };
 
   if (type === "trace") {
     const traceParametersBody = Array.isArray(parameters) && parameters.length > 0
       ? [" "].concat(formatReps(messageBodyConfig))
       : [];
 
     messageBody = [
@@ -132,45 +135,48 @@ function ConsoleApiCall(props) {
     stacktrace,
     attachment,
     serviceContainer,
     dispatch,
     indent,
     timeStamp,
     timestampsVisible,
     parameters,
+    maybeScrollToBottom,
   });
 }
 
 function formatReps(options = {}) {
   const {
     dispatch,
     loadedObjectProperties,
     loadedObjectEntries,
     messageId,
     parameters,
     serviceContainer,
     userProvidedStyles,
     type,
+    maybeScrollToBottom,
   } = options;
 
   return (
     parameters
       // Get all the grips.
       .map((grip, key) => GripMessageBody({
         dispatch,
         messageId,
         grip,
         key,
         userProvidedStyle: userProvidedStyles ? userProvidedStyles[key] : null,
         serviceContainer,
         useQuotes: false,
         loadedObjectProperties,
         loadedObjectEntries,
         type,
+        maybeScrollToBottom,
       }))
       // Interleave spaces.
       .reduce((arr, v, i) => {
         // We need to interleave a space if we are not on the last element AND
         // if we are not between 2 messages with user provided style.
         const needSpace = i + 1 < parameters.length && (
           !userProvidedStyles
           || userProvidedStyles[i] === undefined
--- a/devtools/client/webconsole/components/message-types/ConsoleCommand.js
+++ b/devtools/client/webconsole/components/message-types/ConsoleCommand.js
@@ -12,26 +12,28 @@ const PropTypes = require("devtools/clie
 const Message = createFactory(require("devtools/client/webconsole/components/Message"));
 
 ConsoleCommand.displayName = "ConsoleCommand";
 
 ConsoleCommand.propTypes = {
   message: PropTypes.object.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
   serviceContainer: PropTypes.object,
+  maybeScrollToBottom: PropTypes.func,
 };
 
 /**
  * Displays input from the console.
  */
 function ConsoleCommand(props) {
   const {
     message,
     timestampsVisible,
     serviceContainer,
+    maybeScrollToBottom,
   } = props;
 
   const {
     indent,
     source,
     type,
     level,
     messageText,
@@ -46,12 +48,13 @@ function ConsoleCommand(props) {
     type,
     level,
     topLevelClasses: [],
     messageBody,
     serviceContainer,
     indent,
     timeStamp,
     timestampsVisible,
+    maybeScrollToBottom,
   });
 }
 
 module.exports = ConsoleCommand;
--- a/devtools/client/webconsole/components/message-types/EvaluationResult.js
+++ b/devtools/client/webconsole/components/message-types/EvaluationResult.js
@@ -14,24 +14,26 @@ const GripMessageBody = require("devtool
 
 EvaluationResult.displayName = "EvaluationResult";
 
 EvaluationResult.propTypes = {
   dispatch: PropTypes.func.isRequired,
   message: PropTypes.object.isRequired,
   timestampsVisible: PropTypes.bool.isRequired,
   serviceContainer: PropTypes.object,
+  maybeScrollToBottom: PropTypes.func,
 };
 
 function EvaluationResult(props) {
   const {
     dispatch,
     message,
     serviceContainer,
     timestampsVisible,
+    maybeScrollToBottom,
   } = props;
 
   const {
     source,
     type,
     helperType,
     level,
     id: messageId,
@@ -58,16 +60,17 @@ function EvaluationResult(props) {
       dispatch,
       messageId,
       grip: parameters[0],
       serviceContainer,
       useQuotes: true,
       escapeWhitespace: false,
       type,
       helperType,
+      maybeScrollToBottom,
     });
   }
 
   const topLevelClasses = ["cm-s-mozilla"];
 
   return Message({
     source,
     type,
@@ -78,12 +81,13 @@ function EvaluationResult(props) {
     messageId,
     serviceContainer,
     exceptionDocURL,
     frame,
     timeStamp,
     parameters,
     notes,
     timestampsVisible,
+    maybeScrollToBottom,
   });
 }
 
 module.exports = EvaluationResult;
--- a/devtools/client/webconsole/components/message-types/PageError.js
+++ b/devtools/client/webconsole/components/message-types/PageError.js
@@ -13,31 +13,33 @@ const Message = createFactory(require("d
 
 PageError.displayName = "PageError";
 
 PageError.propTypes = {
   message: PropTypes.object.isRequired,
   open: PropTypes.bool,
   timestampsVisible: PropTypes.bool.isRequired,
   serviceContainer: PropTypes.object,
+  maybeScrollToBottom: PropTypes.func,
 };
 
 PageError.defaultProps = {
   open: false,
 };
 
 function PageError(props) {
   const {
     dispatch,
     message,
     open,
     repeat,
     serviceContainer,
     timestampsVisible,
     isPaused,
+    maybeScrollToBottom,
   } = props;
   const {
     id: messageId,
     executionPoint,
     indent,
     source,
     type,
     level,
@@ -72,12 +74,13 @@ function PageError(props) {
     repeat,
     frame,
     stacktrace,
     serviceContainer,
     exceptionDocURL,
     timeStamp,
     notes,
     timestampsVisible,
+    maybeScrollToBottom,
   });
 }
 
 module.exports = PageError;
--- a/devtools/client/webconsole/test/mochitest/browser_webconsole_scroll.js
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_scroll.js
@@ -3,71 +3,111 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const TEST_URI =
 `data:text/html;charset=utf-8,<p>Web Console test for  scroll.</p>
   <script>
-  for (let i = 0; i < 100; i++) {
-    console.log("init-" + i);
-  }
+    var a = () => b();
+    var b = () => c();
+    var c = () => console.trace("trace in C");
+
+    for (let i = 0; i < 100; i++) {
+      if (i % 10 === 0) {
+        c();
+      }
+      console.log("init-" + i);
+    }
   </script>
 `;
 add_task(async function() {
   const hud = await openNewTabAndConsole(TEST_URI);
   const {ui} = hud;
   const outputContainer = ui.outputNode.querySelector(".webconsole-output");
 
   info("Console should be scrolled to bottom on initial load from page logs");
   await waitFor(() => findMessage(hud, "init-99"));
   ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow");
   ok(isScrolledToBottom(outputContainer), "The console is scrolled to the bottom");
 
+  info("Wait until all stacktraces are rendered");
+  await waitFor(() => outputContainer.querySelectorAll(".frames").length === 10);
+  ok(isScrolledToBottom(outputContainer), "The console is scrolled to the bottom");
+
   await refreshTab();
 
   info("Console should be scrolled to bottom after refresh from page logs");
   await waitFor(() => findMessage(hud, "init-99"));
   ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow");
   ok(isScrolledToBottom(outputContainer), "The console is scrolled to the bottom");
 
+  info("Wait until all stacktraces are rendered");
+  await waitFor(() => outputContainer.querySelectorAll(".frames").length === 10);
+  ok(isScrolledToBottom(outputContainer), "The console is scrolled to the bottom");
+
   info("Scroll up");
   outputContainer.scrollTop = 0;
 
-  info("Add a message to check that the scroll isn't impacted");
-  let receievedMessages = waitForMessages({hud, messages: [{
-    text: "stay",
-  }]});
+  info("Add a console.trace message to check that the scroll isn't impacted");
+  let onMessage = waitForMessage(hud, "trace in C");
   ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
-    content.wrappedJSObject.console.log("stay");
+    content.wrappedJSObject.c();
   });
-  await receievedMessages;
+  let message = await onMessage;
   ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow");
   is(outputContainer.scrollTop, 0, "The console stayed scrolled to the top");
 
+  info("Wait until the stacktrace is rendered");
+  await waitFor(() => message.node.querySelector(".frame"));
+  is(outputContainer.scrollTop, 0, "The console stayed scrolled to the top");
+
   info("Evaluate a command to check that the console scrolls to the bottom");
-  receievedMessages = waitForMessages({hud, messages: [{
-    text: "42",
-  }]});
+  onMessage = waitForMessage(hud, "42");
   ui.jsterm.execute("21 + 21");
-  await receievedMessages;
+  await onMessage;
   ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow");
   ok(isScrolledToBottom(outputContainer), "The console is scrolled to the bottom");
 
   info("Add a message to check that the console do scroll since we're at the bottom");
-  receievedMessages = waitForMessages({hud, messages: [{
-    text: "scroll",
-  }]});
+  onMessage = waitForMessage(hud, "scroll");
   ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
     content.wrappedJSObject.console.log("scroll");
   });
-  await receievedMessages;
+  await onMessage;
   ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow");
   ok(isScrolledToBottom(outputContainer), "The console is scrolled to the bottom");
+
+  info("Evaluate an Error object to check that the console scrolls to the bottom");
+  onMessage = waitForMessage(hud, "myErrorObject", ".message.result");
+  ui.jsterm.execute(`
+    x = new Error("myErrorObject");
+    x.stack = "a@b/c.js:1:2\\nd@e/f.js:3:4";
+    x;`
+  );
+  message = await onMessage;
+  ok(isScrolledToBottom(outputContainer), "The console is scrolled to the bottom");
+
+  info("Wait until the stacktrace is rendered and check the console is scrolled");
+  await waitFor(() => message.node.querySelector(".objectBox-stackTrace .frame"));
+  ok(isScrolledToBottom(outputContainer), "The console is scrolled to the bottom");
+
+  info("Add a console.trace message to check that the console stays scrolled to bottom");
+  onMessage = waitForMessage(hud, "trace in C");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function() {
+    content.wrappedJSObject.c();
+  });
+  message = await onMessage;
+  ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow");
+  ok(isScrolledToBottom(outputContainer), "The console is scrolled to the bottom");
+
+  info("Wait until the stacktrace is rendered");
+  await waitFor(() => message.node.querySelector(".frame"));
+  ok(isScrolledToBottom(outputContainer), "The console is scrolled to the bottom");
 });
 
 function hasVerticalOverflow(container) {
   return container.scrollHeight > container.clientHeight;
 }
 
 function isScrolledToBottom(container) {
   if (!container.lastChild) {
--- a/devtools/client/webconsole/utils/object-inspector.js
+++ b/devtools/client/webconsole/utils/object-inspector.js
@@ -57,16 +57,17 @@ function getObjectInspector(grip, servic
       stacktrace,
       onViewSourceInDebugger: serviceContainer
         ? serviceContainer.onViewSourceInDebugger || serviceContainer.onViewSource
         : null,
       onViewSourceInScratchpad: serviceContainer
         ? serviceContainer.onViewSourceInScratchpad || serviceContainer.onViewSource
         : null,
       onViewSource: serviceContainer.onViewSource,
+      onReady: override.maybeScrollToBottom,
       sourceMapService: serviceContainer ? serviceContainer.sourceMapService : null,
     }),
   };
 
   if (!(typeof grip === "string" || (grip && grip.type === "longString"))) {
     Object.assign(objectInspectorProps, {
       onDOMNodeMouseOver,
       onDOMNodeMouseOut,