Skip to content

Commit 6896c34

Browse files
authored
chore: support opening .trace files via .link indirection (#39975)
1 parent 6992456 commit 6896c34

7 files changed

Lines changed: 64 additions & 20 deletions

File tree

packages/playwright-core/src/cli/program.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ program
249249
.action(async options => {
250250
const { program: cliProgram } = await import('../tools/cli-client/program');
251251
process.argv.splice(process.argv.indexOf('cli'), 1);
252-
cliProgram();
252+
cliProgram().catch(logErrorAndExit);
253253
});
254254

255255
function logErrorAndExit(e: Error) {

packages/playwright-core/src/tools/backend/tracing.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ const tracingStop = defineTool({
6464
await browserContext.tracing.stop();
6565
// eslint-disable-next-line no-restricted-syntax
6666
const traceLegend = (browserContext.tracing as any)[traceLegendSymbol];
67+
if (!traceLegend)
68+
throw new Error('Tracing is not started');
69+
// eslint-disable-next-line no-restricted-syntax
70+
delete (browserContext.tracing as any)[traceLegendSymbol];
71+
6772
response.addTextResult(`Trace recording stopped.`);
6873
response.addFileLink('Trace', `${traceLegend.tracesDir}/${traceLegend.name}.trace`);
6974
response.addFileLink('Network log', `${traceLegend.tracesDir}/${traceLegend.name}.network`);

packages/playwright-core/src/tools/trace/traceUtils.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,28 @@ export async function openTrace(traceFile: string) {
6868
throw new Error(`Trace file not found: ${filePath}`);
6969
await closeTrace();
7070
await fs.promises.mkdir(traceDir, { recursive: true });
71-
await extractTrace(filePath, traceDir);
71+
if (filePath.endsWith('.zip'))
72+
await extractTrace(filePath, traceDir);
73+
else
74+
await fs.promises.writeFile(path.join(traceDir, '.link'), filePath, 'utf-8');
7275
}
7376

7477
export async function loadTrace(): Promise<LoadedTrace> {
7578
const dir = ensureTraceOpen();
76-
const backend = new DirTraceLoaderBackend(dir);
79+
const linkFile = path.join(dir, '.link');
80+
let traceDir: string;
81+
let traceFile: string | undefined;
82+
if (fs.existsSync(linkFile)) {
83+
const tracePath = await fs.promises.readFile(linkFile, 'utf-8');
84+
traceDir = path.dirname(tracePath);
85+
traceFile = path.basename(tracePath);
86+
} else {
87+
traceDir = dir;
88+
}
89+
const backend = new DirTraceLoaderBackend(traceDir);
7790
const loader = new TraceLoader();
78-
await loader.load(backend, () => undefined);
79-
const model = new TraceModel(dir, loader.contextEntries);
91+
await loader.load(backend, traceFile);
92+
const model = new TraceModel(traceDir, loader.contextEntries);
8093
return new LoadedTrace(model, loader, buildOrdinalMap(model));
8194
}
8295

packages/playwright-core/src/utils/isomorphic/trace/traceLoader.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,38 +38,39 @@ export class TraceLoader {
3838
constructor() {
3939
}
4040

41-
async load(backend: TraceLoaderBackend, unzipProgress: (done: number, total: number) => void) {
41+
async load(backend: TraceLoaderBackend, traceFile?: string, unzipProgress?: (done: number, total: number) => void) {
4242
this._backend = backend;
4343

44-
const ordinals: string[] = [];
44+
const prefix = traceFile?.match(/(.+)\.trace$/)?.[1];
45+
const prefixes: string[] = [];
4546
let hasSource = false;
4647
for (const entryName of await this._backend.entryNames()) {
4748
const match = entryName.match(/(.+)\.trace$/);
48-
if (match)
49-
ordinals.push(match[1] || '');
49+
if (match && (!prefix || prefix === match[1]))
50+
prefixes.push(match[1] || '');
5051
if (entryName.includes('src@'))
5152
hasSource = true;
5253
}
53-
if (!ordinals.length)
54+
if (!prefixes.length)
5455
throw new Error('Cannot find .trace file');
5556

5657
this._snapshotStorage = new SnapshotStorage();
5758

5859
// 3 * ordinals progress increments below.
59-
const total = ordinals.length * 3;
60+
const total = prefixes.length * 3;
6061
let done = 0;
61-
for (const ordinal of ordinals) {
62+
for (const prefix of prefixes) {
6263
const contextEntry = createEmptyContext();
6364
contextEntry.hasSource = hasSource;
6465
const modernizer = new TraceModernizer(contextEntry, this._snapshotStorage);
6566

66-
const trace = await this._backend.readText(ordinal + '.trace') || '';
67+
const trace = await this._backend.readText(prefix + '.trace') || '';
6768
modernizer.appendTrace(trace);
68-
unzipProgress(++done, total);
69+
unzipProgress?.(++done, total);
6970

70-
const network = await this._backend.readText(ordinal + '.network') || '';
71+
const network = await this._backend.readText(prefix + '.network') || '';
7172
modernizer.appendTrace(network);
72-
unzipProgress(++done, total);
73+
unzipProgress?.(++done, total);
7374

7475
contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);
7576

@@ -87,13 +88,13 @@ export class TraceLoader {
8788
}
8889
}
8990

90-
const stacks = await this._backend.readText(ordinal + '.stacks');
91+
const stacks = await this._backend.readText(prefix + '.stacks');
9192
if (stacks) {
9293
const callMetadata = parseClientSideCallMetadata(JSON.parse(stacks));
9394
for (const action of contextEntry.actions)
9495
action.stack = action.stack || callMetadata.get(action.callId);
9596
}
96-
unzipProgress(++done, total);
97+
unzipProgress?.(++done, total);
9798

9899
for (const resource of contextEntry.resources) {
99100
if (resource.request.postData?._sha1)

packages/trace-viewer/src/sw/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ async function innerLoadTrace(traceUri: string, progress: Progress): Promise<Loa
111111
// Allow 10% to hop from sw to page.
112112
const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]);
113113
const backend = isLiveTrace(traceUri) || traceUri.endsWith('traces.dir') ? new FetchTraceLoaderBackend(traceUri) : new ZipTraceLoaderBackend(traceUri, fetchProgress);
114-
await traceLoader.load(backend, unzipProgress);
114+
await traceLoader.load(backend, undefined, unzipProgress);
115115
} catch (error: any) {
116116
// eslint-disable-next-line no-console
117117
console.error(error);

tests/config/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export async function parseTrace(file: string): Promise<{ snapshots: SnapshotSto
170170
await extractTrace(file, dir);
171171
const backend = new DirTraceLoaderBackend(dir);
172172
const loader = new TraceLoader();
173-
await loader.load(backend, () => {});
173+
await loader.load(backend);
174174
return { model: new TraceModel(dir, loader.contextEntries), snapshots: loader.storage() };
175175
}
176176

tests/mcp/trace-cli.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,31 @@ test('trace close removes extracted trace', async ({ traceFile, runTraceCli }) =
203203
expect(exitCode3).toBe(0);
204204
});
205205

206+
test('trace open with .trace file creates link', async ({ traceFile, traceCwd, runTraceCli }) => {
207+
// Extract the zip using the CLI to get raw trace files.
208+
const extractCwd = path.join(path.dirname(traceFile), 'extracted');
209+
fs.mkdirSync(extractCwd, { recursive: true });
210+
const cliPath = path.resolve(__dirname, '../../packages/playwright-core/cli.js');
211+
const { execFileSync } = require('child_process');
212+
execFileSync(process.execPath, [cliPath, 'trace', 'open', traceFile], { cwd: extractCwd });
213+
const extractDir = path.join(extractCwd, '.playwright-cli', 'trace');
214+
215+
const traceEntries = fs.readdirSync(extractDir).filter((f: string) => f.endsWith('.trace'));
216+
expect(traceEntries.length).toBeGreaterThan(0);
217+
const dotTraceFile = path.join(extractDir, traceEntries[0]);
218+
const { exitCode } = await runTraceCli(['open', dotTraceFile]);
219+
expect(exitCode).toBe(0);
220+
221+
const linkFile = path.join(traceCwd, '.playwright-cli', 'trace', '.link');
222+
expect(fs.existsSync(linkFile)).toBe(true);
223+
expect(fs.readFileSync(linkFile, 'utf-8')).toBe(dotTraceFile);
224+
225+
const { stdout: actionsOutput, exitCode: actionsExitCode } = await runTraceCli(['actions']);
226+
expect(actionsExitCode).toBe(0);
227+
expect(actionsOutput).toContain('Navigate');
228+
expect(actionsOutput).toContain('Click');
229+
});
230+
206231
test('trace attachments lists attachments', async ({ runTraceCli }) => {
207232
const { stdout, exitCode } = await runTraceCli(['attachments']);
208233
expect(exitCode).toBe(0);

0 commit comments

Comments
 (0)