@monoes/monobrowse 1.0.19 → 1.0.21

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 (61) hide show
  1. package/dist/src/__tests__/browse-concurrency.test.d.ts +15 -0
  2. package/dist/src/__tests__/browse-concurrency.test.d.ts.map +1 -0
  3. package/dist/src/__tests__/browse-concurrency.test.js +198 -0
  4. package/dist/src/__tests__/browse-concurrency.test.js.map +1 -0
  5. package/dist/src/__tests__/close-browser.test.js +2 -2
  6. package/dist/src/__tests__/close-browser.test.js.map +1 -1
  7. package/dist/src/__tests__/frame-command.test.js +12 -2
  8. package/dist/src/__tests__/frame-command.test.js.map +1 -1
  9. package/dist/src/__tests__/reap-idle-browser.test.js +13 -13
  10. package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -1
  11. package/dist/src/__tests__/ref-cache.test.js +96 -58
  12. package/dist/src/__tests__/ref-cache.test.js.map +1 -1
  13. package/dist/src/__tests__/report-exit-code.test.js +1 -1
  14. package/dist/src/__tests__/session-discovery.test.d.ts +13 -0
  15. package/dist/src/__tests__/session-discovery.test.d.ts.map +1 -0
  16. package/dist/src/__tests__/session-discovery.test.js +218 -0
  17. package/dist/src/__tests__/session-discovery.test.js.map +1 -0
  18. package/dist/src/browser/browser.d.ts +2 -1
  19. package/dist/src/browser/browser.d.ts.map +1 -1
  20. package/dist/src/browser/browser.js +17 -9
  21. package/dist/src/browser/browser.js.map +1 -1
  22. package/dist/src/browser/ref-cache.d.ts +50 -22
  23. package/dist/src/browser/ref-cache.d.ts.map +1 -1
  24. package/dist/src/browser/ref-cache.js +122 -57
  25. package/dist/src/browser/ref-cache.js.map +1 -1
  26. package/dist/src/cli/commands-context.js +1 -1
  27. package/dist/src/cli/commands-context.js.map +1 -1
  28. package/dist/src/cli/commands-navigate.js +2 -2
  29. package/dist/src/cli/commands-navigate.js.map +1 -1
  30. package/dist/src/cli/commands-page.js +2 -2
  31. package/dist/src/cli/commands-page.js.map +1 -1
  32. package/dist/src/cli/commands-report.js +2 -2
  33. package/dist/src/cli/commands-report.js.map +1 -1
  34. package/dist/src/cli/commands-session.d.ts.map +1 -1
  35. package/dist/src/cli/commands-session.js +70 -56
  36. package/dist/src/cli/commands-session.js.map +1 -1
  37. package/dist/src/cli/commands.d.ts.map +1 -1
  38. package/dist/src/cli/commands.js +102 -66
  39. package/dist/src/cli/commands.js.map +1 -1
  40. package/dist/src/cli/session.d.ts +66 -2
  41. package/dist/src/cli/session.d.ts.map +1 -1
  42. package/dist/src/cli/session.js +190 -33
  43. package/dist/src/cli/session.js.map +1 -1
  44. package/dist/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +1 -1
  46. package/src/__tests__/browse-concurrency.test.ts +233 -0
  47. package/src/__tests__/close-browser.test.ts +2 -2
  48. package/src/__tests__/frame-command.test.ts +12 -2
  49. package/src/__tests__/reap-idle-browser.test.ts +15 -14
  50. package/src/__tests__/ref-cache.test.ts +112 -59
  51. package/src/__tests__/report-exit-code.test.ts +1 -1
  52. package/src/__tests__/session-discovery.test.ts +259 -0
  53. package/src/browser/browser.ts +23 -9
  54. package/src/browser/ref-cache.ts +144 -72
  55. package/src/cli/commands-context.ts +1 -1
  56. package/src/cli/commands-navigate.ts +2 -2
  57. package/src/cli/commands-page.ts +2 -2
  58. package/src/cli/commands-report.ts +2 -2
  59. package/src/cli/commands-session.ts +74 -55
  60. package/src/cli/commands.ts +106 -66
  61. package/src/cli/session.ts +208 -33
@@ -90,6 +90,7 @@ import {
90
90
  } from './commands-trace.js';
91
91
  import { output } from './output.js';
92
92
  import { createPlatformCommand } from './platform.js';
93
+ import { applySessionPortFlag } from './session.js';
93
94
  import type { Command, CommandContext, CommandResult } from './types.js';
94
95
 
95
96
  // Re-exported for direct unit testing — these were exported from this file
@@ -212,80 +213,119 @@ const batchCommand: Command = {
212
213
  const actionSubcommand: Command = wrapCommanderCommand(createActionCommand);
213
214
  const platformSubcommand: Command = wrapCommanderCommand(createPlatformCommand);
214
215
 
216
+ // ---------------------------------------------------------------------------
217
+ // Session selector
218
+ //
219
+ // Sessions are per-port (#318), so `--port N` is how any command says which
220
+ // one it means; without it a command resolves the newest live session in this
221
+ // directory. Wiring it here, once, gives every subcommand the selector
222
+ // without each action having to read the flag: session.port is set before the
223
+ // action runs, and that is what every ensureConnected(session.port) resolves.
224
+ // ---------------------------------------------------------------------------
225
+
226
+ const PORT_OPTION = {
227
+ name: 'port',
228
+ short: 'p',
229
+ type: 'number',
230
+ description: 'Act on the browse session on this CDP port (default: the newest live session)',
231
+ } as const;
232
+
233
+ function withSessionSelector(cmd: Command): Command {
234
+ const action = cmd.action;
235
+ return {
236
+ ...cmd,
237
+ options: cmd.options?.some((o) => o.name === 'port')
238
+ ? cmd.options
239
+ : [...(cmd.options ?? []), { ...PORT_OPTION }],
240
+ action: action
241
+ ? async (ctx: CommandContext) => {
242
+ applySessionPortFlag(ctx.flags);
243
+ return action(ctx);
244
+ }
245
+ : undefined,
246
+ subcommands: cmd.subcommands?.map(withSessionSelector),
247
+ };
248
+ }
249
+
215
250
  // ---------------------------------------------------------------------------
216
251
  // Root browse command
217
252
  // ---------------------------------------------------------------------------
218
253
 
254
+ // Every subcommand gets the `--port` session selector (see
255
+ // withSessionSelector). batchCommand dispatches over this same array, so a
256
+ // batched command honours the selector too.
257
+ const subcommands: Command[] = [
258
+ openCommand,
259
+ snapshotCommand,
260
+ clickCommand,
261
+ dblclickCommand,
262
+ fillCommand,
263
+ typeCommand,
264
+ pressCommand,
265
+ keyboardCommand,
266
+ keydownCommand,
267
+ keyupCommand,
268
+ hoverCommand,
269
+ focusCommand,
270
+ selectCommand,
271
+ checkCommand,
272
+ uncheckCommand,
273
+ isvisibleCommand,
274
+ isenabledCommand,
275
+ ischeckedCommand,
276
+ tapCommand,
277
+ swipeCommand,
278
+ scrollIntoViewCommand,
279
+ dragCommand,
280
+ uploadCommand,
281
+ downloadCommand,
282
+ mouseCommand,
283
+ clipboardCommand,
284
+ waitCommand,
285
+ screenshotCommand,
286
+ getCommand,
287
+ scrollCommand,
288
+ navigateCommand,
289
+ setCommand,
290
+ stateCommand,
291
+ networkCommand,
292
+ evalCommand,
293
+ dialogCommand,
294
+ frameCommand,
295
+ tabCommand,
296
+ windowCommand,
297
+ consoleLogCommand,
298
+ errorsCommand,
299
+ storageCommand,
300
+ cookiesCommand,
301
+ pdfCommand,
302
+ isCommand,
303
+ findCommand,
304
+ highlightCommand,
305
+ diffCommand,
306
+ pushstateCommand,
307
+ batchCommand,
308
+ addinitscriptCommand,
309
+ removeinitscriptCommand,
310
+ connectCommand,
311
+ recordCommand,
312
+ traceCommand,
313
+ profilerCommand,
314
+ vitalsCommand,
315
+ reportCommand,
316
+ harCommand,
317
+ resizeCommand,
318
+ closeCommand,
319
+ actionSubcommand,
320
+ platformSubcommand,
321
+ ].map(withSessionSelector);
322
+
219
323
  const browseCommand: Command = {
220
324
  name: 'browse',
221
325
  description: 'Native browser automation via Chrome DevTools Protocol',
222
- subcommands: [
223
- openCommand,
224
- snapshotCommand,
225
- clickCommand,
226
- dblclickCommand,
227
- fillCommand,
228
- typeCommand,
229
- pressCommand,
230
- keyboardCommand,
231
- keydownCommand,
232
- keyupCommand,
233
- hoverCommand,
234
- focusCommand,
235
- selectCommand,
236
- checkCommand,
237
- uncheckCommand,
238
- isvisibleCommand,
239
- isenabledCommand,
240
- ischeckedCommand,
241
- tapCommand,
242
- swipeCommand,
243
- scrollIntoViewCommand,
244
- dragCommand,
245
- uploadCommand,
246
- downloadCommand,
247
- mouseCommand,
248
- clipboardCommand,
249
- waitCommand,
250
- screenshotCommand,
251
- getCommand,
252
- scrollCommand,
253
- navigateCommand,
254
- setCommand,
255
- stateCommand,
256
- networkCommand,
257
- evalCommand,
258
- dialogCommand,
259
- frameCommand,
260
- tabCommand,
261
- windowCommand,
262
- consoleLogCommand,
263
- errorsCommand,
264
- storageCommand,
265
- cookiesCommand,
266
- pdfCommand,
267
- isCommand,
268
- findCommand,
269
- highlightCommand,
270
- diffCommand,
271
- pushstateCommand,
272
- batchCommand,
273
- addinitscriptCommand,
274
- removeinitscriptCommand,
275
- connectCommand,
276
- recordCommand,
277
- traceCommand,
278
- profilerCommand,
279
- vitalsCommand,
280
- reportCommand,
281
- harCommand,
282
- resizeCommand,
283
- closeCommand,
284
- actionSubcommand,
285
- platformSubcommand,
286
- ],
326
+ subcommands,
287
327
  options: [
288
- { name: 'port', short: 'p', type: 'number', description: 'CDP debug port', default: 9222 },
328
+ { ...PORT_OPTION },
289
329
  { name: 'session', short: 's', type: 'string', description: 'Named session to use' },
290
330
  ],
291
331
  examples: [
@@ -7,13 +7,36 @@
7
7
  * module-level. It is a mutable record rather than separate bindings because
8
8
  * the subcommand modules that update it import it (see `session`).
9
9
  *
10
+ * THE SESSION RULE (#318). A browse session is one Chrome plus the CDP port
11
+ * it listens on, recorded per working directory in
12
+ * `.monomind/monobrowse/sessions/<port>.json`:
13
+ *
14
+ * - `open` with no `--port` STARTS its own session: Chrome binds a
15
+ * kernel-assigned free port in a profile directory of its own. It never
16
+ * joins an existing one, so two uncoordinated invocations can never land
17
+ * on the same port or profile. `open` reports the port it got.
18
+ * - `open`/`connect` WITH `--port N` keep the old behaviour exactly: attach
19
+ * to a Chrome already listening on N, otherwise launch there.
20
+ * - Any later command with no `--port` acts on the newest session in this
21
+ * directory whose browser still answers; every dead record it passes on
22
+ * the way is dropped (self-heal). With none left it starts a session of
23
+ * its own, exactly as `open` would.
24
+ * - Any later command with `--port N` acts on that session, which is how a
25
+ * concurrent caller pins the one `open` handed it.
26
+ * - `close` ends exactly the session it resolved — one browser, one record.
27
+ *
10
28
  * Split out of commands.ts, which is now the command catalogue.
11
29
  */
12
30
 
13
- import type { CdpClient, ElementRef } from '../index.js';
31
+ import { randomUUID } from 'node:crypto';
32
+ import { tmpdir } from 'node:os';
33
+ import { join } from 'node:path';
34
+ import type { CdpClient, ElementRef, SessionRecord } from '../index.js';
14
35
  import { output } from './output.js';
36
+ import type { ParsedFlags } from './types.js';
15
37
 
16
- const DEFAULT_PORT = 9222;
38
+ /** `session.port` when no `--port` was given: resolve or start one instead. */
39
+ const UNPINNED = 0;
17
40
 
18
41
  // Runtime state (single session per CLI process).
19
42
  //
@@ -33,11 +56,36 @@ export const session: {
33
56
  client: null,
34
57
  sessionId: '',
35
58
  targetId: '',
36
- port: DEFAULT_PORT,
59
+ port: UNPINNED,
37
60
  refs: new Map(),
38
61
  parentSessionId: '',
39
62
  };
40
63
 
64
+ /**
65
+ * The session the caller pinned with `--port`, or undefined for "whichever
66
+ * session this directory's store resolves to". Anything unusable — absent,
67
+ * out of range, unparseable — reads as undefined.
68
+ */
69
+ export function pinnedPort(flags: ParsedFlags): number | undefined {
70
+ const raw = flags.port;
71
+ const port = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : Number.NaN;
72
+ return Number.isInteger(port) && port >= 1024 && port <= 65535 ? port : undefined;
73
+ }
74
+
75
+ /**
76
+ * Pin this process to the session named by `--port` before the subcommand
77
+ * runs (see the wrapper in commands.ts), so every command that resolves its
78
+ * session through `session.port` honours the selector.
79
+ */
80
+ export function applySessionPortFlag(flags: ParsedFlags): void {
81
+ // Only ever pins: a command with no --port must not reset the port of a
82
+ // session this process already established (`batch "open …" "snapshot"`
83
+ // runs both actions in one process, and the second must stay on the
84
+ // session the first started).
85
+ const pinned = pinnedPort(flags);
86
+ if (pinned !== undefined) session.port = pinned;
87
+ }
88
+
41
89
  export async function getBrowser() {
42
90
  return import('../index.js');
43
91
  }
@@ -85,35 +133,151 @@ export function ensureSignalCleanupHandlers(): void {
85
133
  }
86
134
  }
87
135
 
88
- // Each CLI invocation is a fresh process, so this module's `session.port` variable
89
- // is always freshly initialized to DEFAULT_PORT — a `--port 9333` passed to
90
- // an earlier `open` command has no effect on this process's own default.
91
- // Resolve the persisted "active port" (written by `open --port N`, read via
92
- // ref-cache.ts's saveActivePort/loadActivePort) so subsequent commands
93
- // attach to the browser the user actually opened instead of silently
94
- // launching/attaching to a second, unrelated Chrome instance on 9222.
95
- export async function resolveDefaultPort(
136
+ // Each CLI invocation is a fresh process, so the session a previous `open`
137
+ // started only survives as its record on disk. Walk the recorded sessions
138
+ // newest-first and return the first whose browser still answers — that is
139
+ // "the" session for a command that did not pin one with `--port`.
140
+ //
141
+ // Records whose browser is gone are dropped as we pass them (self-heal): a
142
+ // crashed or manually-killed Chrome must not wedge every later command.
143
+ export async function resolveLiveSession(
96
144
  browser: Awaited<ReturnType<typeof getBrowser>>,
97
- ): Promise<number> {
98
- const persisted = await browser.loadActivePortInfo();
99
- if (!persisted) return DEFAULT_PORT;
100
- if (!persisted.launched) {
101
- // connect-origin port: the browser belongs to someone else. If it died,
102
- // relaunching our own headless Chrome on that port would squat the
103
- // user's debug port and silently swap which browser commands act on —
104
- // fail loudly instead.
105
- try {
106
- await fetch(`http://127.0.0.1:${persisted.port}/json/version`, {
107
- signal: AbortSignal.timeout(1500),
108
- });
109
- } catch {
110
- await browser.clearActivePort();
145
+ opts: { strict?: boolean } = {},
146
+ ): Promise<SessionRecord | null> {
147
+ for (const record of await browser.listSessionRecords()) {
148
+ if (await cdpAnswers(record.port)) return record;
149
+ await browser.removeSessionRecord(record.port);
150
+ await browser.clearRefCache(record.port);
151
+ if (!record.launched && opts.strict !== false) {
152
+ // connect-origin port: the browser belonged to someone else. Silently
153
+ // launching our own headless Chrome on that port would squat the
154
+ // user's debug port and swap which browser commands act on — fail
155
+ // loudly instead. `close` passes strict:false; it has nothing to
156
+ // attach to and just cleans up.
111
157
  throw new Error(
112
- `Connected browser on port ${persisted.port} is gone — re-run \`connect\` (or \`open\` to launch a fresh one).`,
158
+ `Connected browser on port ${record.port} is gone — re-run \`connect\` (or \`open\` to launch a fresh one).`,
113
159
  );
114
160
  }
115
161
  }
116
- return persisted.port;
162
+ // Nothing native is live. A session opened by a pre-#318 CLI in this
163
+ // directory may still be — it is the oldest possible candidate, so it is
164
+ // considered last.
165
+ return adoptLegacySession(browser);
166
+ }
167
+
168
+ /**
169
+ * Take over the single session a pre-#318 CLI recorded in this directory, so
170
+ * upgrading monobrowse mid-session does not strand its Chrome with no handle
171
+ * anyone can use. Returns the session — now an ordinary per-port record that
172
+ * `snapshot`, `--port` and `close` treat like any other — or null.
173
+ *
174
+ * `port` restricts adoption to a session the caller already named, so a
175
+ * legacy record for some other port is left for its own resolution.
176
+ *
177
+ * A record that a per-port record already supersedes, or whose browser no
178
+ * longer answers, is simply dropped: the same self-heal as for a dead native
179
+ * record, and nothing is ever written back to the old layout. This is the
180
+ * only place the legacy file is read, and `open` never consults it — a bare
181
+ * `open` still always starts a session of its own.
182
+ */
183
+ export async function adoptLegacySession(
184
+ browser: Awaited<ReturnType<typeof getBrowser>>,
185
+ opts: { port?: number } = {},
186
+ ): Promise<SessionRecord | null> {
187
+ const legacy = await browser.loadLegacySessionRecord();
188
+ if (!legacy) return null;
189
+ if (opts.port !== undefined && opts.port !== legacy.port) return null;
190
+ if (await browser.loadSessionRecord(legacy.port)) {
191
+ await browser.removeLegacySessionRecord();
192
+ return null;
193
+ }
194
+ if (!(await cdpAnswers(legacy.port))) {
195
+ await browser.removeLegacySessionRecord();
196
+ return null;
197
+ }
198
+ await browser.saveSessionRecord(legacy.port, {
199
+ launched: legacy.launched,
200
+ pid: legacy.pid,
201
+ userDataDir: legacy.userDataDir,
202
+ savedAt: legacy.savedAt,
203
+ });
204
+ await browser.removeLegacySessionRecord();
205
+ return legacy;
206
+ }
207
+
208
+ async function cdpAnswers(port: number): Promise<boolean> {
209
+ try {
210
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
211
+ signal: AbortSignal.timeout(1500),
212
+ });
213
+ return res.ok;
214
+ } catch {
215
+ return false;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Start or attach to the browser this session drives, and record it.
221
+ *
222
+ * With a `port`, that is the caller's explicit `--port`: attach to a Chrome
223
+ * already listening there, else launch there (unchanged behaviour). Without
224
+ * one, Chrome binds a kernel-assigned free port in a profile directory of
225
+ * this session's own — the allocation that makes two concurrent `open`
226
+ * invocations independent (#318).
227
+ */
228
+ export async function launchSessionBrowser(
229
+ browser: Awaited<ReturnType<typeof getBrowser>>,
230
+ opts: { port?: number; headless: boolean },
231
+ ): Promise<number> {
232
+ const port = opts.port
233
+ ? await browser.launchBrowser({ port: opts.port, headless: opts.headless })
234
+ : await browser.launchBrowser({
235
+ port: 0,
236
+ headless: opts.headless,
237
+ // Chrome reports the port it bound inside this directory, so it must
238
+ // belong to this session alone.
239
+ userDataDir: join(tmpdir(), `monomind-browse-${process.pid}-${randomUUID().slice(0, 8)}`),
240
+ });
241
+ await recordSession(browser, port);
242
+ ensureSignalCleanupHandlers();
243
+ return port;
244
+ }
245
+
246
+ // Persist the session so later CLI invocations (each a fresh process) can
247
+ // find this browser, together with the launched PID/userDataDir so their
248
+ // closeBrowser can still kill it even though launchedPids (browser.ts) is
249
+ // per-process and empty there.
250
+ //
251
+ // launchBrowser() can either LAUNCH a fresh Chrome or ATTACH to one already
252
+ // listening on an explicitly requested port (see its own "attach if already
253
+ // Chrome" comment) — it returns only a port number, with no signal telling
254
+ // this caller which happened. getLaunchedPid(port) is undefined on the
255
+ // attach path (this process never spawned anything). Unconditionally saving
256
+ // {pid: undefined} on attach used to CLOBBER a real PID a previous `open`
257
+ // had persisted for this exact port, destroying the only way a later
258
+ // process's closeBrowser() PID-kill fallback could ever find it.
259
+ async function recordSession(
260
+ browser: Awaited<ReturnType<typeof getBrowser>>,
261
+ port: number,
262
+ ): Promise<void> {
263
+ const pid = browser.getLaunchedPid(port);
264
+ if (pid !== undefined) {
265
+ await browser.saveSessionRecord(port, {
266
+ pid,
267
+ userDataDir: browser.getLaunchedUserDataDir(port),
268
+ });
269
+ return;
270
+ }
271
+ // A pre-#318 record for this exact port counts as the existing one, so
272
+ // attaching to a session an older CLI opened keeps its provenance (a
273
+ // `connect`ed browser stays launched:false and is never killed).
274
+ const existing =
275
+ (await browser.loadSessionRecord(port)) ?? (await adoptLegacySession(browser, { port }));
276
+ await browser.saveSessionRecord(port, {
277
+ launched: existing?.launched,
278
+ pid: existing?.pid,
279
+ userDataDir: existing?.userDataDir,
280
+ });
117
281
  }
118
282
 
119
283
  export async function ensureConnected(port: number, targetId?: string) {
@@ -126,8 +290,10 @@ export async function ensureConnected(port: number, targetId?: string) {
126
290
  browser.teardownConsoleCapture(session.sessionId);
127
291
  session.client.close();
128
292
  }
129
- const effectivePort = port === DEFAULT_PORT ? await resolveDefaultPort(browser) : port;
130
- session.port = await browser.launchBrowser({ port: effectivePort, headless: true });
293
+ // Pinned port, else the newest live session, else a session of our own —
294
+ // a command run before any `open` still just works.
295
+ const pinned = port > 0 ? port : ((await resolveLiveSession(browser))?.port ?? undefined);
296
+ session.port = await launchSessionBrowser(browser, { port: pinned, headless: true });
131
297
  const conn = await browser.connectToTarget(session.port, targetId);
132
298
  session.client = conn.client;
133
299
  session.sessionId = conn.sessionId;
@@ -159,7 +325,7 @@ export async function hydrateRefsFromCache(
159
325
  targetId: string,
160
326
  currentUrl: string,
161
327
  ): Promise<void> {
162
- const cached = await browser.loadRefCache(targetId);
328
+ const cached = await browser.loadRefCache(session.port, targetId);
163
329
  if (!cached) return;
164
330
  if (currentUrl && cached.url && currentUrl !== cached.url) {
165
331
  output.printError(
@@ -264,10 +430,19 @@ export async function switchToHeaded(url: string, port: number): Promise<void> {
264
430
  session.targetId = '';
265
431
  }
266
432
 
267
- // Launch headed on a neighbouring port to avoid collision
268
- const headedPort = port + 10;
433
+ // Kernel-assigned free port and a profile of its own, so this temporary
434
+ // headed window cannot collide with any other session (#318) — a
435
+ // neighbouring port (port + 10) could well be another session's.
436
+ let headedPort = 0;
269
437
  try {
270
- await browser.launchBrowser({ port: headedPort, headless: false });
438
+ headedPort = await browser.launchBrowser({
439
+ port: 0,
440
+ headless: false,
441
+ userDataDir: join(
442
+ tmpdir(),
443
+ `monomind-browse-headed-${process.pid}-${randomUUID().slice(0, 8)}`,
444
+ ),
445
+ });
271
446
  } catch (err) {
272
447
  // Headed launch failed (no display, locked down env). Restore headless and rethrow.
273
448
  session.port = await browser.launchBrowser({ port, headless: true });