Skip to content

Commit bb8e914

Browse files
authored
feat(ui): collapse repeating console lines (#34857)
1 parent e43d287 commit bb8e914

File tree

3 files changed

+96
-18
lines changed

3 files changed

+96
-18
lines changed

packages/trace-viewer/src/ui/consoleTab.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,16 @@
8383
.console-line .codicon.status-warning::after {
8484
background-color: var(--vscode-list-warningForeground);
8585
}
86+
87+
.console-repeat {
88+
display: inline-block;
89+
padding: 0 2px;
90+
font-size: 12px;
91+
line-height: 18px;
92+
border-radius: 6px;
93+
background-color: #8c959f;
94+
color: white;
95+
margin-right: 10px;
96+
flex: none;
97+
font-weight: 600;
98+
}

packages/trace-viewer/src/ui/consoleTab.tsx

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { PlaceholderPanel } from './placeholderPanel';
2727
export type ConsoleEntry = {
2828
browserMessage?: {
2929
body: JSX.Element[];
30+
bodyString: string;
3031
location: string;
3132
},
3233
browserError?: channels.SerializedError;
@@ -36,6 +37,7 @@ export type ConsoleEntry = {
3637
isError: boolean;
3738
isWarning: boolean;
3839
timestamp: number;
40+
repeat: number;
3941
};
4042

4143
type ConsoleTabModel = {
@@ -50,16 +52,38 @@ export function useConsoleTabModel(model: modelUtil.MultiTraceModel | undefined,
5052
if (!model)
5153
return { entries: [] };
5254
const entries: ConsoleEntry[] = [];
53-
for (const event of model.events) {
55+
function addEntry(entry: Omit<ConsoleEntry, 'repeat'>) {
56+
const lastEntry = entries[entries.length - 1];
57+
const isSameAsLast =
58+
lastEntry
59+
&& entry.browserMessage?.bodyString === lastEntry.browserMessage?.bodyString
60+
&& entry.browserMessage?.location === lastEntry.browserMessage?.location
61+
&& entry.browserError === lastEntry.browserError
62+
&& entry.nodeMessage?.html === lastEntry.nodeMessage?.html
63+
&& entry.isError === lastEntry.isError
64+
&& entry.isWarning === lastEntry.isWarning
65+
&& entry.timestamp - lastEntry.timestamp < 1000;
66+
if (isSameAsLast)
67+
lastEntry.repeat++;
68+
else
69+
entries.push({ ...entry, repeat: 1 });
70+
}
71+
const logEvents = [...model.events, ...model.stdio].sort((a, b) => {
72+
const aTimestamp = 'time' in a ? a.time : a.timestamp;
73+
const bTimestamp = 'time' in b ? b.time : b.timestamp;
74+
return aTimestamp - bTimestamp;
75+
})
76+
for (const event of logEvents) {
5477
if (event.type === 'console') {
5578
const body = event.args && event.args.length ? format(event.args) : formatAnsi(event.text);
5679
const url = event.location.url;
5780
const filename = url ? url.substring(url.lastIndexOf('/') + 1) : '<anonymous>';
5881
const location = `${filename}:${event.location.lineNumber}`;
5982

60-
entries.push({
83+
addEntry({
6184
browserMessage: {
6285
body,
86+
bodyString: event.text,
6387
location,
6488
},
6589
isError: event.messageType === 'error',
@@ -68,29 +92,28 @@ export function useConsoleTabModel(model: modelUtil.MultiTraceModel | undefined,
6892
});
6993
}
7094
if (event.type === 'event' && event.method === 'pageError') {
71-
entries.push({
95+
addEntry({
7296
browserError: event.params.error,
7397
isError: true,
7498
isWarning: false,
7599
timestamp: event.time,
76100
});
77101
}
102+
if (event.type === 'stderr' || event.type === 'stdout') {
103+
let html = '';
104+
if (event.text)
105+
html = ansi2html(event.text.trim()) || '';
106+
if (event.base64)
107+
html = ansi2html(atob(event.base64).trim()) || '';
108+
109+
addEntry({
110+
nodeMessage: { html },
111+
isError: event.type === 'stderr',
112+
isWarning: false,
113+
timestamp: event.timestamp,
114+
});
115+
}
78116
}
79-
for (const event of model.stdio) {
80-
let html = '';
81-
if (event.text)
82-
html = ansi2html(event.text.trim()) || '';
83-
if (event.base64)
84-
html = ansi2html(atob(event.base64).trim()) || '';
85-
86-
entries.push({
87-
nodeMessage: { html },
88-
isError: event.type === 'stderr',
89-
isWarning: false,
90-
timestamp: event.timestamp,
91-
});
92-
}
93-
entries.sort((a, b) => a.timestamp - b.timestamp);
94117
return { entries };
95118
}, [model]);
96119

@@ -154,6 +177,7 @@ export const ConsoleTab: React.FunctionComponent<{
154177
{timestampElement}
155178
{statusElement}
156179
{locationText && <span className='console-location'>{locationText}</span>}
180+
{entry.repeat > 1 && <span className='console-repeat'>{entry.repeat}</span>}
157181
{messageBody && <span className='console-line-message'>{messageBody}</span>}
158182
{messageInnerHTML && <span className='console-line-message' dangerouslySetInnerHTML={{ __html: messageInnerHTML }}></span>}
159183
{messageStack && <div className='console-stack'>{messageStack}</div>}

tests/playwright-test/ui-mode-test-output.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,47 @@ test('should show console messages for test', async ({ runUITest }, testInfo) =>
114114
await expect.soft(page.getByText('GREEN', { exact: true })).toHaveCSS('color', 'rgb(0, 188, 0)');
115115
});
116116

117+
test('should collapse repeated console messages for test', async ({ runUITest }) => {
118+
const { page } = await runUITest({
119+
'a.spec.ts': `
120+
import { test, expect } from '@playwright/test';
121+
test('print', async ({ page }) => {
122+
await page.evaluate(() => {
123+
console.log('page message')
124+
for (let i = 0; i < 10; ++i)
125+
console.log('page message')
126+
});
127+
for (let i = 0; i < 10; ++i)
128+
console.log('node message')
129+
await page.evaluate(async () => {
130+
await new Promise(resolve => {
131+
for (let i = 0; i < 10; ++i)
132+
console.log('page message')
133+
setTimeout(() => {
134+
for (let i = 0; i < 10; ++i)
135+
console.log('page message')
136+
resolve()
137+
}, 1500)
138+
})
139+
});
140+
});
141+
`,
142+
});
143+
await page.getByTitle('Run all').click();
144+
await page.getByRole('tab', { name: 'Console' }).click();
145+
await page.getByText('print').click();
146+
147+
await expect(page.getByRole('tabpanel', { name: 'Console' })).toMatchAriaSnapshot(`
148+
- tabpanel "Console":
149+
- list:
150+
- listitem: /page message/
151+
- listitem: /10 page message/
152+
- listitem: /10 node message/
153+
- listitem: /10 page message/
154+
- listitem: /10 page message/
155+
`);
156+
});
157+
117158
test('should format console messages in page', async ({ runUITest }, testInfo) => {
118159
const { page } = await runUITest({
119160
'a.spec.ts': `

0 commit comments

Comments
 (0)