@empiricalrun/playwright-utils 0.58.3 → 0.58.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAuBnC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,kBAAkB,CAA2B;IAGrD,OAAO,CAAC,YAAY,CAAqB;IAEzC,cAEC;IAED,OAAO,CAAC,MAAM,EAAE,UAAU,QAEzB;IAED,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,6BAA6B,CA2CnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,QAqI3C;IAEK,KAAK,CAAC,MAAM,EAAE,UAAU,iBAiH7B;IAED,OAAO,CAAC,qBAAqB;YAkBf,gBAAgB;YAOhB,iBAAiB;CAqChC;eAEc,iBAAiB"}
1
+ {"version":3,"file":"empirical-reporter.d.ts","sourceRoot":"","sources":["../../src/reporter/empirical-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACX,MAAM,2BAA2B,CAAC;AAuBnC,cAAM,iBAAkB,YAAW,QAAQ;IACzC,OAAO,CAAC,kBAAkB,CAAyB;IACnD,OAAO,CAAC,oBAAoB,CAG1B;IACF,OAAO,CAAC,sBAAsB,CAAuB;IACrD,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,kBAAkB,CAA2B;IAGrD,OAAO,CAAC,YAAY,CAAqB;IAEzC,cAEC;IAED,OAAO,CAAC,MAAM,EAAE,UAAU,QAEzB;IAED,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,6BAA6B,CA2CnC;IAEF,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,QA+I3C;IAEK,KAAK,CAAC,MAAM,EAAE,UAAU,iBAiH7B;IAED,OAAO,CAAC,qBAAqB;YAkBf,gBAAgB;YAOhB,iBAAiB;CAqChC;eAEc,iBAAiB"}
@@ -150,6 +150,15 @@ class EmpiricalReporter {
150
150
  stack: e.stack,
151
151
  location: e.location,
152
152
  })),
153
+ // Playwright seeds result.annotations with the full per-attempt
154
+ // set (static + suite + runtime-pushed); test.annotations is the
155
+ // fallback for older Playwright versions without it.
156
+ annotations: (result.annotations?.length
157
+ ? result.annotations
158
+ : test.annotations).map((a) => ({
159
+ type: a.type,
160
+ description: a.description,
161
+ })),
153
162
  },
154
163
  projectName,
155
164
  attachments: successfulAttachments,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.58.3",
3
+ "version": "0.58.4",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -41,7 +41,7 @@
41
41
  "openapi-typescript": "7.6.1",
42
42
  "playwright-core": "1.62.1",
43
43
  "serve-handler": "^6.1.6",
44
- "@empiricalrun/shared-types": "0.15.3"
44
+ "@empiricalrun/shared-types": "0.15.4"
45
45
  },
46
46
  "dependencies": {
47
47
  "@babel/code-frame": "^7.24.7",
@@ -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/llm": "^0.27.0",
58
57
  "@empiricalrun/cua": "^0.4.1",
58
+ "@empiricalrun/llm": "^0.27.0",
59
59
  "@empiricalrun/r2-uploader": "^0.10.1",
60
60
  "@empiricalrun/reporter": "^0.29.1"
61
61
  },
@@ -9,15 +9,14 @@
9
9
  * format. The fixture patch (intermediate-mode-ibr.patch) lets
10
10
  * PWTEST_IBR_ADAPTER swap IBR in as that reporter.
11
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.
12
+ * Both modes run the same spec files; environment noise cancels in the
13
+ * baseline diff. Any IBR-only failure fails the run — the job stays red
14
+ * until IBR reaches parity on these specs. It staying green is the signal
15
+ * that the built-in blob reporter is safe to drop.
17
16
  *
18
17
  * Usage:
19
18
  * node run.mjs --pw-repo <upstream clone at the pinned tag>
20
- * [--specs basic.spec.ts,stdio.spec.ts] [--workers N]
19
+ * [--specs basic.spec.ts,stdio.spec.ts] [--workers N] [--shard i/n]
21
20
  *
22
21
  * The clone must be bootstrapped (npm ci + npm run build). This script
23
22
  * (re)applies the fixture patch idempotently on every run.
@@ -31,18 +30,68 @@ import { fileURLToPath } from "node:url";
31
30
 
32
31
  const ORACLE_DIR = path.dirname(fileURLToPath(import.meta.url));
33
32
 
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",
33
+ // The default sweep is EVERY spec in upstream's tests/playwright-test —
34
+ // IBR is meant to become the default blob reporter, so it has to stand the
35
+ // whole runner suite. Only structurally out-of-scope specs are excluded,
36
+ // each with its reason printed at startup (no silent caps). First matching
37
+ // rule wins.
38
+ const EXCLUSION_RULES = [
39
+ [
40
+ (name) => name.startsWith("playwright.ct-"),
41
+ "component-testing build; no runner-report signal",
42
+ ],
43
+ [
44
+ (name) => name.startsWith("playwright."),
45
+ "drives real browsers (not installed in the oracle env)",
46
+ ],
47
+ [
48
+ (name) => name.startsWith("ui-mode-"),
49
+ "UI mode; needs trace viewer + browsers",
50
+ ],
51
+ [
52
+ (name) =>
53
+ name === "to-have-screenshot.spec.ts" ||
54
+ name === "aria-snapshot-file.spec.ts" ||
55
+ name === "update-aria-snapshot.spec.ts",
56
+ "browser screenshots/aria snapshots",
57
+ ],
58
+ [
59
+ (name) => name === "types.spec.ts" || name === "types-2.spec.ts",
60
+ "tsc-only; exercises no runner report",
61
+ ],
62
+ [
63
+ (name) => name === "reporter-html.spec.ts",
64
+ "browser-driven HTML-report UI tests; no blob-format signal (merge " +
65
+ "semantics are covered by reporter-blob.spec.ts) and they busy-loop " +
66
+ "workers under load, wedging the sweep",
67
+ ],
44
68
  ];
45
69
 
70
+ // Tests skipped by title via --grep-invert. Reserved for tests that WEDGE
71
+ // the sweep — a busy-looped worker no per-test timeout can interrupt — so
72
+ // exclusion is surgical (exact title match) and every entry carries the
73
+ // observed reason. Everything else in the spec still runs.
74
+ const HANGING_TESTS = [];
75
+
76
+ function discoverSpecs(pwRepo) {
77
+ const specDir = path.join(pwRepo, "tests", "playwright-test");
78
+ const all = fs
79
+ .readdirSync(specDir)
80
+ .filter((f) => f.endsWith(".spec.ts"))
81
+ .sort();
82
+ const included = [];
83
+ const excluded = [];
84
+ for (const name of all) {
85
+ const rule = EXCLUSION_RULES.find(([match]) => match(name));
86
+ if (rule) {
87
+ excluded.push([name, rule[1]]);
88
+ } else {
89
+ included.push(name);
90
+ }
91
+ }
92
+ return { included, excluded };
93
+ }
94
+
46
95
  const ADAPTER_PATH = path.join(ORACLE_DIR, "ibr-adapter.js");
47
96
  const PATCH_PATH = path.join(ORACLE_DIR, "intermediate-mode-ibr.patch");
48
97
  const DEFAULT_IBR_DIST = path.join(
@@ -51,7 +100,7 @@ const DEFAULT_IBR_DIST = path.join(
51
100
  );
52
101
 
53
102
  function parseArgs(argv) {
54
- const args = { specs: DEFAULT_SPECS, workers: 4 };
103
+ const args = { specs: null, workers: 4, shard: null };
55
104
  // Normalize --flag=value into --flag value.
56
105
  argv = argv.flatMap((a) =>
57
106
  a.startsWith("--") && a.includes("=")
@@ -66,6 +115,15 @@ function parseArgs(argv) {
66
115
  args.specs = argv[++i].split(",").filter(Boolean);
67
116
  } else if (arg === "--workers") {
68
117
  args.workers = Number(argv[++i]);
118
+ } else if (arg === "--shard") {
119
+ const match = /^(\d+)\/(\d+)$/.exec(argv[++i] ?? "");
120
+ if (!match) {
121
+ throw new Error("--shard expects i/n (e.g. --shard 2/4)");
122
+ }
123
+ args.shard = { index: Number(match[1]), total: Number(match[2]) };
124
+ if (args.shard.index < 1 || args.shard.index > args.shard.total) {
125
+ throw new Error(`--shard index out of range: ${argv[i]}`);
126
+ }
69
127
  } else {
70
128
  throw new Error(`Unknown argument: ${arg}`);
71
129
  }
@@ -134,7 +192,19 @@ async function applyFixturePatch(pwRepo) {
134
192
  }
135
193
  }
136
194
 
137
- async function runMode({ pwRepo, specs, workers, mode, ibrDist }) {
195
+ // Hard cap per spec-mode run (milliseconds — Playwright's --global-timeout
196
+ // unit). Some upstream tests can busy-loop a worker under load (sync string
197
+ // processing that no per-test timeout can interrupt — the timeouts run on
198
+ // the same blocked event loop); which test wedges is timing-dependent, so
199
+ // title exclusion cannot converge. Playwright's global timeout fires from
200
+ // the main process and SIGKILLs through busy loops. Each spec runs as its
201
+ // own bounded invocation, so a wedge costs one spec's cap, names the spec,
202
+ // and the sweep continues — hangs become inventory instead of aborting the
203
+ // whole mode.
204
+ const SPEC_TIMEOUT_MS = 10 * 60 * 1_000;
205
+
206
+ async function runMode({ pwRepo, spec, workers, mode, ibrDist }) {
207
+ const startedAt = Date.now();
138
208
  const jsonOut = path.join(
139
209
  fs.mkdtempSync(path.join(os.tmpdir(), "ibr-oracle-")),
140
210
  `${mode}.json`,
@@ -149,32 +219,68 @@ async function runMode({ pwRepo, specs, workers, mode, ibrDist }) {
149
219
  env.PWTEST_IBR_ADAPTER = ADAPTER_PATH;
150
220
  env.PWTEST_IBR_DIST = ibrDist;
151
221
  }
222
+ // The inner run writes its JSON report at the end, so nothing streams to
223
+ // stdout while it runs. Heartbeat from the test-results dir (the outer
224
+ // harness creates one dir per executed test) so long modes show progress.
225
+ const testResultsDir = path.join(pwRepo, "test-results");
226
+ const heartbeat = setInterval(() => {
227
+ let dirs = 0;
228
+ try {
229
+ dirs = fs.readdirSync(testResultsDir).length;
230
+ } catch {
231
+ // Not created yet.
232
+ }
233
+ const minutes = Math.round((Date.now() - startedAt) / 60_000);
234
+ console.log(
235
+ ` [${mode} ${spec}] ~${dirs} tests executed, ${minutes}m elapsed`,
236
+ );
237
+ }, 60_000);
152
238
  const { code, output } = await run(
153
239
  "npm",
154
240
  [
155
241
  "run",
156
242
  "ttest",
157
243
  "--",
158
- ...specs,
244
+ spec,
159
245
  `--workers=${workers}`,
160
246
  // Absorbs outer-harness flake: a scenario that fails on IBR's real gaps
161
247
  // fails every retry, while an infra hiccup passes on retry and is
162
248
  // reported "flaky" (not counted as a failure).
163
249
  "--retries=2",
250
+ `--global-timeout=${SPEC_TIMEOUT_MS}`,
251
+ // An empty --grep-invert would match (and thus drop) every test, so
252
+ // the flag only appears when there are titles to skip.
253
+ ...(HANGING_TESTS.length
254
+ ? [`--grep-invert=${HANGING_TESTS.map((h) => h.pattern).join("|")}`]
255
+ : []),
164
256
  "--reporter=json",
165
257
  ],
166
258
  { cwd: pwRepo, env },
167
259
  );
260
+ clearInterval(heartbeat);
261
+ const elapsedSeconds = Math.round((Date.now() - startedAt) / 1_000);
262
+ // A globally-timed-out run still writes a valid JSON report with the
263
+ // remaining tests marked "did not run", which would otherwise diff as a
264
+ // false green — report the hang instead of trusting the partial report.
265
+ if (/Timed out waiting .* for the test suite/.test(output)) {
266
+ return { hung: true, elapsedSeconds };
267
+ }
168
268
  let report;
169
269
  try {
170
270
  report = JSON.parse(fs.readFileSync(jsonOut, "utf8"));
171
271
  } catch {
172
272
  console.error(output.slice(-4000));
173
273
  throw new Error(
174
- `${mode}: no JSON report produced (exit code ${code}) — harness broken, see output above`,
274
+ `${mode} ${spec}: no JSON report produced (exit code ${code}) — harness broken, see output above`,
175
275
  );
176
276
  }
177
- return collectFailures(report);
277
+ const stats = report.stats ?? {};
278
+ const totalRun =
279
+ (stats.expected ?? 0) +
280
+ (stats.unexpected ?? 0) +
281
+ (stats.flaky ?? 0) +
282
+ (stats.skipped ?? 0);
283
+ return { failures: collectFailures(report), totalRun, elapsedSeconds };
178
284
  }
179
285
 
180
286
  const args = parseArgs(process.argv.slice(2));
@@ -188,41 +294,99 @@ if (!fs.existsSync(ibrDist)) {
188
294
 
189
295
  await applyFixturePatch(pwRepo);
190
296
 
297
+ if (!args.specs) {
298
+ const { included, excluded } = discoverSpecs(pwRepo);
299
+ args.specs = included;
300
+ console.log(`Excluded specs (${excluded.length}):`);
301
+ for (const [name, reason] of excluded) {
302
+ console.log(` [excluded] ${name} — ${reason}`);
303
+ }
304
+ }
305
+ if (args.shard) {
306
+ // Round-robin slice so slow specs spread across shards.
307
+ args.specs = args.specs.filter(
308
+ (_, i) => i % args.shard.total === args.shard.index - 1,
309
+ );
310
+ console.log(
311
+ `Shard ${args.shard.index}/${args.shard.total}: ${args.specs.length} specs`,
312
+ );
313
+ }
314
+
191
315
  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}`);
316
+ for (const { pattern, reason } of HANGING_TESTS) {
317
+ console.log(` [skipped-by-title] /${pattern}/ — ${reason}`);
204
318
  }
319
+ // Each spec runs both modes back to back as its own bounded invocation
320
+ // (see SPEC_TIMEOUT_MS); a hang or count mismatch quarantines that spec and
321
+ // the sweep continues.
322
+ const ibrOnly = [];
323
+ const hungSpecs = [];
324
+ const mismatchedSpecs = [];
325
+ let baselineNoise = 0;
205
326
 
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
- });
327
+ for (const spec of args.specs) {
328
+ const blob = await runMode({
329
+ pwRepo,
330
+ spec,
331
+ workers: args.workers,
332
+ mode: "blob",
333
+ });
334
+ if (blob.hung) {
335
+ console.log(` [hung] ${spec} (blob mode, ${blob.elapsedSeconds}s)`);
336
+ hungSpecs.push(`${spec} (blob mode)`);
337
+ continue;
338
+ }
339
+ const ibr = await runMode({
340
+ pwRepo,
341
+ spec,
342
+ workers: args.workers,
343
+ mode: "ibr",
344
+ ibrDist,
345
+ });
346
+ if (ibr.hung) {
347
+ console.log(` [hung] ${spec} (ibr mode, ${ibr.elapsedSeconds}s)`);
348
+ hungSpecs.push(`${spec} (ibr mode)`);
349
+ continue;
350
+ }
351
+ if (blob.totalRun === 0 || blob.totalRun !== ibr.totalRun) {
352
+ console.log(
353
+ ` [mismatch] ${spec}: blob ran ${blob.totalRun}, ibr ran ${ibr.totalRun}`,
354
+ );
355
+ mismatchedSpecs.push(spec);
356
+ continue;
357
+ }
358
+ const specIbrOnly = [...ibr.failures]
359
+ .filter((key) => !blob.failures.has(key))
360
+ .sort();
361
+ baselineNoise += blob.failures.size;
362
+ ibrOnly.push(...specIbrOnly);
363
+ console.log(
364
+ ` ${spec}: ${blob.totalRun} tests, ` +
365
+ `${blob.failures.size} baseline noise, ${specIbrOnly.length} ibr-only ` +
366
+ `(blob ${blob.elapsedSeconds}s, ibr ${ibr.elapsedSeconds}s)`,
367
+ );
368
+ }
214
369
 
215
- const ibrOnly = [...ibrFailures].filter((key) => !blobFailures.has(key)).sort();
370
+ console.log(
371
+ `\nBaseline failures (env noise, cancelled in diff): ${baselineNoise}`,
372
+ );
216
373
  console.log(`IBR-only failures: ${ibrOnly.length}`);
217
- for (const key of ibrOnly) {
374
+ for (const key of ibrOnly.sort()) {
218
375
  console.log(` [ibr-only] ${key}`);
219
376
  }
377
+ for (const spec of hungSpecs) {
378
+ console.log(` [hung] ${spec}`);
379
+ }
380
+ for (const spec of mismatchedSpecs) {
381
+ console.log(` [mismatch] ${spec}`);
382
+ }
220
383
 
221
- if (ibrOnly.length > 0) {
384
+ if (ibrOnly.length > 0 || hungSpecs.length > 0 || mismatchedSpecs.length > 0) {
222
385
  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).`,
386
+ `\nFAIL: ${ibrOnly.length} upstream test(s) fail only through IBR, ` +
387
+ `${hungSpecs.length} spec run(s) hung, ${mismatchedSpecs.length} ` +
388
+ `spec(s) mismatched. The job stays red until IBR reaches blob parity ` +
389
+ `on these specs.`,
226
390
  );
227
391
  process.exit(1);
228
392
  }