Bug 1499289 - Allow to invoke getters from webconsole autocomplete function; r=bgrins,flod.
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Mon, 07 Jan 2019 17:53:55 +0000
changeset 452871 bc5767b55411524f9e137e954bc7f863ab0680c4
parent 452870 9b92ad8d977db7070f1f889df64fa166eb53f676
child 452872 f134e4af88f7ff8dc90160f1e2858767cfe8f751
push id35332
push userdvarga@mozilla.com
push dateTue, 08 Jan 2019 16:21:43 +0000
treeherdermozilla-central@cc4350821ea2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins, flod
bugs1499289
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 1499289 - Allow to invoke getters from webconsole autocomplete function; r=bgrins,flod. This patch introduces a new component, ConfirmDialog, that will be rendered on screen when the autocomplete service indicates that there's an unsafe getter in the completion path. The component is rendered in the toolbox document, like the autocomplete popup. In order to still write it in React, it uses a React portal, which allow to render an element outside of the React component tree. Tests are added to make sure the dialog works as expected. Differential Revision: https://phabricator.services.mozilla.com/D12943
devtools/client/locales/en-US/webconsole.properties
devtools/client/themes/tooltips.css
devtools/client/webconsole/actions/autocomplete.js
devtools/client/webconsole/components/App.js
devtools/client/webconsole/components/ConfirmDialog.js
devtools/client/webconsole/components/JSTerm.js
devtools/client/webconsole/components/moz.build
devtools/client/webconsole/reducers/autocomplete.js
devtools/client/webconsole/test/mochitest/browser.ini
devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_getters_cache.js
devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_getters_cancel.js
devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_getters_confirm.js
devtools/client/webconsole/test/mochitest/head.js
devtools/client/webconsole/webconsole-output-wrapper.js
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -320,8 +320,21 @@ webconsole.reverseSearch.noResult=No res
 # This string is displayed in the tooltip of the "previous result" button in the reverse search toolbar.
 # A keyboard shortcut will be shown inside the latter pair of brackets.
 webconsole.reverseSearch.result.previousButton.tooltip=Previous result (%S)
 
 # LOCALIZATION NOTE (webconsole.reverseSearch.result.nextButton.tooltip):
 # This string is displayed in the tooltip of the "next result" button in the reverse search toolbar.
 # A keyboard shortcut will be shown inside the latter pair of brackets.
 webconsole.reverseSearch.result.nextButton.tooltip=Next result (%S)
+
+# LOCALIZATION NOTE (webconsole.confirmDialog.getter.label)
+# Label used for the "invoke getter" confirm dialog that appears in the console when
+# a user tries to autocomplete a property with a getter.
+# Example: given the following object `x = {get y() {}}`, when the user types `x.y.`, it
+# would return "Invoke getter y to retrieve the property list?".
+# Parameters: %S is the name of the getter.
+webconsole.confirmDialog.getter.label=Invoke getter %S to retrieve the property list?
+
+# LOCALIZATION NOTE (webconsole.confirmDialog.getter.confirmButtonLabel)
+# Label used for the Confirm button in the "invoke getter" dialog that appears in the
+# console when a user tries to autocomplete a property with a getter.
+webconsole.confirmDialog.getter.confirmButtonLabel=Confirm
--- a/devtools/client/themes/tooltips.css
+++ b/devtools/client/themes/tooltips.css
@@ -650,8 +650,51 @@
 .onboarding-close-button {
   align-self: flex-start;
 }
 
 .onboarding-close-button::before {
   background-image: url("chrome://devtools/skin/images/close.svg");
   margin: -6px 0 0 -6px;
 }
+
+/* Tooltip: Invoke getter confirm Tooltip */
+
+.invoke-confirm {
+  font-family: var(--monospace-font-family);
+  color: var(--theme-popup-color);
+  border: 1px solid rgba(0,0,0, 0.1);
+  max-width: 400px;
+}
+
+.invoke-confirm .tooltip-panel {
+  display: flex;
+  flex-direction: column;
+}
+
+.invoke-confirm .confirm-label {
+  margin: 0;
+  padding: 8px;
+  flex-grow: 1;
+}
+
+.invoke-confirm .emphasized {
+  font-weight: bold;
+  overflow-wrap: break-word;
+}
+
+.invoke-confirm .confirm-button {
+  background-color: var(--theme-selection-background);
+  color: white;
+  border: none;
+  padding: 8px;
+  display: block;
+  width: 100%;
+  text-align: left;
+  font-family: var(--monospace-font-family);
+}
+
+/* The button already has a "selected" style, we can remove the focus rings. */
+.confirm-button:-moz-focusring,
+.confirm-button::-moz-focus-inner {
+  outline: none;
+  border: none;
+}
--- a/devtools/client/webconsole/actions/autocomplete.js
+++ b/devtools/client/webconsole/actions/autocomplete.js
@@ -11,18 +11,20 @@ const {
   AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
 } = require("devtools/client/webconsole/constants");
 
 /**
  * Update the data used for the autocomplete popup in the console input (JsTerm).
  *
  * @param {Boolean} force: True to force a call to the server (as opposed to retrieve
  *                         from the cache).
+ * @param {Array<String>} getterPath: Array representing the getter access (i.e.
+ *                                    `a.b.c.d.` is described as ['a', 'b', 'c', 'd'] ).
  */
-function autocompleteUpdate(force) {
+function autocompleteUpdate(force, getterPath) {
   return ({dispatch, getState, services}) => {
     if (services.inputHasSelection()) {
       return dispatch(autocompleteClear());
     }
 
     const inputValue = services.getInputValue();
     const frameActorId = services.getFrameActor();
     const cursor = services.getInputCursor();
@@ -43,20 +45,42 @@ function autocompleteUpdate(force) {
       input.startsWith(cache.input) &&
       /[a-zA-Z0-9]$/.test(input) &&
       frameActorId === cache.frameActorId;
 
     if (retrieveFromCache) {
       return dispatch(autoCompleteDataRetrieveFromCache(input));
     }
 
+    let authorizedEvaluations = (
+      Array.isArray(state.authorizedEvaluations) &&
+      state.authorizedEvaluations.length > 0
+    ) ? state.authorizedEvaluations : [];
+
+    if (Array.isArray(getterPath) && getterPath.length > 0) {
+      // We need to check for any previous authorizations. For example, here if getterPath
+      // is ["a", "b", "c", "d"], we want to see if there was any other path that was
+      // authorized in a previous request. For that, we only add the previous
+      // authorizations if the last auth is contained in getterPath. (for the example, we
+      // would keep if it is [["a", "b"]], not if [["b"]] nor [["f", "g"]])
+      const last = authorizedEvaluations[authorizedEvaluations.length - 1];
+      const concat = !last || last.every((x, index) => x === getterPath[index]);
+      if (concat) {
+        authorizedEvaluations.push(getterPath);
+      } else {
+        authorizedEvaluations = [getterPath];
+      }
+    }
+
     return dispatch(autocompleteDataFetch({
       input,
       frameActorId,
       client: services.getWebConsoleClient(),
+      authorizedEvaluations,
+      force,
     }));
   };
 }
 
 /**
  * Called when the autocompletion data should be cleared.
  */
 function autocompleteClear() {
@@ -84,52 +108,87 @@ function generateRequestId() {
 }
 
 /**
  * Action that fetch autocompletion data from the server.
  *
  * @param {Object} Object of the following shape:
  *        - {String} input: the expression that we want to complete.
  *        - {String} frameActorId: The id of the frame we want to autocomplete in.
+ *        - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space).
  *        - {WebConsoleClient} client: The webconsole client.
+ *        - {Array} authorizedEvaluations: Array of the properties access which can be
+ *                  executed by the engine.
+ *                   Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]]
+ *                  to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`.
  */
 function autocompleteDataFetch({
   input,
   frameActorId,
+  force,
   client,
+  authorizedEvaluations,
 }) {
   return ({dispatch, services}) => {
     const selectedNodeActor = services.getSelectedNodeActor();
     const id = generateRequestId();
     dispatch({type: AUTOCOMPLETE_PENDING_REQUEST, id});
-    client.autocomplete(input, undefined, frameActorId, selectedNodeActor).then(res => {
-      dispatch(autocompleteDataReceive(id, input, frameActorId, res));
+    client.autocomplete(
+      input,
+      undefined,
+      frameActorId,
+      selectedNodeActor,
+      authorizedEvaluations
+    ).then(data => {
+      dispatch(
+        autocompleteDataReceive({
+          id,
+          input,
+          force,
+          frameActorId,
+          data,
+          authorizedEvaluations,
+        }));
     }).catch(e => {
       console.error("failed autocomplete", e);
       dispatch(autocompleteClear());
     });
   };
 }
 
 /**
  * Called when we receive the autocompletion data from the server.
  *
- * @param {Integer} id: The autocompletion request id. This will be used in the reducer to
- *                      check that we update the state with the last request results.
- * @param {String} input: The expression that was evaluated to get the data.
- *        - {String} frameActorId: The id of the frame the evaluation was made in.
- * @param {Object} data: The actual data sent from the server.
+ * @param {Object} Object of the following shape:
+ *        - {Integer} id: The autocompletion request id. This will be used in the reducer
+ *                        to check that we update the state with the last request results.
+ *        - {String} input: the expression that we want to complete.
+ *        - {String} frameActorId: The id of the frame we want to autocomplete in.
+ *        - {Boolean} force: true if the user forced an autocompletion (with Ctrl+Space).
+ *        - {Object} data: The actual data returned from the server.
+ *        - {Array} authorizedEvaluations: Array of the properties access which can be
+ *                  executed by the engine.
+ *                   Example: [["x", "myGetter"], ["x", "myGetter", "y", "glitter"]]
+ *                  to retrieve properties of `x.myGetter.` and `x.myGetter.y.glitter`.
  */
-function autocompleteDataReceive(id, input, frameActorId, data) {
+function autocompleteDataReceive({
+  id,
+  input,
+  frameActorId,
+  force,
+  data,
+  authorizedEvaluations,
+}) {
   return {
     type: AUTOCOMPLETE_DATA_RECEIVE,
     id,
     input,
+    force,
     frameActorId,
     data,
+    authorizedEvaluations,
   };
 }
 
 module.exports = {
+  autocompleteClear,
   autocompleteUpdate,
-  autocompleteDataFetch,
-  autocompleteDataReceive,
 };
--- a/devtools/client/webconsole/components/App.js
+++ b/devtools/client/webconsole/components/App.js
@@ -10,16 +10,17 @@ const dom = require("devtools/client/sha
 const { connect } = require("devtools/client/shared/redux/visibility-handler-connect");
 
 const actions = require("devtools/client/webconsole/actions/index");
 const ConsoleOutput = createFactory(require("devtools/client/webconsole/components/ConsoleOutput"));
 const FilterBar = createFactory(require("devtools/client/webconsole/components/FilterBar"));
 const SideBar = createFactory(require("devtools/client/webconsole/components/SideBar"));
 const ReverseSearchInput = createFactory(require("devtools/client/webconsole/components/ReverseSearchInput"));
 const JSTerm = createFactory(require("devtools/client/webconsole/components/JSTerm"));
+const ConfirmDialog = createFactory(require("devtools/client/webconsole/components/ConfirmDialog"));
 const NotificationBox = createFactory(require("devtools/client/shared/components/NotificationBox").NotificationBox);
 
 const l10n = require("devtools/client/webconsole/webconsole-l10n");
 const { Utils: WebConsoleUtils } = require("devtools/client/webconsole/utils");
 
 const SELF_XSS_OK = l10n.getStr("selfxss.okstring");
 const SELF_XSS_MSG = l10n.getFormatStr("selfxss.msg", [SELF_XSS_OK]);
 
@@ -242,16 +243,21 @@ class App extends Component {
           }),
           ReverseSearchInput({
             hud,
           })
         ),
         SideBar({
           serviceContainer,
         }),
+        ConfirmDialog({
+          hud,
+          serviceContainer,
+          codeMirrorEnabled: jstermCodeMirror,
+        }),
       )
     );
   }
 }
 
 const mapStateToProps = state => ({
   notifications: getAllNotifications(state),
   reverseSearchInputVisible: state.ui.reverseSearchInputVisible,
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/components/ConfirmDialog.js
@@ -0,0 +1,150 @@
+/* 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";
+
+loader.lazyRequireGetter(this, "PropTypes", "devtools/client/shared/vendor/react-prop-types");
+loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
+loader.lazyRequireGetter(this, "HTMLTooltip", "devtools/client/shared/widgets/tooltip/HTMLTooltip", true);
+loader.lazyRequireGetter(this, "createPortal", "devtools/client/shared/vendor/react-dom", true);
+
+// React & Redux
+const { Component } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+const {getAutocompleteState} = require("devtools/client/webconsole/selectors/autocomplete");
+const autocompleteActions = require("devtools/client/webconsole/actions/autocomplete");
+const { l10n } = require("devtools/client/webconsole/utils/messages");
+
+class ConfirmDialog extends Component {
+  static get propTypes() {
+    return {
+      // Console object.
+      hud: PropTypes.object.isRequired,
+      // Update autocomplete popup state.
+      autocompleteUpdate: PropTypes.func.isRequired,
+      autocompleteClear: PropTypes.func.isRequired,
+      // Data to be displayed in the confirm dialog.
+      getterPath: PropTypes.array.isRequired,
+      serviceContainer: PropTypes.object.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    const { hud } = props;
+    hud.confirmDialog = this;
+
+    this.cancel = this.cancel.bind(this);
+    this.confirm = this.confirm.bind(this);
+  }
+
+  componentDidMount() {
+    const doc = this.props.hud.document;
+    const toolbox = gDevTools.getToolbox(this.props.hud.owner.target);
+    const tooltipDoc = toolbox ? toolbox.doc : doc;
+    // The popup will be attached to the toolbox document or HUD document in the case
+    // such as the browser console which doesn't have a toolbox.
+    this.tooltip = new HTMLTooltip(tooltipDoc, {
+      className: "invoke-confirm",
+    });
+  }
+
+  componentDidUpdate() {
+    const {getterPath, serviceContainer} = this.props;
+
+    if (getterPath) {
+      this.tooltip.show(serviceContainer.getJsTermTooltipAnchor(), {y: 5});
+      this.tooltip.focus();
+    } else {
+      this.tooltip.hide();
+      this.props.hud.jsterm.focus();
+    }
+  }
+
+  componentDidThrow(e) {
+    console.error("Error in ConfirmDialog", e);
+    this.setState(state => ({...state, hasError: true}));
+  }
+
+  cancel() {
+    this.tooltip.hide();
+    this.props.autocompleteClear();
+  }
+
+  confirm() {
+    this.tooltip.hide();
+    this.props.autocompleteUpdate(this.props.getterPath);
+  }
+
+  render() {
+    if (
+      (this.state && this.state.hasError) ||
+      (!this.props || !this.props.getterPath)
+    ) {
+      return null;
+    }
+
+    const {getterPath} = this.props;
+    const getterName = getterPath.join(".");
+
+    // We deliberately use getStr, and not getFormatStr, because we want getterName to
+    // be wrapped in its own span.
+    const description = l10n.getStr("webconsole.confirmDialog.getter.label");
+    const [descriptionPrefix, descriptionSuffix] = description.split("%S");
+
+    return createPortal([
+      dom.p({
+        className: "confirm-label",
+      },
+        dom.span({}, descriptionPrefix),
+        dom.span({className: "emphasized"}, getterName),
+        dom.span({}, descriptionSuffix)
+      ),
+      dom.button({
+        className: "confirm-button",
+        onBlur: () => this.cancel(),
+        onKeyDown: event => {
+          const {key} = event;
+          if (["Escape", "ArrowLeft", "Backspace"].includes(key)) {
+            this.cancel();
+            event.stopPropagation();
+            return;
+          }
+
+          if (["Tab", "Enter", " "].includes(key)) {
+            this.confirm();
+            event.stopPropagation();
+          }
+        },
+        // We can't use onClick because it would respond to Enter and Space keypress.
+        // We don't want that because we have a Ctrl+Space shortcut to force an
+        // autocomplete update; if the ConfirmDialog need to be displayed, since
+        // we automatically focus the button, the keyup on space would fire the onClick
+        // handler.
+        onMouseDown: this.confirm,
+      }, l10n.getStr("webconsole.confirmDialog.getter.confirmButtonLabel")),
+    ], this.tooltip.panel);
+  }
+}
+
+// Redux connect
+function mapStateToProps(state) {
+  const autocompleteData = getAutocompleteState(state);
+  return {
+    getterPath: autocompleteData.getterPath,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return {
+    autocompleteUpdate: getterPath =>
+      dispatch(autocompleteActions.autocompleteUpdate(true, getterPath)),
+    autocompleteClear: () => dispatch(autocompleteActions.autocompleteClear()),
+  };
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ConfirmDialog);
--- a/devtools/client/webconsole/components/JSTerm.js
+++ b/devtools/client/webconsole/components/JSTerm.js
@@ -1158,34 +1158,29 @@ class JSTerm extends Component {
     if (items.length >= minimumAutoCompleteLength
       || (items.length === 1 && items[0].preLabel !== matchProp)
       || (
         items.length === 1
         && !this.canDisplayAutoCompletionText()
         && items[0].label !== matchProp
       )
     ) {
-      let popupAlignElement;
+      const popupAlignElement = this.props.serviceContainer.getJsTermTooltipAnchor();
       let xOffset;
       let yOffset;
 
       if (this.editor) {
-        popupAlignElement = this.node.querySelector(".CodeMirror-cursor");
         // We need to show the popup at the "." or "[".
         xOffset = -1 * matchProp.length * this._inputCharWidth;
         yOffset = 5;
       } else if (this.inputNode) {
         const offset = inputUntilCursor.length -
           (inputUntilCursor.lastIndexOf("\n") + 1) -
           matchProp.length;
         xOffset = (offset * this._inputCharWidth) + this._paddingInlineStart;
-        // We use completeNode as the popup anchor as its height never exceeds the
-        // content size, whereas it can be the case for inputNode (when there's no message
-        // in the output, it takes the whole height).
-        popupAlignElement = this.completeNode;
       }
 
       if (popupAlignElement) {
         popup.openPopup(popupAlignElement, xOffset, yOffset, 0, {
           preventSelectCallback: true,
         });
       }
     } else if (items.length < minimumAutoCompleteLength && popup.isOpen) {
--- a/devtools/client/webconsole/components/moz.build
+++ b/devtools/client/webconsole/components/moz.build
@@ -5,16 +5,17 @@
 
 DIRS += [
     'message-types'
 ]
 
 DevToolsModules(
     'App.js',
     'CollapseButton.js',
+    'ConfirmDialog.js',
     'ConsoleOutput.js',
     'ConsoleTable.js',
     'FilterBar.js',
     'FilterButton.js',
     'FilterCheckbox.js',
     'GripMessageBody.js',
     'JSTerm.js',
     'Message.js',
--- a/devtools/client/webconsole/reducers/autocomplete.js
+++ b/devtools/client/webconsole/reducers/autocomplete.js
@@ -3,32 +3,40 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {
   AUTOCOMPLETE_CLEAR,
   AUTOCOMPLETE_DATA_RECEIVE,
   AUTOCOMPLETE_PENDING_REQUEST,
   AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
+  APPEND_TO_HISTORY,
+  UPDATE_HISTORY_POSITION,
+  REVERSE_SEARCH_INPUT_CHANGE,
+  REVERSE_SEARCH_BACK,
+  REVERSE_SEARCH_NEXT,
   WILL_NAVIGATE,
 } = require("devtools/client/webconsole/constants");
 
-function getDefaultState() {
+function getDefaultState(overrides = {}) {
   return Object.freeze({
     cache: null,
     matches: [],
     matchProp: null,
     isElementAccess: false,
     pendingRequestId: null,
+    isUnsafeGetter: false,
+    getterPath: null,
+    authorizedEvaluations: [],
+    ...overrides,
   });
 }
 
 function autocomplete(state = getDefaultState(), action) {
   switch (action.type) {
-    case AUTOCOMPLETE_CLEAR:
     case WILL_NAVIGATE:
       return getDefaultState();
     case AUTOCOMPLETE_RETRIEVE_FROM_CACHE:
       return autoCompleteRetrieveFromCache(state, action);
     case AUTOCOMPLETE_PENDING_REQUEST:
       return {
         ...state,
         cache: null,
@@ -38,26 +46,61 @@ function autocomplete(state = getDefault
       if (action.id !== state.pendingRequestId) {
         return state;
       }
 
       if (action.data.matches === null) {
         return getDefaultState();
       }
 
+      if (action.data.isUnsafeGetter) {
+        // We only want to display the getter confirm popup if the last char is a dot or
+        // an opening bracket, or if the user forced the autocompletion with Ctrl+Space.
+        if (action.input.endsWith(".") || action.input.endsWith("[") || action.force) {
+          return {
+            ...getDefaultState(),
+            isUnsafeGetter: true,
+            getterPath: action.data.getterPath,
+            authorizedEvaluations: action.authorizedEvaluations,
+          };
+        }
+
+        return {
+          ...state,
+          pendingRequestId: null,
+        };
+      }
+
       return {
         ...state,
+        authorizedEvaluations: action.authorizedEvaluations,
+        getterPath: null,
+        isUnsafeGetter: false,
+        pendingRequestId: null,
         cache: {
           input: action.input,
           frameActorId: action.frameActorId,
           ...action.data,
         },
-        pendingRequestId: null,
         ...action.data,
       };
+    // Reset the autocomplete data when:
+    // - clear is explicitely called
+    // - the user navigates the history
+    // - or an item was added to the history (i.e. something was evaluated).
+    case AUTOCOMPLETE_CLEAR:
+      return getDefaultState({
+        authorizedEvaluations: state.authorizedEvaluations,
+      });
+    case APPEND_TO_HISTORY:
+    case UPDATE_HISTORY_POSITION:
+    case REVERSE_SEARCH_INPUT_CHANGE:
+    case REVERSE_SEARCH_BACK:
+    case REVERSE_SEARCH_NEXT:
+      return getDefaultState();
   }
 
   return state;
 }
 
 /**
  * Retrieve from cache action reducer.
  *
@@ -96,15 +139,17 @@ function autoCompleteRetrieveFromCache(s
       return l.toLocaleLowerCase().startsWith(filterByLc);
     }
 
     return l.startsWith(filterBy);
   });
 
   return {
     ...state,
+    isUnsafeGetter: false,
+    getterPath: null,
     matches: newList,
     matchProp: filterBy,
     isElementAccess: cache.isElementAccess,
   };
 }
 
 exports.autocomplete = autocomplete;
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -190,16 +190,19 @@ skip-if = verify
 [browser_jsterm_autocomplete_arrow_keys.js]
 [browser_jsterm_autocomplete_await.js]
 [browser_jsterm_autocomplete_cached_results.js]
 [browser_jsterm_autocomplete_commands.js]
 [browser_jsterm_autocomplete_control_space.js]
 [browser_jsterm_autocomplete_crossdomain_iframe.js]
 [browser_jsterm_autocomplete_escape_key.js]
 [browser_jsterm_autocomplete_extraneous_closing_brackets.js]
+[browser_jsterm_autocomplete_getters_cache.js]
+[browser_jsterm_autocomplete_getters_cancel.js]
+[browser_jsterm_autocomplete_getters_confirm.js]
 [browser_jsterm_autocomplete_helpers.js]
 [browser_jsterm_autocomplete_in_chrome_tab.js]
 [browser_jsterm_autocomplete_in_debugger_stackframe.js]
 [browser_jsterm_autocomplete_inside_text.js]
 [browser_jsterm_autocomplete_native_getters.js]
 [browser_jsterm_autocomplete_nav_and_tab_key.js]
 [browser_jsterm_autocomplete_paste_undo.js]
 [browser_jsterm_autocomplete_return_key_no_selection.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_getters_cache.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the invoke getter authorizations are cleared when expected.
+
+const TEST_URI = `data:text/html;charset=utf-8,
+<head>
+  <script>
+    /* Create a prototype-less object so popup does not contain native
+     * Object prototype properties.
+     */
+    var obj = props => Object.create(null, Object.getOwnPropertyDescriptors(props));
+    window.foo = obj({
+      get bar() {
+        return obj({
+          get baz() {
+            return obj({
+              hello: 1,
+              world: "",
+            });
+          },
+          bloop: true,
+        })
+      }
+    });
+  </script>
+</head>
+<body>Autocomplete popup - invoke getter cache test</body>`;
+
+add_task(async function() {
+  // Run test with legacy JsTerm
+  await pushPref("devtools.webconsole.jsterm.codeMirror", false);
+  await performTests();
+  // And then run it with the CodeMirror-powered one.
+  await pushPref("devtools.webconsole.jsterm.codeMirror", true);
+  await performTests();
+});
+
+async function performTests() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+  const { jsterm } = hud;
+  const { autocompletePopup } = jsterm;
+  const target = await TargetFactory.forTab(gBrowser.selectedTab);
+  const toolbox = gDevTools.getToolbox(target);
+
+  let tooltip = await setInputValueForGetterConfirmDialog(toolbox, jsterm, "foo.bar.");
+  let labelEl = tooltip.querySelector(".confirm-label");
+  is(labelEl.textContent, "Invoke getter foo.bar to retrieve the property list?",
+    "Dialog has expected text content");
+
+  info("Check that hitting Enter does invoke the getter and return its properties");
+  let onPopUpOpen = autocompletePopup.once("popup-opened");
+  EventUtils.synthesizeKey("KEY_Enter");
+  await onPopUpOpen;
+  ok(autocompletePopup.isOpen, "popup is open after Enter");
+  is(getAutocompletePopupLabels(autocompletePopup).join("-"), "baz-bloop",
+    "popup has expected items");
+  checkJsTermValueAndCursor(jsterm, "foo.bar.|");
+  is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is now closed");
+
+  info("Close autocomplete popup");
+  let onPopupClose = autocompletePopup.once("popup-closed");
+  EventUtils.synthesizeKey("KEY_Escape");
+  await onPopupClose;
+
+  info("Ctrl+Space again to ensure the autocomplete is shown, not the confirm dialog");
+  onPopUpOpen = autocompletePopup.once("popup-opened");
+  EventUtils.synthesizeKey(" ", {ctrlKey: true});
+  await onPopUpOpen;
+  is(getAutocompletePopupLabels(autocompletePopup).join("-"), "baz-bloop",
+    "popup has expected items");
+  checkJsTermValueAndCursor(jsterm, "foo.bar.|");
+  is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is not open");
+
+  info("Type a space, then backspace and ensure the autocomplete popup is displayed");
+  let onAutocompleteUpdate = jsterm.once("autocomplete-updated");
+  EventUtils.synthesizeKey(" ");
+  await onAutocompleteUpdate;
+  is(autocompletePopup.isOpen, true, "Autocomplete popup is still opened");
+  is(getAutocompletePopupLabels(autocompletePopup).join("-"), "baz-bloop",
+    "popup has expected items");
+
+  onAutocompleteUpdate = jsterm.once("autocomplete-updated");
+  EventUtils.synthesizeKey("KEY_Backspace");
+  await onAutocompleteUpdate;
+  is(autocompletePopup.isOpen, true, "Autocomplete popup is still opened");
+  is(getAutocompletePopupLabels(autocompletePopup).join("-"), "baz-bloop",
+    "popup has expected items");
+
+  info("Reload the page to ensure asking for autocomplete again show the confirm dialog");
+  onPopupClose = autocompletePopup.once("popup-closed");
+  await refreshTab();
+  await onPopupClose;
+
+  EventUtils.synthesizeKey(" ", {ctrlKey: true});
+  await waitFor(() => isConfirmDialogOpened(toolbox));
+  ok(true, "Confirm Dialog is shown after tab navigation");
+  tooltip = getConfirmDialog(toolbox);
+  labelEl = tooltip.querySelector(".confirm-label");
+  is(labelEl.textContent, "Invoke getter foo.bar to retrieve the property list?",
+    "Dialog has expected text content");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_getters_cancel.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the confirm dialog can be closed with different actions.
+
+const TEST_URI = `data:text/html;charset=utf-8,
+<head>
+  <script>
+    window.foo = {
+      get rab() {
+        return {};
+      }
+    };
+  </script>
+</head>
+<body>Autocomplete popup - invoke getter - close dialog test</body>`;
+
+add_task(async function() {
+  // Run test with legacy JsTerm
+  await pushPref("devtools.webconsole.jsterm.codeMirror", false);
+  await performTests();
+  // And then run it with the CodeMirror-powered one.
+  await pushPref("devtools.webconsole.jsterm.codeMirror", true);
+  await performTests();
+});
+
+async function performTests() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+  const { jsterm } = hud;
+  const target = await TargetFactory.forTab(gBrowser.selectedTab);
+  const toolbox = gDevTools.getToolbox(target);
+
+  let tooltip = await setInputValueForGetterConfirmDialog(toolbox, jsterm, "foo.rab.");
+  let labelEl = tooltip.querySelector(".confirm-label");
+  is(labelEl.textContent, "Invoke getter foo.rab to retrieve the property list?",
+    "Dialog has expected text content");
+
+  info("Check that Escape closes the confirm tooltip");
+  let onConfirmTooltipClosed = waitFor(() => !isConfirmDialogOpened(toolbox));
+  EventUtils.synthesizeKey("KEY_Escape");
+  await onConfirmTooltipClosed;
+
+  info("Check that typing a letter won't show the tooltip");
+  const onAutocompleteUpdate = jsterm.once("autocomplete-updated");
+  EventUtils.sendString("t");
+  await onAutocompleteUpdate;
+  is(isConfirmDialogOpened(toolbox), false, "The confirm dialog is not open");
+
+  info("Check that Ctrl+space show the confirm tooltip again");
+  EventUtils.synthesizeKey(" ", {ctrlKey: true});
+  await waitFor(() => isConfirmDialogOpened(toolbox));
+  tooltip = getConfirmDialog(toolbox);
+  labelEl = tooltip.querySelector(".confirm-label");
+  is(labelEl.textContent, "Invoke getter foo.rab to retrieve the property list?",
+    "Dialog has expected text content");
+
+  info("Check that ArrowLeft closes the confirm tooltip");
+  onConfirmTooltipClosed = waitFor(() => !isConfirmDialogOpened(toolbox));
+  EventUtils.synthesizeKey("KEY_ArrowLeft");
+  await onConfirmTooltipClosed;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_autocomplete_getters_confirm.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that accessing properties with getters displays the confirm dialog to invoke them,
+// and then displays the autocomplete popup with the results.
+
+const TEST_URI = `data:text/html;charset=utf-8,
+<head>
+  <script>
+    /* Create a prototype-less object so popup does not contain native
+     * Object prototype properties.
+     */
+    var obj = props => Object.create(null, Object.getOwnPropertyDescriptors(props));
+    window.foo = obj({
+      get bar() {
+        return obj({
+          get baz() {
+            return obj({
+              hello: 1,
+              world: "",
+            });
+          },
+          bloop: true,
+        })
+      },
+      get rab() {
+        return "";
+      }
+    });
+  </script>
+</head>
+<body>Autocomplete popup - invoke getter usage test</body>`;
+
+add_task(async function() {
+  // Run test with legacy JsTerm
+  await pushPref("devtools.webconsole.jsterm.codeMirror", false);
+  await performTests();
+  // And then run it with the CodeMirror-powered one.
+  await pushPref("devtools.webconsole.jsterm.codeMirror", true);
+  await performTests();
+});
+
+async function performTests() {
+  const hud = await openNewTabAndConsole(TEST_URI);
+  const { jsterm } = hud;
+  const { autocompletePopup } = jsterm;
+  const target = await TargetFactory.forTab(gBrowser.selectedTab);
+  const toolbox = gDevTools.getToolbox(target);
+
+  let tooltip = await setInputValueForGetterConfirmDialog(toolbox, jsterm,
+    "window.foo.bar.");
+  let labelEl = tooltip.querySelector(".confirm-label");
+  is(labelEl.textContent, "Invoke getter window.foo.bar to retrieve the property list?",
+    "Dialog has expected text content");
+
+  info("Check that hitting Enter does invoke the getter and return its properties");
+  let onPopUpOpen = autocompletePopup.once("popup-opened");
+  EventUtils.synthesizeKey("KEY_Enter");
+  await onPopUpOpen;
+  ok(autocompletePopup.isOpen, "popup is open after Enter");
+  is(getAutocompletePopupLabels(autocompletePopup).join("-"), "baz-bloop",
+    "popup has expected items");
+  checkJsTermValueAndCursor(jsterm, "window.foo.bar.|");
+  is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is now closed");
+
+  let onPopUpClose = autocompletePopup.once("popup-closed");
+  EventUtils.synthesizeKey("KEY_Enter");
+  await onPopUpClose;
+  checkJsTermValueAndCursor(jsterm, "window.foo.bar.baz|");
+
+  info("Check that the invoke tooltip is displayed when performing an element access");
+  EventUtils.sendString("[");
+  await waitFor(() => isConfirmDialogOpened(toolbox));
+
+  tooltip = getConfirmDialog(toolbox);
+  labelEl = tooltip.querySelector(".confirm-label");
+  is(labelEl.textContent,
+    "Invoke getter window.foo.bar.baz to retrieve the property list?",
+    "Dialog has expected text content");
+
+  info("Check that hitting Tab does invoke the getter and return its properties");
+  onPopUpOpen = autocompletePopup.once("popup-opened");
+  EventUtils.synthesizeKey("KEY_Tab");
+  await onPopUpOpen;
+  ok(autocompletePopup.isOpen, "popup is open after Tab");
+  is(getAutocompletePopupLabels(autocompletePopup).join("-"), `"hello"-"world"`,
+    "popup has expected items");
+  checkJsTermValueAndCursor(jsterm, "window.foo.bar.baz[|");
+  is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is now closed");
+
+  onPopUpClose = autocompletePopup.once("popup-closed");
+  EventUtils.synthesizeKey("KEY_Tab");
+  await onPopUpClose;
+  checkJsTermValueAndCursor(jsterm, `window.foo.bar.baz["hello"]|`);
+
+  info("Check that autocompletion work on a getter result");
+  onPopUpOpen = autocompletePopup.once("popup-opened");
+  EventUtils.sendString(".");
+  await onPopUpOpen;
+  ok(autocompletePopup.isOpen, "got items of getter result");
+  ok(getAutocompletePopupLabels(autocompletePopup).includes("toExponential"),
+    "popup has expected items");
+
+  tooltip = await setInputValueForGetterConfirmDialog(toolbox, jsterm, "window.foo.rab.");
+  labelEl = tooltip.querySelector(".confirm-label");
+  is(labelEl.textContent, "Invoke getter window.foo.rab to retrieve the property list?",
+    "Dialog has expected text content");
+
+  info("Check clicking the confirm button invokes the getter and return its properties");
+  onPopUpOpen = autocompletePopup.once("popup-opened");
+  EventUtils.synthesizeMouseAtCenter(tooltip.querySelector(".confirm-button"), {
+    type: "mousedown",
+  }, toolbox.win);
+  await onPopUpOpen;
+  ok(autocompletePopup.isOpen, "popup is open after clicking on the confirm button");
+  ok(getAutocompletePopupLabels(autocompletePopup).includes("startsWith"),
+    "popup has expected items");
+  checkJsTermValueAndCursor(jsterm, "window.foo.rab.|");
+  is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is now closed");
+
+  info("Open the tooltip again");
+  tooltip = await setInputValueForGetterConfirmDialog(toolbox, jsterm, "window.foo.bar.");
+  labelEl = tooltip.querySelector(".confirm-label");
+  is(labelEl.textContent, "Invoke getter window.foo.bar to retrieve the property list?",
+    "Dialog has expected text content");
+
+  info("Check that Space invokes the getter and return its properties");
+  onPopUpOpen = autocompletePopup.once("popup-opened");
+  EventUtils.synthesizeKey(" ");
+  await onPopUpOpen;
+  ok(autocompletePopup.isOpen, "popup is open after space");
+  is(getAutocompletePopupLabels(autocompletePopup).join("-"), "baz-bloop",
+    "popup has expected items");
+  checkJsTermValueAndCursor(jsterm, "window.foo.bar.|");
+  is(isConfirmDialogOpened(toolbox), false, "confirm tooltip is now closed");
+}
--- a/devtools/client/webconsole/test/mochitest/head.js
+++ b/devtools/client/webconsole/test/mochitest/head.js
@@ -414,16 +414,32 @@ async function setInputValueForAutocompl
 
     if (jsterm.editor) {
       jsterm.editor.setCursor(jsterm.editor.getPosition(caretPosition));
     }
   }
 }
 
 /**
+ * Set the value of the JsTerm and wait for the confirm dialog to be displayed.
+ *
+ * @param {Toolbox} toolbox
+ * @param {JsTerm} jsterm
+ * @param {String} value : The value to set the jsterm to.
+ *                  Default to value.length (caret set at the end).
+ * @returns {Promise<HTMLElement>} resolves with dialog element when it is opened.
+ */
+async function setInputValueForGetterConfirmDialog(toolbox, jsterm, value) {
+  await setInputValueForAutocompletion(jsterm, value);
+  await waitFor(() => isConfirmDialogOpened(toolbox));
+  ok(true, "The confirm dialog is displayed");
+  return getConfirmDialog(toolbox);
+}
+
+/**
  * Checks if the jsterm has the expected completion value.
  *
  * @param {JsTerm} jsterm
  * @param {String} expectedValue
  * @param {String} assertionInfo: Description of the assertion passed to `is`.
  */
 function checkJsTermCompletionValue(jsterm, expectedValue, assertionInfo) {
   const completionValue = getJsTermCompletionValue(jsterm);
@@ -1071,8 +1087,44 @@ function findObjectInspectorNode(oi, nod
   return [...oi.querySelectorAll(".tree-node")].find(node => {
     const label = node.querySelector(".object-label");
     if (!label) {
       return false;
     }
     return label.textContent === nodeLabel;
   });
 }
+
+/**
+ * Return an array of the label of the autocomplete popup items.
+ *
+ * @param {AutocompletPopup} popup
+ * @returns {Array<String>}
+ */
+function getAutocompletePopupLabels(popup) {
+  return popup.getItems().map(item => item.label);
+}
+
+/**
+ * Return the "Confirm Dialog" element.
+ *
+ * @param toolbox
+ * @returns {HTMLElement|null}
+ */
+function getConfirmDialog(toolbox) {
+  const {doc} = toolbox;
+  return doc.querySelector(".invoke-confirm");
+}
+
+/**
+ * Returns true if the Confirm Dialog is opened.
+ * @param toolbox
+ * @returns {Boolean}
+ */
+function isConfirmDialogOpened(toolbox) {
+  const tooltip = getConfirmDialog(toolbox);
+  if (!tooltip) {
+    return false;
+  }
+
+  return tooltip.classList.contains("tooltip-visible");
+}
+
--- a/devtools/client/webconsole/webconsole-output-wrapper.js
+++ b/devtools/client/webconsole/webconsole-output-wrapper.js
@@ -151,16 +151,23 @@ WebConsoleOutputWrapper.prototype = {
 
         getSelectedNodeActor: () => {
           const inspectorSelection = this.owner.getInspectorSelection();
           if (inspectorSelection && inspectorSelection.nodeFront) {
             return inspectorSelection.nodeFront.actorID;
           }
           return null;
         },
+
+        getJsTermTooltipAnchor: () => {
+          if (jstermCodeMirror) {
+            return hud.jsterm.node.querySelector(".CodeMirror-cursor");
+          }
+          return hud.jsterm.completeNode;
+        },
       };
 
       // Set `openContextMenu` this way so, `serviceContainer` variable
       // is available in the current scope and we can pass it into
       // `createContextMenu` method.
       serviceContainer.openContextMenu = (e, message) => {
         const { screenX, screenY, target } = e;
 
@@ -298,24 +305,26 @@ WebConsoleOutputWrapper.prototype = {
       store = configureStore(this.hud, {
         // We may not have access to the toolbox (e.g. in the browser console).
         sessionId: this.toolbox && this.toolbox.sessionId || -1,
         telemetry: this.telemetry,
         services: serviceContainer,
       });
 
       const {prefs} = store.getState();
+      const jstermCodeMirror = prefs.jstermCodeMirror
+        && !Services.appinfo.accessibilityEnabled;
+
       const app = App({
         attachRefToHud,
         serviceContainer,
         hud,
         onFirstMeaningfulPaint: resolve,
         closeSplitConsole: this.closeSplitConsole.bind(this),
-        jstermCodeMirror: prefs.jstermCodeMirror
-          && !Services.appinfo.accessibilityEnabled,
+        jstermCodeMirror,
       });
 
       // Render the root Application component.
       if (this.parentNode) {
         const provider = createElement(Provider, { store }, app);
         this.body = ReactDOM.render(provider, this.parentNode);
       } else {
         // If there's no parentNode, we are in a test. So we can resolve immediately.