@empiricalrun/playwright-utils 0.58.3 → 0.58.5

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.
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizeJsonReport = normalizeJsonReport;
4
4
  exports.stepSignatures = stepSignatures;
5
+ exports.stepAttachmentsAtStepEnd = stepAttachmentsAtStepEnd;
6
+ exports.configAndProjects = configAndProjects;
5
7
  exports.missingEventMethods = missingEventMethods;
6
8
  const ANSI_RE = /\u001b\[[0-9;]*m/g;
7
9
  function normalizeText(text, cwd) {
@@ -93,11 +95,81 @@ function stepSignatures(events) {
93
95
  continue;
94
96
  }
95
97
  const list = signatures.get(key) ?? [];
96
- list.push(`${params.step.category}:${params.step.title}`);
98
+ // The location is part of the signature: it travels relative to rootDir
99
+ // and the merged HTML report renders it as-is.
100
+ const location = params.step.location
101
+ ? `${params.step.location.file}:${params.step.location.line}`
102
+ : "";
103
+ list.push(`${params.step.category}:${params.step.title}@${location}`);
97
104
  signatures.set(key, list);
98
105
  }
99
106
  return signatures;
100
107
  }
108
+ /**
109
+ * What a reporter would see for `step.attachments` DURING onStepEnd, replayed
110
+ * from the raw event stream the way teleReceiver does it: onAttach pushes onto
111
+ * `result.attachments`, and a step end carries indices into that array
112
+ * (TeleTestStep.attachments resolves them at access time). An attachment that
113
+ * only arrives at test end therefore reads as `<missing>` here.
114
+ *
115
+ * Keyed by `testId:retry`; each entry is one `title -> [names]` line per
116
+ * onStepEnd, in stream order.
117
+ */
118
+ function stepAttachmentsAtStepEnd(events) {
119
+ const resultKey = new Map(); // resultId -> testId:retry
120
+ const attachmentNames = new Map(); // resultId -> names so far
121
+ const stepTitles = new Map(); // stepId -> title
122
+ const views = new Map();
123
+ for (const event of events) {
124
+ if (event.method === "onTestBegin") {
125
+ const params = event.params;
126
+ resultKey.set(params.result.id, `${params.testId}:${params.result.retry}`);
127
+ attachmentNames.set(params.result.id, []);
128
+ }
129
+ else if (event.method === "onAttach") {
130
+ const params = event.params;
131
+ const names = attachmentNames.get(params.resultId);
132
+ names?.push(...params.attachments.map((a) => a.name));
133
+ }
134
+ else if (event.method === "onStepBegin") {
135
+ const params = event.params;
136
+ stepTitles.set(params.step.id, params.step.title);
137
+ }
138
+ else if (event.method === "onStepEnd") {
139
+ const params = event.params;
140
+ const key = resultKey.get(params.resultId);
141
+ if (!key) {
142
+ continue;
143
+ }
144
+ const names = attachmentNames.get(params.resultId) ?? [];
145
+ const resolved = (params.step.attachments ?? []).map((index) => names[index] ?? "<missing>");
146
+ const list = views.get(key) ?? [];
147
+ list.push(`${stepTitles.get(params.step.id) ?? "?"} -> [${resolved.join(", ")}]`);
148
+ views.set(key, list);
149
+ }
150
+ }
151
+ return views;
152
+ }
153
+ /**
154
+ * The onConfigure config payload plus every onProject project payload, keyed
155
+ * by project name. The merged report's FullConfig/FullProject is rebuilt from
156
+ * exactly these, and normalizeJsonReport compares only the suite/stats body —
157
+ * so this is where config and project serialization drift shows up.
158
+ */
159
+ function configAndProjects(events) {
160
+ let config;
161
+ const projects = {};
162
+ for (const event of events) {
163
+ if (event.method === "onConfigure") {
164
+ config = event.params.config;
165
+ }
166
+ else if (event.method === "onProject") {
167
+ const project = event.params.project;
168
+ projects[project.name] = project;
169
+ }
170
+ }
171
+ return { config, projects };
172
+ }
101
173
  /** Event method names present in `reference` but absent from `candidate`. */
102
174
  function missingEventMethods(reference, candidate) {
103
175
  const candidateMethods = new Set(candidate.map((e) => e.method));
@@ -1 +1 @@
1
- {"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,kBAAkB,EAElB,kBAAkB,EAEnB,MAAM,2BAA2B,CAAC;AAKnC,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAAqB;IAEzC,YAAY,MAAM,EAAE,cAAc,EASjC;IAED,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAgBhC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAUhC,MAAM,CAAC,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAKzE;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAEK,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgCxB;IAEK,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC,CAef;IAEK,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAEpC;CACF;AAID,wBAAgB,WAAW,IAAI,QAAQ,GAAG,IAAI,CAM7C"}
1
+ {"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,kBAAkB,EAElB,kBAAkB,EAEnB,MAAM,2BAA2B,CAAC;AAKnC,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,kBAAkB,CAAC;CACjC;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAAqB;IAEzC,YAAY,MAAM,EAAE,cAAc,EASjC;IAED,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAgBhC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAUhC,MAAM,CAAC,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAKzE;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAEK,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgCxB;IAEK,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC,CAef;IAEK,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CA4BpC;CACF;AAID,wBAAgB,WAAW,IAAI,QAAQ,GAAG,IAAI,CAM7C"}
@@ -100,7 +100,28 @@ class Uploader {
100
100
  await (0, r2_uploader_1.sendTaskToQueue)(uploadTask);
101
101
  }
102
102
  async waitForUploads() {
103
- await (0, r2_uploader_1.waitForTaskQueueToFinish)();
103
+ const { waiting, executing } = (0, r2_uploader_1.getTaskQueueStats)();
104
+ const pending = waiting + executing;
105
+ if (pending === 0) {
106
+ await (0, r2_uploader_1.waitForTaskQueueToFinish)();
107
+ return;
108
+ }
109
+ // This drain runs after the last test with no other output (observed at
110
+ // 4+ minutes on spot runs), so make it visible: count up front, progress
111
+ // every 30s, duration at the end.
112
+ const start = Date.now();
113
+ console.log(`[Uploader] Waiting for ${pending} pending uploads (${executing} executing, ${waiting} queued)...`);
114
+ const progress = setInterval(() => {
115
+ const stats = (0, r2_uploader_1.getTaskQueueStats)();
116
+ console.log(`[Uploader] Still draining: ${stats.executing + stats.waiting} uploads pending (${stats.executing} executing, ${stats.waiting} queued)`);
117
+ }, 30_000);
118
+ try {
119
+ await (0, r2_uploader_1.waitForTaskQueueToFinish)();
120
+ }
121
+ finally {
122
+ clearInterval(progress);
123
+ }
124
+ console.log(`[Uploader] Pending uploads drained in ${((Date.now() - start) / 1000).toFixed(1)}s`);
104
125
  }
105
126
  }
106
127
  exports.Uploader = Uploader;
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.5",
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,9 +54,9 @@
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",
59
- "@empiricalrun/r2-uploader": "^0.10.1",
58
+ "@empiricalrun/llm": "^0.27.0",
59
+ "@empiricalrun/r2-uploader": "^0.10.2",
60
60
  "@empiricalrun/reporter": "^0.29.1"
61
61
  },
62
62
  "scripts": {
@@ -20,16 +20,45 @@ Playwright version-pin bump).
20
20
 
21
21
  - `run.mjs` — orchestrator: applies the patch, runs the curated specs in both
22
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/`).
23
+ - `ibr-adapter.js` — bridges IBR's output contract to the blob reporter's: it
24
+ mirrors blob's zip name (`_defaultReportName`), output-dir resolution
25
+ (package root, not cwd) and pre-write cleanup, then relocates IBR's zip into
26
+ `blob-report/`. Sharding needs no bridge — IBR falls back to `config.shard`.
25
27
  - `intermediate-mode-ibr.patch` — one-line upstream fixture change making the
26
28
  intermediate reporter selectable via `PWTEST_IBR_ADAPTER`.
27
29
 
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.
30
+ The failure count reaching zero is the signal that the built-in blob reporter
31
+ is safe to drop. It got there over #7093/#7094/#7095 (the parity gaps and the
32
+ full-suite sweep) and then #7110/#7111/#7114/this PR, which took the full
33
+ sweep from 19 IBR-only failures to 0.
34
+
35
+ ### Expected failures
36
+
37
+ `EXPECTED_IBR_ONLY` in `run.mjs` holds upstream tests that fail for reasons
38
+ outside the blob format — artifacts of injecting an external reporter into
39
+ upstream's harness. Each entry carries the reason it can never pass and is
40
+ printed at startup; the count is reported separately and does not turn the job
41
+ red. An entry whose spec runs and PASSES is reported as `[stale-expected]` and
42
+ fails the run, so the list cannot quietly absorb tests that have been fixed.
43
+ Anything that could be fixed does not belong there.
44
+
45
+ ### Why two slow specs are excluded
46
+
47
+ `only-changed.spec.ts` and `web-server.spec.ts` are in `EXCLUSION_RULES` for a
48
+ different reason from the rest: cost, not scope. The oracle runs ONE spec per
49
+ invocation, and Playwright keeps a file's tests on a single worker
50
+ (`fullyParallel` defaults to false), so a long file runs strictly serially no
51
+ matter what `--workers` says. Measured, `only-changed` gets through 7 of 13
52
+ tests in 7 minutes — steady progress, no wedge — and `web-server` has 39
53
+ server-booting tests. Neither fits the 10-minute `SPEC_TIMEOUT_MS`, so both
54
+ used to burn the cap, get quarantined, and contribute nothing but a hang that
55
+ kept the job red.
56
+
57
+ If the sweep's wall-clock ever needs fixing properly, the lever is `run.mjs`
58
+ running specs sequentially: several invocations could run concurrently with no
59
+ loss of accuracy (each spec's blob/IBR pair still compares like for like),
60
+ once each gets its own `--output` dir instead of sharing
61
+ `<pwRepo>/test-results`.
33
62
 
34
63
  ## Running locally
35
64
 
@@ -45,6 +45,55 @@ class IBRBlobAdapter extends IBR {
45
45
  super();
46
46
  this._adapterOptions = options;
47
47
  this._adapterCwd = process.cwd();
48
+ this._adapterShard = null;
49
+ }
50
+
51
+ onBegin(config, suite) {
52
+ this._adapterShard = config.shard;
53
+ super.onBegin(config, suite);
54
+ }
55
+
56
+ // Mirrors resolveOutputFile('BLOB', …) + resolveReporterOutputPath: an
57
+ // explicit outputDir resolves against the config dir, and the DEFAULT
58
+ // blob-report hangs off the nearest package.json at or above the config
59
+ // dir (process.cwd() only as a last resort). Upstream drives the CLI from
60
+ // a nested dir in several tests and then merges <packageRoot>/blob-report,
61
+ // so resolving against the cwd puts the zip where nothing looks for it.
62
+ _resolveOutputDir() {
63
+ const configDir = this._adapterOptions.configDir || this._adapterCwd;
64
+ if (this._adapterOptions.outputDir) {
65
+ return path.resolve(configDir, this._adapterOptions.outputDir);
66
+ }
67
+ return path.resolve(this._packageRoot(configDir), "blob-report");
68
+ }
69
+
70
+ _packageRoot(configDir) {
71
+ let dir = path.resolve(configDir);
72
+ for (;;) {
73
+ if (fs.existsSync(path.join(dir, "package.json"))) {
74
+ return dir;
75
+ }
76
+ const parent = path.dirname(dir);
77
+ if (parent === dir) {
78
+ return this._adapterCwd;
79
+ }
80
+ dir = parent;
81
+ }
82
+ }
83
+
84
+ // Mirrors blob's _defaultReportName: the shard suffix only appears on a
85
+ // sharded run, zero-padded to the width of the shard total. Upstream tests
86
+ // assert on the file name (reporter-blob.spec's "preserve steps in html
87
+ // report" expects exactly ['report.zip']).
88
+ _defaultReportName() {
89
+ if (!this._adapterShard) {
90
+ return "report.zip";
91
+ }
92
+ const padded = `${this._adapterShard.current}`.padStart(
93
+ `${this._adapterShard.total}`.length,
94
+ "0",
95
+ );
96
+ return `report-${padded}.zip`;
48
97
  }
49
98
 
50
99
  async onEnd(result) {
@@ -59,11 +108,17 @@ class IBRBlobAdapter extends IBR {
59
108
  // Mirrors blob: no zip when there was nothing to report.
60
109
  return;
61
110
  }
62
- const outputDir = path.resolve(
63
- this._adapterCwd,
64
- this._adapterOptions.outputDir || "blob-report",
65
- );
66
- const fileName = this._adapterOptions.fileName || `report-${shard}.zip`;
111
+ const outputDir = this._resolveOutputDir();
112
+ const fileName = this._adapterOptions.fileName || this._defaultReportName();
113
+ // Mirrors blob's _prepareOutputFile, down to the opt-out: it wipes the
114
+ // output dir before writing unless PWTEST_BLOB_DO_NOT_REMOVE is set.
115
+ // Upstream tests merge whatever sits in blob-report/, and they run
116
+ // several shards from one baseDir — those that want the shards to
117
+ // accumulate set the env var, the rest (shard.spec's "shard 2/3 then
118
+ // 3/3") expect each run to start from an empty dir.
119
+ if (!process.env.PWTEST_BLOB_DO_NOT_REMOVE) {
120
+ fs.rmSync(outputDir, { recursive: true, force: true });
121
+ }
67
122
  fs.mkdirSync(outputDir, { recursive: true });
68
123
  fs.copyFileSync(src, path.join(outputDir, fileName));
69
124
  }