@monoes/monobrowse 1.0.15 → 1.0.17

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.
Files changed (37) hide show
  1. package/dist/src/__tests__/launch-close-settle.test.d.ts +32 -0
  2. package/dist/src/__tests__/launch-close-settle.test.d.ts.map +1 -0
  3. package/dist/src/__tests__/launch-close-settle.test.js +155 -0
  4. package/dist/src/__tests__/launch-close-settle.test.js.map +1 -0
  5. package/dist/src/__tests__/report-exit-code.test.d.ts +46 -0
  6. package/dist/src/__tests__/report-exit-code.test.d.ts.map +1 -0
  7. package/dist/src/__tests__/report-exit-code.test.js +188 -0
  8. package/dist/src/__tests__/report-exit-code.test.js.map +1 -0
  9. package/dist/src/browser/actions.d.ts.map +1 -1
  10. package/dist/src/browser/actions.js +19 -9
  11. package/dist/src/browser/actions.js.map +1 -1
  12. package/dist/src/browser/bridge.d.ts.map +1 -1
  13. package/dist/src/browser/bridge.js +4 -1
  14. package/dist/src/browser/bridge.js.map +1 -1
  15. package/dist/src/browser/browser.d.ts.map +1 -1
  16. package/dist/src/browser/browser.js +62 -7
  17. package/dist/src/browser/browser.js.map +1 -1
  18. package/dist/src/browser/cdp.d.ts.map +1 -1
  19. package/dist/src/browser/cdp.js +8 -1
  20. package/dist/src/browser/cdp.js.map +1 -1
  21. package/dist/src/cli/commands-report.d.ts.map +1 -1
  22. package/dist/src/cli/commands-report.js +46 -20
  23. package/dist/src/cli/commands-report.js.map +1 -1
  24. package/dist/src/report/util.d.ts +4 -0
  25. package/dist/src/report/util.d.ts.map +1 -1
  26. package/dist/src/report/util.js +9 -3
  27. package/dist/src/report/util.js.map +1 -1
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +1 -1
  30. package/src/__tests__/launch-close-settle.test.ts +175 -0
  31. package/src/__tests__/report-exit-code.test.ts +207 -0
  32. package/src/browser/actions.ts +21 -12
  33. package/src/browser/bridge.ts +4 -1
  34. package/src/browser/browser.ts +63 -7
  35. package/src/browser/cdp.ts +8 -1
  36. package/src/cli/commands-report.ts +46 -19
  37. package/src/report/util.ts +9 -3
@@ -186,6 +186,34 @@ export const reportCommand: Command = {
186
186
  trendWindow: ctx.flags['trend-window'] as number | undefined,
187
187
  };
188
188
 
189
+ // Closing the browser is cleanup, not part of the verdict. It used to run
190
+ // in a `finally` sitting between "we have the result" and "we print it",
191
+ // which meant anything that wedged the teardown — a Chrome that never
192
+ // acknowledged `Browser.close`, a CDP socket that died silently — took the
193
+ // verdict down with it: no output at all, and exit 0 on a page that FAILED
194
+ // its budgets. That is exactly the signal CI, agents and `&&` chains gate
195
+ // on, so a hung teardown reported success on a broken page. The verdict is
196
+ // now printed and the exit code fixed BEFORE any of this runs.
197
+ const teardown = async (): Promise<void> => {
198
+ // A report is a one-shot command — CI should not be left with an
199
+ // orphan Chrome. Only close a browser THIS process launched: an
200
+ // attached one belongs to the user's own `open`/`connect` session.
201
+ if (ctx.flags['keep-open'] || browser.getLaunchedPid(session.port) === undefined) return;
202
+ browser.stopRequestCapture(sessionId);
203
+ browser.teardownConsoleCapture(sessionId);
204
+ try {
205
+ await browser.closeBrowser(client, session.port);
206
+ } catch {
207
+ /* best-effort */
208
+ }
209
+ session.client = null;
210
+ session.sessionId = '';
211
+ session.targetId = '';
212
+ session.refs = new Map();
213
+ await browser.clearActivePort();
214
+ await browser.clearRefCache();
215
+ };
216
+
189
217
  let result:
190
218
  | Awaited<ReturnType<typeof runReport>>
191
219
  | Awaited<ReturnType<typeof runReportRepeated>>;
@@ -193,25 +221,11 @@ export const reportCommand: Command = {
193
221
  result = repeat
194
222
  ? await runReportRepeated(client, sessionId, { ...runOptions, repeat })
195
223
  : await runReport(client, sessionId, runOptions);
196
- } finally {
197
- // A report is a one-shot command CI should not be left with an
198
- // orphan Chrome. Only close a browser THIS process launched: an
199
- // attached one belongs to the user's own `open`/`connect` session.
200
- if (!ctx.flags['keep-open'] && browser.getLaunchedPid(session.port) !== undefined) {
201
- browser.stopRequestCapture(sessionId);
202
- browser.teardownConsoleCapture(sessionId);
203
- try {
204
- await browser.closeBrowser(client, session.port);
205
- } catch {
206
- /* best-effort */
207
- }
208
- session.client = null;
209
- session.sessionId = '';
210
- session.targetId = '';
211
- session.refs = new Map();
212
- await browser.clearActivePort();
213
- await browser.clearRefCache();
214
- }
224
+ } catch (err) {
225
+ // No verdict to print on this path, so the old ordering still applies:
226
+ // clean up, then let the error propagate to the CLI's error handler.
227
+ await teardown();
228
+ throw err;
215
229
  }
216
230
 
217
231
  const { report, htmlPath, jsonPath, summary, historyDir } = result;
@@ -220,6 +234,15 @@ export const reportCommand: Command = {
220
234
  // runs must not exit 0 just because the last run happened to be green.
221
235
  const passed = flake ? flake.verdict === 'pass' : report.verdict === 'pass';
222
236
 
237
+ // Set the process-wide failure code up front, before printing and before
238
+ // teardown. Returning `exitCode` below is the normal channel, but it only
239
+ // reaches the CLI if this function returns; if teardown never settles,
240
+ // Node drains the event loop and exits on its own — and a natural exit
241
+ // uses process.exitCode. Setting it here means a failing page cannot exit
242
+ // 0 no matter what happens after this line. Only ever set on failure, so
243
+ // this never clears a non-zero code set elsewhere.
244
+ if (!passed) process.exitCode = 1;
245
+
223
246
  if (ctx.flags.json) {
224
247
  const { toJsonReport } = await import('../report/index.js');
225
248
  print(
@@ -265,6 +288,10 @@ export const reportCommand: Command = {
265
288
  if (historyDir) print(`History: ${historyDir}`);
266
289
  }
267
290
 
291
+ // Verdict is printed and the exit code is fixed — only now is it safe to
292
+ // close the browser.
293
+ await teardown();
294
+
268
295
  // Non-zero exit is the point of RIG-06 — CI and agents gate on it.
269
296
  return {
270
297
  success: passed,
@@ -3,10 +3,13 @@
3
3
  /** Budget for one non-navigation collector (AX tree, screenshot, eval). */
4
4
  export const STEP_TIMEOUT_MS = 15_000;
5
5
 
6
+ /** Not unref'd: callers `await sleep(...)` (e.g. the web-vitals settle
7
+ * window), so this timer is on an actively-awaited path. An unref'd timer
8
+ * does not keep the event loop alive, so once the CDP socket closes Node
9
+ * would drain and exit mid-report instead of resuming after the sleep. */
6
10
  export function sleep(ms: number): Promise<void> {
7
11
  return new Promise((r) => {
8
- const t = setTimeout(r, ms);
9
- t.unref?.();
12
+ setTimeout(r, ms);
10
13
  });
11
14
  }
12
15
 
@@ -23,9 +26,12 @@ export async function withTimeout<T>(promise: Promise<T>, ms: number, label: str
23
26
  try {
24
27
  return await Promise.race([
25
28
  promise,
29
+ // Not unref'd: this timer is the only thing that settles the race when
30
+ // `promise` is waiting on a socket that quietly went away, and every
31
+ // caller awaits the result. The finally below clears it, so work that
32
+ // finishes in time never holds the event loop open.
26
33
  new Promise<never>((_, reject) => {
27
34
  timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
28
- timer.unref?.();
29
35
  }),
30
36
  ]);
31
37
  } finally {