diff --git a/packages/playwright-test/src/reporters/base.ts b/packages/playwright-test/src/reporters/base.ts index 0b9844721d..cb234a2220 100644 --- a/packages/playwright-test/src/reporters/base.ts +++ b/packages/playwright-test/src/reporters/base.ts @@ -107,8 +107,12 @@ export class BaseReporter implements ReporterInternal { this.result = result; } + protected ttyWidth() { + return this._ttyWidthForTest || process.stdout.columns || 0; + } + protected fitToScreen(line: string, prefix?: string): string { - const ttyWidth = this._ttyWidthForTest || process.stdout.columns || 0; + const ttyWidth = this.ttyWidth(); if (!ttyWidth) { // Guard against the case where we cannot determine available width. return line; diff --git a/packages/playwright-test/src/reporters/list.ts b/packages/playwright-test/src/reporters/list.ts index 25c3ecb3a1..8f35b1a723 100644 --- a/packages/playwright-test/src/reporters/list.ts +++ b/packages/playwright-test/src/reporters/list.ts @@ -26,6 +26,7 @@ const NEGATIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? 'x' : '✘'; class ListReporter extends BaseReporter { private _lastRow = 0; + private _lastColumn = 0; private _testRows = new Map(); private _resultIndex = new Map(); private _needNewLine = false; @@ -93,9 +94,20 @@ class ListReporter extends BaseReporter { private _updateLineCountAndNewLineFlagForOutput(text: string) { this._needNewLine = text[text.length - 1] !== '\n'; - if (this._liveTerminal) { - const newLineCount = text.split('\n').length - 1; - this._lastRow += newLineCount; + const ttyWidth = this.ttyWidth(); + if (!this._liveTerminal || ttyWidth === 0) + return; + for (const ch of text) { + if (ch === '\n') { + this._lastColumn = 0; + ++this._lastRow; + continue; + } + ++this._lastColumn; + if (this._lastColumn > ttyWidth) { + this._lastColumn = 0; + ++this._lastRow; + } } } diff --git a/tests/playwright-test/reporter-list.spec.ts b/tests/playwright-test/reporter-list.spec.ts index 3e30ba2c9e..98f84b1ed2 100644 --- a/tests/playwright-test/reporter-list.spec.ts +++ b/tests/playwright-test/reporter-list.spec.ts @@ -86,6 +86,26 @@ test('render steps', async ({ runInlineTest }) => { ]); }); +test('very long console line should not mess terminal', async ({ runInlineTest }) => { + const TTY_WIDTH = 80; + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = pwt; + test('passes', async ({}) => { + console.log('a'.repeat(80) + 'b'.repeat(20)); + }); + `, + }, { reporter: 'list' }, { PWTEST_TTY_WIDTH: TTY_WIDTH + '' }); + + const renderedText = simpleAnsiRenderer(result.output, TTY_WIDTH); + if (process.platform === 'win32') + expect(renderedText).toContain(' ok 1 a.test.ts:6:7 › passes'); + else + expect(renderedText).toContain(' ✓ 1 a.test.ts:6:7 › passes'); + expect(renderedText).not.toContain(' 1 a.test.ts:6:7 › passes'); + expect(renderedText).toContain('a'.repeat(80) + '\n' + 'b'.repeat(20)); +}); + test('render retries', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': ` @@ -149,3 +169,63 @@ test('should truncate long test names', async ({ runInlineTest }) => { expect(lines[7]).toBe(` - 4 …› a.test.ts:13:12 › skipped very long name`); }); + +function simpleAnsiRenderer(text, ttyWidth) { + let lineNumber = 0; + let columnNumber = 0; + const screenLines = []; + const ensureScreenSize = () => { + if (lineNumber < 0) + throw new Error('Bad terminal navigation!'); + while (lineNumber >= screenLines.length) + screenLines.push(new Array(ttyWidth).fill('')); + }; + const print = ch => { + ensureScreenSize(); + if (ch === '\n') { + columnNumber = 0; + ++lineNumber; + } else { + screenLines[lineNumber][columnNumber++] = ch; + if (columnNumber === ttyWidth) { + columnNumber = 0; + ++lineNumber; + } + } + ensureScreenSize(); + }; + + let index = 0; + + const ansiCodes = [...text.matchAll(/\u001B\[(\d*)(.)/g)]; + for (const ansiCode of ansiCodes) { + const [matchText, codeValue, codeType] = ansiCode; + const code = (codeValue + codeType).toUpperCase(); + while (index < ansiCode.index) + print(text[index++]); + if (codeType.toUpperCase() === 'E') { + // Go X lines down + lineNumber += +codeValue; + ensureScreenSize(); + } else if (codeType.toUpperCase() === 'A') { + // Go X lines up + lineNumber -= +codeValue; + ensureScreenSize(); + } else if (code === '2K') { + // Erase full line + ensureScreenSize(); + screenLines[lineNumber] = new Array(ttyWidth).fill(''); + } else if (code === '0G') { + // Go to start + columnNumber = 0; + } else { + // Unsupported ANSI code (e.g. all colors). + } + index += matchText.length; + } + while (index < text.length) + print(text[index++]); + + return screenLines.map(line => line.join('')).join('\n'); +} +