diff --git a/devtools/client/debugger/new/dist/parser-worker.js b/devtools/client/debugger/new/dist/parser-worker.js index 664d31268497..ce9cb35ebab0 100644 --- a/devtools/client/debugger/new/dist/parser-worker.js +++ b/devtools/client/debugger/new/dist/parser-worker.js @@ -25911,23 +25911,38 @@ var _mapAwaitExpression2 = _interopRequireDefault(_mapAwaitExpression); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function mapExpression(expression, mappings, bindings, shouldMapBindings = true, shouldMapAwait = true) { + const mapped = { + originalExpression: false, + bindings: false, + await: false, + }; + try { if (mappings) { + const beforeOriginalExpression = expression; expression = (0, _mapOriginalExpression2.default)(expression, mappings); + mapped.originalExpression = beforeOriginalExpression !== expression; } if (shouldMapBindings) { + const beforeBindings = expression; expression = (0, _mapBindings2.default)(expression, bindings); + mapped.bindings = beforeBindings !== expression; } if (shouldMapAwait) { + const beforeAwait = expression; expression = (0, _mapAwaitExpression2.default)(expression); + mapped.await = beforeAwait !== expression; } } catch (e) { console.log(e); } - return expression; + return { + expression, + mapped, + }; } /* 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 . */ @@ -46752,7 +46767,7 @@ function handleTopLevelAwait(expression) { const ast = hasTopLevelAwait(expression); if (ast) { const func = wrapExpression(ast); - return (0, _generator2.default)(_template2.default.ast(`(${func})().then(r => console.log(r));`)).code; + return (0, _generator2.default)(_template2.default.ast(`(${func})().then(console.log).catch(console.error)`)).code; } return expression; diff --git a/devtools/client/debugger/new/src/actions/expressions.js b/devtools/client/debugger/new/src/actions/expressions.js index 52ca43c3648f..04683cca3cff 100644 --- a/devtools/client/debugger/new/src/actions/expressions.js +++ b/devtools/client/debugger/new/src/actions/expressions.js @@ -191,7 +191,10 @@ function evaluateExpression(expression) { const selectedSource = (0, _selectors.getSelectedSource)(getState()); if (selectedSource && !(0, _devtoolsSourceMap.isGeneratedId)(sourceId) && !(0, _devtoolsSourceMap.isGeneratedId)(selectedSource.id)) { - input = await dispatch(getMappedExpression(input)); + const mapResult = await dispatch(getMappedExpression(input)); + if (mapResult !== null) { + input = mapResult.expression; + } } } @@ -226,7 +229,7 @@ function getMappedExpression(expression) { // 3. does not contain `=` - we do not need to map assignments if (!mappings && !expression.match(/(await|=)/)) { - return expression; + return null; } return parser.mapExpression(expression, mappings, bindings || [], _prefs.features.mapExpressionBindings, _prefs.features.mapAwaitExpression); diff --git a/devtools/client/debugger/new/src/actions/preview.js b/devtools/client/debugger/new/src/actions/preview.js index d53586828786..80102104bece 100644 --- a/devtools/client/debugger/new/src/actions/preview.js +++ b/devtools/client/debugger/new/src/actions/preview.js @@ -97,7 +97,11 @@ function setPreview(expression, location, tokenPos, cursorPos) { const selectedFrame = (0, _selectors.getSelectedFrame)(getState()); if (location && !(0, _devtoolsSourceMap.isGeneratedId)(sourceId)) { - expression = await dispatch((0, _expressions.getMappedExpression)(expression)); + const mapResult = + await dispatch((0, _expressions.getMappedExpression)(expression)); + if (mapResult !== null) { + expression = mapResult.expression; + } } if (!selectedFrame) { diff --git a/devtools/client/debugger/panel.js b/devtools/client/debugger/panel.js index 1614b167cafe..e53da9bb5941 100644 --- a/devtools/client/debugger/panel.js +++ b/devtools/client/debugger/panel.js @@ -134,7 +134,7 @@ DebuggerPanel.prototype = { getMappedExpression(expression) { // No-op implementation since this feature doesn't exist in the older // debugger implementation. - return expression; + return null; }, isPaused() { diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js index 5c8747aafb75..51bb7512434f 100644 --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -680,6 +680,21 @@ Toolbox.prototype = { return this._createSourceMapService(); }, + /** + * A common access point for the client-side parser service that any panel can use. + */ + get parserService() { + if (this._parserService) { + return this._parserService; + } + + this._parserService = + this.browserRequire("devtools/client/debugger/new/src/workers/parser/index"); + this._parserService + .start("resource://devtools/client/debugger/new/dist/parser-worker.js", this.win); + return this._parserService; + }, + /** * Clients wishing to use source maps but that want the toolbox to * track the source and style sheet actor mapping can use this @@ -2812,6 +2827,11 @@ Toolbox.prototype = { this._sourceMapService = null; } + if (this._parserService) { + this._parserService.stop(); + this._parserService = null; + } + if (this.webconsolePanel) { this._saveSplitConsoleHeight(); this.webconsolePanel.removeEventListener("resize", diff --git a/devtools/client/webconsole/components/JSTerm.js b/devtools/client/webconsole/components/JSTerm.js index f892c96d796a..cb0e224f6cd7 100644 --- a/devtools/client/webconsole/components/JSTerm.js +++ b/devtools/client/webconsole/components/JSTerm.js @@ -428,13 +428,26 @@ class JSTerm extends Component { * The JavaScript evaluation response handler. * * @private - * @param object response + * @param {Object} response * The message received from the server. + * @param {Object} options + * On options object that can contain the following properties: + * - {Object} mapped: An object indicating if the input was modified by the + * parser worker. */ - async _executeResultCallback(response) { + async _executeResultCallback(response, options = {}) { if (!this.hud) { return null; } + + // If the expression was a top-level await that the parser-worker transformed, we + // don't want to show the result returned by the server as it's a Promise that was + // created on our end by wrapping the input in an instantly called async function + // (e.g. `await 42` -> `(async () => {return await 42})()`). + if (options && options.mapped && options.mapped.await) { + return null; + } + if (response.error) { console.error("Evaluation error " + response.error + ": " + response.message); return null; @@ -546,18 +559,28 @@ class JSTerm extends Component { }); this.hud.proxy.dispatchMessageAdd(cmdMessage); + let mappedExpressionRes = null; + try { + mappedExpressionRes = await this.hud.owner.getMappedExpression(executeString); + } catch (e) { + console.warn("Error when calling getMappedExpression", e); + } + + executeString = mappedExpressionRes ? mappedExpressionRes.expression : executeString; + const options = { frame: this.SELECTED_FRAME, selectedNodeActor, }; - const mappedString = await this.hud.owner.getMappedExpression(executeString); // Even if requestEvaluation rejects (because of webConsoleClient.evaluateJSAsync), // we still need to pass the error response to executeResultCallback. - const onEvaluated = this.requestEvaluation(mappedString, options) + const onEvaluated = this.requestEvaluation(executeString, options) .then(res => res, res => res); const response = await onEvaluated; - return this._executeResultCallback(response); + return this._executeResultCallback(response, { + mapped: mappedExpressionRes ? mappedExpressionRes.mapped : null + }); } /** diff --git a/devtools/client/webconsole/test/mochitest/browser.ini b/devtools/client/webconsole/test/mochitest/browser.ini index a2112ec8d5b1..c4be8112a7c7 100644 --- a/devtools/client/webconsole/test/mochitest/browser.ini +++ b/devtools/client/webconsole/test/mochitest/browser.ini @@ -203,6 +203,8 @@ skip-if = verify [browser_jsterm_autocomplete_return_key.js] [browser_jsterm_autocomplete_width.js] [browser_jsterm_autocomplete-properties-with-non-alphanumeric-names.js] +[browser_jsterm_await_paused.js] +[browser_jsterm_await.js] [browser_jsterm_completion_case_sensitivity.js] [browser_jsterm_completion.js] [browser_jsterm_content_defined_helpers.js] diff --git a/devtools/client/webconsole/test/mochitest/browser_jsterm_await.js b/devtools/client/webconsole/test/mochitest/browser_jsterm_await.js new file mode 100644 index 000000000000..9e5690aad080 --- /dev/null +++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_await.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that top-level await expression work as expected. + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf-8,Web Console test top-level await"; + +add_task(async function() { + // Enable await mapping. + await pushPref("devtools.debugger.features.map-await-expression", true); + + // 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; + + info("Evaluate a top-level await expression"); + let onMessage = waitForMessage(hud, "await1"); + // We use "await" + 1 to not match the evaluated command message. + const simpleAwait = `await new Promise(r => setTimeout(() => r("await" + 1), 1000))`; + jsterm.execute(simpleAwait); + await onMessage; + // Check that the resulting promise of the async iife is not displayed. + let messages = hud.ui.outputNode.querySelectorAll(".message .message-body"); + let messagesText = Array.from(messages).map(n => n.textContent).join(" - "); + is(messagesText, `${simpleAwait} - await1`, + "The output contains the the expected messages"); + + info("Check that assigning the result of a top-level await expression works"); + onMessage = waitForMessage(hud, "await2"); + jsterm.execute(`x = await new Promise(r => setTimeout(() => r("await" + 2), 1000))`); + await onMessage; + + onMessage = waitForMessage(hud, `"-await2-"`); + jsterm.execute(`"-" + x + "-"`); + let message = await onMessage; + ok(message.node, "`x` was assigned as expected"); + + info("Check that awaiting for a rejecting promise displays an error"); + onMessage = waitForMessage(hud, "await-rej", ".message.error"); + jsterm.execute(`x = await new Promise((resolve,reject) => + setTimeout(() => reject("await-" + "rej"), 1000))`); + message = await onMessage; + ok(message.node, "awaiting for a rejecting promise displays an error message"); + + info("Check that concurrent await expression work fine"); + hud.ui.clearOutput(); + const delays = [2000, 500, 1000, 1500]; + const inputs = delays.map(delay => `await new Promise( + r => setTimeout(() => r("await-concurrent-" + ${delay}), ${delay}))`); + + // Let's wait for the message that sould be displayed last. + onMessage = waitForMessage(hud, "await-concurrent-2000"); + for (const input of inputs) { + jsterm.execute(input); + } + await onMessage; + + messages = hud.ui.outputNode.querySelectorAll(".message .message-body"); + messagesText = Array.from(messages).map(n => n.textContent); + const expectedMessages = [ + ...inputs, + "await-concurrent-500", + "await-concurrent-1000", + "await-concurrent-1500", + "await-concurrent-2000", + ]; + is(JSON.stringify(messagesText, null, 2), JSON.stringify(expectedMessages, null, 2), + "The output contains the the expected messages, in the expected order"); + + info("Check that a logged promise is still displayed as a promise"); + onMessage = waitForMessage(hud, "Promise {"); + jsterm.execute(`new Promise(r => setTimeout(() => r(1), 1000))`); + message = await onMessage; + ok(message, "Promise are displayed as expected"); +} diff --git a/devtools/client/webconsole/test/mochitest/browser_jsterm_await_paused.js b/devtools/client/webconsole/test/mochitest/browser_jsterm_await_paused.js new file mode 100644 index 000000000000..e4466e801ef7 --- /dev/null +++ b/devtools/client/webconsole/test/mochitest/browser_jsterm_await_paused.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that top-level await expression work as expected when debugger is paused. + +"use strict"; + +const TEST_URI = + `data:text/html;charset=utf-8,Web Console test top-level await when debugger paused`; + +add_task(async function() { + // Enable await mapping. + await pushPref("devtools.debugger.features.map-await-expression", true); + + // 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 pauseExpression = `(() => { + var foo = "bar"; + /* Will pause the script and open the debugger panel */ + debugger; + })()`; + jsterm.execute(pauseExpression); + + // wait for the debugger to be opened and paused. + const target = TargetFactory.forTab(gBrowser.selectedTab); + const toolbox = gDevTools.getToolbox(target); + const dbg = await waitFor(() => toolbox.getPanel("jsdebugger")); + await waitFor(() => dbg._selectors.isPaused(dbg._getState())); + await toolbox.toggleSplitConsole(); + + const onMessage = waitForMessage(hud, "res: bar"); + const awaitExpression = `await new Promise(res => { + setTimeout(() => res("res: " + foo), 1000); + })`; + await jsterm.execute(awaitExpression); + + // Click on the resume button to not be paused anymore. + dbg.panelWin.document.querySelector("button.resume").click(); + + await onMessage; + const messages = hud.ui.outputNode.querySelectorAll(".message .message-body"); + const messagesText = Array.from(messages).map(n => n.textContent); + const expectedMessages = [ + pauseExpression, + awaitExpression, + // The result of pauseExpression + "undefined", + "res: bar", + ]; + is(JSON.stringify(messagesText, null, 2), JSON.stringify(expectedMessages, null, 2), + "The output contains the the expected messages, in the expected order"); +} diff --git a/devtools/client/webconsole/webconsole.js b/devtools/client/webconsole/webconsole.js index 96232ad65597..4908804d79b5 100644 --- a/devtools/client/webconsole/webconsole.js +++ b/devtools/client/webconsole/webconsole.js @@ -226,18 +226,37 @@ WebConsole.prototype = { return panel.getFrames(); }, - async getMappedExpression(expression) { + /** + * Given an expression, returns an object containing a new expression, mapped by the + * parser worker to provide additional feature for the user (top-level await, + * original languages mapping, …). + * + * @param {String} expression: The input to maybe map. + * @returns {Object|null} + * Returns null if the input can't be mapped. + * If it can, returns an object containing the following: + * - {String} expression: The mapped expression + * - {Object} mapped: An object containing the different mapping that could + * be done and if they were applied on the input. + * At the moment, contains `await`, `bindings` and + * `originalExpression`. + */ + getMappedExpression(expression) { const toolbox = gDevTools.getToolbox(this.target); if (!toolbox) { - return expression; + return null; } + const panel = toolbox.getPanel("jsdebugger"); - - if (!panel) { - return expression; + if (panel) { + return panel.getMappedExpression(expression); } - return panel.getMappedExpression(expression); + if (toolbox.parserService && expression.includes("await ")) { + return toolbox.parserService.mapExpression(expression); + } + + return null; }, /**