@empiricalrun/playwright-utils 0.58.1 → 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.
@@ -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
+ );