Bug 1567174 - Move expression evaluation to a proper Redux action. r=jdescottes.

The goal is to not directly use the panel.hud.ui.proxy reference
directly from the JsTerm, so we're in better shape when Fission
comes.
It's also a nice refactor to make the JSTerm component more
React-like.
As a nice benefit, we can handle telemetry and history persistence
from their middleware.
As the `requestEvaluation` method is removed from the JSTerm, some
callsites needed to be updated to still work.

Differential Revision: https://phabricator.services.mozilla.com/D39022

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Nicolas Chevobbe 2019-08-02 11:49:21 +00:00
Родитель 65526f541b
Коммит 734f57b508
16 изменённых файлов: 255 добавлений и 244 удалений

Просмотреть файл

@ -359,26 +359,27 @@ class MarkupContextMenu {
* temp variable on the content window. Also opens the split console and
* autofills it with the temp variable.
*/
_useInConsole() {
this.toolbox.openSplitConsole().then(() => {
const { hud } = this.toolbox.getPanel("webconsole");
async _useInConsole() {
await this.toolbox.openSplitConsole();
const { hud } = this.toolbox.getPanel("webconsole");
const evalString = `{ let i = 0;
while (window.hasOwnProperty("temp" + i) && i < 1000) {
i++;
}
window["temp" + i] = $0;
"temp" + i;
}`;
const evalString = `{ let i = 0;
while (window.hasOwnProperty("temp" + i) && i < 1000) {
i++;
}
window["temp" + i] = $0;
"temp" + i;
}`;
const options = {
selectedNodeActor: this.selection.nodeFront.actorID,
};
hud.jsterm.requestEvaluation(evalString, options).then(res => {
hud.setInputValue(res.result);
this.inspector.emit("console-var-ready");
});
});
const options = {
selectedNodeActor: this.selection.nodeFront.actorID,
};
const res = await hud.ui.webConsoleClient.evaluateJSAsync(
evalString,
options
);
hud.setInputValue(res.result);
this.inspector.emit("console-var-ready");
}
_buildA11YMenuItem(menu) {

Просмотреть файл

@ -9,6 +9,7 @@
const actionModules = [
require("./autocomplete"),
require("./filters"),
require("./input"),
require("./messages"),
require("./ui"),
require("./notifications"),

Просмотреть файл

@ -0,0 +1,182 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Utils: WebConsoleUtils } = require("devtools/client/webconsole/utils");
const { EVALUATE_EXPRESSION } = require("devtools/client/webconsole/constants");
loader.lazyServiceGetter(
this,
"clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper"
);
loader.lazyRequireGetter(
this,
"saveScreenshot",
"devtools/shared/screenshot/save"
);
loader.lazyRequireGetter(
this,
"messagesActions",
"devtools/client/webconsole/actions/messages"
);
loader.lazyRequireGetter(
this,
"historyActions",
"devtools/client/webconsole/actions/history"
);
loader.lazyRequireGetter(
this,
"ConsoleCommand",
"devtools/client/webconsole/types",
true
);
const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
function evaluateExpression(expression) {
return async ({ dispatch, services }) => {
if (!expression) {
expression = services.getInputValue();
}
if (!expression) {
return null;
}
// We use the messages action as it's doing additional transformation on the message.
dispatch(
messagesActions.messagesAdd([
new ConsoleCommand({
messageText: expression,
timeStamp: Date.now(),
}),
])
);
dispatch({
type: EVALUATE_EXPRESSION,
expression,
});
WebConsoleUtils.usageCount++;
let mappedExpressionRes;
try {
mappedExpressionRes = await services.getMappedExpression(expression);
} catch (e) {
console.warn("Error when calling getMappedExpression", e);
}
expression = mappedExpressionRes
? mappedExpressionRes.expression
: expression;
const { frameActor, client } = services.getFrameActor();
// Even if requestEvaluation rejects (because of webConsoleClient.evaluateJSAsync),
// we still need to pass the error response to onExpressionEvaluated.
const onSettled = res => res;
const response = await client
.evaluateJSAsync(expression, {
frameActor,
selectedNodeActor: services.getSelectedNodeActor(),
mapped: mappedExpressionRes ? mappedExpressionRes.mapped : null,
})
.then(onSettled, onSettled);
return onExpressionEvaluated(response, {
dispatch,
services,
});
};
}
/**
* The JavaScript evaluation response handler.
*
* @private
* @param {Object} response
* The message received from the server.
*/
async function onExpressionEvaluated(response, { dispatch, services } = {}) {
if (response.error) {
console.error(`Evaluation error`, response.error, ": ", response.message);
return;
}
// If the evaluation was a top-level await expression that was rejected, there will
// be an uncaught exception reported, so we don't need to do anything.
if (response.topLevelAwaitRejected === true) {
return;
}
if (!response.helperResult) {
dispatch(messagesActions.messagesAdd([response]));
return;
}
await handleHelperResult(response, { dispatch, services });
}
async function handleHelperResult(response, { dispatch, services }) {
const result = response.result;
const helperResult = response.helperResult;
const helperHasRawOutput = !!(helperResult || {}).rawOutput;
if (helperResult && helperResult.type) {
switch (helperResult.type) {
case "clearOutput":
dispatch(messagesActions.messagesClear());
break;
case "clearHistory":
dispatch(historyActions.clearHistory());
break;
case "inspectObject":
services.inspectObjectActor(helperResult.object);
break;
case "help":
services.openLink(HELP_URL);
break;
case "copyValueToClipboard":
clipboardHelper.copyString(helperResult.value);
break;
case "screenshotOutput":
const { args, value } = helperResult;
const screenshotMessages = await saveScreenshot(
services.getPanelWindow(),
args,
value
);
dispatch(
messagesActions.messagesAdd(
screenshotMessages.map(message => ({
message,
type: "logMessage",
}))
)
);
// early return as we already dispatched necessary messages.
return;
}
}
const hasErrorMessage =
response.exceptionMessage ||
(helperResult && helperResult.type === "error");
// Hide undefined results coming from helper functions.
const hasUndefinedResult =
result && typeof result == "object" && result.type == "undefined";
if (hasErrorMessage || helperHasRawOutput || !hasUndefinedResult) {
dispatch(messagesActions.messagesAdd([response]));
}
}
module.exports = {
evaluateExpression,
};

Просмотреть файл

@ -8,6 +8,7 @@ DevToolsModules(
'filters.js',
'history.js',
'index.js',
'input.js',
'messages.js',
'notifications.js',
'ui.js',

Просмотреть файл

@ -300,7 +300,6 @@ class App extends Component {
key: "reverse-search-input",
setInputValue: serviceContainer.setInputValue,
focusInput: serviceContainer.focusInput,
evaluateInput: serviceContainer.evaluateInput,
initialValue: reverseSearchInitialValue,
});
}

Просмотреть файл

@ -23,14 +23,14 @@ const {
class EditorToolbar extends Component {
static get propTypes() {
return {
webConsoleUI: PropTypes.object.isRequired,
editorMode: PropTypes.bool,
dispatch: PropTypes.func.isRequired,
webConsoleUI: PropTypes.object.isRequired,
};
}
render() {
const { editorMode, webConsoleUI, dispatch } = this.props;
const { editorMode, dispatch, webConsoleUI } = this.props;
if (!editorMode) {
return null;
@ -48,7 +48,7 @@ class EditorToolbar extends Component {
"webconsole.editor.toolbar.executeButton.tooltip",
[isMacOS ? "Cmd + Enter" : "Ctrl + Enter"]
),
onClick: () => webConsoleUI.jsterm.execute(),
onClick: () => dispatch(actions.evaluateExpression()),
},
l10n.getStr("webconsole.editor.toolbar.executeButton.label")
),

Просмотреть файл

@ -8,12 +8,6 @@ const { Utils: WebConsoleUtils } = require("devtools/client/webconsole/utils");
const Services = require("Services");
const { debounce } = require("devtools/shared/debounce");
loader.lazyServiceGetter(
this,
"clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1",
"nsIClipboardHelper"
);
loader.lazyRequireGetter(this, "Debugger", "Debugger");
loader.lazyRequireGetter(this, "EventEmitter", "devtools/shared/event-emitter");
loader.lazyRequireGetter(
@ -37,12 +31,6 @@ loader.lazyRequireGetter(
"Editor",
"devtools/client/shared/sourceeditor/editor"
);
loader.lazyRequireGetter(this, "Telemetry", "devtools/client/shared/telemetry");
loader.lazyRequireGetter(
this,
"saveScreenshot",
"devtools/shared/screenshot/save"
);
loader.lazyRequireGetter(
this,
"focusableSelector",
@ -50,10 +38,6 @@ loader.lazyRequireGetter(
true
);
const l10n = require("devtools/client/webconsole/utils/l10n");
const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
// React & Redux
const { Component } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
@ -67,8 +51,7 @@ const {
const {
getAutocompleteState,
} = require("devtools/client/webconsole/selectors/autocomplete");
const historyActions = require("devtools/client/webconsole/actions/history");
const autocompleteActions = require("devtools/client/webconsole/actions/autocomplete");
const actions = require("devtools/client/webconsole/actions/index");
// Constants used for defining the direction of JSTerm input history navigation.
const {
@ -100,6 +83,8 @@ class JSTerm extends Component {
// Handler for clipboard 'paste' event (also used for 'drop' event, callback).
onPaste: PropTypes.func,
codeMirrorEnabled: PropTypes.bool,
// Evaluate provided expression.
evaluateExpression: PropTypes.func.isRequired,
// Update position in the history after executing an expression (action).
updateHistoryPosition: PropTypes.func.isRequired,
// Update autocomplete popup state.
@ -142,8 +127,6 @@ class JSTerm extends Component {
this.inputNode = null;
this.completeNode = null;
this._telemetry = new Telemetry();
EventEmitter.decorate(this);
webConsoleUI.jsterm = this;
}
@ -596,204 +579,26 @@ class JSTerm extends Component {
}
}
/**
* The JavaScript evaluation response handler.
*
* @private
* @param {Object} response
* The message received from the server.
*/
/* eslint-disable complexity */
async _executeResultCallback(response) {
if (!this.webConsoleUI) {
return null;
}
if (response.error) {
console.error(
"Evaluation error " + response.error + ": " + response.message
);
return null;
}
// If the evaluation was a top-level await expression that was rejected, there will
// be an uncaught exception reported, so we don't want need to print anything here.
if (response.topLevelAwaitRejected === true) {
return null;
}
let errorMessage = response.exceptionMessage;
// Wrap thrown strings in Error objects, so `throw "foo"` outputs "Error: foo"
if (typeof response.exception === "string") {
errorMessage = new Error(errorMessage).toString();
}
const result = response.result;
const helperResult = response.helperResult;
const helperHasRawOutput = !!(helperResult || {}).rawOutput;
if (helperResult && helperResult.type) {
switch (helperResult.type) {
case "clearOutput":
this.webConsoleUI.clearOutput();
break;
case "clearHistory":
this.props.clearHistory();
break;
case "inspectObject":
this.webConsoleUI.inspectObjectActor(helperResult.object);
break;
case "error":
try {
errorMessage = l10n.getStr(helperResult.message);
} catch (ex) {
errorMessage = helperResult.message;
}
break;
case "help":
this.webConsoleUI.hud.openLink(HELP_URL);
break;
case "copyValueToClipboard":
clipboardHelper.copyString(helperResult.value);
break;
case "screenshotOutput":
const { args, value } = helperResult;
const results = await saveScreenshot(
this.webConsoleUI.window,
args,
value
);
this.screenshotNotify(results);
// early return as screenshot notify has dispatched all necessary messages
return null;
}
}
// Hide undefined results coming from JSTerm helper functions.
if (
!errorMessage &&
result &&
typeof result == "object" &&
result.type == "undefined" &&
helperResult &&
!helperHasRawOutput
) {
return null;
}
if (this.webConsoleUI.wrapper) {
return this.webConsoleUI.wrapper.dispatchMessageAdd(response, true);
}
return null;
}
/* eslint-enable complexity */
screenshotNotify(results) {
const wrappedResults = results.map(message => ({
message,
type: "logMessage",
}));
this.webConsoleUI.wrapper.dispatchMessagesAdd(wrappedResults);
}
/**
* Execute a string. Execution happens asynchronously in the content process.
*
* @param {String} executeString
* The string you want to execute. If this is not provided, the current
* user input is used - taken from |this._getValue()|.
* @returns {Promise}
* Resolves with the message once the result is displayed.
*/
async execute(executeString) {
execute(executeString) {
// attempt to execute the content of the inputNode
executeString = executeString || this._getValue();
if (!executeString) {
return null;
return;
}
// Append executed expression into the history list.
this.props.appendToHistory(executeString);
WebConsoleUtils.usageCount++;
if (!this.props.editorMode) {
this._setValue("");
}
this.clearCompletion();
let selectedNodeActor = null;
const inspectorSelection = this.webConsoleUI.hud.getInspectorSelection();
if (inspectorSelection && inspectorSelection.nodeFront) {
selectedNodeActor = inspectorSelection.nodeFront.actorID;
}
const { ConsoleCommand } = require("devtools/client/webconsole/types");
const cmdMessage = new ConsoleCommand({
messageText: executeString,
timeStamp: Date.now(),
});
this.webConsoleUI.proxy.dispatchMessageAdd(cmdMessage);
let mappedExpressionRes = null;
try {
mappedExpressionRes = await this.webConsoleUI.hud.getMappedExpression(
executeString
);
} catch (e) {
console.warn("Error when calling getMappedExpression", e);
}
executeString = mappedExpressionRes
? mappedExpressionRes.expression
: executeString;
const options = {
selectedNodeActor,
mapped: mappedExpressionRes ? mappedExpressionRes.mapped : null,
};
// Even if requestEvaluation rejects (because of webConsoleClient.evaluateJSAsync),
// we still need to pass the error response to executeResultCallback.
const onEvaluated = this.requestEvaluation(executeString, options).then(
res => res,
res => res
);
const response = await onEvaluated;
return this._executeResultCallback(response);
}
/**
* Request a JavaScript string evaluation from the server.
*
* @param string str
* String to execute.
* @param object [options]
* Options for evaluation:
* - selectedNodeActor: tells the NodeActor ID of the current selection
* in the Inspector, if such a selection exists. This is used by
* helper functions that can evaluate on the current selection.
* - mapped: basically getMappedExpression().mapped. An object that indicates
* which modifications were done to the input entered by the user.
* @return object
* A promise object that is resolved when the server response is
* received.
*/
requestEvaluation(str, options = {}) {
// Send telemetry event. If we are in the browser toolbox we send -1 as the
// toolbox session id.
this.props.serviceContainer.recordTelemetryEvent("execute_js", {
lines: str.split(/\n/).length,
});
const { frameActor, client } = this.props.serviceContainer.getFrameActor();
return client.evaluateJSAsync(str, {
frameActor,
...options,
});
this.props.evaluateExpression(executeString);
}
/**
@ -1800,13 +1605,15 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
appendToHistory: expr => dispatch(historyActions.appendToHistory(expr)),
clearHistory: () => dispatch(historyActions.clearHistory()),
appendToHistory: expr => dispatch(actions.appendToHistory(expr)),
clearHistory: () => dispatch(actions.clearHistory()),
updateHistoryPosition: (direction, expression) =>
dispatch(historyActions.updateHistoryPosition(direction, expression)),
dispatch(actions.updateHistoryPosition(direction, expression)),
autocompleteUpdate: (force, getterPath) =>
dispatch(autocompleteActions.autocompleteUpdate(force, getterPath)),
autocompleteClear: () => dispatch(autocompleteActions.autocompleteClear()),
dispatch(actions.autocompleteUpdate(force, getterPath)),
autocompleteClear: () => dispatch(actions.autocompleteClear()),
evaluateExpression: expression =>
dispatch(actions.evaluateExpression(expression)),
};
}

Просмотреть файл

@ -53,7 +53,6 @@ class ReverseSearchInput extends Component {
dispatch: PropTypes.func.isRequired,
setInputValue: PropTypes.func.isRequired,
focusInput: PropTypes.func.isRequired,
evaluateInput: PropTypes.func.isRequired,
reverseSearchResult: PropTypes.string,
reverseSearchTotalResults: PropTypes.number,
reverseSearchResultPosition: PropTypes.number,
@ -92,10 +91,10 @@ class ReverseSearchInput extends Component {
}
onEnterKeyboardShortcut(event) {
const { dispatch, evaluateInput } = this.props;
const { dispatch } = this.props;
event.stopPropagation();
dispatch(actions.reverseSearchInputToggle());
evaluateInput();
dispatch(actions.evaluateExpression());
}
onEscapeKeyboardShortcut(event) {

Просмотреть файл

@ -15,6 +15,7 @@ const actionTypes = {
BATCH_ACTIONS: "BATCH_ACTIONS",
CLEAR_HISTORY: "CLEAR_HISTORY",
EDITOR_TOGGLE: "EDITOR_TOGGLE",
EVALUATE_EXPRESSION: "EVALUATE_EXPRESSION",
FILTER_TEXT_SET: "FILTER_TEXT_SET",
FILTER_TOGGLE: "FILTER_TOGGLE",
FILTERS_CLEAR: "FILTERS_CLEAR",

Просмотреть файл

@ -8,6 +8,7 @@ const {
FILTER_TEXT_SET,
FILTER_TOGGLE,
DEFAULT_FILTERS_RESET,
EVALUATE_EXPRESSION,
MESSAGES_ADD,
PERSIST_TOGGLE,
} = require("devtools/client/webconsole/constants");
@ -50,6 +51,13 @@ function eventTelemetryMiddleware(telemetry, sessionId, store) {
session_id: sessionId,
}
);
} else if (action.type === EVALUATE_EXPRESSION) {
// Send telemetry event. If we are in the browser toolbox we send -1 as the
// toolbox session id.
telemetry.recordEvent("execute_js", "webconsole", null, {
lines: action.expression.split(/\n/).length,
session_id: sessionId,
});
}
return res;

Просмотреть файл

@ -7,6 +7,7 @@
const {
APPEND_TO_HISTORY,
CLEAR_HISTORY,
EVALUATE_EXPRESSION,
} = require("devtools/client/webconsole/constants");
const historyActions = require("devtools/client/webconsole/actions/history");
@ -35,7 +36,11 @@ function historyPersistenceMiddleware(store) {
return next => action => {
const res = next(action);
const triggerStoreActions = [APPEND_TO_HISTORY, CLEAR_HISTORY];
const triggerStoreActions = [
APPEND_TO_HISTORY,
CLEAR_HISTORY,
EVALUATE_EXPRESSION,
];
// Save the current history entries when modified, but wait till
// entries from the previous session are loaded.

Просмотреть файл

@ -8,6 +8,7 @@
const {
APPEND_TO_HISTORY,
CLEAR_HISTORY,
EVALUATE_EXPRESSION,
HISTORY_LOADED,
UPDATE_HISTORY_POSITION,
HISTORY_BACK,
@ -46,6 +47,7 @@ function getInitialState() {
function history(state = getInitialState(), action, prefsState) {
switch (action.type) {
case APPEND_TO_HISTORY:
case EVALUATE_EXPRESSION:
return appendToHistory(state, prefsState, action.expression);
case CLEAR_HISTORY:
return clearHistory(state);

Просмотреть файл

@ -20,7 +20,7 @@ const WebConsoleWrapper = require("devtools/client/webconsole/webconsole-wrapper
const { messagesAdd } = require("devtools/client/webconsole/actions/messages");
async function getWebConsoleWrapper() {
const hud = { target: { client: {} } };
const hud = { target: { client: {} }, getMappedExpression: () => {} };
const webConsoleUi = {
emit: () => {},
hud,
@ -30,6 +30,7 @@ async function getWebConsoleWrapper() {
ensureCSSErrorReportingEnabled: () => {},
},
},
inspectObjectActor: () => {},
};
const wcow = new WebConsoleWrapper(null, webConsoleUi, null, null);

Просмотреть файл

@ -109,8 +109,11 @@ async function storeAsVariable(hud, msg, type, varIdx, equalTo) {
is(getInputValue(hud), "temp" + varIdx, "Input was set");
const equal = await hud.jsterm.requestEvaluation(
"temp" + varIdx + " === " + equalTo
await executeAndWaitForMessage(
hud,
`temp${varIdx} === ${equalTo}`,
true,
".result"
);
is(equal.result, true, "Correct variable assigned into console.");
ok(true, "Correct variable assigned into console.");
}

Просмотреть файл

@ -164,10 +164,12 @@ function createContextMenu(
selectedObjectActor: actor,
};
webConsoleUI.jsterm.requestEvaluation(evalString, options).then(res => {
webConsoleUI.jsterm.focus();
webConsoleUI.hud.setInputValue(res.result);
});
webConsoleUI.webConsoleClient
.evaluateJSAsync(evalString, options)
.then(res => {
webConsoleUI.jsterm.focus();
webConsoleUI.hud.setInputValue(res.result);
});
},
})
);

Просмотреть файл

@ -204,10 +204,6 @@ class WebConsoleWrapper {
return webConsoleUI.jsterm && webConsoleUI.jsterm.focus();
},
evaluateInput: expression => {
return webConsoleUI.jsterm && webConsoleUI.jsterm.execute(expression);
},
requestEvaluation: (string, options) => {
return webConsoleUI.webConsoleClient.evaluateJSAsync(string, options);
},
@ -230,6 +226,9 @@ class WebConsoleWrapper {
}
return webConsoleUI.jsterm.completeNode;
},
getMappedExpression: this.hud.getMappedExpression.bind(this.hud),
getPanelWindow: () => webConsoleUI.window,
inspectObjectActor: webConsoleUI.inspectObjectActor.bind(webConsoleUI),
};
// Set `openContextMenu` this way so, `serviceContainer` variable