diff --git a/packages/trace-viewer/src/ui/consoleTab.css b/packages/trace-viewer/src/ui/consoleTab.css index 3c31395001..391b181bee 100644 --- a/packages/trace-viewer/src/ui/consoleTab.css +++ b/packages/trace-viewer/src/ui/consoleTab.css @@ -83,3 +83,16 @@ .console-line .codicon.status-warning::after { background-color: var(--vscode-list-warningForeground); } + +.console-repeat { + display: inline-block; + padding: 0 2px; + font-size: 12px; + line-height: 18px; + border-radius: 6px; + background-color: #8c959f; + color: white; + margin-right: 10px; + flex: none; + font-weight: 600; +} diff --git a/packages/trace-viewer/src/ui/consoleTab.tsx b/packages/trace-viewer/src/ui/consoleTab.tsx index cdc3cb7e93..077fc9768a 100644 --- a/packages/trace-viewer/src/ui/consoleTab.tsx +++ b/packages/trace-viewer/src/ui/consoleTab.tsx @@ -27,6 +27,7 @@ import { PlaceholderPanel } from './placeholderPanel'; export type ConsoleEntry = { browserMessage?: { body: JSX.Element[]; + bodyString: string; location: string; }, browserError?: channels.SerializedError; @@ -36,6 +37,7 @@ export type ConsoleEntry = { isError: boolean; isWarning: boolean; timestamp: number; + repeat: number; }; type ConsoleTabModel = { @@ -50,16 +52,38 @@ export function useConsoleTabModel(model: modelUtil.MultiTraceModel | undefined, if (!model) return { entries: [] }; const entries: ConsoleEntry[] = []; - for (const event of model.events) { + function addEntry(entry: Omit) { + const lastEntry = entries[entries.length - 1]; + const isSameAsLast = + lastEntry + && entry.browserMessage?.bodyString === lastEntry.browserMessage?.bodyString + && entry.browserMessage?.location === lastEntry.browserMessage?.location + && entry.browserError === lastEntry.browserError + && entry.nodeMessage?.html === lastEntry.nodeMessage?.html + && entry.isError === lastEntry.isError + && entry.isWarning === lastEntry.isWarning + && entry.timestamp - lastEntry.timestamp < 1000; + if (isSameAsLast) + lastEntry.repeat++; + else + entries.push({ ...entry, repeat: 1 }); + } + const logEvents = [...model.events, ...model.stdio].sort((a, b) => { + const aTimestamp = 'time' in a ? a.time : a.timestamp; + const bTimestamp = 'time' in b ? b.time : b.timestamp; + return aTimestamp - bTimestamp; + }) + for (const event of logEvents) { if (event.type === 'console') { const body = event.args && event.args.length ? format(event.args) : formatAnsi(event.text); const url = event.location.url; const filename = url ? url.substring(url.lastIndexOf('/') + 1) : ''; const location = `${filename}:${event.location.lineNumber}`; - entries.push({ + addEntry({ browserMessage: { body, + bodyString: event.text, location, }, isError: event.messageType === 'error', @@ -68,29 +92,28 @@ export function useConsoleTabModel(model: modelUtil.MultiTraceModel | undefined, }); } if (event.type === 'event' && event.method === 'pageError') { - entries.push({ + addEntry({ browserError: event.params.error, isError: true, isWarning: false, timestamp: event.time, }); } - } - for (const event of model.stdio) { - let html = ''; - if (event.text) - html = ansi2html(event.text.trim()) || ''; - if (event.base64) - html = ansi2html(atob(event.base64).trim()) || ''; + if (event.type === 'stderr' || event.type === 'stdout') { + let html = ''; + if (event.text) + html = ansi2html(event.text.trim()) || ''; + if (event.base64) + html = ansi2html(atob(event.base64).trim()) || ''; - entries.push({ - nodeMessage: { html }, - isError: event.type === 'stderr', - isWarning: false, - timestamp: event.timestamp, - }); + addEntry({ + nodeMessage: { html }, + isError: event.type === 'stderr', + isWarning: false, + timestamp: event.timestamp, + }); + } } - entries.sort((a, b) => a.timestamp - b.timestamp); return { entries }; }, [model]); @@ -154,6 +177,7 @@ export const ConsoleTab: React.FunctionComponent<{ {timestampElement} {statusElement} {locationText && {locationText}} + {entry.repeat > 1 && {entry.repeat}} {messageBody && {messageBody}} {messageInnerHTML && } {messageStack &&
{messageStack}
} diff --git a/tests/playwright-test/ui-mode-test-output.spec.ts b/tests/playwright-test/ui-mode-test-output.spec.ts index 46cdc2e478..ccec39df74 100644 --- a/tests/playwright-test/ui-mode-test-output.spec.ts +++ b/tests/playwright-test/ui-mode-test-output.spec.ts @@ -114,6 +114,47 @@ test('should show console messages for test', async ({ runUITest }, testInfo) => await expect.soft(page.getByText('GREEN', { exact: true })).toHaveCSS('color', 'rgb(0, 188, 0)'); }); +test('should collapse repeated console messages for test', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('print', async ({ page }) => { + await page.evaluate(() => { + console.log('page message') + for (let i = 0; i < 10; ++i) + console.log('page message') + }); + for (let i = 0; i < 10; ++i) + console.log('node message') + await page.evaluate(async () => { + await new Promise(resolve => { + for (let i = 0; i < 10; ++i) + console.log('page message') + setTimeout(() => { + for (let i = 0; i < 10; ++i) + console.log('page message') + resolve() + }, 1500) + }) + }); + }); + `, + }); + await page.getByTitle('Run all').click(); + await page.getByRole('tab', { name: 'Console' }).click(); + await page.getByText('print').click(); + + await expect(page.getByRole('tabpanel', { name: 'Console' })).toMatchAriaSnapshot(` + - tabpanel "Console": + - list: + - listitem: /page message/ + - listitem: /10 page message/ + - listitem: /10 node message/ + - listitem: /10 page message/ + - listitem: /10 page message/ + `); +}); + test('should format console messages in page', async ({ runUITest }, testInfo) => { const { page } = await runUITest({ 'a.spec.ts': `