@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 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;AAqCD,eAAO,MAAM,UAAU,EAAE,oBA+BxB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,iBAarB,CAAC"}
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"}
@@ -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
- const middle = process.env.HAS_SHARDING
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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"}
@@ -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
- const reportBuffer = await (0, zip_1.readZipEntry)(ctx.zipPath, "report.jsonl");
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: ${ctx.zipPath}`);
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,EACT,MAAM,2BAA2B,CAAC;AAwBnC;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AASnD,cAAM,uBAAwB,YAAW,QAAQ;IAC/C,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,WAAW,CAIjB;IACF,OAAO,CAAC,UAAU,CAGhB;IACF,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,mBAAmB,CAA0B;IACrD,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,YAAY,CAAsD;IAC1E,OAAO,CAAC,UAAU,CAAsB;IACxC,OAAO,CAAC,gBAAgB,CAA8B;IAEtD,cAWC;IAED,OAAO,CAAC,cAAc,CAA6B;IAEnD,OAAO,CAAC,mBAAmB;IA+B3B,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,qBAAqB;YASf,uBAAuB;IAqErC;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAuB9B,OAAO,CAAC,qBAAqB;IA0B7B,OAAO,CAAC,8BAA8B;IAWtC,OAAO,CAAC,mBAAmB;IAmB3B,OAAO,KAAK,QAAQ,GAKnB;IAED,OAAO,KAAK,UAAU,GAErB;IAED,OAAO,KAAK,aAAa,GAExB;IAED,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,aAAa;YAUP,SAAS;IAwBvB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,CAc9C;IAED,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAEpD;IAED,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CA0BlD;IAED,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAUpE;IAED,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAUlE;IAEK,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAiC7C;IAED;;OAEG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAE3D;IAED,aAAa,IAAI,OAAO,CAEvB;CACF;AAID,wBAAgB,0BAA0B,IAAI,uBAAuB,GAAG,IAAI,CAE3E;AAED,wBAAgB,kCAAkC,CAChD,QAAQ,EAAE,uBAAuB,GAChC,IAAI,CAEN;eAEc,uBAAuB"}
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("[IncrementalBlobReporterFailure] No uploader available, skipping upload");
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
- // Normal completion already has Playwright's blob/html reporters.
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>): JsonEvent;
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,EACT,MAAM,2BAA2B,CAAC;AAgBnC,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,GACjB,SAAS,EAAE,CAqDb;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,GAC/B,SAAS,CAuBX;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAW7D"}
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.2",
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
  },
@@ -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
+ );