feat(trace): render Node console messages in trace (#24139)

This commit is contained in:
Pavel Feldman 2023-07-10 18:36:28 -07:00 committed by GitHub
parent e234a6a037
commit 63915dc07a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 118 additions and 13 deletions

View File

@ -312,6 +312,15 @@ export class TestInfoImpl implements TestInfo {
return step;
}
_appendStdioToTrace(type: 'stdout' | 'stderr', chunk: string | Buffer) {
this._traceEvents.push({
type,
timestamp: monotonicTime(),
text: typeof chunk === 'string' ? chunk : undefined,
base64: typeof chunk === 'string' ? undefined : chunk.toString('base64'),
});
}
_interrupt() {
// Mark as interrupted so we can ignore TimeoutError thrown by interrupt() call.
this._wasInterrupted = true;

View File

@ -81,6 +81,7 @@ export class WorkerMain extends ProcessRunner {
...chunkToParams(chunk)
};
this.dispatchEvent('stdOut', outPayload);
this._currentTest?._appendStdioToTrace('stdout', chunk);
return true;
};
@ -90,6 +91,7 @@ export class WorkerMain extends ProcessRunner {
...chunkToParams(chunk)
};
this.dispatchEvent('stdErr', outPayload);
this._currentTest?._appendStdioToTrace('stderr', chunk);
return true;
};
}
@ -633,9 +635,9 @@ function formatTestTitle(test: TestCase, projectName: string) {
return `${projectTitle}${location} ${titles.join(' ')}`;
}
function chunkToParams(chunk: Buffer | string): { text?: string, buffer?: string } {
if (chunk instanceof Buffer)
return { buffer: chunk.toString('base64') };
function chunkToParams(chunk: Uint8Array | string, encoding?: BufferEncoding): { text?: string, buffer?: string } {
if (chunk instanceof Uint8Array)
return { buffer: Buffer.from(chunk).toString('base64') };
if (typeof chunk !== 'string')
return { text: util.inspect(chunk) };
return { text: chunk };

View File

@ -34,6 +34,7 @@ export type ContextEntry = {
resources: ResourceSnapshot[];
actions: trace.ActionTraceEvent[];
events: trace.EventTraceEvent[];
stdio: trace.StdioTraceEvent[];
initializers: { [key: string]: any };
hasSource: boolean;
};
@ -62,6 +63,7 @@ export function createEmptyContext(): ContextEntry {
resources: [],
actions: [],
events: [],
stdio: [],
initializers: {},
hasSource: false
};

View File

@ -177,6 +177,14 @@ export class TraceModel {
contextEntry!.events.push(event);
break;
}
case 'stdout': {
contextEntry!.stdio.push(event);
break;
}
case 'stderr': {
contextEntry!.stdio.push(event);
break;
}
case 'object': {
contextEntry!.initializers[event.guid] = event.initializer;
break;

View File

@ -20,10 +20,17 @@ import * as React from 'react';
import './consoleTab.css';
import * as modelUtil from './modelUtil';
import { ListView } from '@web/components/listView';
import { ansi2htmlMarkup } from '@web/components/errorMessage';
type ConsoleEntry = {
message?: channels.ConsoleMessageInitializer;
error?: channels.SerializedError;
nodeMessage?: {
text?: string;
base64?: string;
isError: boolean;
},
timestamp: number;
highlight: boolean;
};
@ -46,25 +53,39 @@ export const ConsoleTab: React.FunctionComponent<{
entries.push({
message: modelUtil.context(event).initializers[guid],
highlight: actionEvents.includes(event),
timestamp: event.time,
});
}
if (event.method === 'pageError') {
entries.push({
error: event.params.error,
highlight: actionEvents.includes(event),
timestamp: event.time,
});
}
}
for (const event of model.stdio) {
entries.push({
nodeMessage: {
text: event.text,
base64: event.base64,
isError: event.type === 'stderr',
},
timestamp: event.timestamp,
highlight: false,
});
}
entries.sort((a, b) => a.timestamp - b.timestamp);
return { entries };
}, [model, action]);
return <div className='console-tab'>
<ConsoleListView
items={entries}
isError={entry => !!entry.error || entry.message?.type === 'error'}
isError={entry => !!entry.error || entry.message?.type === 'error' || entry.nodeMessage?.isError || false}
isWarning={entry => entry.message?.type === 'warning'}
render={entry => {
const { message, error } = entry;
const { message, error, nodeMessage } = entry;
if (message) {
const url = message.location.url;
const filename = url ? url.substring(url.lastIndexOf('/') + 1) : '<anonymous>';
@ -82,15 +103,27 @@ export const ConsoleTab: React.FunctionComponent<{
<span className='console-line-message'>{errorObject.message}</span>
<div className='console-stack'>{errorObject.stack}</div>
</div>;
} else {
return <div className='console-line'>
<span className={'codicon codicon-error'}></span>
<span className='console-line-message'>{String(value)}</span>
</div>;
}
return <div className='console-line'>
<span className={'codicon codicon-error'}></span>
<span className='console-line-message'>{String(value)}</span>
</div>;
}
if (nodeMessage?.text) {
return <div className='console-line'>
<span className={'codicon codicon-' + stdioClass(nodeMessage.isError)}></span>
<span className='console-line-message' dangerouslySetInnerHTML={{ __html: ansi2htmlMarkup(nodeMessage.text.trim()) || '' }}></span>
</div>;
}
if (nodeMessage?.base64) {
return <div className='console-line'>
<span className={'codicon codicon-' + stdioClass(nodeMessage.isError)}></span>
<span className='console-line-message' dangerouslySetInnerHTML={{ __html: ansi2htmlMarkup(atob(nodeMessage.base64).trim()) || '' }}></span>
</div>;
}
return null;
}}
}
}
isHighlighted={entry => !!entry.highlight}
/>
</div>;
@ -103,3 +136,7 @@ function iconClass(message: channels.ConsoleMessageInitializer): string {
}
return 'blank';
}
function stdioClass(isError: boolean): string {
return isError ? 'error' : 'blank';
}

View File

@ -59,6 +59,7 @@ export class MultiTraceModel {
readonly pages: PageEntry[];
readonly actions: ActionTraceEventInContext[];
readonly events: trace.EventTraceEvent[];
readonly stdio: trace.StdioTraceEvent[];
readonly hasSource: boolean;
readonly sdkLanguage: Language | undefined;
readonly testIdAttributeName: string | undefined;
@ -81,6 +82,7 @@ export class MultiTraceModel {
this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages));
this.actions = mergeActions(contexts);
this.events = ([] as EventTraceEvent[]).concat(...contexts.map(c => c.events));
this.stdio = ([] as trace.StdioTraceEvent[]).concat(...contexts.map(c => c.stdio));
this.hasSource = contexts.some(c => c.hasSource);
this.resources = [...contexts.map(c => c.resources)].flat();

View File

@ -122,6 +122,13 @@ export type ActionTraceEvent = {
& Omit<AfterActionTraceEvent, 'type'>
& Omit<InputActionTraceEvent, 'type'>;
export type StdioTraceEvent = {
type: 'stdout' | 'stderr';
timestamp: number;
text?: string;
base64?: string;
};
export type TraceEvent =
ContextCreatedTraceEvent |
ScreencastFrameTraceEvent |
@ -132,4 +139,5 @@ export type TraceEvent =
EventTraceEvent |
ObjectTraceEvent |
ResourceSnapshotTraceEvent |
FrameSnapshotTraceEvent;
FrameSnapshotTraceEvent |
StdioTraceEvent;

View File

@ -20,7 +20,7 @@
flex: auto;
position: relative;
user-select: none;
overflow-y: auto;
overflow: hidden auto;
outline: 1px solid transparent;
}

View File

@ -74,3 +74,40 @@ test('should print buffers', async ({ runUITest }) => {
await page.getByTitle('Run all').click();
await expect(page.getByTestId('output')).toContainText('HELLO');
});
test('should show console messages for test', async ({ runUITest }, testInfo) => {
const { page } = await runUITest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('print', async ({ page }) => {
await page.evaluate(() => console.log('page message'));
console.log('node message');
await page.evaluate(() => console.error('page error'));
console.error('node error');
console.log('Colors: \x1b[31mRED\x1b[0m \x1b[32mGREEN\x1b[0m');
});
`,
});
await page.getByTitle('Run all').click();
await page.getByText('Console').click();
await page.getByText('print').click();
await expect(page.locator('.console-tab .console-line-message')).toHaveText([
'page message',
'node message',
'page error',
'node error',
'Colors: RED GREEN',
]);
await expect(page.locator('.console-tab .list-view-entry .codicon')).toHaveClass([
'codicon codicon-blank',
'codicon codicon-blank',
'codicon codicon-error',
'codicon codicon-error',
'codicon codicon-blank',
]);
await expect(page.getByText('RED', { exact: true })).toHaveCSS('color', 'rgb(204, 0, 0)');
await expect(page.getByText('GREEN', { exact: true })).toHaveCSS('color', 'rgb(0, 204, 0)');
});