From 61b11252b434639a9e603431ee038dec5c7ebe0a Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 13 Jun 2020 13:17:12 -0700 Subject: [PATCH] chore(debug): various debug mode improvements (#2561) --- src/browserContext.ts | 18 +-- src/debug/debugController.ts | 43 +++++++ src/debug/injected/debugScript.ts | 2 +- src/debug/injected/recorder.ts | 93 +++++++++++---- src/debug/recorderController.ts | 38 +++---- src/debug/recorderScript.ts | 170 --------------------------- src/debug/terminalOutput.ts | 183 ++++++++++++++++++++++++++++++ src/page.ts | 7 -- src/progress.ts | 12 +- src/utils/formatter.ts | 10 +- 10 files changed, 334 insertions(+), 242 deletions(-) create mode 100644 src/debug/debugController.ts delete mode 100644 src/debug/recorderScript.ts create mode 100644 src/debug/terminalOutput.ts diff --git a/src/browserContext.ts b/src/browserContext.ts index 88325f18d9..bc3f3642c8 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -27,6 +27,7 @@ import { BrowserBase } from './browser'; import { InnerLogger, Logger } from './logger'; import { EventEmitter } from 'events'; import { ProgressController } from './progress'; +import { DebugController } from './debug/debugController'; type CommonContextOptions = { viewport?: types.Size | null, @@ -98,21 +99,8 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser } async _initialize() { - if (!helper.isDebugMode()) - return; - - const installInFrame = async (frame: frames.Frame) => { - try { - const mainContext = await frame._mainContext(); - await mainContext.debugScript(); - } catch (e) { - } - }; - this.on(Events.BrowserContext.Page, (page: Page) => { - for (const frame of page.frames()) - installInFrame(frame); - page.on(Events.Page.FrameNavigated, installInFrame); - }); + if (helper.isDebugMode()) + new DebugController(this); } async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise { diff --git a/src/debug/debugController.ts b/src/debug/debugController.ts new file mode 100644 index 0000000000..5cdf1360b8 --- /dev/null +++ b/src/debug/debugController.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BrowserContextBase } from '../browserContext'; +import { Events } from '../events'; +import * as frames from '../frames'; +import { Page } from '../page'; +import { RecorderController } from './recorderController'; + +export class DebugController { + private _context: BrowserContextBase; + + constructor(context: BrowserContextBase) { + this._context = context; + const installInFrame = async (frame: frames.Frame) => { + try { + const mainContext = await frame._mainContext(); + await mainContext.debugScript(); + } catch (e) { + } + }; + + context.on(Events.BrowserContext.Page, (page: Page) => { + for (const frame of page.frames()) + installInFrame(frame); + page.on(Events.Page.FrameNavigated, installInFrame); + new RecorderController(page); + }); + } +} diff --git a/src/debug/injected/debugScript.ts b/src/debug/injected/debugScript.ts index 3a0979db08..a126f07d27 100644 --- a/src/debug/injected/debugScript.ts +++ b/src/debug/injected/debugScript.ts @@ -27,6 +27,6 @@ export default class DebugScript { initialize(injectedScript: InjectedScript) { this.consoleAPI = new ConsoleAPI(injectedScript); - this.recorder = new Recorder(); + this.recorder = new Recorder(injectedScript); } } diff --git a/src/debug/injected/recorder.ts b/src/debug/injected/recorder.ts index 09b97398aa..e5f738a44f 100644 --- a/src/debug/injected/recorder.ts +++ b/src/debug/injected/recorder.ts @@ -15,24 +15,28 @@ */ import type * as actions from '../recorderActions'; +import InjectedScript from '../../injected/injectedScript'; +import { parseSelector } from '../../common/selectorParser'; declare global { interface Window { - recordPlaywrightAction?: (action: actions.Action) => void; + recordPlaywrightAction: (action: actions.Action) => void; } } export class Recorder { - constructor() { + private _injectedScript: InjectedScript; + + constructor(injectedScript: InjectedScript) { + this._injectedScript = injectedScript; + document.addEventListener('click', event => this._onClick(event), true); document.addEventListener('input', event => this._onInput(event), true); document.addEventListener('keydown', event => this._onKeyDown(event), true); } private _onClick(event: MouseEvent) { - if (!window.recordPlaywrightAction) - return; - const selector = this._buildSelector(event.target as Node); + const selector = this._buildSelector(event.target as Element); if ((event.target as Element).nodeName === 'SELECT') return; window.recordPlaywrightAction({ @@ -46,9 +50,7 @@ export class Recorder { } private _onInput(event: Event) { - if (!window.recordPlaywrightAction) - return; - const selector = this._buildSelector(event.target as Node); + const selector = this._buildSelector(event.target as Element); if ((event.target as Element).nodeName === 'INPUT') { const inputElement = event.target as HTMLInputElement; if ((inputElement.type || '').toLowerCase() === 'checkbox') { @@ -78,11 +80,9 @@ export class Recorder { } private _onKeyDown(event: KeyboardEvent) { - if (!window.recordPlaywrightAction) - return; if (event.key !== 'Tab' && event.key !== 'Enter' && event.key !== 'Escape') return; - const selector = this._buildSelector(event.target as Node); + const selector = this._buildSelector(event.target as Element); window.recordPlaywrightAction({ name: 'press', selector, @@ -92,24 +92,73 @@ export class Recorder { }); } - private _buildSelector(node: Node): string { - const element = node as Element; - for (const attribute of ['data-testid', 'aria-label', 'id', 'data-test-id', 'data-test']) { + private _buildSelector(targetElement: Element): string { + const path: string[] = []; + const root = document.documentElement; + for (let element: Element | null = targetElement; element && element !== root; element = element.parentElement) { + const selector = this._buildSelectorCandidate(element); + if (selector) + path.unshift(selector.selector); + + const fullSelector = path.join(' '); + if (selector && selector.final) + return fullSelector; + if (targetElement === this._injectedScript.querySelector(parseSelector(fullSelector), root)) + return fullSelector; + } + return ''; + } + + private _buildSelectorCandidate(element: Element): { final: boolean, selector: string } | null { + for (const attribute of ['data-testid', 'data-test-id', 'data-test']) { if (element.hasAttribute(attribute)) - return `[${attribute}=${element.getAttribute(attribute)}]`; + return { final: true, selector: `${element.nodeName.toLocaleLowerCase()}[${attribute}=${element.getAttribute(attribute)}]` }; + } + for (const attribute of ['aria-label']) { + if (element.hasAttribute(attribute)) + return { final: false, selector: `${element.nodeName.toLocaleLowerCase()}[${attribute}=${element.getAttribute(attribute)}]` }; } if (element.nodeName === 'INPUT') { if (element.hasAttribute('name')) - return `[input name=${element.getAttribute('name')}]`; + return { final: false, selector: `input[name=${element.getAttribute('name')}]` }; if (element.hasAttribute('type')) - return `[input type=${element.getAttribute('type')}]`; + return { final: false, selector: `input[type=${element.getAttribute('type')}]` }; + } else if (element.nodeName === 'IMG') { + if (element.hasAttribute('alt')) + return { final: false, selector: `img[alt="${element.getAttribute('alt')}"]` }; } - if (element.firstChild && element.firstChild === element.lastChild && element.firstChild.nodeType === Node.TEXT_NODE) - return `text="${element.textContent}"`; - return ''; + const textSelector = textSelectorForElement(element); + if (textSelector) + return { final: false, selector: textSelector }; + + // Depreoritize id, but still use it as a last resort. + if (element.hasAttribute('id')) + return { final: true, selector: `${element.nodeName.toLocaleLowerCase()}[id=${element.getAttribute('id')}]` }; + + return null; } } +function textSelectorForElement(node: Node): string | null { + let needsTrim = false; + let onlyText: string | null = null; + for (const child of node.childNodes) { + if (child.nodeType !== Node.TEXT_NODE) + continue; + if (child.textContent && child.textContent.trim()) { + if (onlyText) + return null; + onlyText = child.textContent.trim(); + needsTrim = child.textContent !== child.textContent.trim(); + } else { + needsTrim = true; + } + } + if (!onlyText) + return null; + return needsTrim ? `text=/\\s*${escapeForRegex(onlyText)}\\s*/` : `text="${onlyText}"`; +} + function modifiersForEvent(event: MouseEvent | KeyboardEvent): number { return (event.altKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.metaKey ? 4 : 0) | (event.shiftKey ? 8 : 0); } @@ -122,3 +171,7 @@ function buttonForEvent(event: MouseEvent): 'left' | 'middle' | 'right' { } return 'left'; } + +function escapeForRegex(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/src/debug/recorderController.ts b/src/debug/recorderController.ts index 24eaf8ad14..088286eab0 100644 --- a/src/debug/recorderController.ts +++ b/src/debug/recorderController.ts @@ -18,42 +18,34 @@ import * as actions from './recorderActions'; import * as frames from '../frames'; import { Page } from '../page'; import { Events } from '../events'; -import { Script } from './recorderScript'; +import { TerminalOutput } from './terminalOutput'; export class RecorderController { private _page: Page; - private _script = new Script(); + private _output = new TerminalOutput(); constructor(page: Page) { this._page = page; - } - - start() { - this._script.addAction({ - name: 'navigate', - url: this._page.url(), - signals: [], - }); - this._printScript(); this._page.exposeBinding('recordPlaywrightAction', (source, action: actions.Action) => { - action.frameUrl = source.frame.url(); - this._script.addAction(action); - this._printScript(); + if (source.frame !== this._page.mainFrame()) + action.frameUrl = source.frame.url(); + this._output.addAction(action); }); this._page.on(Events.Page.FrameNavigated, (frame: frames.Frame) => { if (frame.parentFrame()) return; - const action = this._script.lastAction(); - if (action) - action.signals.push({ name: 'navigation', url: frame.url() }); - this._printScript(); + const action = this._output.lastAction(); + if (action) { + this._output.signal({ name: 'navigation', url: frame.url() }); + } else { + this._output.addAction({ + name: 'navigate', + url: this._page.url(), + signals: [], + }); + } }); } - - _printScript() { - console.log('\x1Bc'); // eslint-disable-line no-console - console.log(this._script.generate('chromium')); // eslint-disable-line no-console - } } diff --git a/src/debug/recorderScript.ts b/src/debug/recorderScript.ts deleted file mode 100644 index 1f8be9a061..0000000000 --- a/src/debug/recorderScript.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as dom from '../dom'; -import { Formatter, formatColors } from '../utils/formatter'; -import { Action, NavigationSignal, actionTitle } from './recorderActions'; - -export class Script { - private _actions: Action[] = []; - - addAction(action: Action) { - this._actions.push(action); - } - - lastAction(): Action | undefined { - return this._actions[this._actions.length - 1]; - } - - private _compact(): Action[] { - const result: Action[] = []; - let lastAction: Action | undefined; - for (const action of this._actions) { - if (lastAction && action.name === 'fill' && lastAction.name === 'fill') { - if (action.selector === lastAction.selector) - result.pop(); - } - if (lastAction && action.name === 'click' && lastAction.name === 'click') { - if (action.selector === lastAction.selector && action.clickCount > lastAction.clickCount) - result.pop(); - } - for (const name of ['check', 'uncheck']) { - if (lastAction && action.name === name && lastAction.name === 'click') { - if ((action as any).selector === (lastAction as any).selector) - result.pop(); - } - } - lastAction = action; - result.push(action); - } - return result; - } - - generate(browserType: string) { - const formatter = new Formatter(); - const { cst, cmt, fnc, kwd, prp, str } = formatColors; - - formatter.add(` - ${kwd('const')} { ${cst('chromium')}. ${cst('firefox')}, ${cst('webkit')} } = ${fnc('require')}(${str('playwright')}); - - (${kwd('async')}() => { - ${kwd('const')} ${cst('browser')} = ${kwd('await')} ${cst(`${browserType}`)}.${fnc('launch')}(); - ${kwd('const')} ${cst('page')} = ${kwd('await')} ${cst('browser')}.${fnc('newPage')}(); - `); - - for (const action of this._compact()) { - formatter.newLine(); - formatter.add(cmt(actionTitle(action))); - let navigationSignal: NavigationSignal | undefined; - if (action.name !== 'navigate' && action.signals && action.signals.length) - navigationSignal = action.signals[action.signals.length - 1]; - - if (navigationSignal) { - formatter.add(`${kwd('await')} ${cst('Promise')}.${fnc('all')}([ - ${cst('page')}.${fnc('waitForNavigation')}({ ${prp('url')}: ${str(navigationSignal.url)} }),`); - } - - const subject = action.frameUrl ? - `${cst('page')}.${fnc('frame')}(${formatObject({ url: action.frameUrl })})` : cst('page'); - - const prefix = navigationSignal ? '' : kwd('await') + ' '; - const suffix = navigationSignal ? '' : ';'; - switch (action.name) { - case 'click': { - let method = 'click'; - if (action.clickCount === 2) - method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: dom.ClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - const optionsString = formatOptions(options); - formatter.add(`${prefix}${subject}.${fnc(method)}(${str(action.selector)}${optionsString})${suffix}`); - break; - } - case 'check': - formatter.add(`${prefix}${subject}.${fnc('check')}(${str(action.selector)})${suffix}`); - break; - case 'uncheck': - formatter.add(`${prefix}${subject}.${fnc('uncheck')}(${str(action.selector)})${suffix}`); - break; - case 'fill': - formatter.add(`${prefix}${subject}.${fnc('fill')}(${str(action.selector)}, ${str(action.text)})${suffix}`); - break; - case 'press': { - const modifiers = toModifiers(action.modifiers); - const shortcut = [...modifiers, action.key].join('+'); - formatter.add(`${prefix}${subject}.${fnc('press')}(${str(action.selector)}, ${str(shortcut)})${suffix}`); - break; - } - case 'navigate': - formatter.add(`${prefix}${subject}.${fnc('goto')}(${str(action.url)})${suffix}`); - break; - case 'select': - formatter.add(`${prefix}${subject}.${fnc('select')}(${str(action.selector)}, ${formatObject(action.options.length > 1 ? action.options : action.options[0])})${suffix}`); - break; - } - if (navigationSignal) - formatter.add(`]);`); - } - formatter.add(` - })(); - `); - return formatter.format(); - } -} - -function formatOptions(value: any): string { - const keys = Object.keys(value); - if (!keys.length) - return ''; - return ', ' + formatObject(value); -} - -function formatObject(value: any): string { - const { prp, str } = formatColors; - if (typeof value === 'string') - return str(value); - if (Array.isArray(value)) - return `[${value.map(o => formatObject(o)).join(', ')}]`; - if (typeof value === 'object') { - const keys = Object.keys(value); - if (!keys.length) - return '{}'; - const tokens: string[] = []; - for (const key of keys) - tokens.push(`${prp(key)}: ${formatObject(value[key])}`); - return `{${tokens.join(', ')}}`; - } - return String(value); -} - -function toModifiers(modifiers: number): ('Alt' | 'Control' | 'Meta' | 'Shift')[] { - const result: ('Alt' | 'Control' | 'Meta' | 'Shift')[] = []; - if (modifiers & 1) - result.push('Alt'); - if (modifiers & 2) - result.push('Control'); - if (modifiers & 4) - result.push('Meta'); - if (modifiers & 8) - result.push('Shift'); - return result; -} diff --git a/src/debug/terminalOutput.ts b/src/debug/terminalOutput.ts new file mode 100644 index 0000000000..a2113374e7 --- /dev/null +++ b/src/debug/terminalOutput.ts @@ -0,0 +1,183 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as dom from '../dom'; +import { Formatter, formatColors } from '../utils/formatter'; +import { Action, NavigationSignal, actionTitle } from './recorderActions'; + +export class TerminalOutput { + private _lastAction: Action | undefined; + private _lastActionText: string | undefined; + + constructor() { + const formatter = new Formatter(); + const { cst, fnc, kwd, str } = formatColors; + + formatter.add(` + ${kwd('const')} { ${cst('chromium')}, ${cst('firefox')}, ${cst('webkit')} } = ${fnc('require')}(${str('playwright')}); + + (${kwd('async')}() => { + ${kwd('const')} ${cst('browser')} = ${kwd('await')} ${cst(`chromium`)}.${fnc('launch')}(); + ${kwd('const')} ${cst('page')} = ${kwd('await')} ${cst('browser')}.${fnc('newPage')}(); + `); + process.stdout.write(formatter.format()); + process.stdout.write(`\n})();`); + } + + addAction(action: Action) { + let eraseLastAction = false; + if (this._lastAction && action.name === 'fill' && this._lastAction.name === 'fill') { + if (action.selector === this._lastAction.selector) + eraseLastAction = true; + } + if (this._lastAction && action.name === 'click' && this._lastAction.name === 'click') { + if (action.selector === this._lastAction.selector && action.clickCount > this._lastAction.clickCount) + eraseLastAction = true; + } + for (const name of ['check', 'uncheck']) { + if (this._lastAction && action.name === name && this._lastAction.name === 'click') { + if ((action as any).selector === (this._lastAction as any).selector) + eraseLastAction = true; + } + } + this._printAction(action, eraseLastAction); + } + + _printAction(action: Action, eraseLastAction: boolean) { + let eraseLines = 1; + if (eraseLastAction && this._lastActionText) + eraseLines += this._lastActionText.split('\n').length; + for (let i = 0; i < eraseLines; ++i) + process.stdout.write('\u001B[F\u001B[2K'); + + this._lastAction = action; + this._lastActionText = this._generateAction(action); + console.log(this._lastActionText); // eslint-disable-line no-console + console.log(`})();`); // eslint-disable-line no-console + } + + lastAction(): Action | undefined { + return this._lastAction; + } + + signal(signal: NavigationSignal) { + if (this._lastAction) { + this._lastAction.signals.push(signal); + this._printAction(this._lastAction, true); + } + } + + private _generateAction(action: Action): string { + const formatter = new Formatter(2); + const { cst, cmt, fnc, kwd, prp, str } = formatColors; + formatter.newLine(); + formatter.add(cmt(actionTitle(action))); + let navigationSignal: NavigationSignal | undefined; + if (action.name !== 'navigate' && action.signals && action.signals.length) + navigationSignal = action.signals[action.signals.length - 1]; + + if (navigationSignal) { + formatter.add(`${kwd('await')} ${cst('Promise')}.${fnc('all')}([ + ${cst('page')}.${fnc('waitForNavigation')}({ ${prp('url')}: ${str(navigationSignal.url)} }),`); + } + + const subject = action.frameUrl ? + `${cst('page')}.${fnc('frame')}(${formatObject({ url: action.frameUrl })})` : cst('page'); + + const prefix = navigationSignal ? '' : kwd('await') + ' '; + const suffix = navigationSignal ? '' : ';'; + switch (action.name) { + case 'click': { + let method = 'click'; + if (action.clickCount === 2) + method = 'dblclick'; + const modifiers = toModifiers(action.modifiers); + const options: dom.ClickOptions = {}; + if (action.button !== 'left') + options.button = action.button; + if (modifiers.length) + options.modifiers = modifiers; + if (action.clickCount > 2) + options.clickCount = action.clickCount; + const optionsString = formatOptions(options); + formatter.add(`${prefix}${subject}.${fnc(method)}(${str(action.selector)}${optionsString})${suffix}`); + break; + } + case 'check': + formatter.add(`${prefix}${subject}.${fnc('check')}(${str(action.selector)})${suffix}`); + break; + case 'uncheck': + formatter.add(`${prefix}${subject}.${fnc('uncheck')}(${str(action.selector)})${suffix}`); + break; + case 'fill': + formatter.add(`${prefix}${subject}.${fnc('fill')}(${str(action.selector)}, ${str(action.text)})${suffix}`); + break; + case 'press': { + const modifiers = toModifiers(action.modifiers); + const shortcut = [...modifiers, action.key].join('+'); + formatter.add(`${prefix}${subject}.${fnc('press')}(${str(action.selector)}, ${str(shortcut)})${suffix}`); + break; + } + case 'navigate': + formatter.add(`${prefix}${subject}.${fnc('goto')}(${str(action.url)})${suffix}`); + break; + case 'select': + formatter.add(`${prefix}${subject}.${fnc('select')}(${str(action.selector)}, ${formatObject(action.options.length > 1 ? action.options : action.options[0])})${suffix}`); + break; + } + if (navigationSignal) + formatter.add(`]);`); + return formatter.format(); + } +} + +function formatOptions(value: any): string { + const keys = Object.keys(value); + if (!keys.length) + return ''; + return ', ' + formatObject(value); +} + +function formatObject(value: any): string { + const { prp, str } = formatColors; + if (typeof value === 'string') + return str(value); + if (Array.isArray(value)) + return `[${value.map(o => formatObject(o)).join(', ')}]`; + if (typeof value === 'object') { + const keys = Object.keys(value); + if (!keys.length) + return '{}'; + const tokens: string[] = []; + for (const key of keys) + tokens.push(`${prp(key)}: ${formatObject(value[key])}`); + return `{${tokens.join(', ')}}`; + } + return String(value); +} + +function toModifiers(modifiers: number): ('Alt' | 'Control' | 'Meta' | 'Shift')[] { + const result: ('Alt' | 'Control' | 'Meta' | 'Shift')[] = []; + if (modifiers & 1) + result.push('Alt'); + if (modifiers & 2) + result.push('Control'); + if (modifiers & 4) + result.push('Meta'); + if (modifiers & 8) + result.push('Shift'); + return result; +} diff --git a/src/page.ts b/src/page.ts index 48d5d6ce67..ead67a55f7 100644 --- a/src/page.ts +++ b/src/page.ts @@ -32,7 +32,6 @@ import { EventEmitter } from 'events'; import { FileChooser } from './fileChooser'; import { logError, InnerLogger } from './logger'; import { ProgressController } from './progress'; -import { RecorderController } from './debug/recorderController'; export interface PageDelegate { readonly rawMouse: input.RawMouse; @@ -504,12 +503,6 @@ export class Page extends EventEmitter { return this.mainFrame().uncheck(selector, options); } - async _startRecordingUser() { - if (!helper.isDebugMode()) - throw new Error('page._startRecordingUser is only available with PWDEBUG=1 environment variable'); - new RecorderController(this).start(); - } - async waitForTimeout(timeout: number) { await this.mainFrame().waitForTimeout(timeout); } diff --git a/src/progress.ts b/src/progress.ts index 655fb86756..67103cf7c7 100644 --- a/src/progress.ts +++ b/src/progress.ts @@ -29,6 +29,12 @@ export interface Progress { throwIfAborted(): void; } +let runningTaskCount = 0; + +export function isRunningTask(): boolean { + return !!runningTaskCount; +} + export async function runAbortableTask(task: (progress: Progress) => Promise, logger: InnerLogger, timeout: number, apiName?: string): Promise { const controller = new ProgressController(logger, timeout, apiName); return controller.run(task); @@ -70,6 +76,7 @@ export class ProgressController { async run(task: (progress: Progress) => Promise): Promise { assert(this._state === 'before'); this._state = 'running'; + ++runningTaskCount; const progress: Progress = { apiName: this._apiName, @@ -104,7 +111,6 @@ export class ProgressController { const result = await Promise.race([promise, this._forceAbortPromise]); clearTimeout(timer); this._state = 'finished'; - this._logRecording = []; this._logger.log(apiLog, `<= ${this._apiName} succeeded`); return result; } catch (e) { @@ -112,10 +118,12 @@ export class ProgressController { rewriteErrorMessage(e, e.message + formatLogRecording(this._logRecording, this._apiName) + kLoggingNote); clearTimeout(timer); this._state = 'aborted'; - this._logRecording = []; this._logger.log(apiLog, `<= ${this._apiName} failed`); await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup))); throw e; + } finally { + this._logRecording = []; + --runningTaskCount; } } diff --git a/src/utils/formatter.ts b/src/utils/formatter.ts index 99314d3fce..bacdb388a6 100644 --- a/src/utils/formatter.ts +++ b/src/utils/formatter.ts @@ -16,10 +16,12 @@ export class Formatter { private _baseIndent: string; + private _baseOffset: string; private _lines: string[] = []; - constructor(indent: number = 2) { - this._baseIndent = ' '.repeat(indent); + constructor(offset = 0) { + this._baseIndent = ' '.repeat(2); + this._baseOffset = ' '.repeat(offset); } prepend(text: string) { @@ -49,7 +51,7 @@ export class Formatter { line = spaces + extraSpaces + line; if (line.endsWith('{') || line.endsWith('[')) spaces += this._baseIndent; - return line; + return this._baseOffset + line; }).join('\n'); } } @@ -62,7 +64,7 @@ export const formatColors: { cst: StringFormatter; kwd: StringFormatter; fnc: St fnc: text => `\u001b[38;5;223m${text}\x1b[0m`, prp: text => `\u001b[38;5;159m${text}\x1b[0m`, str: text => `\u001b[38;5;130m${quote(text)}\x1b[0m`, - cmt: text => `// \u001b[38;5;23m${text}\x1b[0m` + cmt: text => `\u001b[38;5;23m// ${text}\x1b[0m` }; function quote(text: string, char: string = '\'') {