chore: make stacks rendering live in ui mode (#21728)

Co-authored-by: Max Schmitt <max@schmitt.mx>
This commit is contained in:
Pavel Feldman 2023-03-16 18:17:07 -07:00 committed by GitHub
parent f37f38e553
commit ecd0f927f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 186 additions and 53 deletions

View file

@ -71,7 +71,7 @@ export class Connection extends EventEmitter {
private _localUtils?: LocalUtils; private _localUtils?: LocalUtils;
// Some connections allow resolving in-process dispatchers. // Some connections allow resolving in-process dispatchers.
toImpl: ((client: ChannelOwner) => any) | undefined; toImpl: ((client: ChannelOwner) => any) | undefined;
private _stackCollectors = new Set<channels.ClientSideCallMetadata[]>(); private _tracingCount = 0;
constructor(localUtils?: LocalUtils) { constructor(localUtils?: LocalUtils) {
super(); super();
@ -103,12 +103,11 @@ export class Connection extends EventEmitter {
return this._objects.get(guid)!; return this._objects.get(guid)!;
} }
startCollectingCallMetadata(collector: channels.ClientSideCallMetadata[]) { async setIsTracing(isTracing: boolean) {
this._stackCollectors.add(collector); if (isTracing)
} this._tracingCount++;
else
stopCollectingCallMetadata(collector: channels.ClientSideCallMetadata[]) { this._tracingCount--;
this._stackCollectors.delete(collector);
} }
async sendMessageToServer(object: ChannelOwner, type: string, method: string, params: any, stackTrace: ParsedStackTrace | null, wallTime: number | undefined): Promise<any> { async sendMessageToServer(object: ChannelOwner, type: string, method: string, params: any, stackTrace: ParsedStackTrace | null, wallTime: number | undefined): Promise<any> {
@ -121,12 +120,11 @@ export class Connection extends EventEmitter {
const converted = { id, guid, method, params }; const converted = { id, guid, method, params };
// Do not include metadata in debug logs to avoid noise. // Do not include metadata in debug logs to avoid noise.
debugLogger.log('channel:command', converted); debugLogger.log('channel:command', converted);
for (const collector of this._stackCollectors)
collector.push({ stack: frames, id: id });
const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined; const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined;
const metadata: channels.Metadata = { wallTime, apiName, location, internal: !apiName }; const metadata: channels.Metadata = { wallTime, apiName, location, internal: !apiName };
this.onmessage({ ...converted, metadata }); this.onmessage({ ...converted, metadata });
if (this._tracingCount && frames && type !== 'LocalUtils')
this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, stackTrace, type, method })); return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, stackTrace, type, method }));
} }

View file

@ -21,8 +21,9 @@ import { ChannelOwner } from './channelOwner';
export class Tracing extends ChannelOwner<channels.TracingChannel> implements api.Tracing { export class Tracing extends ChannelOwner<channels.TracingChannel> implements api.Tracing {
private _includeSources = false; private _includeSources = false;
private _metadataCollector: channels.ClientSideCallMetadata[] = [];
_tracesDir: string | undefined; _tracesDir: string | undefined;
private _stacksId: string | undefined;
private _isTracing = false;
static from(channel: channels.TracingChannel): Tracing { static from(channel: channels.TracingChannel): Tracing {
return (channel as any)._object; return (channel as any)._object;
@ -34,18 +35,26 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
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 } = {}) {
this._includeSources = !!options.sources; this._includeSources = !!options.sources;
await this._wrapApiCall(async () => { const traceName = await this._wrapApiCall(async () => {
await this._channel.tracingStart(options); await this._channel.tracingStart(options);
await this._channel.tracingStartChunk({ name: options.name, title: options.title }); const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
return response.traceName;
}); });
this._metadataCollector = []; await this._startCollectingStacks(traceName);
this._connection.startCollectingCallMetadata(this._metadataCollector);
} }
async startChunk(options: { name?: string, title?: string } = {}) { async startChunk(options: { name?: string, title?: string } = {}) {
await this._channel.tracingStartChunk(options); const { traceName } = await this._channel.tracingStartChunk(options);
this._metadataCollector = []; await this._startCollectingStacks(traceName);
this._connection.startCollectingCallMetadata(this._metadataCollector); }
private async _startCollectingStacks(traceName: string) {
if (!this._isTracing) {
this._isTracing = true;
this._connection.setIsTracing(true);
}
const result = await this._connection.localUtils()._channel.tracingStarted({ tracesDir: this._tracesDir, traceName });
this._stacksId = result.stacksId;
} }
async stopChunk(options: { path?: string } = {}) { async stopChunk(options: { path?: string } = {}) {
@ -60,12 +69,16 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
} }
private async _doStopChunk(filePath: string | undefined) { private async _doStopChunk(filePath: string | undefined) {
this._connection.stopCollectingCallMetadata(this._metadataCollector); if (this._isTracing) {
const metadata = this._metadataCollector; this._isTracing = false;
this._metadataCollector = []; this._connection.setIsTracing(false);
}
if (!filePath) { if (!filePath) {
await this._channel.tracingStopChunk({ mode: 'discard' });
// Not interested in artifacts. // Not interested in artifacts.
await this._channel.tracingStopChunk({ mode: 'discard' });
if (this._stacksId)
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
return; return;
} }
@ -73,23 +86,24 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
if (isLocal) { if (isLocal) {
const result = await this._channel.tracingStopChunk({ mode: 'entries' }); const result = await this._channel.tracingStopChunk({ mode: 'entries' });
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: result.entries!, metadata, mode: 'write', includeSources: this._includeSources }); await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources });
return; return;
} }
const result = await this._channel.tracingStopChunk({ mode: 'archive' }); const result = await this._channel.tracingStopChunk({ mode: 'archive' });
// The artifact may be missing if the browser closed while stopping tracing. // The artifact may be missing if the browser closed while stopping tracing.
if (!result.artifact) if (!result.artifact) {
if (this._stacksId)
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
return; return;
}
// Save trace to the final local file. // Save trace to the final local file.
const artifact = Artifact.from(result.artifact); const artifact = Artifact.from(result.artifact);
await artifact.saveAs(filePath); await artifact.saveAs(filePath);
await artifact.delete(); await artifact.delete();
// Add local sources to the remote trace if necessary. await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources });
if (metadata.length)
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: [], metadata, mode: 'append', includeSources: this._includeSources });
} }
} }

View file

@ -220,8 +220,8 @@ scheme.LocalUtilsInitializer = tOptional(tObject({}));
scheme.LocalUtilsZipParams = tObject({ scheme.LocalUtilsZipParams = tObject({
zipFile: tString, zipFile: tString,
entries: tArray(tType('NameValue')), entries: tArray(tType('NameValue')),
stacksId: tOptional(tString),
mode: tEnum(['write', 'append']), mode: tEnum(['write', 'append']),
metadata: tArray(tType('ClientSideCallMetadata')),
includeSources: tBoolean, includeSources: tBoolean,
}); });
scheme.LocalUtilsZipResult = tOptional(tObject({})); scheme.LocalUtilsZipResult = tOptional(tObject({}));
@ -268,6 +268,21 @@ scheme.LocalUtilsConnectParams = tObject({
scheme.LocalUtilsConnectResult = tObject({ scheme.LocalUtilsConnectResult = tObject({
pipe: tChannel(['JsonPipe']), pipe: tChannel(['JsonPipe']),
}); });
scheme.LocalUtilsTracingStartedParams = tObject({
tracesDir: tOptional(tString),
traceName: tString,
});
scheme.LocalUtilsTracingStartedResult = tObject({
stacksId: tString,
});
scheme.LocalUtilsAddStackToTracingNoReplyParams = tObject({
callData: tType('ClientSideCallMetadata'),
});
scheme.LocalUtilsAddStackToTracingNoReplyResult = tOptional(tObject({}));
scheme.LocalUtilsTraceDiscardedParams = tObject({
stacksId: tString,
});
scheme.LocalUtilsTraceDiscardedResult = tOptional(tObject({}));
scheme.RootInitializer = tOptional(tObject({})); scheme.RootInitializer = tOptional(tObject({}));
scheme.RootInitializeParams = tObject({ scheme.RootInitializeParams = tObject({
sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']), sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']),
@ -2095,7 +2110,9 @@ scheme.TracingTracingStartChunkParams = tObject({
name: tOptional(tString), name: tOptional(tString),
title: tOptional(tString), title: tOptional(tString),
}); });
scheme.TracingTracingStartChunkResult = tOptional(tObject({})); scheme.TracingTracingStartChunkResult = tObject({
traceName: tString,
});
scheme.TracingTracingStopChunkParams = tObject({ scheme.TracingTracingStopChunkParams = tObject({
mode: tEnum(['archive', 'discard', 'entries']), mode: tEnum(['archive', 'discard', 'entries']),
}); });

View file

@ -17,9 +17,10 @@
import type EventEmitter from 'events'; import type EventEmitter from 'events';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import os from 'os';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { ManualPromise } from '../../utils/manualPromise'; import { ManualPromise } from '../../utils/manualPromise';
import { assert, calculateSha1, createGuid } from '../../utils'; import { assert, calculateSha1, createGuid, removeFolders } from '../../utils';
import type { RootDispatcher } from './dispatcher'; import type { RootDispatcher } from './dispatcher';
import { Dispatcher } from './dispatcher'; import { Dispatcher } from './dispatcher';
import { yazl, yauzl } from '../../zipBundle'; import { yazl, yauzl } from '../../zipBundle';
@ -42,7 +43,13 @@ import { serializeClientSideCallMetadata } from '../../utils';
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel { export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
_type_LocalUtils: boolean; _type_LocalUtils: boolean;
private _harBakends = new Map<string, HarBackend>(); private _harBackends = new Map<string, HarBackend>();
private _stackSessions = new Map<string, {
file: string,
writer: Promise<void>,
tmpDir: string | undefined,
callStacks: channels.ClientSideCallMetadata[]
}>();
constructor(scope: RootDispatcher, playwright: Playwright) { constructor(scope: RootDispatcher, playwright: Playwright) {
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils'); const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
@ -67,12 +74,21 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
addFile(entry.value, entry.name); addFile(entry.value, entry.name);
// Add stacks and the sources. // Add stacks and the sources.
zipFile.addBuffer(Buffer.from(JSON.stringify(serializeClientSideCallMetadata(params.metadata))), 'trace.stacks'); const stackSession = params.stacksId ? this._stackSessions.get(params.stacksId) : undefined;
if (stackSession?.callStacks.length) {
await stackSession.writer;
if (process.env.PW_LIVE_TRACE_STACKS) {
zipFile.addFile(stackSession.file, 'trace.stacks');
} else {
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(stackSession.callStacks)));
zipFile.addBuffer(buffer, 'trace.stacks');
}
}
// Collect sources from stacks. // Collect sources from stacks.
if (params.includeSources) { if (params.includeSources) {
const sourceFiles = new Set<string>(); const sourceFiles = new Set<string>();
for (const { stack } of params.metadata) { for (const { stack } of stackSession?.callStacks || []) {
if (!stack) if (!stack)
continue; continue;
for (const { file } of stack) for (const { file } of stack)
@ -88,7 +104,9 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
zipFile.end(undefined, () => { zipFile.end(undefined, () => {
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => promise.resolve()); zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => promise.resolve());
}); });
return promise; await promise;
await this._deleteStackSession(params.stacksId);
return;
} }
// File already exists. Repack and add new entries. // File already exists. Repack and add new entries.
@ -121,7 +139,8 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
}); });
}); });
}); });
return promise; await promise;
await this._deleteStackSession(params.stacksId);
} }
async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarOpenResult> { async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarOpenResult> {
@ -139,21 +158,21 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile; const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile;
harBackend = new HarBackend(harFile, path.dirname(params.file), null); harBackend = new HarBackend(harFile, path.dirname(params.file), null);
} }
this._harBakends.set(harBackend.id, harBackend); this._harBackends.set(harBackend.id, harBackend);
return { harId: harBackend.id }; return { harId: harBackend.id };
} }
async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarLookupResult> { async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarLookupResult> {
const harBackend = this._harBakends.get(params.harId); const harBackend = this._harBackends.get(params.harId);
if (!harBackend) if (!harBackend)
return { action: 'error', message: `Internal error: har was not opened` }; return { action: 'error', message: `Internal error: har was not opened` };
return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest); return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest);
} }
async harClose(params: channels.LocalUtilsHarCloseParams, metadata: CallMetadata): Promise<void> { async harClose(params: channels.LocalUtilsHarCloseParams, metadata: CallMetadata): Promise<void> {
const harBackend = this._harBakends.get(params.harId); const harBackend = this._harBackends.get(params.harId);
if (harBackend) { if (harBackend) {
this._harBakends.delete(harBackend.id); this._harBackends.delete(harBackend.id);
harBackend.dispose(); harBackend.dispose();
} }
} }
@ -213,6 +232,40 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
}, params.timeout || 0); }, params.timeout || 0);
} }
async tracingStarted(params: channels.LocalUtilsTracingStartedParams, metadata?: CallMetadata | undefined): Promise<channels.LocalUtilsTracingStartedResult> {
let tmpDir = undefined;
if (!params.tracesDir)
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-tracing-'));
const traceStacksFile = path.join(params.tracesDir || tmpDir!, params.traceName + '.stacks');
this._stackSessions.set(traceStacksFile, { callStacks: [], file: traceStacksFile, writer: Promise.resolve(), tmpDir });
return { stacksId: traceStacksFile };
}
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise<void> {
await this._deleteStackSession(params.stacksId);
}
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise<void> {
for (const session of this._stackSessions.values()) {
session.callStacks.push(params.callData);
if (process.env.PW_LIVE_TRACE_STACKS) {
session.writer = session.writer.then(() => {
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(session.callStacks)));
return fs.promises.writeFile(session.file, buffer);
});
}
}
}
private async _deleteStackSession(stacksId?: string) {
const session = stacksId ? this._stackSessions.get(stacksId) : undefined;
if (!session)
return;
await session.writer;
if (session.tmpDir)
await removeFolders([session.tmpDir]);
this._stackSessions.delete(stacksId!);
}
} }
const redirectStatus = [301, 302, 303, 307, 308]; const redirectStatus = [301, 302, 303, 307, 308];

View file

@ -38,7 +38,7 @@ export class TracingDispatcher extends Dispatcher<Tracing, channels.TracingChann
} }
async tracingStartChunk(params: channels.TracingTracingStartChunkParams): Promise<channels.TracingTracingStartChunkResult> { async tracingStartChunk(params: channels.TracingTracingStartChunkParams): Promise<channels.TracingTracingStartChunkResult> {
await this._object.startChunk(params); return await this._object.startChunk(params);
} }
async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> { async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> {

View file

@ -58,7 +58,7 @@ type RecordingState = {
traceFile: string, traceFile: string,
tracesDir: string, tracesDir: string,
resourcesDir: string, resourcesDir: string,
filesCount: number, chunkOrdinal: number,
networkSha1s: Set<string>, networkSha1s: Set<string>,
traceSha1s: Set<string>, traceSha1s: Set<string>,
recording: boolean; recording: boolean;
@ -132,7 +132,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
// and conflict. // and conflict.
const traceName = options.name || createGuid(); const traceName = options.name || createGuid();
// Init the state synchronously. // Init the state synchronously.
this._state = { options, traceName, traceFile: '', networkFile: '', tracesDir: '', resourcesDir: '', filesCount: 0, traceSha1s: new Set(), networkSha1s: new Set(), recording: false }; this._state = { options, traceName, traceFile: '', networkFile: '', tracesDir: '', resourcesDir: '', chunkOrdinal: 0, traceSha1s: new Set(), networkSha1s: new Set(), recording: false };
const state = this._state; const state = this._state;
state.tracesDir = await this._createTracesDirIfNeeded(); state.tracesDir = await this._createTracesDirIfNeeded();
@ -144,7 +144,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
this._harTracer.start(); this._harTracer.start();
} }
async startChunk(options: { name?: string, title?: string } = {}) { async startChunk(options: { name?: string, title?: string } = {}): Promise<{ traceName: string }> {
if (this._state && this._state.recording) if (this._state && this._state.recording)
await this.stopChunk({ mode: 'discard' }); await this.stopChunk({ mode: 'discard' });
@ -154,13 +154,14 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
throw new Error('Cannot start a trace chunk while stopping'); throw new Error('Cannot start a trace chunk while stopping');
const state = this._state; const state = this._state;
const suffix = state.filesCount ? `-${state.filesCount}` : ``; const suffix = state.chunkOrdinal ? `-${state.chunkOrdinal}` : ``;
state.filesCount++; state.chunkOrdinal++;
state.traceFile = path.join(state.tracesDir, `${state.traceName}${suffix}.trace`); state.traceFile = path.join(state.tracesDir, `${state.traceName}${suffix}.trace`);
state.recording = true; state.recording = true;
if (options.name && options.name !== this._state.traceName) if (options.name && options.name !== this._state.traceName)
this._changeTraceName(this._state, options.name); this._changeTraceName(this._state, options.name);
this._appendTraceOperation(async () => { this._appendTraceOperation(async () => {
await mkdirIfNeeded(state.traceFile); await mkdirIfNeeded(state.traceFile);
await fs.promises.appendFile(state.traceFile, JSON.stringify({ ...this._contextCreatedEvent, title: options.title, wallTime: Date.now() }) + '\n'); await fs.promises.appendFile(state.traceFile, JSON.stringify({ ...this._contextCreatedEvent, title: options.title, wallTime: Date.now() }) + '\n');
@ -171,6 +172,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
this._startScreencast(); this._startScreencast();
if (state.options.snapshots) if (state.options.snapshots)
await this._snapshotter?.start(); await this._snapshotter?.start();
return { traceName: state.traceName };
} }
private _startScreencast() { private _startScreencast() {
@ -194,6 +196,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
private async _changeTraceName(state: RecordingState, name: string) { private async _changeTraceName(state: RecordingState, name: string) {
await this._appendTraceOperation(async () => { await this._appendTraceOperation(async () => {
const oldNetworkFile = state.networkFile; const oldNetworkFile = state.networkFile;
state.traceName = name;
state.traceFile = path.join(state.tracesDir, name + '.trace'); state.traceFile = path.join(state.tracesDir, name + '.trace');
state.networkFile = path.join(state.tracesDir, name + '.network'); state.networkFile = path.join(state.tracesDir, name + '.network');
// Network file survives across chunks, so make a copy with the new name. // Network file survives across chunks, so make a copy with the new name.
@ -258,7 +261,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return {}; return {};
// Network file survives across chunks, make a snapshot before returning the resulting entries. // Network file survives across chunks, make a snapshot before returning the resulting entries.
const networkFile = path.join(state.networkFile, '..', createGuid()); const suffix = state.chunkOrdinal ? `-${state.chunkOrdinal}` : ``;
const networkFile = path.join(state.tracesDir, state.traceName + `${suffix}.network`);
await fs.promises.copyFile(state.networkFile, networkFile); await fs.promises.copyFile(state.networkFile, networkFile);
const entries: NameValue[] = []; const entries: NameValue[] = [];

View file

@ -18,7 +18,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core'; import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
import * as playwrightLibrary from 'playwright-core'; import * as playwrightLibrary from 'playwright-core';
import { createGuid, debugMode, removeFolders, addInternalStackPrefix, mergeTraceFiles, saveTraceFile } from 'playwright-core/lib/utils'; import { createGuid, debugMode, addInternalStackPrefix, mergeTraceFiles, saveTraceFile, removeFolders } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test';
import type { TestInfoImpl } from './worker/testInfo'; import type { TestInfoImpl } from './worker/testInfo';
import { rootTestType } from './common/testType'; import { rootTestType } from './common/testType';

View file

@ -39,6 +39,7 @@ class UIMode {
constructor(config: FullConfigInternal) { constructor(config: FullConfigInternal) {
this._config = config; this._config = config;
process.env.PW_LIVE_TRACE_STACKS = '1';
config._internal.configCLIOverrides.forbidOnly = false; config._internal.configCLIOverrides.forbidOnly = false;
config._internal.configCLIOverrides.globalTimeout = 0; config._internal.configCLIOverrides.globalTimeout = 0;
config._internal.configCLIOverrides.repeatEach = 0; config._internal.configCLIOverrides.repeatEach = 0;

View file

@ -402,16 +402,19 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
harClose(params: LocalUtilsHarCloseParams, metadata?: CallMetadata): Promise<LocalUtilsHarCloseResult>; harClose(params: LocalUtilsHarCloseParams, metadata?: CallMetadata): Promise<LocalUtilsHarCloseResult>;
harUnzip(params: LocalUtilsHarUnzipParams, metadata?: CallMetadata): Promise<LocalUtilsHarUnzipResult>; harUnzip(params: LocalUtilsHarUnzipParams, metadata?: CallMetadata): Promise<LocalUtilsHarUnzipResult>;
connect(params: LocalUtilsConnectParams, metadata?: CallMetadata): Promise<LocalUtilsConnectResult>; connect(params: LocalUtilsConnectParams, metadata?: CallMetadata): Promise<LocalUtilsConnectResult>;
tracingStarted(params: LocalUtilsTracingStartedParams, metadata?: CallMetadata): Promise<LocalUtilsTracingStartedResult>;
addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata): Promise<LocalUtilsAddStackToTracingNoReplyResult>;
traceDiscarded(params: LocalUtilsTraceDiscardedParams, metadata?: CallMetadata): Promise<LocalUtilsTraceDiscardedResult>;
} }
export type LocalUtilsZipParams = { export type LocalUtilsZipParams = {
zipFile: string, zipFile: string,
entries: NameValue[], entries: NameValue[],
stacksId?: string,
mode: 'write' | 'append', mode: 'write' | 'append',
metadata: ClientSideCallMetadata[],
includeSources: boolean, includeSources: boolean,
}; };
export type LocalUtilsZipOptions = { export type LocalUtilsZipOptions = {
stacksId?: string,
}; };
export type LocalUtilsZipResult = void; export type LocalUtilsZipResult = void;
export type LocalUtilsHarOpenParams = { export type LocalUtilsHarOpenParams = {
@ -476,6 +479,30 @@ export type LocalUtilsConnectOptions = {
export type LocalUtilsConnectResult = { export type LocalUtilsConnectResult = {
pipe: JsonPipeChannel, pipe: JsonPipeChannel,
}; };
export type LocalUtilsTracingStartedParams = {
tracesDir?: string,
traceName: string,
};
export type LocalUtilsTracingStartedOptions = {
tracesDir?: string,
};
export type LocalUtilsTracingStartedResult = {
stacksId: string,
};
export type LocalUtilsAddStackToTracingNoReplyParams = {
callData: ClientSideCallMetadata,
};
export type LocalUtilsAddStackToTracingNoReplyOptions = {
};
export type LocalUtilsAddStackToTracingNoReplyResult = void;
export type LocalUtilsTraceDiscardedParams = {
stacksId: string,
};
export type LocalUtilsTraceDiscardedOptions = {
};
export type LocalUtilsTraceDiscardedResult = void;
export interface LocalUtilsEvents { export interface LocalUtilsEvents {
} }
@ -3756,7 +3783,9 @@ export type TracingTracingStartChunkOptions = {
name?: string, name?: string,
title?: string, title?: string,
}; };
export type TracingTracingStartChunkResult = void; export type TracingTracingStartChunkResult = {
traceName: string,
};
export type TracingTracingStopChunkParams = { export type TracingTracingStopChunkParams = {
mode: 'archive' | 'discard' | 'entries', mode: 'archive' | 'discard' | 'entries',
}; };

View file

@ -500,14 +500,12 @@ LocalUtils:
entries: entries:
type: array type: array
items: NameValue items: NameValue
stacksId: string?
mode: mode:
type: enum type: enum
literals: literals:
- write - write
- append - append
metadata:
type: array
items: ClientSideCallMetadata
includeSources: boolean includeSources: boolean
harOpen: harOpen:
@ -563,6 +561,21 @@ LocalUtils:
returns: returns:
pipe: JsonPipe pipe: JsonPipe
tracingStarted:
parameters:
tracesDir: string?
traceName: string
returns:
stacksId: string
addStackToTracingNoReply:
parameters:
callData: ClientSideCallMetadata
traceDiscarded:
parameters:
stacksId: string
Root: Root:
type: interface type: interface
@ -2927,6 +2940,8 @@ Tracing:
parameters: parameters:
name: string? name: string?
title: string? title: string?
returns:
traceName: string
tracingStopChunk: tracingStopChunk:
parameters: parameters:

View file

@ -18,6 +18,8 @@ import { test, expect } from './playwright-test-fixtures';
import { parseTrace } from '../config/utils'; import { parseTrace } from '../config/utils';
import fs from 'fs'; import fs from 'fs';
test.describe.configure({ mode: 'parallel' });
test('should stop tracing with trace: on-first-retry, when not retrying', async ({ runInlineTest }, testInfo) => { test('should stop tracing with trace: on-first-retry, when not retrying', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.ts': ` 'playwright.config.ts': `