camou 0.2.0 → 0.3.1

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is loosely based on Keep a Changelog and uses semantic versioning.
6
6
 
7
+ ## [0.3.1] - 2026-03-16
8
+
9
+ ### Fixed
10
+
11
+ - Fixed the packaged `camou` CLI so it runs correctly when executed through npm-installed bin symlinks on Linux and macOS.
12
+ - Added regression coverage for symlinked bin execution to prevent silent no-op CLI failures in future releases.
13
+ - Made the new CLI bin regression test path-independent so it works correctly in CI and other checkout locations.
14
+
15
+ ## [0.3.0] - 2026-03-16
16
+
17
+ ### Added
18
+
19
+ - Added Linux/macOS GitHub Actions CI for test, build, and package validation.
20
+ - Added compatibility-matrix workflows and local scripts for probing Camoufox vs `playwright-core` compatibility.
21
+ - Added broader browser automation commands including navigation, hover, type, check/uncheck, select, scroll, `get value`, and richer wait modes.
22
+ - Added higher-level Node API wrappers including `Camoufox`, `AsyncCamoufox`, and `resolveCamoufoxLaunchSpec()`.
23
+
24
+ ### Changed
25
+
26
+ - Updated `README.md`, skill docs, and compatibility docs to reflect the first-class Node API and expanded command surface.
27
+ - Improved CI reliability by removing slow spawned `tsx` subprocesses from the CLI JSON tests.
28
+
29
+ ### Fixed
30
+
31
+ - Fixed the macOS installer integration test to use platform-aware asset names and executable paths.
32
+
7
33
  ## [0.2.0] - 2026-03-15
8
34
 
9
35
  ### Added
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # Camoucli
1
+ # Camou
2
2
 
3
- Camoucli is a local-first CLI and background daemon for driving [Camoufox](https://github.com/daijro/camoufox) through Playwright, without depending on the Camoufox Python SDK.
3
+ Camou is a local-first CLI and background daemon for driving [Camoufox](https://github.com/daijro/camoufox) through Playwright, without depending on the Camoufox Python SDK.
4
4
 
5
5
  - npm package: `camou`
6
6
  - installed command: `camou`
7
- - project/repo name: Camoucli
7
+ - project/repo name: camoucli
8
8
 
9
9
  Camou is built for agent-style browser workflows:
10
10
 
@@ -113,15 +113,14 @@ Camou can also be used as a Node library, not just a CLI.
113
113
  The programmatic API is Playwright-based: it launches Camoufox for you and gives you a real Playwright `BrowserContext`, similar in spirit to the Camoufox Python wrapper.
114
114
 
115
115
  ```ts
116
- import { launchCamoufox } from 'camou';
116
+ import { Camoufox } from 'camou';
117
117
 
118
- const camou = await launchCamoufox({
118
+ const camou = await Camoufox.launch({
119
119
  session: 'script',
120
120
  headless: false,
121
121
  });
122
122
 
123
- const page = await camou.context.newPage();
124
- await page.goto('https://example.com');
123
+ const page = await camou.open('https://example.com');
125
124
  console.log(await page.title());
126
125
 
127
126
  await camou.close();
@@ -130,19 +129,23 @@ await camou.close();
130
129
  If you prefer a scoped helper:
131
130
 
132
131
  ```ts
133
- import { withCamoufox } from 'camou';
132
+ import { Camoufox } from 'camou';
134
133
 
135
- await withCamoufox({ session: 'script' }, async ({ context }) => {
136
- const page = await context.newPage();
137
- await page.goto('https://example.com');
134
+ await Camoufox.with({ session: 'script' }, async (camou) => {
135
+ const page = await camou.open('https://example.com');
138
136
  console.log(await page.title());
139
137
  });
140
138
  ```
141
139
 
142
140
  Useful exported helpers include:
143
141
 
142
+ - `Camoufox.launch()`
143
+ - `Camoufox.launchContext()`
144
+ - `Camoufox.with()`
145
+ - `AsyncCamoufox`
144
146
  - `launchCamoufox()`
145
147
  - `launchCamoufoxContext()`
148
+ - `resolveCamoufoxLaunchSpec()`
146
149
  - `withCamoufox()`
147
150
  - `installCamoufox()`
148
151
  - `listInstalledBrowsers()`
@@ -290,15 +293,26 @@ camou doctor
290
293
 
291
294
  ```bash
292
295
  camou open <url>
296
+ camou back
297
+ camou forward
298
+ camou reload
293
299
  camou snapshot [-i]
294
300
  camou click <selectorOrRef>
301
+ camou hover <selectorOrRef>
295
302
  camou fill <selectorOrRef> <text>
303
+ camou type <selectorOrRef> <text>
304
+ camou check <selectorOrRef>
305
+ camou uncheck <selectorOrRef>
306
+ camou select <selectorOrRef> <value>
296
307
  camou press <key>
297
- camou wait <selectorOrRef>
308
+ camou scroll <direction> [amount]
309
+ camou scrollintoview <selectorOrRef>
310
+ camou wait [selectorOrRef] [--text <text>] [--load <state>]
298
311
  camou screenshot [path]
299
312
  camou get url
300
313
  camou get title
301
314
  camou get text <selectorOrRef>
315
+ camou get value <selectorOrRef>
302
316
  ```
303
317
 
304
318
  ### Sessions and tabs
@@ -332,6 +346,11 @@ Most browser commands support:
332
346
  - `--json`
333
347
  - `--verbose`
334
348
 
349
+ `wait` also supports:
350
+
351
+ - `--text <text>`
352
+ - `--load <domcontentloaded|load|networkidle>`
353
+
335
354
  ## Presets
336
355
 
337
356
  Built-in presets give you a small layer of tested ergonomics on top of raw config and prefs JSON.
@@ -396,6 +415,14 @@ Current local verification with `playwright-core` `1.51.1`:
396
415
  | `135.0.1-beta.24` | launches | smoke-tested successfully |
397
416
  | `135.0.1-beta.23` | incompatible | `Browser.setContrast` is not supported |
398
417
 
418
+ The repo now also includes:
419
+
420
+ - Linux/macOS CI in `.github/workflows/ci.yml`
421
+ - a workflow-driven compatibility probe in `.github/workflows/compatibility-matrix.yml`
422
+ - local scripts to generate compatibility reports and markdown summaries
423
+
424
+ See `docs/compatibility-matrix.md` for the workflow and local tooling.
425
+
399
426
  ## Storage Layout
400
427
 
401
428
  Camou keeps its own runtime state and profiles, but stores browser binaries in the shared Camoufox cache layout when possible.
@@ -430,9 +457,19 @@ npm run dev -- --help
430
457
  npm run dev:daemon
431
458
  ```
432
459
 
460
+ Compatibility tooling:
461
+
462
+ ```bash
463
+ # produce a raw compatibility report JSON
464
+ node scripts/run-compatibility-report.mjs --output compatibility-report.json
465
+
466
+ # turn one or more reports into a markdown table
467
+ node scripts/generate-compatibility-matrix.mjs compatibility-report.json
468
+ ```
469
+
433
470
  ## Acknowledgements
434
471
 
435
- Camoucli learned a lot from these projects:
472
+ Camou learned a lot from these projects:
436
473
 
437
474
  - [vercel-labs/agent-browser](https://github.com/vercel-labs/agent-browser) for the agent-oriented command workflow and skill ecosystem patterns
438
475
  - [BUNotesAI/agent-browser-session](https://github.com/BUNotesAI/agent-browser-session) for persistent-session and named-tab ergonomics
package/dist/api.d.ts CHANGED
@@ -1,11 +1,23 @@
1
1
  import type { BrowserContext, Page } from 'playwright-core';
2
2
  import type { LaunchInput, ResolvedLaunchConfig } from './camoufox/config.js';
3
+ import { type PreparedPersistentCamoufoxLaunch } from './camoufox/launcher.js';
3
4
  import { type CamoucliPaths } from './state/paths.js';
4
5
  export interface LaunchCamoufoxOptions extends LaunchInput {
5
6
  session?: string | undefined;
6
7
  paths?: CamoucliPaths | undefined;
7
8
  verbose?: boolean | undefined;
8
9
  }
10
+ export interface ResolvedCamoufoxLaunchSpec {
11
+ sessionName: string;
12
+ browserVersion: string;
13
+ executablePath: string;
14
+ profileDir: string;
15
+ downloadsDir: string;
16
+ artifactsDir: string;
17
+ resolvedConfig: ResolvedLaunchConfig;
18
+ userDataDir: string;
19
+ launchOptions: PreparedPersistentCamoufoxLaunch['launchOptions'];
20
+ }
9
21
  export declare class CamoufoxSession {
10
22
  readonly context: BrowserContext;
11
23
  readonly sessionName: string;
@@ -25,10 +37,22 @@ export declare class CamoufoxSession {
25
37
  artifactsDir: string;
26
38
  resolvedConfig: ResolvedLaunchConfig;
27
39
  });
28
- newPage(): Promise<Page>;
40
+ newPage(url?: string): Promise<Page>;
29
41
  pages(): Page[];
42
+ firstPage(): Page | undefined;
43
+ ensurePage(): Promise<Page>;
44
+ open(url: string, page?: Page): Promise<Page>;
30
45
  close(): Promise<void>;
31
46
  }
47
+ export declare class Camoufox extends CamoufoxSession {
48
+ static launch(options?: LaunchCamoufoxOptions): Promise<Camoufox>;
49
+ static launchContext(options?: LaunchCamoufoxOptions): Promise<BrowserContext>;
50
+ static resolveLaunch(options?: LaunchCamoufoxOptions): Promise<ResolvedCamoufoxLaunchSpec>;
51
+ static with<T>(options: LaunchCamoufoxOptions, callback: (browser: Camoufox) => Promise<T> | T): Promise<T>;
52
+ }
53
+ export declare class AsyncCamoufox extends Camoufox {
54
+ }
32
55
  export declare function launchCamoufox(options?: LaunchCamoufoxOptions): Promise<CamoufoxSession>;
56
+ export declare function resolveCamoufoxLaunchSpec(options?: LaunchCamoufoxOptions): Promise<ResolvedCamoufoxLaunchSpec>;
33
57
  export declare function launchCamoufoxContext(options?: LaunchCamoufoxOptions): Promise<BrowserContext>;
34
58
  export declare function withCamoufox<T>(options: LaunchCamoufoxOptions, callback: (session: CamoufoxSession) => Promise<T> | T): Promise<T>;
package/dist/api.js CHANGED
@@ -1,4 +1,4 @@
1
- import { launchPersistentCamoufox } from './camoufox/launcher.js';
1
+ import { launchPersistentCamoufox, preparePersistentCamoufoxLaunch } from './camoufox/launcher.js';
2
2
  import { ensureBasePaths, getCamoucliPaths } from './state/paths.js';
3
3
  import { Logger } from './util/log.js';
4
4
  export class CamoufoxSession {
@@ -20,16 +20,63 @@ export class CamoufoxSession {
20
20
  this.artifactsDir = input.artifactsDir;
21
21
  this.resolvedConfig = input.resolvedConfig;
22
22
  }
23
- async newPage() {
24
- return this.context.newPage();
23
+ async newPage(url) {
24
+ const page = await this.context.newPage();
25
+ if (url) {
26
+ await page.goto(url, { waitUntil: 'domcontentloaded' });
27
+ }
28
+ return page;
25
29
  }
26
30
  pages() {
27
31
  return this.context.pages();
28
32
  }
33
+ firstPage() {
34
+ return this.pages()[0];
35
+ }
36
+ async ensurePage() {
37
+ return this.firstPage() ?? this.context.newPage();
38
+ }
39
+ async open(url, page) {
40
+ const targetPage = page ?? await this.ensurePage();
41
+ await targetPage.goto(url, { waitUntil: 'domcontentloaded' });
42
+ return targetPage;
43
+ }
29
44
  async close() {
30
45
  await this.context.close();
31
46
  }
32
47
  }
48
+ export class Camoufox extends CamoufoxSession {
49
+ static async launch(options = {}) {
50
+ const session = await launchCamoufox(options);
51
+ return new Camoufox({
52
+ context: session.context,
53
+ sessionName: session.sessionName,
54
+ browserVersion: session.browserVersion,
55
+ executablePath: session.executablePath,
56
+ profileDir: session.profileDir,
57
+ downloadsDir: session.downloadsDir,
58
+ artifactsDir: session.artifactsDir,
59
+ resolvedConfig: session.resolvedConfig,
60
+ });
61
+ }
62
+ static async launchContext(options = {}) {
63
+ return launchCamoufoxContext(options);
64
+ }
65
+ static async resolveLaunch(options = {}) {
66
+ return resolveCamoufoxLaunchSpec(options);
67
+ }
68
+ static async with(options, callback) {
69
+ const browser = await Camoufox.launch(options);
70
+ try {
71
+ return await callback(browser);
72
+ }
73
+ finally {
74
+ await browser.close();
75
+ }
76
+ }
77
+ }
78
+ export class AsyncCamoufox extends Camoufox {
79
+ }
33
80
  function createApiLogger(verbose = false) {
34
81
  if (!verbose) {
35
82
  return undefined;
@@ -57,6 +104,24 @@ export async function launchCamoufox(options = {}) {
57
104
  resolvedConfig: launched.resolvedConfig,
58
105
  });
59
106
  }
107
+ export async function resolveCamoufoxLaunchSpec(options = {}) {
108
+ const sessionName = options.session ?? 'default';
109
+ const paths = options.paths ?? getCamoucliPaths();
110
+ await ensureBasePaths(paths);
111
+ const logger = createApiLogger(options.verbose);
112
+ const prepared = await preparePersistentCamoufoxLaunch(paths, sessionName, options, logger);
113
+ return {
114
+ sessionName,
115
+ browserVersion: prepared.browserVersion,
116
+ executablePath: prepared.installPath,
117
+ profileDir: prepared.sessionPaths.profileDir,
118
+ downloadsDir: prepared.sessionPaths.downloadsDir,
119
+ artifactsDir: prepared.sessionPaths.artifactsDir,
120
+ resolvedConfig: prepared.resolvedConfig,
121
+ userDataDir: prepared.userDataDir,
122
+ launchOptions: prepared.launchOptions,
123
+ };
124
+ }
60
125
  export async function launchCamoufoxContext(options = {}) {
61
126
  const session = await launchCamoufox(options);
62
127
  return session.context;
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -22,6 +22,18 @@ export declare class BrowserManager {
22
22
  tabName: string;
23
23
  url: string;
24
24
  }): Promise<Record<string, unknown>>;
25
+ back(input: LaunchInput & {
26
+ session: string;
27
+ tabName: string;
28
+ }): Promise<Record<string, unknown>>;
29
+ forward(input: LaunchInput & {
30
+ session: string;
31
+ tabName: string;
32
+ }): Promise<Record<string, unknown>>;
33
+ reload(input: LaunchInput & {
34
+ session: string;
35
+ tabName: string;
36
+ }): Promise<Record<string, unknown>>;
25
37
  snapshot(input: LaunchInput & {
26
38
  session: string;
27
39
  tabName: string;
@@ -32,17 +44,55 @@ export declare class BrowserManager {
32
44
  tabName: string;
33
45
  target: string;
34
46
  }): Promise<Record<string, unknown>>;
47
+ hover(input: LaunchInput & {
48
+ session: string;
49
+ tabName: string;
50
+ target: string;
51
+ }): Promise<Record<string, unknown>>;
35
52
  fill(input: LaunchInput & {
36
53
  session: string;
37
54
  tabName: string;
38
55
  target: string;
39
56
  text: string;
40
57
  }): Promise<Record<string, unknown>>;
58
+ type(input: LaunchInput & {
59
+ session: string;
60
+ tabName: string;
61
+ target: string;
62
+ text: string;
63
+ }): Promise<Record<string, unknown>>;
64
+ check(input: LaunchInput & {
65
+ session: string;
66
+ tabName: string;
67
+ target: string;
68
+ }): Promise<Record<string, unknown>>;
69
+ uncheck(input: LaunchInput & {
70
+ session: string;
71
+ tabName: string;
72
+ target: string;
73
+ }): Promise<Record<string, unknown>>;
74
+ select(input: LaunchInput & {
75
+ session: string;
76
+ tabName: string;
77
+ target: string;
78
+ value: string;
79
+ }): Promise<Record<string, unknown>>;
41
80
  press(input: LaunchInput & {
42
81
  session: string;
43
82
  tabName: string;
44
83
  key: string;
45
84
  }): Promise<Record<string, unknown>>;
85
+ scroll(input: LaunchInput & {
86
+ session: string;
87
+ tabName: string;
88
+ direction: 'up' | 'down' | 'left' | 'right';
89
+ amount?: number | undefined;
90
+ }): Promise<Record<string, unknown>>;
91
+ scrollIntoView(input: LaunchInput & {
92
+ session: string;
93
+ tabName: string;
94
+ target: string;
95
+ }): Promise<Record<string, unknown>>;
46
96
  screenshot(input: LaunchInput & {
47
97
  session: string;
48
98
  tabName: string;
@@ -61,10 +111,17 @@ export declare class BrowserManager {
61
111
  tabName: string;
62
112
  target: string;
63
113
  }): Promise<Record<string, unknown>>;
64
- wait(input: LaunchInput & {
114
+ getValue(input: LaunchInput & {
65
115
  session: string;
66
116
  tabName: string;
67
117
  target: string;
118
+ }): Promise<Record<string, unknown>>;
119
+ wait(input: LaunchInput & {
120
+ session: string;
121
+ tabName: string;
122
+ target?: string | undefined;
123
+ text?: string | undefined;
124
+ loadState?: 'domcontentloaded' | 'load' | 'networkidle' | undefined;
68
125
  timeoutMs?: number | undefined;
69
126
  }): Promise<Record<string, unknown>>;
70
127
  listTabs(sessionName: string): Promise<Array<Record<string, unknown>>>;
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { launchPersistentCamoufox } from '../camoufox/launcher.js';
3
- import { SessionError } from '../util/errors.js';
3
+ import { SessionError, ValidationError } from '../util/errors.js';
4
4
  import { locatorForTarget } from './actions.js';
5
5
  import { clearSnapshotRefs, takeSnapshot } from './snapshot.js';
6
6
  import { createTabRuntime } from './tabs.js';
@@ -51,6 +51,36 @@ export class BrowserManager {
51
51
  title: await tab.page.title(),
52
52
  };
53
53
  }
54
+ async back(input) {
55
+ const tab = await this.ensureTab(input.session, input.tabName, input);
56
+ await tab.page.goBack({ waitUntil: 'domcontentloaded' }).catch(() => null);
57
+ return {
58
+ sessionName: input.session,
59
+ tabName: tab.name,
60
+ url: tab.page.url(),
61
+ title: await tab.page.title(),
62
+ };
63
+ }
64
+ async forward(input) {
65
+ const tab = await this.ensureTab(input.session, input.tabName, input);
66
+ await tab.page.goForward({ waitUntil: 'domcontentloaded' }).catch(() => null);
67
+ return {
68
+ sessionName: input.session,
69
+ tabName: tab.name,
70
+ url: tab.page.url(),
71
+ title: await tab.page.title(),
72
+ };
73
+ }
74
+ async reload(input) {
75
+ const tab = await this.ensureTab(input.session, input.tabName, input);
76
+ await tab.page.reload({ waitUntil: 'domcontentloaded' });
77
+ return {
78
+ sessionName: input.session,
79
+ tabName: tab.name,
80
+ url: tab.page.url(),
81
+ title: await tab.page.title(),
82
+ };
83
+ }
54
84
  async snapshot(input) {
55
85
  const tab = await this.ensureTab(input.session, input.tabName, input);
56
86
  const result = await takeSnapshot(tab.page, input.interactive);
@@ -75,6 +105,15 @@ export class BrowserManager {
75
105
  url: tab.page.url(),
76
106
  };
77
107
  }
108
+ async hover(input) {
109
+ const tab = await this.ensureTab(input.session, input.tabName, input);
110
+ await locatorForTarget(tab.page, tab, input.target).hover();
111
+ return {
112
+ sessionName: input.session,
113
+ tabName: tab.name,
114
+ target: input.target,
115
+ };
116
+ }
78
117
  async fill(input) {
79
118
  const tab = await this.ensureTab(input.session, input.tabName, input);
80
119
  await locatorForTarget(tab.page, tab, input.target).fill(input.text);
@@ -85,6 +124,46 @@ export class BrowserManager {
85
124
  valueLength: input.text.length,
86
125
  };
87
126
  }
127
+ async type(input) {
128
+ const tab = await this.ensureTab(input.session, input.tabName, input);
129
+ await locatorForTarget(tab.page, tab, input.target).type(input.text);
130
+ return {
131
+ sessionName: input.session,
132
+ tabName: tab.name,
133
+ target: input.target,
134
+ valueLength: input.text.length,
135
+ };
136
+ }
137
+ async check(input) {
138
+ const tab = await this.ensureTab(input.session, input.tabName, input);
139
+ await locatorForTarget(tab.page, tab, input.target).check();
140
+ return {
141
+ sessionName: input.session,
142
+ tabName: tab.name,
143
+ target: input.target,
144
+ checked: true,
145
+ };
146
+ }
147
+ async uncheck(input) {
148
+ const tab = await this.ensureTab(input.session, input.tabName, input);
149
+ await locatorForTarget(tab.page, tab, input.target).uncheck();
150
+ return {
151
+ sessionName: input.session,
152
+ tabName: tab.name,
153
+ target: input.target,
154
+ checked: false,
155
+ };
156
+ }
157
+ async select(input) {
158
+ const tab = await this.ensureTab(input.session, input.tabName, input);
159
+ await locatorForTarget(tab.page, tab, input.target).selectOption(input.value);
160
+ return {
161
+ sessionName: input.session,
162
+ tabName: tab.name,
163
+ target: input.target,
164
+ value: input.value,
165
+ };
166
+ }
88
167
  async press(input) {
89
168
  const tab = await this.ensureTab(input.session, input.tabName, input);
90
169
  await tab.page.keyboard.press(input.key);
@@ -94,6 +173,34 @@ export class BrowserManager {
94
173
  key: input.key,
95
174
  };
96
175
  }
176
+ async scroll(input) {
177
+ const tab = await this.ensureTab(input.session, input.tabName, input);
178
+ const amount = input.amount ?? 500;
179
+ const delta = input.direction === 'up'
180
+ ? { x: 0, y: -amount }
181
+ : input.direction === 'down'
182
+ ? { x: 0, y: amount }
183
+ : input.direction === 'left'
184
+ ? { x: -amount, y: 0 }
185
+ : { x: amount, y: 0 };
186
+ await tab.page.mouse.wheel(delta.x, delta.y);
187
+ return {
188
+ sessionName: input.session,
189
+ tabName: tab.name,
190
+ direction: input.direction,
191
+ amount,
192
+ url: tab.page.url(),
193
+ };
194
+ }
195
+ async scrollIntoView(input) {
196
+ const tab = await this.ensureTab(input.session, input.tabName, input);
197
+ await locatorForTarget(tab.page, tab, input.target).scrollIntoViewIfNeeded();
198
+ return {
199
+ sessionName: input.session,
200
+ tabName: tab.name,
201
+ target: input.target,
202
+ };
203
+ }
97
204
  async screenshot(input) {
98
205
  const tab = await this.ensureTab(input.session, input.tabName, input);
99
206
  const session = await this.ensureSession(input.session, input);
@@ -131,14 +238,37 @@ export class BrowserManager {
131
238
  text,
132
239
  };
133
240
  }
241
+ async getValue(input) {
242
+ const tab = await this.ensureTab(input.session, input.tabName, input);
243
+ const value = await locatorForTarget(tab.page, tab, input.target).inputValue();
244
+ return {
245
+ sessionName: input.session,
246
+ tabName: tab.name,
247
+ target: input.target,
248
+ value,
249
+ };
250
+ }
134
251
  async wait(input) {
135
252
  const tab = await this.ensureTab(input.session, input.tabName, input);
253
+ if (!input.target && !input.text && !input.loadState) {
254
+ throw new ValidationError('wait requires a target, --text value, or --load state.');
255
+ }
136
256
  const waitOptions = input.timeoutMs ? { timeout: input.timeoutMs } : undefined;
137
- await locatorForTarget(tab.page, tab, input.target).waitFor(waitOptions);
257
+ if (input.target) {
258
+ await locatorForTarget(tab.page, tab, input.target).waitFor(waitOptions);
259
+ }
260
+ if (input.text) {
261
+ await tab.page.getByText(input.text).first().waitFor(waitOptions);
262
+ }
263
+ if (input.loadState) {
264
+ await tab.page.waitForLoadState(input.loadState, waitOptions);
265
+ }
138
266
  return {
139
267
  sessionName: input.session,
140
268
  tabName: tab.name,
141
- target: input.target,
269
+ ...(input.target ? { target: input.target } : {}),
270
+ ...(input.text ? { text: input.text } : {}),
271
+ ...(input.loadState ? { loadState: input.loadState } : {}),
142
272
  url: tab.page.url(),
143
273
  };
144
274
  }
@@ -187,6 +317,7 @@ export class BrowserManager {
187
317
  async ensureSession(sessionName, input) {
188
318
  const existing = this.sessions.get(sessionName);
189
319
  if (existing) {
320
+ this.assertSessionCompatible(existing, input);
190
321
  return existing;
191
322
  }
192
323
  const inFlight = this.startingSessions.get(sessionName);