Skip to content

Commit 030e7d2

Browse files
authored
chore(har): allow replaying from zip har (#14962)
1 parent 822b86d commit 030e7d2

File tree

7 files changed

+146
-152
lines changed

7 files changed

+146
-152
lines changed

packages/playwright-core/src/client/harRouter.ts

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,48 +18,89 @@ import fs from 'fs';
1818
import type { HAREntry, HARFile, HARResponse } from '../../types/types';
1919
import { debugLogger } from '../common/debugLogger';
2020
import { rewriteErrorMessage } from '../utils/stackTrace';
21+
import { ZipFile } from '../utils/zipFile';
2122
import type { BrowserContext } from './browserContext';
23+
import { Events } from './events';
2224
import type { Route } from './network';
2325
import type { BrowserContextOptions } from './types';
2426

2527
type HarOptions = NonNullable<BrowserContextOptions['har']>;
2628

2729
export class HarRouter {
2830
private _pattern: string | RegExp;
29-
private _handler: (route: Route) => Promise<void>;
31+
private _harFile: HARFile;
32+
private _zipFile: ZipFile | null;
33+
private _options: HarOptions | undefined;
3034

3135
static async create(options: HarOptions): Promise<HarRouter> {
36+
if (options.path.endsWith('.zip')) {
37+
const zipFile = new ZipFile(options.path);
38+
const har = await zipFile.read('har.har');
39+
const harFile = JSON.parse(har.toString()) as HARFile;
40+
return new HarRouter(harFile, zipFile, options);
41+
}
3242
const harFile = JSON.parse(await fs.promises.readFile(options.path, 'utf-8')) as HARFile;
33-
return new HarRouter(harFile, options);
43+
return new HarRouter(harFile, null, options);
3444
}
3545

36-
constructor(harFile: HARFile, options?: HarOptions) {
46+
constructor(harFile: HARFile, zipFile: ZipFile | null, options?: HarOptions) {
47+
this._harFile = harFile;
48+
this._zipFile = zipFile;
3749
this._pattern = options?.urlFilter ?? /.*/;
38-
this._handler = async (route: Route) => {
39-
let response;
40-
try {
41-
response = harFindResponse(harFile, {
42-
url: route.request().url(),
43-
method: route.request().method()
50+
this._options = options;
51+
}
52+
53+
private async _handle(route: Route) {
54+
let response;
55+
try {
56+
response = harFindResponse(this._harFile, {
57+
url: route.request().url(),
58+
method: route.request().method()
59+
});
60+
} catch (e) {
61+
rewriteErrorMessage(e, `Error while finding entry for ${route.request().method()} ${route.request().url()} in HAR file:\n${e.message}`);
62+
debugLogger.log('api', e);
63+
}
64+
65+
if (response) {
66+
debugLogger.log('api', `serving from HAR: ${route.request().method()} ${route.request().url()}`);
67+
const sha1 = (response.content as any)._sha1;
68+
69+
if (this._zipFile && sha1) {
70+
const body = await this._zipFile.read(sha1).catch(() => {
71+
debugLogger.log('api', `payload ${sha1} for request ${route.request().url()} is not found in archive`);
72+
return null;
4473
});
45-
} catch (e) {
46-
rewriteErrorMessage(e, `Error while finding entry for ${route.request().method()} ${route.request().url()} in HAR file:\n${e.message}`);
47-
debugLogger.log('api', e);
74+
if (body) {
75+
await route.fulfill({
76+
status: response.status,
77+
headers: Object.fromEntries(response.headers.map(h => [h.name, h.value])),
78+
body
79+
});
80+
return;
81+
}
4882
}
49-
if (response) {
50-
debugLogger.log('api', `serving from HAR: ${route.request().method()} ${route.request().url()}`);
51-
await route.fulfill({ response });
52-
} else if (options?.fallback === 'continue') {
53-
await route.fallback();
54-
} else {
55-
debugLogger.log('api', `request not in HAR, aborting: ${route.request().method()} ${route.request().url()}`);
56-
await route.abort();
57-
}
58-
};
83+
84+
await route.fulfill({ response });
85+
return;
86+
}
87+
88+
if (this._options?.fallback === 'continue') {
89+
await route.fallback();
90+
return;
91+
}
92+
93+
debugLogger.log('api', `request not in HAR, aborting: ${route.request().method()} ${route.request().url()}`);
94+
await route.abort();
5995
}
6096

6197
async addRoute(context: BrowserContext) {
62-
await context.route(this._pattern, this._handler);
98+
await context.route(this._pattern, route => this._handle(route));
99+
context.once(Events.BrowserContext.Close, () => this.dispose());
100+
}
101+
102+
dispose() {
103+
this._zipFile?.close();
63104
}
64105
}
65106

packages/playwright-core/src/utils/DEPS.list

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
../third_party/diff_match_patch
44
../third_party/pixelmatch
55
../utilsBundle.ts
6+
../zipBundle.ts
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { yauzl } from '../zipBundle';
18+
import type { UnzipFile, Entry } from '../zipBundle';
19+
20+
export class ZipFile {
21+
private _fileName: string;
22+
private _zipFile: UnzipFile | undefined;
23+
private _entries = new Map<string, Entry>();
24+
private _openedPromise: Promise<void>;
25+
26+
constructor(fileName: string) {
27+
this._fileName = fileName;
28+
this._openedPromise = this._open();
29+
}
30+
31+
private async _open() {
32+
await new Promise<UnzipFile>((fulfill, reject) => {
33+
yauzl.open(this._fileName, { autoClose: false }, (e, z) => {
34+
if (e) {
35+
reject(e);
36+
return;
37+
}
38+
this._zipFile = z;
39+
this._zipFile!.on('entry', (entry: Entry) => {
40+
this._entries.set(entry.fileName, entry);
41+
});
42+
this._zipFile!.on('end', fulfill);
43+
});
44+
});
45+
}
46+
47+
async entries(): Promise<string[]> {
48+
await this._openedPromise;
49+
return [...this._entries.keys()];
50+
}
51+
52+
async read(entryPath: string): Promise<Buffer> {
53+
await this._openedPromise;
54+
const entry = this._entries.get(entryPath)!;
55+
if (!entry)
56+
throw new Error(`${entryPath} not found in file ${this._fileName}`);
57+
58+
return new Promise((resolve, reject) => {
59+
this._zipFile!.openReadStream(entry, (error, readStream) => {
60+
if (error || !readStream) {
61+
reject(error || 'Entry not found');
62+
return;
63+
}
64+
65+
const buffers: Buffer[] = [];
66+
readStream.on('data', data => buffers.push(data));
67+
readStream.on('end', () => resolve(Buffer.concat(buffers)));
68+
});
69+
});
70+
}
71+
72+
close() {
73+
this._zipFile?.close();
74+
}
75+
}

packages/playwright-core/src/zipBundle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@
1717
export const yazl: typeof import('../bundles/zip/node_modules/@types/yazl') = require('./zipBundleImpl').yazl;
1818
export type { ZipFile } from '../bundles/zip/node_modules/@types/yazl';
1919
export const yauzl: typeof import('../bundles/zip/node_modules/@types/yauzl') = require('./zipBundleImpl').yauzl;
20+
export type { ZipFile as UnzipFile, Entry } from '../bundles/zip/node_modules/@types/yauzl';
2021
export const extract: typeof import('../bundles/zip/node_modules/extract-zip') = require('./zipBundleImpl').extract;

tests/config/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { expect } from '@playwright/test';
1818
import type { Frame, Page } from 'playwright-core';
19-
import { ZipFileSystem } from './vfs';
19+
import { ZipFile } from '../../packages/playwright-core/lib/utils/zipFile';
2020

2121
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
2222
const handle = await page.evaluateHandle(async ({ frameId, url }) => {
@@ -91,7 +91,7 @@ export function suppressCertificateWarning() {
9191
}
9292

9393
export async function parseTrace(file: string): Promise<{ events: any[], resources: Map<string, Buffer> }> {
94-
const zipFS = new ZipFileSystem(file);
94+
const zipFS = new ZipFile(file);
9595
const resources = new Map<string, Buffer>();
9696
for (const entry of await zipFS.entries())
9797
resources.set(entry, await zipFS.read(entry));
@@ -113,7 +113,7 @@ export async function parseTrace(file: string): Promise<{ events: any[], resourc
113113
}
114114

115115
export async function parseHar(file: string): Promise<Map<string, Buffer>> {
116-
const zipFS = new ZipFileSystem(file);
116+
const zipFS = new ZipFile(file);
117117
const resources = new Map<string, Buffer>();
118118
for (const entry of await zipFS.entries())
119119
resources.set(entry, await zipFS.read(entry));

tests/config/vfs.ts

Lines changed: 0 additions & 124 deletions
This file was deleted.

tests/playwright-test/playwright.trace.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { test, expect } from './playwright-test-fixtures';
18-
import { ZipFileSystem } from '../config/vfs';
18+
import { ZipFile } from '../../packages/playwright-core/lib/utils/zipFile';
1919
import fs from 'fs';
2020

2121
test('should stop tracing with trace: on-first-retry, when not retrying', async ({ runInlineTest }, testInfo) => {
@@ -243,7 +243,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve
243243

244244

245245
async function parseTrace(file: string): Promise<Map<string, Buffer>> {
246-
const zipFS = new ZipFileSystem(file);
246+
const zipFS = new ZipFile(file);
247247
const resources = new Map<string, Buffer>();
248248
for (const entry of await zipFS.entries())
249249
resources.set(entry, await zipFS.read(entry));

0 commit comments

Comments
 (0)