cssprobe-cli 0.3.1 → 0.4.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.
Files changed (40) hide show
  1. package/AGENTS.md +39 -7
  2. package/CHANGELOG.md +30 -0
  3. package/README.md +58 -5
  4. package/dist/browser/frames.d.ts +15 -0
  5. package/dist/browser/frames.js +60 -0
  6. package/dist/browser/interactions.d.ts +3 -2
  7. package/dist/browser/interactions.js +16 -4
  8. package/dist/browser/launcher.d.ts +30 -2
  9. package/dist/browser/launcher.js +141 -34
  10. package/dist/browser/sampler.d.ts +43 -0
  11. package/dist/browser/sampler.js +111 -0
  12. package/dist/cli/commands.d.ts +3 -0
  13. package/dist/cli/commands.js +93 -5
  14. package/dist/cli/input.d.ts +6 -0
  15. package/dist/cli/input.js +24 -0
  16. package/dist/cli/program.js +152 -1
  17. package/dist/cli/session-ops.d.ts +8 -0
  18. package/dist/cli/session-ops.js +24 -1
  19. package/dist/cli/state-check.d.ts +17 -0
  20. package/dist/cli/state-check.js +43 -0
  21. package/dist/cli/url-guard.d.ts +2 -0
  22. package/dist/cli/url-guard.js +24 -0
  23. package/dist/daemon/daemon.js +70 -20
  24. package/dist/daemon/expect.d.ts +8 -0
  25. package/dist/daemon/expect.js +47 -0
  26. package/dist/daemon/flowRunner.d.ts +13 -1
  27. package/dist/daemon/flowRunner.js +28 -3
  28. package/dist/engine/evalSerializer.js +2 -2
  29. package/dist/help.json +96 -17
  30. package/dist/scenario/assertions.d.ts +13 -0
  31. package/dist/scenario/assertions.js +111 -0
  32. package/dist/scenario/reporters.d.ts +13 -0
  33. package/dist/scenario/reporters.js +79 -0
  34. package/dist/scenario/runner.d.ts +10 -0
  35. package/dist/scenario/runner.js +251 -0
  36. package/dist/scenario/schema.d.ts +1270 -0
  37. package/dist/scenario/schema.js +151 -0
  38. package/docs/guide/installation.md +1 -1
  39. package/llms.txt +1 -1
  40. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  `cssprobe-cli` — Runtime CSS probe for layout/scroll/overflow/color/font inspection with session-based browser management. Entrypoint: `bin/cssprobe-cli.js` → `require('../dist/index')`.
6
6
  Build: `npm run build` (= `tsc` + `esbuild` collector IIFE bundle + `esbuild` daemon entry + `tsx scripts/generate-help.ts`).
7
- Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner, 108 tests).
7
+ Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner, 173 tests).
8
8
  Dependencies: `playwright-core`, `zod`. Dev: `typescript`, `@types/node`, `tsx`, `esbuild`.
9
9
 
10
10
  ## Architecture
@@ -17,11 +17,15 @@ src/
17
17
  │ ├── commands.ts # Command definitions — all Zod schemas
18
18
  │ ├── command.ts # declareCommand(), parseCommand() (Zod validation)
19
19
  │ ├── output.ts # TextOutput / JsonOutput strategy pattern
20
+ │ ├── input.ts # @file / stdin argument expansion for eval and playwright
21
+ │ ├── state-check.ts # State file validation (cookie expiry, target domain)
22
+ │ ├── url-guard.ts # Login-redirect detection + --expect-url matching
20
23
  │ └── minimist.ts # Arguments parser (forked from playwright-cli)
21
24
  ├── daemon/
22
25
  │ ├── session.ts # Session management (single session, Unix socket)
23
26
  │ ├── daemon.ts # Daemon process: browser lifecycle + command execution
24
- │ ├── flowRunner.ts # Flow script loader/runner (run command): dynamic import, API injection, console capture
27
+ │ ├── flowRunner.ts # Flow script loader/runner (run command): dynamic import, API injection, console/pageerror capture
28
+ │ ├── expect.ts # Assertion matchers for flow scripts
25
29
  │ └── daemonEntry.ts # Daemon entry point (spawned as child process)
26
30
  ├── engine/
27
31
  │ ├── types.ts # Shared types: Snapshot, TreeNode, Finding, Confidence
@@ -30,8 +34,15 @@ src/
30
34
  │ ├── evalSerializer.ts # Self-contained serializer for eval results (runs in-page via JSHandle.evaluate)
31
35
  │ └── renderer.ts # Markdown / JSON rendering
32
36
  ├── browser/
33
- │ ├── launcher.ts # Playwright wrapper: launch, navigate, inject collector
37
+ │ ├── launcher.ts # Playwright wrapper: launch, navigate, browser discovery/doctor
38
+ │ ├── frames.ts # Frame listing + resolution (index/name/URL substring)
39
+ │ ├── sampler.ts # Timeline sampling + wait-stable (pure aggregation)
34
40
  │ └── interactions.ts # Interaction primitives: click/hover/type/wait-for (shared by daemon + run API)
41
+ ├── scenario/
42
+ │ ├── schema.ts # Zod schemas for JSON suites, cases and steps
43
+ │ ├── runner.ts # Suite execution: browser per case, vars, artifacts, JUnit
44
+ │ ├── assertions.ts # Pure assertion evaluation (paths, matchers, globals, URL)
45
+ │ └── reporters.ts # Console summary + JUnit XML
35
46
  ├── config/
36
47
  │ ├── config.ts # Config loader (CLI → env → ~/.cssprobe-clirc profiles)
37
48
  │ └── helpGenerator.ts # Build-time: Zod schemas → dist/help.json
@@ -52,7 +63,8 @@ src/
52
63
  - **Output strategy**: `Output` interface → `TextOutput` (Markdown for inspect, tables for lists) / `JsonOutput` (machine-readable JSON).
53
64
  - **Config priority**: CLI flags → env vars → `~/.cssprobe-clirc` profiles. Switch with `-p <profile>`.
54
65
  - **State management**: `state-import` converts Netscape cookies; `state-save` persists the current session's cookies + localStorage after manual login. Session persists browser state until `close`.
55
- - **Browser discovery**: `findBrowserExecutable()` scans `~/Library/Caches/ms-playwright/chromium-*` for existing Chrome installations.
66
+ - **Iframe guidance**: pages may contain hidden helper iframes (`about:blank`); use `frames` to pick the real one and `state: "attached"` for hidden targets. Preview frames can render in 6–8s while analytics keep `networkIdle` busy — use a fixed `wait` plus an in-frame `waitFor` instead of `networkIdle`.
67
+ - **Browser discovery**: `findBrowserExecutable()` scans the platform Playwright cache (macOS/Linux/Windows, `PLAYWRIGHT_BROWSERS_PATH` override), prefers the revision required by the bundled `playwright-core`, and falls back to the newest install. `doctor` reports expected vs installed revisions.
56
68
  - **Error format**: `Error: <message>` → JSON mode: `{ isError: true, error: "<message>" }` → `process.exit(1)`
57
69
 
58
70
  ## Commands
@@ -62,7 +74,9 @@ src/
62
74
  - `--viewport <WxH>` — Set viewport size (e.g. `--viewport 1280x720`)
63
75
  - `--state <file>` — Load saved state (cookies + localStorage)
64
76
  - `close` — Close the browser session
65
- - `status` — Show current session status
77
+ - `status` — Show current session status (URL, title, frame count)
78
+ - `frames` — List frames: index, name, URL, main marker
79
+ - `doctor` — Diagnose browser installation vs expected Playwright revision
66
80
 
67
81
  ### CSS Inspection
68
82
  - `inspect <selector>` — Inspect runtime CSS of a page element
@@ -78,6 +92,9 @@ src/
78
92
  - `eval <expression>` — Evaluate JavaScript (browser context)
79
93
  - Returns raw string values (not JSON-quoted) for string results
80
94
  - DOM nodes / circular structures are auto-serialized (descriptors, `[Circular]`, `[Function: name]`, depth cap 6)
95
+ - Promises are awaited; `undefined` object/array values become `null`
96
+ - `@file` / `-` (stdin) reads long expressions from disk or stdin
97
+ - `--frame <index|name|url-substring>` evaluates inside an iframe
81
98
  - `playwright <call>` — Execute Playwright API call (Node.js context)
82
99
  - Returns `{ ok: true, value: null, note: ... }` when the call returns no value; throws a clear error message on failure
83
100
  - `screenshot` — Take a screenshot
@@ -93,13 +110,25 @@ src/
93
110
  - Default: `fill()` (replaces the value); `--sequential` for real key events (custom editors), `--clear` clears first
94
111
  - `--press <key>` — press a key after typing (e.g. `Enter`)
95
112
  - `--delay <ms>` — keystroke delay for `--sequential`
96
- - `wait-for` — Wait for a condition (three mutually exclusive modes)
113
+ - `sample "<expr>"` — Sample an expression on an interval (`--interval`, `--duration`, `--key`, `--frame`), returning `summary` + `series`
114
+ - `wait-stable "<expr>"` — Wait until the expression stops changing (`--tolerance`, `--window`, `--timeout`, `--interval`, `--frame`)
115
+ - `wait-for` — Wait for a condition (three mutually exclusive modes); arrow-function/function predicates are invoked, bare expressions are evaluated as-is
97
116
  - `wait-for <selector>` — selector state: `--state visible|hidden|attached|detached` (default visible)
98
117
  - `wait-for --js "<predicate>"` — wait until a JS predicate is truthy
99
118
  - `wait-for --network-idle` — wait until the page is network-idle
100
119
  - `--timeout <ms>` — default 30000; timeout errors name the target
120
+ - `--frame <index|name|url-substring>` — wait inside a frame (selector/js only; `networkIdle` is page-level)
121
+ - `scenario run <file>` — Run a JSON scenario suite (fresh browser per case, assertions, artifacts, JUnit)
122
+ - `--state <file>` — Fallback state file for cases without one
123
+ - `--artifacts <dir>` — Artifact directory (default `~/.cssprobe-cli/scenario-runs/<suite>-<timestamp>`)
124
+ - `--junit <file>` — Write a JUnit XML report
125
+ - `--bail` — Stop after the first failing case
126
+ - Steps: `goto`, `reload`, `wait`, `eval`, `sample`, `waitStable`, `click`, `waitFor`, `screenshot`, `assert`
127
+ - `wait` takes a fixed `ms`; `waitFor` accepts `frame` for in-iframe selector/js waits
101
128
  - `run <file>` — Run a multi-step flow script inside the daemon
102
- - Script default-exports an async function receiving the API object: `page`, `click`, `hover`, `type`, `waitFor`, `inspect`, `screenshot`, `log` (CJS also supported)
129
+ - Script default-exports an async function receiving the API object: `page`, `context`, `frames()`, `newPage()`, `click`, `hover`, `type`, `waitFor`, `inspect`, `screenshot`, `expect`, `log` (CJS also supported)
130
+ - `waitFor(target, { state, timeout, frame })`; `frame` targets iframe selector/js waits
131
+ - Uncaught page errors are collected in the envelope as `pageErrors[]`
103
132
  - `--timeout <ms>` — whole-script guard (default 300000, 0 = unlimited); failure exits 1 and keeps the session alive
104
133
  - `screenshot` relative paths resolve against the script's directory; logs collected and returned
105
134
 
@@ -110,6 +139,9 @@ src/
110
139
  - `--merge <file>` — Existing state file to merge into
111
140
  - `state-save` — Save current browser session state (cookies + localStorage)
112
141
  - `--name <name>` — State file name (default: `session`)
142
+ - `state-check` — Validate a saved state file (cookie count, expiry, target domain coverage)
143
+ - `--state <file>` — State file (default: `~/.cssprobe-cli/states/session.json`)
144
+ - `--url <url>` — Require the cookies to cover this hostname
113
145
 
114
146
  ### Config
115
147
  - `config-show` / `config-set` / `config-list` / `config-use` / `config-new` / `config-path`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1] - 2026-10-23
4
+
5
+ ### Added
6
+ - `wait-for --frame` (selector/js), scenario `waitFor.frame`, and flow `waitFor(target, { frame })`
7
+ - Scenario `wait` step for fixed delays before slow-rendering preview frames
8
+
9
+ ### Documentation
10
+ - Iframe/preview guidance: hidden helper frames need `state: "attached"`; prefer in-frame `waitFor` over `networkIdle` when analytics keep the page busy
11
+
12
+ ## [0.4.0] - 2026-10-23
13
+
14
+ ### Added
15
+ - `frames` command and `--frame <index|name|url-substring>` on `eval` / `inspect` / `tree` / `layout` / `findings` / `inject-css`
16
+ - `sample "<expr>"` (interval timeline + summary) and `wait-stable "<expr>"` (tolerance + stable window)
17
+ - `doctor` command: platform-aware browser cache discovery, expected vs installed Playwright revisions and install hint
18
+ - `state-check` command: cookie expiry and target-domain coverage for saved state files
19
+ - `open --expect-url <regex>` and `open --fail-on-login` guards
20
+ - Flow API: `context`, `frames()`, `newPage()`, `expect` matchers; `pageErrors` in the result envelope
21
+ - `scenario run <suite.json>`: JSON regression suites with steps (goto/reload/eval/sample/waitStable/click/waitFor/screenshot/assert), per-case variables, artifacts and JUnit output
22
+ - `sample --tail-from` and per-step `tailFrom` / `ignoreErrors`
23
+
24
+ ### Fixed
25
+ - `wait-for --js` (and flow `waitFor({ js })`) now invokes arrow-function/function predicates instead of treating the function source as a truthy expression
26
+
27
+ ### Changed
28
+ - `eval` / `playwright` accept `@file` and `-` (stdin)
29
+ - `eval` awaits promise results and preserves `undefined` object/array fields as `null`
30
+ - `status` now reports the current URL, title and frame count
31
+ - Browser discovery supports macOS, Linux and Windows cache roots and prefers the revision expected by `playwright-core`
32
+
3
33
  All notable changes are documented on the [GitHub Releases](https://github.com/mack-peng/cssprobe-cli/releases) page.
4
34
 
5
35
  ## [0.3.0] - 2026-09-16
package/README.md CHANGED
@@ -155,9 +155,13 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
155
155
  | `open [url] --headed` | Show browser window |
156
156
  | `open [url] --state <file>` | Open with saved cookies + localStorage |
157
157
  | `open [url] --viewport 1280x720` | Custom viewport size |
158
+ | `open [url] --expect-url <regex>` | Fail unless the final URL matches the pattern |
159
+ | `open [url] --fail-on-login` | Fail fast when redirected to a login URL |
158
160
  | `close` | Close browser session |
159
161
  | `close --all` | Close all sessions across all workspaces |
160
- | `status` | Show session status |
162
+ | `status` | Show session status (URL, title, frame count) |
163
+ | `frames` | List frames: index, name, URL, main marker |
164
+ | `doctor` | Diagnose browser install vs Playwright expected revision |
161
165
 
162
166
  ### CSS Inspection
163
167
 
@@ -171,6 +175,8 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
171
175
  | `layout <selector>` | ASCII layout diagram |
172
176
  | `findings <selector>` | Only issues/warnings/errors |
173
177
 
178
+ Inspection commands accept `--frame <index|name|url-substring>` to target an iframe; the default is the main frame.
179
+
174
180
  ### CSS Injection
175
181
 
176
182
  | Command | Description |
@@ -182,7 +188,8 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
182
188
  | Command | Description |
183
189
  |---------|-------------|
184
190
  | `resize <width> <height>` | Resize browser viewport |
185
- | `eval <expression>` | Evaluate JavaScript (browser context); DOM nodes are auto-serialized |
191
+ | `eval <expression>` | Evaluate JavaScript (browser context); DOM nodes are auto-serialized; promises are awaited, `undefined` fields become `null` |
192
+ | `eval @file` / `eval -` | Read the expression from a file or stdin |
186
193
  | `playwright <call>` | Execute Playwright API (Node.js context) |
187
194
  | `screenshot` | Take screenshot (saves to ~/.cssprobe-cli/screenshots/, --out to override) |
188
195
 
@@ -195,7 +202,10 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
195
202
  | `type <selector> <text>` | Fill text (replaces value); `--press Enter` to submit; `--sequential` for real key events |
196
203
  | `wait-for <selector>` | Wait for selector state (`--state visible\|hidden\|attached\|detached`, `--timeout ms`) |
197
204
  | `wait-for --js "<predicate>"` | Wait until a JS predicate is truthy |
198
- | `wait-for --network-idle` | Wait until the page is network-idle |
205
+ | `wait-for --network-idle` | Wait until the page is network-idle (page-level; no `--frame`) |
206
+ | `wait-for ... --frame <spec>` | Wait inside an iframe (selector/js modes) |
207
+ | `sample "<expr>"` | Sample an expression over time (`--interval`, `--duration`, `--key`, `--frame`); returns timeline + summary |
208
+ | `wait-stable "<expr>"` | Wait until an expression stops changing (`--tolerance`, `--window`, `--timeout`) |
199
209
  | `run <file>` | Run a multi-step flow script |
200
210
 
201
211
  `click` / `hover` support `--nth N` (1-based, default first match), `--force` (skip actionability checks) and `--timeout ms`. Without `--nth`, multiple matches fail (strict) to prevent wrong-target clicks.
@@ -206,6 +216,7 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
206
216
  |---------|-------------|
207
217
  | `state-import <file>` | Import cookies from Netscape format |
208
218
  | `state-save` | Save current browser session state (cookies + localStorage) |
219
+ | `state-check` | Validate a state file: cookie count, expiry, target domain coverage (`--state`, `--url`) |
209
220
 
210
221
  ### Configuration
211
222
 
@@ -237,7 +248,7 @@ For repeatable flows, write a script and run it with one command. The script run
237
248
 
238
249
  ```js
239
250
  // flow.js
240
- export default async ({ page, click, waitFor, type, inspect, screenshot, log }) => {
251
+ export default async ({ page, context, frames, newPage, click, waitFor, type, inspect, screenshot, expect, log }) => {
241
252
  await page.goto('https://example.com/editor', { waitUntil: 'domcontentloaded' });
242
253
  await waitFor('.editor-canvas', { state: 'visible', timeout: 10000 });
243
254
 
@@ -259,12 +270,54 @@ export default async ({ page, click, waitFor, type, inspect, screenshot, log })
259
270
  cssprobe-cli run ./flow.js # --timeout ms for the whole script (default 300000, 0 = unlimited)
260
271
  ```
261
272
 
262
- Scripts must `default-export` an async function receiving the API object: `page`, `click`, `hover`, `type`, `waitFor`, `inspect`, `screenshot`, `log`. `screenshot` relative paths resolve against the script's directory; logs (`log()` and `console.*`) are collected and returned with the result. On failure the command exits `1`, prints the collected logs, and keeps the session alive.
273
+ Scripts must `default-export` an async function receiving the API object: `page`, `context`, `frames()`, `newPage()`, `click`, `hover`, `type`, `waitFor`, `inspect`, `screenshot`, `expect`, `log`. `waitFor` accepts `{ state, timeout, frame }`. Uncaught page errors are collected into the result envelope as `pageErrors`. `screenshot` relative paths resolve against the script's directory; logs (`log()` and `console.*`) are collected and returned with the result. On failure the command exits `1`, prints the collected logs, and keeps the session alive.
263
274
 
264
275
  > Scripts are executed by the local daemon with full Node privileges — only run trusted scripts.
265
276
 
266
277
  ---
267
278
 
279
+ ## Scenario Suites (Regression)
280
+
281
+ Run repeatable regression cases from a JSON suite: each case opens a fresh browser, executes steps, asserts on saved values, and writes artifacts + an optional JUnit report.
282
+
283
+ ```bash
284
+ cssprobe-cli scenario run suites/ticket-26571394.json \
285
+ --state ~/.cssprobe-cli/states/preprod.json \
286
+ --artifacts out/ticket-26571394 \
287
+ --junit out/junit.xml
288
+ ```
289
+
290
+ ```json
291
+ {
292
+ "name": "ticket 26571394 preprod",
293
+ "defaults": { "viewport": { "width": 1280, "height": 800 } },
294
+ "cases": [
295
+ {
296
+ "name": "refresh keeps the hash section aligned",
297
+ "url": "https://example.com/#listen-to-tcmi",
298
+ "steps": [
299
+ { "do": "sample", "expression": "({ hash: location.hash, top: Math.round(document.querySelector('#listen-to-tcmi')?.getBoundingClientRect().top ?? -1) })", "interval": 250, "duration": 12000, "key": "top", "tailFrom": 9500, "save": "timeline" },
300
+ { "do": "assert", "label": "hash retained", "on": "timeline", "path": "summary.last.value.hash", "eq": "#listen-to-tcmi" },
301
+ { "do": "assert", "label": "anchor stable", "on": "timeline", "path": "summary.tail.min", "min": 70 },
302
+ { "do": "assert", "label": "anchor stable", "on": "timeline", "path": "summary.tail.max", "max": 90 }
303
+ ]
304
+ },
305
+ {
306
+ "name": "deployment fingerprint",
307
+ "url": "https://example.com/",
308
+ "steps": [
309
+ { "do": "waitFor", "js": "() => !!window.DEBUG?.NavigatorStore", "timeout": 20000 },
310
+ { "do": "assert", "globals": { "DEBUG.NavigatorStore.setHashRestoreProtected": "function" }, "absent": ["DEBUG.NavigatorStore.navigateToSectionId"] }
311
+ ]
312
+ }
313
+ ]
314
+ }
315
+ ```
316
+
317
+ Steps: `goto`, `reload`, `wait`, `eval`, `sample`, `waitStable`, `click`, `waitFor`, `screenshot`, `assert`. `wait` takes a fixed `ms`; `waitFor` supports `frame` for in-iframe selector/js waits. `sample` and `eval` save values with `save: "<name>"`; assertions read them with `on: "<name>"` + `path`, or check live `state` (`url`, `title`, `frames`), `url`, `globals`, `absent`. `sample` supports `key`, `tailFrom`, `ignoreErrors`; every run writes `summary.json`, per-case `case.json`, sample series and failure screenshots.
318
+
319
+ **Iframe and preview pages.** A page can contain hidden helper iframes (`about:blank`); use `frames` to pick the real one, and `state: "attached"` when waiting for a frame element that is not visible. Preview frames can take 6–8s to render and analytics requests keep `networkIdle` busy — prefer a fixed `wait` step followed by a `waitFor` inside the frame (`frame: "i/preview"`) instead of `networkIdle`.
320
+
268
321
  ## Confidence Model
269
322
 
270
323
  Every finding carries a confidence level:
@@ -0,0 +1,15 @@
1
+ import type { Frame, Page } from 'playwright-core';
2
+ export interface FrameInfo {
3
+ index: number;
4
+ name: string;
5
+ url: string;
6
+ isMain: boolean;
7
+ }
8
+ export interface FrameLike {
9
+ name(): string;
10
+ url(): string;
11
+ }
12
+ export declare function describeFrames(page: Page): FrameInfo[];
13
+ export declare function selectFrame<T extends FrameLike>(frames: T[], spec: string): T;
14
+ export declare function resolveFrame(page: Page, spec?: string): Frame;
15
+ export declare function formatFrames(frames: FrameInfo[]): string;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeFrames = describeFrames;
4
+ exports.selectFrame = selectFrame;
5
+ exports.resolveFrame = resolveFrame;
6
+ exports.formatFrames = formatFrames;
7
+ function describeFrames(page) {
8
+ const main = page.mainFrame();
9
+ return page.frames().map((frame, index) => ({
10
+ index,
11
+ name: frame.name(),
12
+ url: frame.url(),
13
+ isMain: frame === main,
14
+ }));
15
+ }
16
+ function renderAvailable(frames) {
17
+ return frames
18
+ .map((frame, index) => `[${index}] name="${frame.name()}" url=${frame.url()}`)
19
+ .join('\n');
20
+ }
21
+ function selectFrame(frames, spec) {
22
+ const raw = spec.trim();
23
+ if (!raw)
24
+ throw new Error('Frame spec is empty');
25
+ if (/^\d+$/.test(raw)) {
26
+ const index = parseInt(raw, 10);
27
+ if (index >= frames.length) {
28
+ throw new Error(`Frame index ${index} is out of range (${frames.length} frame(s)).\n${renderAvailable(frames)}`);
29
+ }
30
+ return frames[index];
31
+ }
32
+ const byName = frames.filter(frame => frame.name() === raw);
33
+ if (byName.length === 1)
34
+ return byName[0];
35
+ if (byName.length > 1) {
36
+ throw new Error(`Frame name "${raw}" is ambiguous.\n${renderAvailable(byName)}`);
37
+ }
38
+ const byUrl = frames.filter(frame => frame.url().includes(raw));
39
+ if (byUrl.length === 1)
40
+ return byUrl[0];
41
+ if (byUrl.length > 1) {
42
+ throw new Error(`Frame spec "${raw}" matches multiple frames.\n${renderAvailable(byUrl)}`);
43
+ }
44
+ throw new Error(`No frame matches "${raw}".\n${renderAvailable(frames)}`);
45
+ }
46
+ function resolveFrame(page, spec) {
47
+ if (spec === undefined)
48
+ return page.mainFrame();
49
+ return selectFrame(page.frames(), spec);
50
+ }
51
+ function formatFrames(frames) {
52
+ return frames
53
+ .map(frame => {
54
+ const marker = frame.isMain ? ' (main)' : '';
55
+ const name = frame.name ? ` name="${frame.name}"` : '';
56
+ return `[${frame.index}]${marker}${name} ${frame.url}`;
57
+ })
58
+ .join('\n');
59
+ }
60
+ //# sourceMappingURL=frames.js.map
@@ -1,4 +1,4 @@
1
- import type { Page } from 'playwright-core';
1
+ import type { Frame, Page } from 'playwright-core';
2
2
  export interface ElementDescriptor {
3
3
  tag: string;
4
4
  classes: string[];
@@ -45,4 +45,5 @@ export declare function resolveWaitTarget(selector: string | undefined, opts: {
45
45
  networkIdle?: boolean;
46
46
  state?: string;
47
47
  }): WaitTarget;
48
- export declare function waitForCondition(page: Page, target: WaitTarget, timeout: number): Promise<number>;
48
+ export declare function normalizePredicateExpression(expression: string): string;
49
+ export declare function waitForCondition(host: Page | Frame, target: WaitTarget, timeout: number): Promise<number>;
@@ -7,6 +7,7 @@ exports.hoverElement = hoverElement;
7
7
  exports.typeInto = typeInto;
8
8
  exports.describeWaitTarget = describeWaitTarget;
9
9
  exports.resolveWaitTarget = resolveWaitTarget;
10
+ exports.normalizePredicateExpression = normalizePredicateExpression;
10
11
  exports.waitForCondition = waitForCondition;
11
12
  const DEFAULT_TIMEOUT = 30_000;
12
13
  function resolveLocator(page, selector, nth) {
@@ -70,17 +71,28 @@ function resolveWaitTarget(selector, opts) {
70
71
  return { kind: 'js', expression: opts.js };
71
72
  return { kind: 'networkIdle' };
72
73
  }
73
- async function waitForCondition(page, target, timeout) {
74
+ const FUNCTION_LIKE_PREDICATE = /^\s*(async\s+)?(function\b|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/;
75
+ function normalizePredicateExpression(expression) {
76
+ const trimmed = expression.trim();
77
+ if (!FUNCTION_LIKE_PREDICATE.test(trimmed))
78
+ return trimmed;
79
+ return `(${trimmed})()`;
80
+ }
81
+ async function waitForCondition(host, target, timeout) {
82
+ const isPage = typeof host.setViewportSize === 'function';
83
+ if (target.kind === 'networkIdle' && !isPage) {
84
+ throw new Error('networkIdle waits are page-level; remove the frame target');
85
+ }
74
86
  const started = Date.now();
75
87
  switch (target.kind) {
76
88
  case 'selector':
77
- await page.waitForSelector(target.selector, { state: target.state, timeout });
89
+ await host.waitForSelector(target.selector, { state: target.state, timeout });
78
90
  break;
79
91
  case 'js':
80
- await page.waitForFunction(target.expression, null, { timeout });
92
+ await host.waitForFunction(normalizePredicateExpression(target.expression), null, { timeout });
81
93
  break;
82
94
  case 'networkIdle':
83
- await page.waitForLoadState('networkidle', { timeout });
95
+ await host.waitForLoadState('networkidle', { timeout });
84
96
  break;
85
97
  }
86
98
  return Date.now() - started;
@@ -1,4 +1,4 @@
1
- import { type BrowserContext, type Page } from 'playwright-core';
1
+ import { type BrowserContext, type Frame, type Page } from 'playwright-core';
2
2
  import type { Snapshot, CollectConfig } from '../engine/types';
3
3
  export interface LauncherOptions {
4
4
  browser?: string;
@@ -11,6 +11,34 @@ export interface LauncherOptions {
11
11
  height: number;
12
12
  };
13
13
  }
14
+ export type BrowserKind = 'chromium' | 'chromium-headless-shell';
15
+ export interface BrowserInstall {
16
+ kind: BrowserKind;
17
+ revision: number;
18
+ dir: string;
19
+ executable: string | null;
20
+ }
21
+ export interface BrowserExpectation {
22
+ chromium?: number;
23
+ headlessShell?: number;
24
+ playwrightVersion?: string;
25
+ }
26
+ export type BrowserDiagnosisStatus = 'exact' | 'fallback' | 'missing';
27
+ export interface BrowserDiagnosis {
28
+ platform: string;
29
+ cacheRoots: string[];
30
+ expected: BrowserExpectation;
31
+ installs: BrowserInstall[];
32
+ status: BrowserDiagnosisStatus;
33
+ hint: string | null;
34
+ }
35
+ export declare function browserCacheRoots(): string[];
36
+ export declare function resolveExecutableIn(dir: string, platform: string): string | null;
37
+ export declare function listBrowserInstalls(cacheRoots?: string[]): BrowserInstall[];
38
+ export declare function expectedBrowsers(): BrowserExpectation;
39
+ export declare function diagnoseBrowser(engine?: string, cacheRoots?: string[]): BrowserDiagnosis;
40
+ /** Try to find an existing browser executable in the Playwright cache. */
41
+ export declare function findBrowserExecutable(engine: string, cacheRoots?: string[]): string | null;
14
42
  export declare class BrowserLauncher {
15
43
  private options;
16
44
  private browser;
@@ -18,7 +46,7 @@ export declare class BrowserLauncher {
18
46
  private page;
19
47
  constructor(options?: LauncherOptions);
20
48
  open(url: string): Promise<void>;
21
- collect(cfg: CollectConfig): Promise<Snapshot>;
49
+ collect(cfg: CollectConfig, target?: Page | Frame): Promise<Snapshot>;
22
50
  /** Navigate to a new URL in the existing page. */
23
51
  navigate(url: string): Promise<void>;
24
52
  /** Get the current page object. */