Commit b0290b07 authored by Erik Luo's avatar Erik Luo Committed by Commit Bot

DevTools: continuously update Console pins on interval

Part 2: live updating pins
- same JSAutocomplete, `await` processing, previews as
  ConsolePrompt
- 250ms interval to update pins
- prevent side-effects while editing text (editor has focus)

Screenshot: https://imgur.com/a/zaPlRTC

Bug: 849875
Change-Id: I0cd18101e58a6ff4e71e674633409ca0e015e1e7
Reviewed-on: https://chromium-review.googlesource.com/1096417
Commit-Queue: Erik Luo <luoe@chromium.org>
Reviewed-by: default avatarDmitry Gozman <dgozman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#577397}
parent 1958f5d0
Tests that console can pin expressions.
Running: testBeforeAdding
No pins
Running: testAddingExpression
Name: document.activeElement
Value: div#div1
Running: testPinUpdatesDynamically
Focusing the div2 on target page.
Name: document.activeElement
Value: div#div2
Running: testNoSideEffectsWhileEditing
Focusing the first pin's editor.
Setting text to: "window.flag = true".
Name: window.flag = true
Value: (...)
window.flag is now: false
Running: testRemoveSinglePin
Name: document.activeElement
Value: div#div2
Name: "Second pin"
Value: "Second pin"
Removing second pin
Name: document.activeElement
Value: div#div2
Running: testRemoveAllPins
No pins
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(async function() {
TestRunner.addResult(`Tests that console can pin expressions.\n`);
await TestRunner.loadModule('console_test_runner');
await TestRunner.showPanel('console');
await TestRunner.loadHTML(`
<div tabIndex="-1" id="div1">foo 1</div>
<div tabIndex="-1" id="div2">foo 2</div>
<script>
window.flag = false;
</script>
`);
await TestRunner.evaluateInPagePromise(`div1.focus()`);
const consoleView = Console.ConsoleView.instance();
const pinPane = consoleView._pinPane;
TestRunner.runTestSuite([
async function testBeforeAdding(next) {
await dumpPinPaneContents();
next();
},
async function testAddingExpression(next) {
pinPane.addPin(`document.activeElement`);
await waitForEditors();
await waitForPinUpdate();
await dumpPinPaneContents();
next();
},
async function testPinUpdatesDynamically(next) {
TestRunner.addResult(`Focusing the div2 on target page.`);
await TestRunner.evaluateInPagePromise(`div2.focus()`);
await waitForPinUpdate();
await dumpPinPaneContents();
next();
},
async function testNoSideEffectsWhileEditing(next) {
TestRunner.addResult(`Focusing the first pin's editor.`);
await pinAt(0).focus();
const sideEffectExpression = `window.flag = true`;
TestRunner.addResult(`Setting text to: "${sideEffectExpression}".`);
pinAt(0)._editor.setText(sideEffectExpression);
await waitForPinUpdate();
await dumpPinPaneContents();
const flagResult = await TestRunner.evaluateInPagePromise(`window.flag`);
TestRunner.addResult(`window.flag is now: ${flagResult}`);
pinAt(0)._editor.setText(`document.activeElement`);
next();
},
async function testRemoveSinglePin(next) {
pinPane.addPin(`"Second pin"`);
await waitForEditors();
await waitForPinUpdate();
await dumpPinPaneContents();
TestRunner.addResult(`\nRemoving second pin\n`);
pinPane._removePin(pinAt(1));
await dumpPinPaneContents();
next();
},
async function testRemoveAllPins(next) {
pinPane._removeAllPins();
await dumpPinPaneContents();
next();
}
]);
async function dumpPinPaneContents() {
if (!pinPane._pins.size) {
TestRunner.addResult(`No pins`);
return;
}
for (const pin of pinPane._pins)
TestRunner.addResult(`Name: ${pin._editor.text()}\nValue: ${pin._pinPreview.deepTextContent()}`);
}
async function waitForEditors() {
for (const pin of pinPane._pins)
await pin._editorPromise;
}
async function waitForPinUpdate(index) {
await TestRunner.addSnifferPromise(pinPane, '_updatedForTest');
}
function pinAt(index) {
return Array.from(pinPane._pins)[index];
}
})();
......@@ -2,10 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Console.ConsolePinPane = class extends UI.VBox {
Console.ConsolePinPane = class extends UI.ThrottledWidget {
constructor() {
super(true);
super(true, 250);
this.registerRequiredCSS('console/consolePinPane.css');
this.registerRequiredCSS('object_ui/objectValue.css');
this.contentElement.classList.add('console-pins', 'monospace');
this.contentElement.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false);
......@@ -25,6 +26,7 @@ Console.ConsolePinPane = class extends UI.VBox {
const targetPin = targetPinElement[Console.ConsolePin._PinSymbol];
contextMenu.editSection().appendItem(ls`Edit pin`, targetPin.focus.bind(targetPin));
contextMenu.editSection().appendItem(ls`Remove pin`, this._removePin.bind(this, targetPin));
targetPin.appendToContextMenu(contextMenu);
}
}
contextMenu.editSection().appendItem(ls`Remove all pins`, this._removeAllPins.bind(this));
......@@ -52,6 +54,22 @@ Console.ConsolePinPane = class extends UI.VBox {
this.contentElement.appendChild(pin.element());
this._pins.add(pin);
pin.focus();
this.update();
}
/**
* @override
*/
doUpdate() {
if (!this._pins.size)
return Promise.resolve();
if (this.isShowing())
this.update();
const updatePromises = Array.from(this._pins, pin => pin.updatePreview());
return Promise.all(updatePromises).then(this._updatedForTest.bind(this));
}
_updatedForTest() {
}
};
......@@ -68,13 +86,16 @@ Console.ConsolePin = class {
<div class='console-pin'>
${deletePinIcon}
<div class='console-pin-name' $='name'></div>
<div class='console-pin-preview'>${ls`not available`}</div>
<div class='console-pin-preview' $='preview'>${ls`not available`}</div>
</div>`;
this._pinElement = fragment.element();
this._pinPreview = fragment.$('preview');
const nameElement = fragment.$('name');
nameElement.title = expression;
this._pinElement[Console.ConsolePin._PinSymbol] = this;
/** @type {?SDK.RemoteObject} */
this._resultObject = null;
/** @type {?UI.TextEditor} */
this._editor = null;
......@@ -86,6 +107,7 @@ Console.ConsolePin = class {
autoHeight: true,
placeholder: ls`Expression`
});
this._editor.configureAutocomplete(ObjectUI.JavaScriptAutocompleteConfig.createConfigForEditor(this._editor));
this._editor.widget().show(nameElement);
this._editor.widget().element.classList.add('console-pin-editor');
this._editor.widget().element.tabIndex = -1;
......@@ -109,6 +131,35 @@ Console.ConsolePin = class {
this._editor.widget().focus();
this._editor.setSelection(TextUtils.TextRange.createFromLocation(Infinity, Infinity));
}
/**
* @param {!UI.ContextMenu} contextMenu
*/
appendToContextMenu(contextMenu) {
if (this._resultObject)
contextMenu.appendApplicableItems(this._resultObject);
}
/**
* @return {!Promise}
*/
async updatePreview() {
if (!this._editor)
return;
const text = this._editor.textWithCurrentSuggestion().trim();
const isEditing = this._pinElement.hasFocus();
const timeout = isEditing ? 250 : undefined;
const {preview, result} = await ObjectUI.JavaScriptREPL.evaluateAndBuildPreview(text, isEditing, timeout);
this._resultObject = result ? (result.object || null) : null;
const previewText = preview.deepTextContent();
if (!previewText || previewText !== this._pinPreview.deepTextContent()) {
this._pinPreview.removeChildren();
if (result && SDK.RuntimeModel.isSideEffectFailure(result))
this._pinPreview.appendChild(createTextNode(`(...)`));
else
this._pinPreview.appendChild(previewText ? preview : createTextNode(ls`not available`));
}
}
};
Console.ConsolePin._PinSymbol = Symbol('pinSymbol');
......@@ -56,7 +56,7 @@ Console.ConsolePrompt = class extends UI.Widget {
this._editor.addEventListener(UI.TextEditor.Events.SuggestionChanged, this._onTextChanged, this);
if (pinsEnabled) {
const pinButton = this.element.createChild('span', 'command-pin-button');
pinButton.title = ls`Pin expression`;
pinButton.title = ls`Pin expression and continuously evaluate`;
pinButton.addEventListener('click', () => {
this.dispatchEventToListeners(Console.ConsolePrompt.Events.ExpressionPinned, this.text());
});
......
......@@ -143,6 +143,8 @@ Main.Main = class {
Runtime.experiments.enableForTest('networkSearch');
if (testPath.indexOf('console/viewport-testing/') !== -1)
Runtime.experiments.enableForTest('consoleBelowPrompt');
if (testPath.indexOf('console/') !== -1)
Runtime.experiments.enableForTest('pinnedExpressions');
}
Runtime.experiments.setDefaultExperiments(
......
......@@ -53,6 +53,16 @@ SDK.RuntimeModel = class extends SDK.SDKModel {
Common.moduleSetting('customFormatters').addChangeListener(this._customFormattersStateChanged.bind(this));
}
/**
* @param {!SDK.RuntimeModel.EvaluationResult} response
*/
static isSideEffectFailure(response) {
const exceptionDetails = !response[Protocol.Error] && response.exceptionDetails;
return !!(
exceptionDetails && exceptionDetails.exception &&
exceptionDetails.exception.description.startsWith('EvalError: Possible side-effect in debug-evaluate'));
}
/**
* @return {!SDK.DebuggerModel}
*/
......@@ -469,12 +479,8 @@ SDK.RuntimeModel = class extends SDK.SDKModel {
const response = await this._agent.invoke_evaluate(
{expression: SDK.RuntimeModel._sideEffectTestExpression, contextId: testContext.id, throwOnSideEffect: true});
const exceptionDetails = !response[Protocol.Error] && response.exceptionDetails;
const supports =
!!(exceptionDetails && exceptionDetails.exception &&
exceptionDetails.exception.description.startsWith('EvalError: Possible side-effect in debug-evaluate'));
this._hasSideEffectSupport = supports;
return supports;
this._hasSideEffectSupport = SDK.RuntimeModel.isSideEffectFailure(response);
return this._hasSideEffectSupport;
}
/**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment