diff --git a/packages/playwright-test/src/worker/testInfo.ts b/packages/playwright-test/src/worker/testInfo.ts
index 0d497fe294..58b6361d70 100644
--- a/packages/playwright-test/src/worker/testInfo.ts
+++ b/packages/playwright-test/src/worker/testInfo.ts
@@ -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;
diff --git a/packages/playwright-test/src/worker/workerMain.ts b/packages/playwright-test/src/worker/workerMain.ts
index 7dc70cc8bb..51111849d2 100644
--- a/packages/playwright-test/src/worker/workerMain.ts
+++ b/packages/playwright-test/src/worker/workerMain.ts
@@ -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 };
diff --git a/packages/trace-viewer/src/entries.ts b/packages/trace-viewer/src/entries.ts
index e8c82f31a2..0adec7eb76 100644
--- a/packages/trace-viewer/src/entries.ts
+++ b/packages/trace-viewer/src/entries.ts
@@ -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
};
diff --git a/packages/trace-viewer/src/traceModel.ts b/packages/trace-viewer/src/traceModel.ts
index 3518a3ab80..820a9e7a6e 100644
--- a/packages/trace-viewer/src/traceModel.ts
+++ b/packages/trace-viewer/src/traceModel.ts
@@ -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;
diff --git a/packages/trace-viewer/src/ui/consoleTab.tsx b/packages/trace-viewer/src/ui/consoleTab.tsx
index ed97674c6f..e36839df13 100644
--- a/packages/trace-viewer/src/ui/consoleTab.tsx
+++ b/packages/trace-viewer/src/ui/consoleTab.tsx
@@ -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
!!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) : '';
@@ -82,15 +103,27 @@ export const ConsoleTab: React.FunctionComponent<{
{errorObject.message}
{errorObject.stack}
;
- } else {
- return
-
- {String(value)}
-
;
}
+ return
+
+ {String(value)}
+
;
+ }
+ if (nodeMessage?.text) {
+ return
+
+
+
;
+ }
+ if (nodeMessage?.base64) {
+ return
+
+
+
;
}
return null;
- }}
+ }
+ }
isHighlighted={entry => !!entry.highlight}
/>
;
@@ -103,3 +136,7 @@ function iconClass(message: channels.ConsoleMessageInitializer): string {
}
return 'blank';
}
+
+function stdioClass(isError: boolean): string {
+ return isError ? 'error' : 'blank';
+}
diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts
index 73b7457aad..0b007a4d02 100644
--- a/packages/trace-viewer/src/ui/modelUtil.ts
+++ b/packages/trace-viewer/src/ui/modelUtil.ts
@@ -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();
diff --git a/packages/trace/src/trace.ts b/packages/trace/src/trace.ts
index 0348281ff6..891d7be97e 100644
--- a/packages/trace/src/trace.ts
+++ b/packages/trace/src/trace.ts
@@ -122,6 +122,13 @@ export type ActionTraceEvent = {
& Omit
& Omit;
+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;
diff --git a/packages/web/src/components/listView.css b/packages/web/src/components/listView.css
index 3fbb74f3d0..e64e47dd80 100644
--- a/packages/web/src/components/listView.css
+++ b/packages/web/src/components/listView.css
@@ -20,7 +20,7 @@
flex: auto;
position: relative;
user-select: none;
- overflow-y: auto;
+ overflow: hidden auto;
outline: 1px solid transparent;
}
diff --git a/tests/playwright-test/ui-mode-test-output.spec.ts b/tests/playwright-test/ui-mode-test-output.spec.ts
index 32d8e15719..14d1bb8f0f 100644
--- a/tests/playwright-test/ui-mode-test-output.spec.ts
+++ b/tests/playwright-test/ui-mode-test-output.spec.ts
@@ -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)');
+});