@empiricalrun/playwright-utils 0.58.2 → 0.58.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +12 -2
- package/dist/reporter/harness.d.ts +36 -0
- package/dist/reporter/harness.d.ts.map +1 -1
- package/dist/reporter/harness.js +81 -2
- package/dist/reporter/ibr-utils.d.ts +6 -0
- package/dist/reporter/ibr-utils.d.ts.map +1 -1
- package/dist/reporter/ibr-utils.js +15 -1
- package/dist/reporter/incremental-blob-reporter.d.ts +6 -1
- package/dist/reporter/incremental-blob-reporter.d.ts.map +1 -1
- package/dist/reporter/incremental-blob-reporter.js +30 -6
- package/dist/reporter/lifecycle-events.d.ts +5 -3
- package/dist/reporter/lifecycle-events.d.ts.map +1 -1
- package/dist/reporter/lifecycle-events.js +54 -3
- package/dist/reporter/parity-utils.d.ts +63 -0
- package/dist/reporter/parity-utils.d.ts.map +1 -0
- package/dist/reporter/parity-utils.js +109 -0
- package/package.json +2 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/upstream-oracle/README.md +48 -0
- package/upstream-oracle/ibr-adapter.js +72 -0
- package/upstream-oracle/intermediate-mode-ibr.patch +16 -0
- package/upstream-oracle/run.mjs +231 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @empiricalrun/playwright-utils
|
|
2
2
|
|
|
3
|
+
## 0.58.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- edef32d: feat: env var to disable the native blob reporter
|
|
8
|
+
- 9f1f2d1: feat: incremental blob reporter emits step-level annotations and attachments, falls back to config.shard when shard env vars are unset
|
|
9
|
+
- 760771b: feat: incremental blob reporter emits stdio, run-level errors, body attachments and annotations
|
|
10
|
+
- 8b883dd: feat: incremental blob reporter uploads on clean finish, with a completeness marker
|
|
11
|
+
|
|
3
12
|
## 0.58.2
|
|
4
13
|
|
|
5
14
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAgBzD,wBAAgB,gBAAgB,IAAI,MAAM,CAazC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAgBzD,wBAAgB,gBAAgB,IAAI,MAAM,CAazC;AA+CD,eAAO,MAAM,UAAU,EAAE,oBA+BxB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,iBAarB,CAAC"}
|
package/dist/config/index.js
CHANGED
|
@@ -39,8 +39,14 @@ function getReporters() {
|
|
|
39
39
|
if (process.env.HAS_SHARDING && !process.env.SHARD_INDEX) {
|
|
40
40
|
throw new Error("Invalid env variables: HAS_SHARDING needs SHARD_INDEX");
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// Set from the ibr_only_blob_reporter feature flag by test-run-worker: the
|
|
43
|
+
// incremental blob reporter's clean-finish upload (with its completeness
|
|
44
|
+
// marker) is the merge input, so the native blob reporter would only write
|
|
45
|
+
// a zip the merge step discards as superseded.
|
|
46
|
+
const ibrOnly = process.env.IBR_ONLY_BLOB_REPORTER === "true";
|
|
47
|
+
const nativeBlob = ibrOnly
|
|
48
|
+
? []
|
|
49
|
+
: [
|
|
44
50
|
[
|
|
45
51
|
"blob",
|
|
46
52
|
{
|
|
@@ -48,6 +54,10 @@ function getReporters() {
|
|
|
48
54
|
fileName: `report-${process.env.SHARD_INDEX}.zip`,
|
|
49
55
|
},
|
|
50
56
|
],
|
|
57
|
+
];
|
|
58
|
+
const middle = process.env.HAS_SHARDING
|
|
59
|
+
? [
|
|
60
|
+
...nativeBlob,
|
|
51
61
|
// Salvage path for a blob report that never gets written: on SIGINT the
|
|
52
62
|
// blob reporter can be cut off mid-run, and this one has already been
|
|
53
63
|
// streaming events to disk. It is only useful where a merge step exists
|
|
@@ -92,4 +92,40 @@ export interface BlobEvent {
|
|
|
92
92
|
* Reads `report.jsonl` from the blob zip and returns parsed events.
|
|
93
93
|
*/
|
|
94
94
|
export declare function readBlobEvents(ctx: IBRTestContext): Promise<BlobEvent[]>;
|
|
95
|
+
/**
|
|
96
|
+
* Reads `report.jsonl` from any blob-format zip (standard or incremental).
|
|
97
|
+
*/
|
|
98
|
+
export declare function readEventsFromZip(zipPath: string): Promise<BlobEvent[]>;
|
|
99
|
+
export interface ParityTestContext {
|
|
100
|
+
tempDir: string;
|
|
101
|
+
/** Playwright's built-in blob reporter output: blob-report/report-1.zip */
|
|
102
|
+
standardZipPath: string;
|
|
103
|
+
standardBlobDir: string;
|
|
104
|
+
/** IBR output: blob-report-incremental/incremental-report-1.zip */
|
|
105
|
+
incrementalZipPath: string;
|
|
106
|
+
incrementalBlobDir: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Sets up a fixture whose config runs BOTH the built-in blob reporter and the
|
|
110
|
+
* IBR on the same invocation, so one event stream feeds both zips.
|
|
111
|
+
*/
|
|
112
|
+
export declare function setupParityFixture(suite: string): Promise<{
|
|
113
|
+
ctx: ParityTestContext;
|
|
114
|
+
cleanup: () => Promise<void>;
|
|
115
|
+
}>;
|
|
116
|
+
export interface CleanRunResult extends RunResult {
|
|
117
|
+
exitCode: number | null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Spawns `playwright test` and lets it run to completion — the clean-finish
|
|
121
|
+
* half of the matrix (the SIGINT helpers above cover interruption).
|
|
122
|
+
*/
|
|
123
|
+
export declare function runPlaywrightClean(ctx: ParityTestContext, opts: {
|
|
124
|
+
workers: number;
|
|
125
|
+
}): Promise<CleanRunResult>;
|
|
126
|
+
/**
|
|
127
|
+
* Runs `playwright merge-reports <blobDir> --reporter json` and returns the
|
|
128
|
+
* parsed summary. Distinct output names let one temp dir hold both merges.
|
|
129
|
+
*/
|
|
130
|
+
export declare function mergeBlobDirToSummary(cwd: string, blobDir: string, outputName: string): Record<string, unknown>;
|
|
95
131
|
//# sourceMappingURL=harness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/reporter/harness.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/reporter/harness.ts"],"names":[],"mappings":"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"}
|
package/dist/reporter/harness.js
CHANGED
|
@@ -12,6 +12,10 @@ exports.runPlaywrightWithSigint = runPlaywrightWithSigint;
|
|
|
12
12
|
exports.markerNames = markerNames;
|
|
13
13
|
exports.runPlaywrightList = runPlaywrightList;
|
|
14
14
|
exports.readBlobEvents = readBlobEvents;
|
|
15
|
+
exports.readEventsFromZip = readEventsFromZip;
|
|
16
|
+
exports.setupParityFixture = setupParityFixture;
|
|
17
|
+
exports.runPlaywrightClean = runPlaywrightClean;
|
|
18
|
+
exports.mergeBlobDirToSummary = mergeBlobDirToSummary;
|
|
15
19
|
const zip_1 = require("@empiricalrun/r2-uploader/zip");
|
|
16
20
|
const child_process_1 = require("child_process");
|
|
17
21
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -250,9 +254,15 @@ async function runPlaywrightList(ctx) {
|
|
|
250
254
|
* Reads `report.jsonl` from the blob zip and returns parsed events.
|
|
251
255
|
*/
|
|
252
256
|
async function readBlobEvents(ctx) {
|
|
253
|
-
|
|
257
|
+
return readEventsFromZip(ctx.zipPath);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Reads `report.jsonl` from any blob-format zip (standard or incremental).
|
|
261
|
+
*/
|
|
262
|
+
async function readEventsFromZip(zipPath) {
|
|
263
|
+
const reportBuffer = await (0, zip_1.readZipEntry)(zipPath, "report.jsonl");
|
|
254
264
|
if (!reportBuffer) {
|
|
255
|
-
throw new Error(`report.jsonl not found in zip: ${
|
|
265
|
+
throw new Error(`report.jsonl not found in zip: ${zipPath}`);
|
|
256
266
|
}
|
|
257
267
|
return reportBuffer
|
|
258
268
|
.toString("utf8")
|
|
@@ -260,3 +270,72 @@ async function readBlobEvents(ctx) {
|
|
|
260
270
|
.split("\n")
|
|
261
271
|
.map((line) => JSON.parse(line));
|
|
262
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Sets up a fixture whose config runs BOTH the built-in blob reporter and the
|
|
275
|
+
* IBR on the same invocation, so one event stream feeds both zips.
|
|
276
|
+
*/
|
|
277
|
+
async function setupParityFixture(suite) {
|
|
278
|
+
const { tempDir, cleanup } = await setupReporterFixture({
|
|
279
|
+
suite,
|
|
280
|
+
reporterPath: REPORTER_PATH,
|
|
281
|
+
tempPrefix: "blob-ibr-parity-",
|
|
282
|
+
});
|
|
283
|
+
const standardBlobDir = path_1.default.join(tempDir, "blob-report");
|
|
284
|
+
const incrementalBlobDir = path_1.default.join(tempDir, "blob-report-incremental");
|
|
285
|
+
return {
|
|
286
|
+
ctx: {
|
|
287
|
+
tempDir,
|
|
288
|
+
standardBlobDir,
|
|
289
|
+
standardZipPath: path_1.default.join(standardBlobDir, "report-1.zip"),
|
|
290
|
+
incrementalBlobDir,
|
|
291
|
+
incrementalZipPath: path_1.default.join(incrementalBlobDir, "incremental-report-1.zip"),
|
|
292
|
+
},
|
|
293
|
+
cleanup,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Spawns `playwright test` and lets it run to completion — the clean-finish
|
|
298
|
+
* half of the matrix (the SIGINT helpers above cover interruption).
|
|
299
|
+
*/
|
|
300
|
+
async function runPlaywrightClean(ctx, opts) {
|
|
301
|
+
const child = (0, child_process_1.spawn)(PLAYWRIGHT_BIN, ["test", `--workers=${opts.workers}`], {
|
|
302
|
+
cwd: ctx.tempDir,
|
|
303
|
+
env: {
|
|
304
|
+
...process.env,
|
|
305
|
+
LOCAL_TEST: "false",
|
|
306
|
+
SHARD_INDEX: "1",
|
|
307
|
+
TOTAL_SHARDS: "1",
|
|
308
|
+
FORCE_COLOR: "0",
|
|
309
|
+
},
|
|
310
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
311
|
+
});
|
|
312
|
+
let stdout = "";
|
|
313
|
+
let stderr = "";
|
|
314
|
+
child.stdout?.on("data", (data) => {
|
|
315
|
+
stdout += data.toString();
|
|
316
|
+
});
|
|
317
|
+
child.stderr?.on("data", (data) => {
|
|
318
|
+
stderr += data.toString();
|
|
319
|
+
});
|
|
320
|
+
const exitCode = await new Promise((resolve) => {
|
|
321
|
+
child.on("close", (code) => resolve(code));
|
|
322
|
+
});
|
|
323
|
+
return { stdout, stderr, exitCode };
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Runs `playwright merge-reports <blobDir> --reporter json` and returns the
|
|
327
|
+
* parsed summary. Distinct output names let one temp dir hold both merges.
|
|
328
|
+
*/
|
|
329
|
+
function mergeBlobDirToSummary(cwd, blobDir, outputName) {
|
|
330
|
+
const outputPath = path_1.default.join(cwd, outputName);
|
|
331
|
+
(0, child_process_1.execFileSync)(PLAYWRIGHT_BIN, ["merge-reports", blobDir, "--reporter", "json"], {
|
|
332
|
+
cwd,
|
|
333
|
+
env: {
|
|
334
|
+
...process.env,
|
|
335
|
+
PLAYWRIGHT_JSON_OUTPUT_NAME: outputPath,
|
|
336
|
+
FORCE_COLOR: "0",
|
|
337
|
+
},
|
|
338
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
339
|
+
});
|
|
340
|
+
return JSON.parse(fs_1.default.readFileSync(outputPath, "utf8"));
|
|
341
|
+
}
|
|
@@ -32,6 +32,12 @@ export type JsonSuite = {
|
|
|
32
32
|
location: JsonLocation;
|
|
33
33
|
entries: (JsonTestCase | JsonSuite)[];
|
|
34
34
|
};
|
|
35
|
+
export type TestAnnotation = {
|
|
36
|
+
type: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
location?: JsonLocation;
|
|
39
|
+
};
|
|
40
|
+
export declare function relativeAnnotationLocations(annotations: TestAnnotation[], rootDir: string): TestAnnotation[];
|
|
35
41
|
export declare function serializeTest(test: TestCase, rootDir: string): JsonTestCase;
|
|
36
42
|
export declare function serializeSuite(suite: Suite, rootDir: string): JsonSuite;
|
|
37
43
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ibr-utils.d.ts","sourceRoot":"","sources":["../../src/reporter/ibr-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAO3E,MAAM,MAAM,WAAW,GAAG;IACxB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACvC,CAAC;AAIF,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAChD,WAAW,EAAE,CAOf;AAED,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAO3E;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,EAC1B,IAAI,EAAE,QAAQ,GACb,MAAM,CAOR;AAID,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAIF,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACvD,CAAC;AAIF,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,CAAC,YAAY,GAAG,SAAS,CAAC,EAAE,CAAC;CACvC,CAAC;AAIF,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CAc3E;AAID,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAsBvE;AAMD;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,SAAS,EAChB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,GAC5B,SAAS,GAAG,IAAI,CAqBlB;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAK7D;AAED,wBAAgB,aAAa,CAC3B,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAI3D;AAED,wBAAgB,eAAe,CAC7B,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAoBf"}
|
|
1
|
+
{"version":3,"file":"ibr-utils.d.ts","sourceRoot":"","sources":["../../src/reporter/ibr-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAO3E,MAAM,MAAM,WAAW,GAAG;IACxB,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACvC,CAAC;AAIF,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAChD,WAAW,EAAE,CAOf;AAED,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAO3E;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,EAC1B,IAAI,EAAE,QAAQ,GACb,MAAM,CAOR;AAID,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAIF,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACvD,CAAC;AAIF,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,CAAC,YAAY,GAAG,SAAS,CAAC,EAAE,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,YAAY,CAAC;CACzB,CAAC;AAIF,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,cAAc,EAAE,EAC7B,OAAO,EAAE,MAAM,GACd,cAAc,EAAE,CAUlB;AAID,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CAc3E;AAID,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAsBvE;AAMD;;;;GAIG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,SAAS,EAChB,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,GAC5B,SAAS,GAAG,IAAI,CAqBlB;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAK7D;AAED,wBAAgB,aAAa,CAC3B,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAED,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAI3D;AAED,wBAAgB,eAAe,CAC7B,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAoBf"}
|
|
@@ -7,6 +7,7 @@ exports.serializeRegexPatterns = serializeRegexPatterns;
|
|
|
7
7
|
exports.generateId = generateId;
|
|
8
8
|
exports.getOrCreateId = getOrCreateId;
|
|
9
9
|
exports.getOrCreateStepId = getOrCreateStepId;
|
|
10
|
+
exports.relativeAnnotationLocations = relativeAnnotationLocations;
|
|
10
11
|
exports.serializeTest = serializeTest;
|
|
11
12
|
exports.serializeSuite = serializeSuite;
|
|
12
13
|
exports.filterSuiteByCompletedTests = filterSuiteByCompletedTests;
|
|
@@ -45,6 +46,19 @@ function getOrCreateStepId(map, step) {
|
|
|
45
46
|
}
|
|
46
47
|
return id;
|
|
47
48
|
}
|
|
49
|
+
// Mirrors _relativeAnnotationLocations from Playwright's TeleReporterEmitter
|
|
50
|
+
// See: packages/playwright/src/reporters/teleEmitter.ts
|
|
51
|
+
function relativeAnnotationLocations(annotations, rootDir) {
|
|
52
|
+
return annotations.map((annotation) => ({
|
|
53
|
+
...annotation,
|
|
54
|
+
location: annotation.location
|
|
55
|
+
? {
|
|
56
|
+
...annotation.location,
|
|
57
|
+
file: path_1.default.relative(rootDir, annotation.location.file),
|
|
58
|
+
}
|
|
59
|
+
: undefined,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
48
62
|
// Mirrors _serializeTest from Playwright's TeleReporterEmitter
|
|
49
63
|
// See: packages/playwright/src/reporters/teleEmitter.ts
|
|
50
64
|
function serializeTest(test, rootDir) {
|
|
@@ -59,7 +73,7 @@ function serializeTest(test, rootDir) {
|
|
|
59
73
|
retries: test.retries,
|
|
60
74
|
tags: test.tags,
|
|
61
75
|
repeatEachIndex: test.repeatEachIndex,
|
|
62
|
-
annotations: test.annotations,
|
|
76
|
+
annotations: relativeAnnotationLocations(test.annotations, rootDir),
|
|
63
77
|
};
|
|
64
78
|
}
|
|
65
79
|
// Mirrors _serializeSuite from Playwright's TeleReporterEmitter
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult, TestStep } from "@playwright/test/reporter";
|
|
1
|
+
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestError, TestResult, TestStep, WorkerInfo } from "@playwright/test/reporter";
|
|
2
2
|
/**
|
|
3
3
|
* IncrementalBlobReporter
|
|
4
4
|
*
|
|
@@ -25,6 +25,7 @@ declare class IncrementalBlobReporter implements Reporter {
|
|
|
25
25
|
private _testResults;
|
|
26
26
|
private _startTime;
|
|
27
27
|
private _finalizePromise;
|
|
28
|
+
private _rootDir;
|
|
28
29
|
constructor();
|
|
29
30
|
private _sigintHandler;
|
|
30
31
|
private _setupSignalHandler;
|
|
@@ -42,6 +43,7 @@ declare class IncrementalBlobReporter implements Reporter {
|
|
|
42
43
|
private _removeIncompleteTestArtifacts;
|
|
43
44
|
private _patchProjectSuites;
|
|
44
45
|
private get _zipPath();
|
|
46
|
+
private get _markerPath();
|
|
45
47
|
private get _jsonlPath();
|
|
46
48
|
private get _urlsJsonPath();
|
|
47
49
|
private _ensureDirs;
|
|
@@ -51,6 +53,9 @@ declare class IncrementalBlobReporter implements Reporter {
|
|
|
51
53
|
onBegin(config: FullConfig, suite: Suite): void;
|
|
52
54
|
onTestBegin(test: TestCase, result: TestResult): void;
|
|
53
55
|
onTestEnd(test: TestCase, result: TestResult): void;
|
|
56
|
+
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
|
|
57
|
+
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult): void;
|
|
58
|
+
onError(error: TestError, workerInfo?: WorkerInfo): void;
|
|
54
59
|
onStepBegin(test: TestCase, result: TestResult, step: TestStep): void;
|
|
55
60
|
onStepEnd(test: TestCase, result: TestResult, step: TestStep): void;
|
|
56
61
|
onEnd(result: FullResult): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"incremental-blob-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/incremental-blob-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,UAAU,EACV,QAAQ,
|
|
1
|
+
{"version":3,"file":"incremental-blob-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/incremental-blob-reporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -31,6 +31,7 @@ class IncrementalBlobReporter {
|
|
|
31
31
|
_testResults = new Map(); // testId → retryIndex → RecordedRetry
|
|
32
32
|
_startTime = Date.now();
|
|
33
33
|
_finalizePromise = null;
|
|
34
|
+
_rootDir = process.cwd();
|
|
34
35
|
constructor() {
|
|
35
36
|
if (process.env.SHARD_INDEX && process.env.TOTAL_SHARDS) {
|
|
36
37
|
this._shardIndex = Number.parseInt(process.env.SHARD_INDEX, 10);
|
|
@@ -108,12 +109,17 @@ class IncrementalBlobReporter {
|
|
|
108
109
|
return;
|
|
109
110
|
}
|
|
110
111
|
if (!this._uploader) {
|
|
111
|
-
console.warn("[
|
|
112
|
+
console.warn("[IncrementalBlobReporter] No uploader available, skipping upload");
|
|
112
113
|
logFinalizeComplete();
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
await this._uploader.uploadFile(this._zipPath, `blobs/incremental-report-${this._shardIndex}.zip`);
|
|
116
117
|
await this._uploader.waitForUploads();
|
|
118
|
+
if (source === "onEnd") {
|
|
119
|
+
fs_1.default.writeFileSync(this._markerPath, JSON.stringify({ status: result.status, events: lines.length }), "utf8");
|
|
120
|
+
await this._uploader.uploadFile(this._markerPath, `blobs/incremental-report-${this._shardIndex}.zip.complete`);
|
|
121
|
+
await this._uploader.waitForUploads();
|
|
122
|
+
}
|
|
117
123
|
console.log(`[IncrementalBlobReporter] Finalize and upload complete from ${source}`);
|
|
118
124
|
if ((0, ibr_utils_1.isLocalTesting)()) {
|
|
119
125
|
await (0, local_test_1.mergeForLocalTest)(this._currentWorkingDir, this._outputDir);
|
|
@@ -188,6 +194,9 @@ class IncrementalBlobReporter {
|
|
|
188
194
|
get _zipPath() {
|
|
189
195
|
return path_1.default.join(this._outputDir, `incremental-report-${this._shardIndex}.zip`);
|
|
190
196
|
}
|
|
197
|
+
get _markerPath() {
|
|
198
|
+
return `${this._zipPath}.complete`;
|
|
199
|
+
}
|
|
191
200
|
get _jsonlPath() {
|
|
192
201
|
return path_1.default.join(this._stagingDir, "report.jsonl");
|
|
193
202
|
}
|
|
@@ -230,6 +239,14 @@ class IncrementalBlobReporter {
|
|
|
230
239
|
}
|
|
231
240
|
onBegin(config, suite) {
|
|
232
241
|
this._ensureDirs();
|
|
242
|
+
this._rootDir = config.rootDir;
|
|
243
|
+
// The env vars win (production sets them), but a sharded run without
|
|
244
|
+
// them should still stamp the right shard into the blob metadata and
|
|
245
|
+
// the zip name instead of defaulting every shard to 1/1.
|
|
246
|
+
if (!process.env.SHARD_INDEX && config.shard) {
|
|
247
|
+
this._shardIndex = config.shard.current;
|
|
248
|
+
this._totalShards = config.shard.total;
|
|
249
|
+
}
|
|
233
250
|
this._appendEvents((0, lifecycle_events_1.buildOnBeginEvents)(config, suite, this._currentWorkingDir, this._shardIndex, this._totalShards));
|
|
234
251
|
console.log(`[IncrementalBlobReporter] Started with ${suite.allTests().length} tests`);
|
|
235
252
|
}
|
|
@@ -237,7 +254,7 @@ class IncrementalBlobReporter {
|
|
|
237
254
|
this._appendEvent((0, lifecycle_events_1.buildOnTestBeginEvent)(test, result, this._resultIdMap));
|
|
238
255
|
}
|
|
239
256
|
onTestEnd(test, result) {
|
|
240
|
-
const events = (0, lifecycle_events_1.buildOnTestEndEvents)(test, result, this._resultIdMap, this._stagingDir);
|
|
257
|
+
const events = (0, lifecycle_events_1.buildOnTestEndEvents)(test, result, this._resultIdMap, this._stagingDir, this._rootDir);
|
|
241
258
|
if (this._appendEvents(events)) {
|
|
242
259
|
if (!this._testRetries.has(test.id)) {
|
|
243
260
|
this._testRetries.set(test.id, test.retries);
|
|
@@ -258,11 +275,20 @@ class IncrementalBlobReporter {
|
|
|
258
275
|
}
|
|
259
276
|
}
|
|
260
277
|
}
|
|
278
|
+
onStdOut(chunk, test, result) {
|
|
279
|
+
this._appendEvent((0, lifecycle_events_1.buildOnStdIOEvent)("stdout", chunk, test, result, this._resultIdMap));
|
|
280
|
+
}
|
|
281
|
+
onStdErr(chunk, test, result) {
|
|
282
|
+
this._appendEvent((0, lifecycle_events_1.buildOnStdIOEvent)("stderr", chunk, test, result, this._resultIdMap));
|
|
283
|
+
}
|
|
284
|
+
onError(error, workerInfo) {
|
|
285
|
+
this._appendEvent((0, lifecycle_events_1.buildOnErrorEvent)(error, workerInfo));
|
|
286
|
+
}
|
|
261
287
|
onStepBegin(test, result, step) {
|
|
262
288
|
this._appendEvent((0, lifecycle_events_1.buildOnStepBeginEvent)(test, result, step, this._resultIdMap, this._stepIdMap));
|
|
263
289
|
}
|
|
264
290
|
onStepEnd(test, result, step) {
|
|
265
|
-
this._appendEvent((0, lifecycle_events_1.buildOnStepEndEvent)(test, result, step, this._resultIdMap, this._stepIdMap));
|
|
291
|
+
this._appendEvent((0, lifecycle_events_1.buildOnStepEndEvent)(test, result, step, this._resultIdMap, this._stepIdMap, this._rootDir));
|
|
266
292
|
}
|
|
267
293
|
async onEnd(result) {
|
|
268
294
|
this._removeSignalHandler();
|
|
@@ -287,9 +313,7 @@ class IncrementalBlobReporter {
|
|
|
287
313
|
source: "onEnd",
|
|
288
314
|
markGlobalFinalized: false,
|
|
289
315
|
pruneIncompleteTests: false,
|
|
290
|
-
|
|
291
|
-
// Only the SIGINT path uploads this incremental fallback blob.
|
|
292
|
-
upload: false,
|
|
316
|
+
upload: true,
|
|
293
317
|
});
|
|
294
318
|
await this._finalizePromise;
|
|
295
319
|
console.log(`[IncrementalBlobReporter] Finished with status: ${result.status}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FullConfig, FullProject, FullResult, Suite, TestCase, TestResult, TestStep } from "@playwright/test/reporter";
|
|
1
|
+
import type { FullConfig, FullProject, FullResult, Suite, TestCase, TestError, TestResult, TestStep, WorkerInfo } from "@playwright/test/reporter";
|
|
2
2
|
export type JsonEvent = {
|
|
3
3
|
method: string;
|
|
4
4
|
params?: object;
|
|
@@ -6,8 +6,10 @@ export type JsonEvent = {
|
|
|
6
6
|
export declare function buildOnBeginEvents(config: FullConfig, suite: Suite, currentWorkingDir: string, shardIndex: number, totalShards: number): JsonEvent[];
|
|
7
7
|
export declare function buildOnProjectEvent(config: FullConfig, project: FullProject, projectSuite: Suite, currentWorkingDir: string): JsonEvent;
|
|
8
8
|
export declare function buildOnTestBeginEvent(test: TestCase, result: TestResult, resultIdMap: Map<string, string>): JsonEvent;
|
|
9
|
-
export declare function buildOnTestEndEvents(test: TestCase, result: TestResult, resultIdMap: Map<string, string>, stagingDir: string): JsonEvent[];
|
|
9
|
+
export declare function buildOnTestEndEvents(test: TestCase, result: TestResult, resultIdMap: Map<string, string>, stagingDir: string, rootDir: string): JsonEvent[];
|
|
10
|
+
export declare function buildOnStdIOEvent(type: "stdout" | "stderr", chunk: string | Buffer, test: TestCase | undefined, result: TestResult | undefined, resultIdMap: Map<string, string>): JsonEvent;
|
|
11
|
+
export declare function buildOnErrorEvent(error: TestError, workerInfo?: WorkerInfo): JsonEvent;
|
|
10
12
|
export declare function buildOnStepBeginEvent(test: TestCase, result: TestResult, step: TestStep, resultIdMap: Map<string, string>, stepIdMap: Map<TestStep, string>): JsonEvent;
|
|
11
|
-
export declare function buildOnStepEndEvent(test: TestCase, result: TestResult, step: TestStep, resultIdMap: Map<string, string>, stepIdMap: Map<TestStep, string
|
|
13
|
+
export declare function buildOnStepEndEvent(test: TestCase, result: TestResult, step: TestStep, resultIdMap: Map<string, string>, stepIdMap: Map<TestStep, string>, rootDir: string): JsonEvent;
|
|
12
14
|
export declare function buildOnEndEvent(result: FullResult): JsonEvent;
|
|
13
15
|
//# sourceMappingURL=lifecycle-events.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-events.d.ts","sourceRoot":"","sources":["../../src/reporter/lifecycle-events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,WAAW,EACX,UAAU,EACV,KAAK,EACL,QAAQ,EACR,UAAU,EACV,QAAQ,
|
|
1
|
+
{"version":3,"file":"lifecycle-events.d.ts","sourceRoot":"","sources":["../../src/reporter/lifecycle-events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,WAAW,EACX,UAAU,EACV,KAAK,EACL,QAAQ,EACR,SAAS,EACT,UAAU,EACV,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAiBnC,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,KAAK,EACZ,iBAAiB,EAAE,MAAM,EACzB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,SAAS,EAAE,CAqDb;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,KAAK,EACnB,iBAAiB,EAAE,MAAM,GACxB,SAAS,CA+BX;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,SAAS,CAeX;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,SAAS,EAAE,CA+Db;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,QAAQ,GAAG,QAAQ,EACzB,KAAK,EAAE,MAAM,GAAG,MAAM,EACtB,IAAI,EAAE,QAAQ,GAAG,SAAS,EAC1B,MAAM,EAAE,UAAU,GAAG,SAAS,EAC9B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,SAAS,CAgBX;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,EAChB,UAAU,CAAC,EAAE,UAAU,GACtB,SAAS,CAcX;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,QAAQ,EACd,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,SAAS,EAAE,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,GAC/B,SAAS,CAsBX;AAED,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,QAAQ,EACd,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAChC,SAAS,EAAE,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,EAChC,OAAO,EAAE,MAAM,GACd,SAAS,CA+BX;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAW7D"}
|
|
@@ -7,6 +7,8 @@ exports.buildOnBeginEvents = buildOnBeginEvents;
|
|
|
7
7
|
exports.buildOnProjectEvent = buildOnProjectEvent;
|
|
8
8
|
exports.buildOnTestBeginEvent = buildOnTestBeginEvent;
|
|
9
9
|
exports.buildOnTestEndEvents = buildOnTestEndEvents;
|
|
10
|
+
exports.buildOnStdIOEvent = buildOnStdIOEvent;
|
|
11
|
+
exports.buildOnErrorEvent = buildOnErrorEvent;
|
|
10
12
|
exports.buildOnStepBeginEvent = buildOnStepBeginEvent;
|
|
11
13
|
exports.buildOnStepEndEvent = buildOnStepEndEvent;
|
|
12
14
|
exports.buildOnEndEvent = buildOnEndEvent;
|
|
@@ -108,7 +110,7 @@ function buildOnTestBeginEvent(test, result, resultIdMap) {
|
|
|
108
110
|
},
|
|
109
111
|
};
|
|
110
112
|
}
|
|
111
|
-
function buildOnTestEndEvents(test, result, resultIdMap, stagingDir) {
|
|
113
|
+
function buildOnTestEndEvents(test, result, resultIdMap, stagingDir, rootDir) {
|
|
112
114
|
const resultId = (0, ibr_utils_1.getOrCreateId)(resultIdMap, `${test.id}-${result.retry}`);
|
|
113
115
|
const events = [];
|
|
114
116
|
// onAttach for each attachment (matching Playwright's blob format)
|
|
@@ -131,6 +133,11 @@ function buildOnTestEndEvents(test, result, resultIdMap, stagingDir) {
|
|
|
131
133
|
name: attachment.name,
|
|
132
134
|
contentType: attachment.contentType,
|
|
133
135
|
path: attachmentPath,
|
|
136
|
+
// Body-based attachments travel inline, mirroring
|
|
137
|
+
// TeleReporterEmitter._serializeAttachments.
|
|
138
|
+
base64: attachment.body
|
|
139
|
+
? attachment.body.toString("base64")
|
|
140
|
+
: undefined,
|
|
134
141
|
},
|
|
135
142
|
],
|
|
136
143
|
},
|
|
@@ -143,7 +150,7 @@ function buildOnTestEndEvents(test, result, resultIdMap, stagingDir) {
|
|
|
143
150
|
testId: test.id,
|
|
144
151
|
expectedStatus: test.expectedStatus,
|
|
145
152
|
timeout: test.timeout,
|
|
146
|
-
annotations: test.annotations,
|
|
153
|
+
annotations: (0, ibr_utils_1.relativeAnnotationLocations)(test.annotations, rootDir),
|
|
147
154
|
},
|
|
148
155
|
result: {
|
|
149
156
|
id: resultId,
|
|
@@ -155,11 +162,47 @@ function buildOnTestEndEvents(test, result, resultIdMap, stagingDir) {
|
|
|
155
162
|
location: e.location,
|
|
156
163
|
snippet: e.snippet,
|
|
157
164
|
})),
|
|
165
|
+
// Blob v2 carries annotations on the result; the merge reads them
|
|
166
|
+
// from here (result-level), not from the test payload above.
|
|
167
|
+
annotations: result.annotations?.length
|
|
168
|
+
? (0, ibr_utils_1.relativeAnnotationLocations)(result.annotations, rootDir)
|
|
169
|
+
: undefined,
|
|
158
170
|
},
|
|
159
171
|
},
|
|
160
172
|
});
|
|
161
173
|
return events;
|
|
162
174
|
}
|
|
175
|
+
function buildOnStdIOEvent(type, chunk, test, result, resultIdMap) {
|
|
176
|
+
const isBase64 = typeof chunk !== "string";
|
|
177
|
+
const data = isBase64 ? chunk.toString("base64") : chunk;
|
|
178
|
+
return {
|
|
179
|
+
method: "onStdIO",
|
|
180
|
+
params: {
|
|
181
|
+
testId: test?.id,
|
|
182
|
+
resultId: test && result
|
|
183
|
+
? (0, ibr_utils_1.getOrCreateId)(resultIdMap, `${test.id}-${result.retry}`)
|
|
184
|
+
: undefined,
|
|
185
|
+
type,
|
|
186
|
+
data,
|
|
187
|
+
isBase64,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function buildOnErrorEvent(error, workerInfo) {
|
|
192
|
+
return {
|
|
193
|
+
method: "onError",
|
|
194
|
+
params: {
|
|
195
|
+
error,
|
|
196
|
+
workerInfo: workerInfo
|
|
197
|
+
? {
|
|
198
|
+
workerIndex: workerInfo.workerIndex,
|
|
199
|
+
parallelIndex: workerInfo.parallelIndex,
|
|
200
|
+
projectName: workerInfo.project.name,
|
|
201
|
+
}
|
|
202
|
+
: undefined,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
163
206
|
function buildOnStepBeginEvent(test, result, step, resultIdMap, stepIdMap) {
|
|
164
207
|
const resultId = (0, ibr_utils_1.getOrCreateId)(resultIdMap, `${test.id}-${result.retry}`);
|
|
165
208
|
const stepId = (0, ibr_utils_1.getOrCreateStepId)(stepIdMap, step);
|
|
@@ -182,7 +225,7 @@ function buildOnStepBeginEvent(test, result, step, resultIdMap, stepIdMap) {
|
|
|
182
225
|
},
|
|
183
226
|
};
|
|
184
227
|
}
|
|
185
|
-
function buildOnStepEndEvent(test, result, step, resultIdMap, stepIdMap) {
|
|
228
|
+
function buildOnStepEndEvent(test, result, step, resultIdMap, stepIdMap, rootDir) {
|
|
186
229
|
const resultId = (0, ibr_utils_1.getOrCreateId)(resultIdMap, `${test.id}-${result.retry}`);
|
|
187
230
|
const stepId = (0, ibr_utils_1.getOrCreateStepId)(stepIdMap, step);
|
|
188
231
|
return {
|
|
@@ -201,6 +244,14 @@ function buildOnStepEndEvent(test, result, step, resultIdMap, stepIdMap) {
|
|
|
201
244
|
snippet: step.error.snippet,
|
|
202
245
|
}
|
|
203
246
|
: undefined,
|
|
247
|
+
// Indices into the result's attachments, matching
|
|
248
|
+
// TeleReporterEmitter._serializeStepEnd.
|
|
249
|
+
attachments: step.attachments.length
|
|
250
|
+
? step.attachments.map((a) => result.attachments.indexOf(a))
|
|
251
|
+
: undefined,
|
|
252
|
+
annotations: step.annotations.length
|
|
253
|
+
? (0, ibr_utils_1.relativeAnnotationLocations)(step.annotations, rootDir)
|
|
254
|
+
: undefined,
|
|
204
255
|
},
|
|
205
256
|
},
|
|
206
257
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { BlobEvent } from "./harness";
|
|
2
|
+
export interface NormalizedAttachment {
|
|
3
|
+
name: string;
|
|
4
|
+
contentType: string;
|
|
5
|
+
/**
|
|
6
|
+
* Whether the merged report still points at real content (path or inline
|
|
7
|
+
* body). Compared only where a scenario opts in: the empirical pipeline
|
|
8
|
+
* resolves attachment bytes through _empirical_urls.json, so name+type is
|
|
9
|
+
* the baseline parity contract.
|
|
10
|
+
*/
|
|
11
|
+
hasContent: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface NormalizedResult {
|
|
14
|
+
retry: number;
|
|
15
|
+
status: string;
|
|
16
|
+
errorMessages: string[];
|
|
17
|
+
stdout: string[];
|
|
18
|
+
stderr: string[];
|
|
19
|
+
attachments: NormalizedAttachment[];
|
|
20
|
+
}
|
|
21
|
+
export interface NormalizedTest {
|
|
22
|
+
projectName: string;
|
|
23
|
+
expectedStatus: string;
|
|
24
|
+
status: string;
|
|
25
|
+
annotations: {
|
|
26
|
+
type: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
}[];
|
|
29
|
+
results: NormalizedResult[];
|
|
30
|
+
}
|
|
31
|
+
export interface NormalizedSpec {
|
|
32
|
+
title: string;
|
|
33
|
+
file: string;
|
|
34
|
+
ok: boolean;
|
|
35
|
+
tests: NormalizedTest[];
|
|
36
|
+
}
|
|
37
|
+
export interface NormalizedSuite {
|
|
38
|
+
title: string;
|
|
39
|
+
file: string;
|
|
40
|
+
specs: NormalizedSpec[];
|
|
41
|
+
suites: NormalizedSuite[];
|
|
42
|
+
}
|
|
43
|
+
export interface NormalizedReport {
|
|
44
|
+
suites: NormalizedSuite[];
|
|
45
|
+
errorMessages: string[];
|
|
46
|
+
stats: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
export interface NormalizeOptions {
|
|
49
|
+
/** Absolute fixture dir, replaced with a stable token in messages. */
|
|
50
|
+
cwd: string;
|
|
51
|
+
/** Include attachment `hasContent` in the comparison (see above). */
|
|
52
|
+
compareAttachmentContent?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export declare function normalizeJsonReport(report: Record<string, unknown>, opts: NormalizeOptions): NormalizedReport;
|
|
55
|
+
/**
|
|
56
|
+
* Per-result step signatures from a raw blob event stream, keyed by
|
|
57
|
+
* `testId:retry` (stable across both reporters — they see the same run).
|
|
58
|
+
* The merged JSON report drops steps, so step parity is checked here.
|
|
59
|
+
*/
|
|
60
|
+
export declare function stepSignatures(events: BlobEvent[]): Map<string, string[]>;
|
|
61
|
+
/** Event method names present in `reference` but absent from `candidate`. */
|
|
62
|
+
export declare function missingEventMethods(reference: BlobEvent[], candidate: BlobEvent[]): string[];
|
|
63
|
+
//# sourceMappingURL=parity-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parity-utils.d.ts","sourceRoot":"","sources":["../../src/reporter/parity-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAiD3C,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,oBAAoB,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ,qEAAqE;IACrE,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAwED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,gBAAgB,GACrB,gBAAgB,CAclB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CA+BzE;AAED,6EAA6E;AAC7E,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,SAAS,EAAE,EACtB,SAAS,EAAE,SAAS,EAAE,GACrB,MAAM,EAAE,CASV"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeJsonReport = normalizeJsonReport;
|
|
4
|
+
exports.stepSignatures = stepSignatures;
|
|
5
|
+
exports.missingEventMethods = missingEventMethods;
|
|
6
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
7
|
+
function normalizeText(text, cwd) {
|
|
8
|
+
return text.replace(ANSI_RE, "").split(cwd).join("<cwd>").trimEnd();
|
|
9
|
+
}
|
|
10
|
+
function normalizeStdio(entries, cwd) {
|
|
11
|
+
return (entries ?? []).map((entry) => typeof entry === "string"
|
|
12
|
+
? normalizeText(entry, cwd)
|
|
13
|
+
: entry.text !== undefined
|
|
14
|
+
? normalizeText(entry.text, cwd)
|
|
15
|
+
: `<buffer:${entry.buffer?.length ?? 0}>`);
|
|
16
|
+
}
|
|
17
|
+
function normalizeResult(result, opts) {
|
|
18
|
+
return {
|
|
19
|
+
retry: result.retry,
|
|
20
|
+
status: result.status,
|
|
21
|
+
errorMessages: (result.errors ?? []).map((e) => normalizeText(e.message ?? "", opts.cwd)),
|
|
22
|
+
stdout: normalizeStdio(result.stdout, opts.cwd),
|
|
23
|
+
stderr: normalizeStdio(result.stderr, opts.cwd),
|
|
24
|
+
attachments: (result.attachments ?? []).map((a) => ({
|
|
25
|
+
name: a.name,
|
|
26
|
+
contentType: a.contentType,
|
|
27
|
+
hasContent: opts.compareAttachmentContent
|
|
28
|
+
? Boolean(a.path || a.body)
|
|
29
|
+
: true,
|
|
30
|
+
})),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function normalizeSuite(suite, opts) {
|
|
34
|
+
return {
|
|
35
|
+
title: suite.title,
|
|
36
|
+
file: suite.file,
|
|
37
|
+
specs: (suite.specs ?? [])
|
|
38
|
+
.map((spec) => ({
|
|
39
|
+
title: spec.title,
|
|
40
|
+
file: spec.file,
|
|
41
|
+
ok: spec.ok,
|
|
42
|
+
tests: (spec.tests ?? []).map((test) => ({
|
|
43
|
+
projectName: test.projectName,
|
|
44
|
+
expectedStatus: test.expectedStatus,
|
|
45
|
+
status: test.status,
|
|
46
|
+
annotations: test.annotations ?? [],
|
|
47
|
+
results: (test.results ?? [])
|
|
48
|
+
.map((r) => normalizeResult(r, opts))
|
|
49
|
+
.sort((a, b) => a.retry - b.retry),
|
|
50
|
+
})),
|
|
51
|
+
}))
|
|
52
|
+
.sort((a, b) => a.title.localeCompare(b.title)),
|
|
53
|
+
suites: (suite.suites ?? [])
|
|
54
|
+
.map((s) => normalizeSuite(s, opts))
|
|
55
|
+
.sort((a, b) => a.title.localeCompare(b.title)),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function normalizeJsonReport(report, opts) {
|
|
59
|
+
const typed = report;
|
|
60
|
+
const stats = { ...(typed.stats ?? {}) };
|
|
61
|
+
delete stats.startTime;
|
|
62
|
+
delete stats.duration;
|
|
63
|
+
return {
|
|
64
|
+
suites: (typed.suites ?? [])
|
|
65
|
+
.map((s) => normalizeSuite(s, opts))
|
|
66
|
+
.sort((a, b) => (a.file + a.title).localeCompare(b.file + b.title)),
|
|
67
|
+
errorMessages: (typed.errors ?? []).map((e) => normalizeText(e.message ?? "", opts.cwd)),
|
|
68
|
+
stats,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Per-result step signatures from a raw blob event stream, keyed by
|
|
73
|
+
* `testId:retry` (stable across both reporters — they see the same run).
|
|
74
|
+
* The merged JSON report drops steps, so step parity is checked here.
|
|
75
|
+
*/
|
|
76
|
+
function stepSignatures(events) {
|
|
77
|
+
const resultKey = new Map(); // resultId -> testId:retry
|
|
78
|
+
for (const event of events) {
|
|
79
|
+
if (event.method !== "onTestBegin") {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const params = event.params;
|
|
83
|
+
resultKey.set(params.result.id, `${params.testId}:${params.result.retry}`);
|
|
84
|
+
}
|
|
85
|
+
const signatures = new Map();
|
|
86
|
+
for (const event of events) {
|
|
87
|
+
if (event.method !== "onStepBegin") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const params = event.params;
|
|
91
|
+
const key = resultKey.get(params.resultId);
|
|
92
|
+
if (!key) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const list = signatures.get(key) ?? [];
|
|
96
|
+
list.push(`${params.step.category}:${params.step.title}`);
|
|
97
|
+
signatures.set(key, list);
|
|
98
|
+
}
|
|
99
|
+
return signatures;
|
|
100
|
+
}
|
|
101
|
+
/** Event method names present in `reference` but absent from `candidate`. */
|
|
102
|
+
function missingEventMethods(reference, candidate) {
|
|
103
|
+
const candidateMethods = new Set(candidate.map((e) => e.method));
|
|
104
|
+
return [
|
|
105
|
+
...new Set(reference
|
|
106
|
+
.map((e) => e.method)
|
|
107
|
+
.filter((method) => !candidateMethods.has(method))),
|
|
108
|
+
].sort();
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empiricalrun/playwright-utils",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"puppeteer-extra-plugin-recaptcha": "^3.6.8",
|
|
55
55
|
"rimraf": "^6.0.1",
|
|
56
56
|
"ts-morph": "^23.0.0",
|
|
57
|
-
"@empiricalrun/cua": "^0.4.1",
|
|
58
57
|
"@empiricalrun/llm": "^0.27.0",
|
|
58
|
+
"@empiricalrun/cua": "^0.4.1",
|
|
59
59
|
"@empiricalrun/r2-uploader": "^0.10.1",
|
|
60
60
|
"@empiricalrun/reporter": "^0.29.1"
|
|
61
61
|
},
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"7.0.0-dev.20260623.1","root":["./src/api-worker-client.ts","./src/email.ts","./src/inbox-client.ts","./src/index.ts","./src/kv.ts","./src/logger.ts","./src/mailosaur-client.ts","./src/playwright-extensions.ts","./src/telemetry.ts","./src/webhook.ts","./src/auth/google.ts","./src/auth/index.ts","./src/auth/types.ts","./src/captcha/index.ts","./src/config/index.ts","./src/config/proxy.ts","./src/config/devices/types.ts","./src/generated/api-worker-openapi.ts","./src/overlay-tests/cache.spec.ts","./src/overlay-tests/click.spec.ts","./src/overlay-tests/fixtures.ts","./src/overlay-tests/patch.spec.ts","./src/overlay-tests/visual.spec.ts","./src/reporter/attachment-cleanup-test-reporter.ts","./src/reporter/attachment-cleanup.ts","./src/reporter/blob-utils.ts","./src/reporter/empirical-reporter.ts","./src/reporter/failing-line.ts","./src/reporter/harness.ts","./src/reporter/ibr-utils.ts","./src/reporter/incremental-blob-reporter.ts","./src/reporter/lifecycle-events.ts","./src/reporter/local-test.ts","./src/reporter/reporter-state.ts","./src/reporter/test-case-sync-reporter.ts","./src/reporter/test-case-sync.ts","./src/reporter/uploader.ts","./src/reporter/util.ts","./src/test/constants.ts","./src/test/coverage.ts","./src/test/index.ts","./src/test/types.ts","./src/test/video-labels.ts","./src/test/expect/index.ts","./src/test/expect/types.ts","./src/test/expect/visual.ts","./src/test/expect/webhook.ts","./src/test/scripts/agent-capabilities.ts","./src/test/scripts/index.ts","./src/test/scripts/locator-highlights.ts","./src/test/scripts/locator-vision.ts","./src/test/scripts/mouse-pointer.ts","./src/test/scripts/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/prompt.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts","./src/test/scripts/pw-locator-patch/highlight/click.ts","./src/test/scripts/pw-locator-patch/highlight/expect.ts","./src/test/scripts/pw-locator-patch/highlight/hover.ts","./src/test/scripts/pw-locator-patch/highlight/inner-text.ts","./src/test/scripts/pw-locator-patch/highlight/input-value.ts","./src/test/scripts/pw-locator-patch/highlight/is-checked.ts","./src/test/scripts/pw-locator-patch/highlight/is-disabled.ts","./src/test/scripts/pw-locator-patch/highlight/is-editable.ts","./src/test/scripts/pw-locator-patch/highlight/text-content.ts","./src/test/scripts/pw-locator-patch/utils/index.ts","./src/test/scripts/pw-locator-patch/vision/query.ts"]}
|
|
1
|
+
{"version":"7.0.0-dev.20260623.1","root":["./src/api-worker-client.ts","./src/email.ts","./src/inbox-client.ts","./src/index.ts","./src/kv.ts","./src/logger.ts","./src/mailosaur-client.ts","./src/playwright-extensions.ts","./src/telemetry.ts","./src/webhook.ts","./src/auth/google.ts","./src/auth/index.ts","./src/auth/types.ts","./src/captcha/index.ts","./src/config/index.ts","./src/config/proxy.ts","./src/config/devices/types.ts","./src/generated/api-worker-openapi.ts","./src/overlay-tests/cache.spec.ts","./src/overlay-tests/click.spec.ts","./src/overlay-tests/fixtures.ts","./src/overlay-tests/patch.spec.ts","./src/overlay-tests/visual.spec.ts","./src/reporter/attachment-cleanup-test-reporter.ts","./src/reporter/attachment-cleanup.ts","./src/reporter/blob-utils.ts","./src/reporter/empirical-reporter.ts","./src/reporter/failing-line.ts","./src/reporter/harness.ts","./src/reporter/ibr-utils.ts","./src/reporter/incremental-blob-reporter.ts","./src/reporter/lifecycle-events.ts","./src/reporter/local-test.ts","./src/reporter/parity-utils.ts","./src/reporter/reporter-state.ts","./src/reporter/test-case-sync-reporter.ts","./src/reporter/test-case-sync.ts","./src/reporter/uploader.ts","./src/reporter/util.ts","./src/test/constants.ts","./src/test/coverage.ts","./src/test/index.ts","./src/test/types.ts","./src/test/video-labels.ts","./src/test/expect/index.ts","./src/test/expect/types.ts","./src/test/expect/visual.ts","./src/test/expect/webhook.ts","./src/test/scripts/agent-capabilities.ts","./src/test/scripts/index.ts","./src/test/scripts/locator-highlights.ts","./src/test/scripts/locator-vision.ts","./src/test/scripts/mouse-pointer.ts","./src/test/scripts/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/prompt.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts","./src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts","./src/test/scripts/pw-locator-patch/highlight/click.ts","./src/test/scripts/pw-locator-patch/highlight/expect.ts","./src/test/scripts/pw-locator-patch/highlight/hover.ts","./src/test/scripts/pw-locator-patch/highlight/inner-text.ts","./src/test/scripts/pw-locator-patch/highlight/input-value.ts","./src/test/scripts/pw-locator-patch/highlight/is-checked.ts","./src/test/scripts/pw-locator-patch/highlight/is-disabled.ts","./src/test/scripts/pw-locator-patch/highlight/is-editable.ts","./src/test/scripts/pw-locator-patch/highlight/text-content.ts","./src/test/scripts/pw-locator-patch/utils/index.ts","./src/test/scripts/pw-locator-patch/vision/query.ts"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Upstream Playwright oracle for the IncrementalBlobReporter
|
|
2
|
+
|
|
3
|
+
Runs upstream Playwright's own runner test suite (`tests/playwright-test`)
|
|
4
|
+
through our IncrementalBlobReporter (IBR) and diffs failures against the
|
|
5
|
+
built-in blob reporter as baseline. Their
|
|
6
|
+
`PWTEST_INTERMEDIATE_BLOB_REPORT=1` harness mode reroutes every inline
|
|
7
|
+
scenario through `<reporter> -> merge-reports -> json` before applying the
|
|
8
|
+
normal assertions — Playwright's own completeness check for the blob format.
|
|
9
|
+
Swapping IBR in (via `ibr-adapter.js` + the one-line
|
|
10
|
+
`intermediate-mode-ibr.patch`) turns it into a parity oracle for us.
|
|
11
|
+
|
|
12
|
+
This backs the plan to drop the built-in blob reporter from sharded runs:
|
|
13
|
+
before trusting IBR as the only report, its zip must merge identically on
|
|
14
|
+
scenarios we did not think to write ourselves. The fast, per-PR half of that
|
|
15
|
+
story is `src/reporter/blob-ibr-parity.test.ts`; this oracle is the deep
|
|
16
|
+
audit, run by `.github/workflows/ibr-blob-oracle.yaml` (and re-run on every
|
|
17
|
+
Playwright version-pin bump).
|
|
18
|
+
|
|
19
|
+
## Files
|
|
20
|
+
|
|
21
|
+
- `run.mjs` — orchestrator: applies the patch, runs the curated specs in both
|
|
22
|
+
modes, fails on any IBR-only failure.
|
|
23
|
+
- `ibr-adapter.js` — bridges IBR's env/paths contract to the blob reporter's
|
|
24
|
+
(config.shard → SHARD_INDEX env, zip relocation to `blob-report/`).
|
|
25
|
+
- `intermediate-mode-ibr.patch` — one-line upstream fixture change making the
|
|
26
|
+
intermediate reporter selectable via `PWTEST_IBR_ADAPTER`.
|
|
27
|
+
|
|
28
|
+
The job is EXPECTED to stay red until IBR reaches parity: today ~55 upstream
|
|
29
|
+
tests fail only through IBR, all tracing to the known gaps (`onStdIO`,
|
|
30
|
+
`onError`, result-level annotations, attachment bodies — encoded as `it.fails`
|
|
31
|
+
in `src/reporter/blob-ibr-parity.test.ts`). The failure count shrinking to
|
|
32
|
+
zero is the signal that the built-in blob reporter is safe to drop.
|
|
33
|
+
|
|
34
|
+
## Running locally
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# One-time: clone + bootstrap upstream at the pinned tag (~10 min)
|
|
38
|
+
git clone --depth 1 --branch v1.62.1 https://github.com/microsoft/playwright.git ~/playwright-upstream
|
|
39
|
+
cd ~/playwright-upstream && npm ci && npm run build
|
|
40
|
+
|
|
41
|
+
# Build IBR, then run the oracle
|
|
42
|
+
cd <monorepo>/packages/playwright-utils && pnpm build
|
|
43
|
+
node upstream-oracle/run.mjs --pw-repo ~/playwright-upstream
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The pin lives in the workflow (`PLAYWRIGHT_UPSTREAM_TAG`) and must move with
|
|
47
|
+
the `@playwright/test` dev dependency; IBR mirrors blob format version 2
|
|
48
|
+
(`BLOB_REPORT_VERSION` in `src/reporter/lifecycle-events.ts`).
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter that lets the IncrementalBlobReporter (IBR) stand in for
|
|
3
|
+
* Playwright's built-in blob reporter inside the upstream Playwright repo's
|
|
4
|
+
* test harness (tests/playwright-test).
|
|
5
|
+
*
|
|
6
|
+
* Used with PWTEST_INTERMEDIATE_BLOB_REPORT=1 and the fixture patch in
|
|
7
|
+
* intermediate-mode-ibr.patch: every inline scenario in upstream's runner
|
|
8
|
+
* suite then executes through IBR -> merge-reports instead of blob ->
|
|
9
|
+
* merge-reports, turning Playwright's own tests into a completeness oracle
|
|
10
|
+
* for the IBR blob format. See run.mjs for orchestration.
|
|
11
|
+
*
|
|
12
|
+
* A subclass rather than a wrapper so every event handler — including ones
|
|
13
|
+
* IBR gains later — is inherited automatically instead of hand-forwarded
|
|
14
|
+
* (a wrapper once silently swallowed onStdOut/onError). The only remaining
|
|
15
|
+
* bridge is output location: the harness merges <packageRoot>/blob-report/
|
|
16
|
+
* report-N.zip, IBR writes blob-report-incremental/incremental-report-N.zip,
|
|
17
|
+
* so the zip is copied over after onEnd. Sharding needs no bridge: IBR falls
|
|
18
|
+
* back to config.shard when SHARD_INDEX/TOTAL_SHARDS are not set.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require("fs");
|
|
22
|
+
const path = require("path");
|
|
23
|
+
|
|
24
|
+
const IBR_DIST = process.env.PWTEST_IBR_DIST;
|
|
25
|
+
if (!IBR_DIST) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"PWTEST_IBR_DIST must point at a built incremental-blob-reporter.js " +
|
|
28
|
+
"(e.g. <monorepo>/packages/playwright-utils/dist/reporter/incremental-blob-reporter.js)",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Keep IBR off its local-testing path (attachment embedding + auto-merge)
|
|
33
|
+
// and away from real uploads. Must happen before construction: IBR reads
|
|
34
|
+
// env when instantiated.
|
|
35
|
+
delete process.env.LOCAL_TEST;
|
|
36
|
+
delete process.env.R2_ACCOUNT_ID;
|
|
37
|
+
delete process.env.S3_UPLOAD_BUCKET;
|
|
38
|
+
delete process.env.SHARD_INDEX;
|
|
39
|
+
delete process.env.TOTAL_SHARDS;
|
|
40
|
+
|
|
41
|
+
const IBR = require(IBR_DIST).default;
|
|
42
|
+
|
|
43
|
+
class IBRBlobAdapter extends IBR {
|
|
44
|
+
constructor(options = {}) {
|
|
45
|
+
super();
|
|
46
|
+
this._adapterOptions = options;
|
|
47
|
+
this._adapterCwd = process.cwd();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async onEnd(result) {
|
|
51
|
+
await super.onEnd(result);
|
|
52
|
+
const shard = this._shardIndex;
|
|
53
|
+
const src = path.join(
|
|
54
|
+
this._adapterCwd,
|
|
55
|
+
"blob-report-incremental",
|
|
56
|
+
`incremental-report-${shard}.zip`,
|
|
57
|
+
);
|
|
58
|
+
if (!fs.existsSync(src)) {
|
|
59
|
+
// Mirrors blob: no zip when there was nothing to report.
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const outputDir = path.resolve(
|
|
63
|
+
this._adapterCwd,
|
|
64
|
+
this._adapterOptions.outputDir || "blob-report",
|
|
65
|
+
);
|
|
66
|
+
const fileName = this._adapterOptions.fileName || `report-${shard}.zip`;
|
|
67
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
68
|
+
fs.copyFileSync(src, path.join(outputDir, fileName));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = IBRBlobAdapter;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts
|
|
2
|
+
index 28ef919e2..350fe8b5c 100644
|
|
3
|
+
--- a/tests/playwright-test/playwright-test-fixtures.ts
|
|
4
|
+
+++ b/tests/playwright-test/playwright-test-fixtures.ts
|
|
5
|
+
@@ -143,7 +143,10 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
|
|
6
|
+
let reporter;
|
|
7
|
+
if (useIntermediateMergeReport) {
|
|
8
|
+
reporter = params.reporter;
|
|
9
|
+
- params.reporter = 'blob';
|
|
10
|
+
+ // PWTEST_IBR_ADAPTER routes the intermediate report through Empirical's
|
|
11
|
+
+ // IncrementalBlobReporter instead of the built-in blob reporter, so this
|
|
12
|
+
+ // suite doubles as a blob-format parity oracle for IBR.
|
|
13
|
+
+ params.reporter = process.env.PWTEST_IBR_ADAPTER || 'blob';
|
|
14
|
+
}
|
|
15
|
+
const reportFile = path.join(baseDir, 'report.json');
|
|
16
|
+
// When we have useIntermediateMergeReport, we want the JSON reporter only at the merge step.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs upstream Playwright's own runner test suite (tests/playwright-test)
|
|
3
|
+
* through the IncrementalBlobReporter and compares failures against the
|
|
4
|
+
* built-in blob reporter baseline.
|
|
5
|
+
*
|
|
6
|
+
* How: upstream's PWTEST_INTERMEDIATE_BLOB_REPORT=1 mode reroutes every
|
|
7
|
+
* inline scenario through <reporter> -> merge-reports -> json before applying
|
|
8
|
+
* the normal assertions — Playwright's own completeness oracle for the blob
|
|
9
|
+
* format. The fixture patch (intermediate-mode-ibr.patch) lets
|
|
10
|
+
* PWTEST_IBR_ADAPTER swap IBR in as that reporter.
|
|
11
|
+
*
|
|
12
|
+
* Both modes run the same curated spec files; environment noise cancels in
|
|
13
|
+
* the baseline diff. Any IBR-only failure fails the run — the job stays red
|
|
14
|
+
* until IBR reaches parity (known gaps today: onStdIO, onError, result-level
|
|
15
|
+
* annotations, attachment bodies). It turning green is the signal that the
|
|
16
|
+
* built-in blob reporter is safe to drop.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* node run.mjs --pw-repo <upstream clone at the pinned tag>
|
|
20
|
+
* [--specs basic.spec.ts,stdio.spec.ts] [--workers N]
|
|
21
|
+
*
|
|
22
|
+
* The clone must be bootstrapped (npm ci + npm run build). This script
|
|
23
|
+
* (re)applies the fixture patch idempotently on every run.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { spawn } from "node:child_process";
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import os from "node:os";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
const ORACLE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
|
|
34
|
+
// Curated, node-only (no browser) runner specs with high report-content
|
|
35
|
+
// signal. stdio.spec.ts is the direct probe for the known onStdIO gap.
|
|
36
|
+
const DEFAULT_SPECS = [
|
|
37
|
+
"basic.spec.ts",
|
|
38
|
+
"hooks.spec.ts",
|
|
39
|
+
"max-failures.spec.ts",
|
|
40
|
+
"stdio.spec.ts",
|
|
41
|
+
"test-modifiers.spec.ts",
|
|
42
|
+
"test-serial.spec.ts",
|
|
43
|
+
"test-step.spec.ts",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const ADAPTER_PATH = path.join(ORACLE_DIR, "ibr-adapter.js");
|
|
47
|
+
const PATCH_PATH = path.join(ORACLE_DIR, "intermediate-mode-ibr.patch");
|
|
48
|
+
const DEFAULT_IBR_DIST = path.join(
|
|
49
|
+
ORACLE_DIR,
|
|
50
|
+
"../dist/reporter/incremental-blob-reporter.js",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const args = { specs: DEFAULT_SPECS, workers: 4 };
|
|
55
|
+
// Normalize --flag=value into --flag value.
|
|
56
|
+
argv = argv.flatMap((a) =>
|
|
57
|
+
a.startsWith("--") && a.includes("=")
|
|
58
|
+
? [a.slice(0, a.indexOf("=")), a.slice(a.indexOf("=") + 1)]
|
|
59
|
+
: [a],
|
|
60
|
+
);
|
|
61
|
+
for (let i = 0; i < argv.length; i++) {
|
|
62
|
+
const arg = argv[i];
|
|
63
|
+
if (arg === "--pw-repo") {
|
|
64
|
+
args.pwRepo = argv[++i];
|
|
65
|
+
} else if (arg === "--specs") {
|
|
66
|
+
args.specs = argv[++i].split(",").filter(Boolean);
|
|
67
|
+
} else if (arg === "--workers") {
|
|
68
|
+
args.workers = Number(argv[++i]);
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!args.pwRepo) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"--pw-repo <path to upstream playwright clone> is required",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return args;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function run(command, cmdArgs, options) {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const child = spawn(command, cmdArgs, {
|
|
84
|
+
...options,
|
|
85
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
86
|
+
});
|
|
87
|
+
let output = "";
|
|
88
|
+
child.stdout.on("data", (d) => {
|
|
89
|
+
output += d.toString();
|
|
90
|
+
});
|
|
91
|
+
child.stderr.on("data", (d) => {
|
|
92
|
+
output += d.toString();
|
|
93
|
+
});
|
|
94
|
+
child.on("close", (code) => resolve({ code, output }));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Failed-test keys ("file:line › suite › title") from a Playwright JSON report. */
|
|
99
|
+
function collectFailures(report) {
|
|
100
|
+
const failures = new Set();
|
|
101
|
+
const visit = (suite, titles) => {
|
|
102
|
+
for (const spec of suite.specs ?? []) {
|
|
103
|
+
for (const test of spec.tests ?? []) {
|
|
104
|
+
if (test.status === "unexpected") {
|
|
105
|
+
failures.add(
|
|
106
|
+
[`${spec.file}:${spec.line}`, ...titles, spec.title].join(" › "),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const child of suite.suites ?? []) {
|
|
112
|
+
visit(child, [...titles, child.title]);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
for (const suite of report.suites ?? []) {
|
|
116
|
+
visit(suite, []);
|
|
117
|
+
}
|
|
118
|
+
return failures;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function applyFixturePatch(pwRepo) {
|
|
122
|
+
const fixtureFile = "tests/playwright-test/playwright-test-fixtures.ts";
|
|
123
|
+
const restore = await run("git", ["checkout", "--", fixtureFile], {
|
|
124
|
+
cwd: pwRepo,
|
|
125
|
+
});
|
|
126
|
+
if (restore.code !== 0) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`git checkout of ${fixtureFile} failed:\n${restore.output}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const apply = await run("git", ["apply", PATCH_PATH], { cwd: pwRepo });
|
|
132
|
+
if (apply.code !== 0) {
|
|
133
|
+
throw new Error(`git apply ${PATCH_PATH} failed:\n${apply.output}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function runMode({ pwRepo, specs, workers, mode, ibrDist }) {
|
|
138
|
+
const jsonOut = path.join(
|
|
139
|
+
fs.mkdtempSync(path.join(os.tmpdir(), "ibr-oracle-")),
|
|
140
|
+
`${mode}.json`,
|
|
141
|
+
);
|
|
142
|
+
const env = {
|
|
143
|
+
...process.env,
|
|
144
|
+
PWTEST_INTERMEDIATE_BLOB_REPORT: "1",
|
|
145
|
+
PLAYWRIGHT_JSON_OUTPUT_NAME: jsonOut,
|
|
146
|
+
FORCE_COLOR: "0",
|
|
147
|
+
};
|
|
148
|
+
if (mode === "ibr") {
|
|
149
|
+
env.PWTEST_IBR_ADAPTER = ADAPTER_PATH;
|
|
150
|
+
env.PWTEST_IBR_DIST = ibrDist;
|
|
151
|
+
}
|
|
152
|
+
const { code, output } = await run(
|
|
153
|
+
"npm",
|
|
154
|
+
[
|
|
155
|
+
"run",
|
|
156
|
+
"ttest",
|
|
157
|
+
"--",
|
|
158
|
+
...specs,
|
|
159
|
+
`--workers=${workers}`,
|
|
160
|
+
// Absorbs outer-harness flake: a scenario that fails on IBR's real gaps
|
|
161
|
+
// fails every retry, while an infra hiccup passes on retry and is
|
|
162
|
+
// reported "flaky" (not counted as a failure).
|
|
163
|
+
"--retries=2",
|
|
164
|
+
"--reporter=json",
|
|
165
|
+
],
|
|
166
|
+
{ cwd: pwRepo, env },
|
|
167
|
+
);
|
|
168
|
+
let report;
|
|
169
|
+
try {
|
|
170
|
+
report = JSON.parse(fs.readFileSync(jsonOut, "utf8"));
|
|
171
|
+
} catch {
|
|
172
|
+
console.error(output.slice(-4000));
|
|
173
|
+
throw new Error(
|
|
174
|
+
`${mode}: no JSON report produced (exit code ${code}) — harness broken, see output above`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return collectFailures(report);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const args = parseArgs(process.argv.slice(2));
|
|
181
|
+
const pwRepo = path.resolve(args.pwRepo);
|
|
182
|
+
const ibrDist = path.resolve(process.env.PWTEST_IBR_DIST || DEFAULT_IBR_DIST);
|
|
183
|
+
if (!fs.existsSync(ibrDist)) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`IBR dist not found at ${ibrDist} — build playwright-utils first`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await applyFixturePatch(pwRepo);
|
|
190
|
+
|
|
191
|
+
console.log(`Specs: ${args.specs.join(", ")}`);
|
|
192
|
+
console.log("Running baseline (built-in blob reporter)...");
|
|
193
|
+
const blobFailures = await runMode({
|
|
194
|
+
pwRepo,
|
|
195
|
+
specs: args.specs,
|
|
196
|
+
workers: args.workers,
|
|
197
|
+
mode: "blob",
|
|
198
|
+
});
|
|
199
|
+
console.log(
|
|
200
|
+
`Baseline failures (env noise, cancelled in diff): ${blobFailures.size}`,
|
|
201
|
+
);
|
|
202
|
+
for (const key of [...blobFailures].sort()) {
|
|
203
|
+
console.log(` [baseline] ${key}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log("Running through IBR adapter...");
|
|
207
|
+
const ibrFailures = await runMode({
|
|
208
|
+
pwRepo,
|
|
209
|
+
specs: args.specs,
|
|
210
|
+
workers: args.workers,
|
|
211
|
+
mode: "ibr",
|
|
212
|
+
ibrDist,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const ibrOnly = [...ibrFailures].filter((key) => !blobFailures.has(key)).sort();
|
|
216
|
+
console.log(`IBR-only failures: ${ibrOnly.length}`);
|
|
217
|
+
for (const key of ibrOnly) {
|
|
218
|
+
console.log(` [ibr-only] ${key}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (ibrOnly.length > 0) {
|
|
222
|
+
console.error(
|
|
223
|
+
`\nFAIL: ${ibrOnly.length} upstream test(s) fail only through IBR. ` +
|
|
224
|
+
`The job stays red until IBR reaches blob parity (known gaps: onStdIO, ` +
|
|
225
|
+
`onError, result-level annotations, attachment bodies).`,
|
|
226
|
+
);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
console.log(
|
|
230
|
+
"\nOK: IBR at parity with the built-in blob reporter on these specs",
|
|
231
|
+
);
|