diff --git a/packages/playwright-core/src/client/tracing.ts b/packages/playwright-core/src/client/tracing.ts index b31a94ab72788..80d185c45a31e 100644 --- a/packages/playwright-core/src/client/tracing.ts +++ b/packages/playwright-core/src/client/tracing.ts @@ -33,10 +33,15 @@ export class Tracing extends ChannelOwner implements ap super(parent, type, guid, initializer); } - async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean } = {}) { + async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) { this._includeSources = !!options.sources; const traceName = await this._wrapApiCall(async () => { - await this._channel.tracingStart(options); + await this._channel.tracingStart({ + name: options.name, + snapshots: options.snapshots, + screenshots: options.screenshots, + live: options._live, + }); const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title }); return response.traceName; }); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index c40db1e2e36f3..33b280a52ff1f 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2130,7 +2130,7 @@ scheme.TracingTracingStartParams = tObject({ name: tOptional(tString), snapshots: tOptional(tBoolean), screenshots: tOptional(tBoolean), - sources: tOptional(tBoolean), + live: tOptional(tBoolean), }); scheme.TracingTracingStartResult = tOptional(tObject({})); scheme.TracingTracingStartChunkParams = tObject({ diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index cd95c08aded5a..efbf34f4814c7 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -49,6 +49,7 @@ export type TracerOptions = { name?: string; snapshots?: boolean; screenshots?: boolean; + live?: boolean; }; type RecordingState = { @@ -455,8 +456,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps private _appendTraceEvent(event: trace.TraceEvent) { const visited = visitTraceEvent(event, this._state!.traceSha1s); - // Do not flush events, they are too noisy. - const flush = event.type !== 'event' && event.type !== 'object'; + // Do not flush (console) events, they are too noisy, unless we are in ui mode (live). + const flush = this._state!.options.live || (event.type !== 'event' && event.type !== 'object'); this._fs.appendFile(this._state!.traceFile, JSON.stringify(visited) + '\n', flush); } diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index 7c816225e784e..10d5cc2676e21 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -526,7 +526,7 @@ class ArtifactsRecorder { private _traceMode: TraceMode; private _captureTrace = false; private _screenshotOptions: { mode: ScreenshotMode } & Pick | undefined; - private _traceOptions: { screenshots: boolean, snapshots: boolean, sources: boolean, attachments: boolean, mode?: TraceMode }; + private _traceOptions: { screenshots: boolean, snapshots: boolean, sources: boolean, attachments: boolean, _live: boolean, mode?: TraceMode }; private _temporaryTraceFiles: string[] = []; private _temporaryScreenshots: string[] = []; private _reusedContexts = new Set(); @@ -541,7 +541,7 @@ class ArtifactsRecorder { this._screenshotMode = normalizeScreenshotMode(screenshot); this._screenshotOptions = typeof screenshot === 'string' ? undefined : screenshot; this._traceMode = normalizeTraceMode(trace); - const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true, attachments: true }; + const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true, attachments: true, _live: false }; this._traceOptions = typeof trace === 'string' ? defaultTraceOptions : { ...defaultTraceOptions, ...trace, mode: undefined }; this._screenshottedSymbol = Symbol('screenshotted'); this._startedCollectingArtifacts = Symbol('startedCollectingArtifacts'); diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index 940d76d28c9a8..5451363abfdfe 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -52,7 +52,7 @@ class UIMode { p.project.repeatEach = 1; } config.configCLIOverrides.use = config.configCLIOverrides.use || {}; - config.configCLIOverrides.use.trace = { mode: 'on', sources: false }; + config.configCLIOverrides.use.trace = { mode: 'on', sources: false, _live: true }; this._originalStdoutWrite = process.stdout.write; this._originalStderrWrite = process.stderr.write; diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index ee616ab07bd72..d5fff0b580cf0 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -3803,13 +3803,13 @@ export type TracingTracingStartParams = { name?: string, snapshots?: boolean, screenshots?: boolean, - sources?: boolean, + live?: boolean, }; export type TracingTracingStartOptions = { name?: string, snapshots?: boolean, screenshots?: boolean, - sources?: boolean, + live?: boolean, }; export type TracingTracingStartResult = void; export type TracingTracingStartChunkParams = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index ed7dd92e6913f..0ebaed4a88e71 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -3014,7 +3014,7 @@ Tracing: name: string? snapshots: boolean? screenshots: boolean? - sources: boolean? + live: boolean? tracingStartChunk: parameters: diff --git a/packages/trace-viewer/src/ui/consoleTab.tsx b/packages/trace-viewer/src/ui/consoleTab.tsx index 29d298dd737b9..c76a9d0783f0b 100644 --- a/packages/trace-viewer/src/ui/consoleTab.tsx +++ b/packages/trace-viewer/src/ui/consoleTab.tsx @@ -200,10 +200,14 @@ function format(args: { preview: string, value: any }[]): JSX.Element[] { function parseCSSStyle(cssFormat: string): Record { try { const styleObject: Record = {}; - const cssText = cssFormat.replace(/;$/, '').replace(/: /g, ':').replace(/; /g, ';'); - const cssProperties = cssText.split(';'); - for (const property of cssProperties) { - const [key, value] = property.split(':'); + const cssProperties = cssFormat.split(';'); + for (const token of cssProperties) { + const property = token.trim(); + if (!property) + continue; + let [key, value] = property.split(':'); + key = key.trim(); + value = value.trim(); if (!supportProperty(key)) continue; // cssProperties are background-color, JSDom ones are backgroundColor diff --git a/tests/playwright-test/ui-mode-test-output.spec.ts b/tests/playwright-test/ui-mode-test-output.spec.ts index 7c74bed65b611..23f254707a6a4 100644 --- a/tests/playwright-test/ui-mode-test-output.spec.ts +++ b/tests/playwright-test/ui-mode-test-output.spec.ts @@ -151,3 +151,28 @@ test('should format console messages in page', async ({ runUITest }, testInfo) = await expect(link).toHaveCSS('color', 'rgb(0, 0, 255)'); await expect(link).toHaveCSS('text-decoration', 'none solid rgb(0, 0, 255)'); }); + +test('should stream console messages live', async ({ runUITest }, testInfo) => { + const { page } = await runUITest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('print', async ({ page }) => { + await page.setContent(''); + const button = page.getByRole('button', { name: 'Click me' }); + await button.evaluate(node => node.addEventListener('click', () => { + setTimeout(() => { console.log('I was clicked'); }, 1000); + })); + await button.click(); + await page.locator('#not-there').waitFor(); + }); + `, + }); + 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([ + 'I was clicked', + ]); + await page.getByTitle('Stop').click(); +});