cssprobe-cli 0.3.0 → 0.4.0

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 (44) hide show
  1. package/AGENTS.md +59 -6
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +111 -2
  4. package/dist/browser/frames.d.ts +15 -0
  5. package/dist/browser/frames.js +60 -0
  6. package/dist/browser/interactions.d.ts +49 -0
  7. package/dist/browser/interactions.js +96 -0
  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 +162 -4
  14. package/dist/cli/input.d.ts +6 -0
  15. package/dist/cli/input.js +24 -0
  16. package/dist/cli/program.js +171 -1
  17. package/dist/cli/session-ops.d.ts +10 -1
  18. package/dist/cli/session-ops.js +27 -56
  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 +162 -18
  24. package/dist/daemon/expect.d.ts +8 -0
  25. package/dist/daemon/expect.js +47 -0
  26. package/dist/daemon/flowRunner.d.ts +73 -0
  27. package/dist/daemon/flowRunner.js +177 -0
  28. package/dist/engine/evalSerializer.d.ts +1 -0
  29. package/dist/engine/evalSerializer.js +43 -0
  30. package/dist/help.json +156 -16
  31. package/dist/installer/skill-template.js +43 -0
  32. package/dist/scenario/assertions.d.ts +13 -0
  33. package/dist/scenario/assertions.js +111 -0
  34. package/dist/scenario/reporters.d.ts +13 -0
  35. package/dist/scenario/reporters.js +79 -0
  36. package/dist/scenario/runner.d.ts +10 -0
  37. package/dist/scenario/runner.js +244 -0
  38. package/dist/scenario/schema.d.ts +1210 -0
  39. package/dist/scenario/schema.js +145 -0
  40. package/dist/utils/screenshot.d.ts +1 -0
  41. package/dist/utils/screenshot.js +58 -0
  42. package/docs/guide/installation.md +1 -1
  43. package/llms.txt +1 -1
  44. 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, 58 tests).
7
+ Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner, 170 tests).
8
8
  Dependencies: `playwright-core`, `zod`. Dev: `typescript`, `@types/node`, `tsx`, `esbuild`.
9
9
 
10
10
  ## Architecture
@@ -17,23 +17,38 @@ 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
27
+ │ ├── flowRunner.ts # Flow script loader/runner (run command): dynamic import, API injection, console/pageerror capture
28
+ │ ├── expect.ts # Assertion matchers for flow scripts
24
29
  │ └── daemonEntry.ts # Daemon entry point (spawned as child process)
25
30
  ├── engine/
26
31
  │ ├── types.ts # Shared types: Snapshot, TreeNode, Finding, Confidence
27
32
  │ ├── collector.ts # Browser-side: DOM traversal, computed styles, declared values
28
33
  │ ├── analyzer.ts # Node-side: pure functions, confidence model, pattern detection
34
+ │ ├── evalSerializer.ts # Self-contained serializer for eval results (runs in-page via JSHandle.evaluate)
29
35
  │ └── renderer.ts # Markdown / JSON rendering
30
36
  ├── browser/
31
- │ └── 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)
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
32
46
  ├── config/
33
47
  │ ├── config.ts # Config loader (CLI → env → ~/.cssprobe-clirc profiles)
34
48
  │ └── helpGenerator.ts # Build-time: Zod schemas → dist/help.json
35
49
  └── utils/
36
- └── socketConnection.ts # Socket communication (adapted from playwright-cli)
50
+ ├── socketConnection.ts # Socket communication (adapted from playwright-cli)
51
+ └── screenshot.ts # Screenshot data-URL → file saving (shared by CLI + flow runner)
37
52
  ```
38
53
 
39
54
  ## Key Patterns
@@ -48,7 +63,7 @@ src/
48
63
  - **Output strategy**: `Output` interface → `TextOutput` (Markdown for inspect, tables for lists) / `JsonOutput` (machine-readable JSON).
49
64
  - **Config priority**: CLI flags → env vars → `~/.cssprobe-clirc` profiles. Switch with `-p <profile>`.
50
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`.
51
- - **Browser discovery**: `findBrowserExecutable()` scans `~/Library/Caches/ms-playwright/chromium-*` for existing Chrome installations.
66
+ - **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.
52
67
  - **Error format**: `Error: <message>` → JSON mode: `{ isError: true, error: "<message>" }` → `process.exit(1)`
53
68
 
54
69
  ## Commands
@@ -58,7 +73,9 @@ src/
58
73
  - `--viewport <WxH>` — Set viewport size (e.g. `--viewport 1280x720`)
59
74
  - `--state <file>` — Load saved state (cookies + localStorage)
60
75
  - `close` — Close the browser session
61
- - `status` — Show current session status
76
+ - `status` — Show current session status (URL, title, frame count)
77
+ - `frames` — List frames: index, name, URL, main marker
78
+ - `doctor` — Diagnose browser installation vs expected Playwright revision
62
79
 
63
80
  ### CSS Inspection
64
81
  - `inspect <selector>` — Inspect runtime CSS of a page element
@@ -73,11 +90,44 @@ src/
73
90
  - `resize <width> <height>` — Resize browser viewport
74
91
  - `eval <expression>` — Evaluate JavaScript (browser context)
75
92
  - Returns raw string values (not JSON-quoted) for string results
93
+ - DOM nodes / circular structures are auto-serialized (descriptors, `[Circular]`, `[Function: name]`, depth cap 6)
94
+ - Promises are awaited; `undefined` object/array values become `null`
95
+ - `@file` / `-` (stdin) reads long expressions from disk or stdin
96
+ - `--frame <index|name|url-substring>` evaluates inside an iframe
76
97
  - `playwright <call>` — Execute Playwright API call (Node.js context)
77
98
  - Returns `{ ok: true, value: null, note: ... }` when the call returns no value; throws a clear error message on failure
78
99
  - `screenshot` — Take a screenshot
79
100
  - `--out <path>` — Output file path (default: `~/.cssprobe-cli/screenshots/<timestamp>.png`)
80
101
 
102
+ ### Interaction
103
+ - `click <selector>` — Real mouse click (auto-scroll + actionability wait, works with custom React components)
104
+ - `--nth <n>` — 1-based match index (default: first match; multiple matches without `--nth` fail — strict)
105
+ - `--force` — skip actionability checks
106
+ - `--timeout <ms>` — action timeout (default 30000)
107
+ - `hover <selector>` — Real mouse hover (`--nth` / `--force` / `--timeout` as above)
108
+ - `type <selector> <text>` — Type text
109
+ - Default: `fill()` (replaces the value); `--sequential` for real key events (custom editors), `--clear` clears first
110
+ - `--press <key>` — press a key after typing (e.g. `Enter`)
111
+ - `--delay <ms>` — keystroke delay for `--sequential`
112
+ - `sample "<expr>"` — Sample an expression on an interval (`--interval`, `--duration`, `--key`, `--frame`), returning `summary` + `series`
113
+ - `wait-stable "<expr>"` — Wait until the expression stops changing (`--tolerance`, `--window`, `--timeout`, `--interval`, `--frame`)
114
+ - `wait-for` — Wait for a condition (three mutually exclusive modes); arrow-function/function predicates are invoked, bare expressions are evaluated as-is
115
+ - `wait-for <selector>` — selector state: `--state visible|hidden|attached|detached` (default visible)
116
+ - `wait-for --js "<predicate>"` — wait until a JS predicate is truthy
117
+ - `wait-for --network-idle` — wait until the page is network-idle
118
+ - `--timeout <ms>` — default 30000; timeout errors name the target
119
+ - `scenario run <file>` — Run a JSON scenario suite (fresh browser per case, assertions, artifacts, JUnit)
120
+ - `--state <file>` — Fallback state file for cases without one
121
+ - `--artifacts <dir>` — Artifact directory (default `~/.cssprobe-cli/scenario-runs/<suite>-<timestamp>`)
122
+ - `--junit <file>` — Write a JUnit XML report
123
+ - `--bail` — Stop after the first failing case
124
+ - Steps: `goto`, `reload`, `eval`, `sample`, `waitStable`, `click`, `waitFor`, `screenshot`, `assert`
125
+ - `run <file>` — Run a multi-step flow script inside the daemon
126
+ - 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)
127
+ - Uncaught page errors are collected in the envelope as `pageErrors[]`
128
+ - `--timeout <ms>` — whole-script guard (default 300000, 0 = unlimited); failure exits 1 and keeps the session alive
129
+ - `screenshot` relative paths resolve against the script's directory; logs collected and returned
130
+
81
131
  ### State
82
132
  - `state-import [file]` — Import cookies from Netscape format
83
133
  - `--out <file>` — Output state file path (default: `~/.cssprobe-cli/states/imported.json`)
@@ -85,6 +135,9 @@ src/
85
135
  - `--merge <file>` — Existing state file to merge into
86
136
  - `state-save` — Save current browser session state (cookies + localStorage)
87
137
  - `--name <name>` — State file name (default: `session`)
138
+ - `state-check` — Validate a saved state file (cookie count, expiry, target domain coverage)
139
+ - `--state <file>` — State file (default: `~/.cssprobe-cli/states/session.json`)
140
+ - `--url <url>` — Require the cookies to cover this hostname
88
141
 
89
142
  ### Config
90
143
  - `config-show` / `config-set` / `config-list` / `config-use` / `config-new` / `config-path`
@@ -151,6 +204,6 @@ cssprobe-cli close
151
204
  | Output | Single markdown string | Markdown (default) / JSON (--json) |
152
205
  | Config | `window.__DOM_REPORT_CFG` global + eval two-step | CLI flags → env → rc file, one command |
153
206
  | ROOT_SELECTOR | Manual or eval probe | `--auto-root` auto-detection |
154
- | Tests | None | 45 unit tests (command, analyzer, output, renderer) |
207
+ | Tests | None | 45+ unit tests (command, analyzer, output, renderer) |
155
208
  | Build | Regex stripping CJS module.exports | esbuild IIFE + CJS bundle, clean build |
156
209
  | Session | None (blocking commands) | Session-based daemon architecture |
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0] - 2026-10-23
4
+
5
+ ### Added
6
+ - `frames` command and `--frame <index|name|url-substring>` on `eval` / `inspect` / `tree` / `layout` / `findings` / `inject-css`
7
+ - `sample "<expr>"` (interval timeline + summary) and `wait-stable "<expr>"` (tolerance + stable window)
8
+ - `doctor` command: platform-aware browser cache discovery, expected vs installed Playwright revisions and install hint
9
+ - `state-check` command: cookie expiry and target-domain coverage for saved state files
10
+ - `open --expect-url <regex>` and `open --fail-on-login` guards
11
+ - Flow API: `context`, `frames()`, `newPage()`, `expect` matchers; `pageErrors` in the result envelope
12
+ - `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
13
+ - `sample --tail-from` and per-step `tailFrom` / `ignoreErrors`
14
+
15
+ ### Fixed
16
+ - `wait-for --js` (and flow `waitFor({ js })`) now invokes arrow-function/function predicates instead of treating the function source as a truthy expression
17
+
18
+ ### Changed
19
+ - `eval` / `playwright` accept `@file` and `-` (stdin)
20
+ - `eval` awaits promise results and preserves `undefined` object/array fields as `null`
21
+ - `status` now reports the current URL, title and frame count
22
+ - Browser discovery supports macOS, Linux and Windows cache roots and prefers the revision expected by `playwright-core`
23
+
3
24
  All notable changes are documented on the [GitHub Releases](https://github.com/mack-peng/cssprobe-cli/releases) page.
4
25
 
5
26
  ## [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,16 +188,34 @@ 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) |
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
 
196
+ ### Interaction
197
+
198
+ | Command | Description |
199
+ |---------|-------------|
200
+ | `click <selector>` | Real mouse click (auto-scroll + actionability wait; works with custom React components) |
201
+ | `hover <selector>` | Real mouse hover |
202
+ | `type <selector> <text>` | Fill text (replaces value); `--press Enter` to submit; `--sequential` for real key events |
203
+ | `wait-for <selector>` | Wait for selector state (`--state visible\|hidden\|attached\|detached`, `--timeout ms`) |
204
+ | `wait-for --js "<predicate>"` | Wait until a JS predicate is truthy |
205
+ | `wait-for --network-idle` | Wait until the page is network-idle |
206
+ | `sample "<expr>"` | Sample an expression over time (`--interval`, `--duration`, `--key`, `--frame`); returns timeline + summary |
207
+ | `wait-stable "<expr>"` | Wait until an expression stops changing (`--tolerance`, `--window`, `--timeout`) |
208
+ | `run <file>` | Run a multi-step flow script |
209
+
210
+ `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.
211
+
189
212
  ### State
190
213
 
191
214
  | Command | Description |
192
215
  |---------|-------------|
193
216
  | `state-import <file>` | Import cookies from Netscape format |
194
217
  | `state-save` | Save current browser session state (cookies + localStorage) |
218
+ | `state-check` | Validate a state file: cookie count, expiry, target domain coverage (`--state`, `--url`) |
195
219
 
196
220
  ### Configuration
197
221
 
@@ -206,6 +230,91 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
206
230
 
207
231
  ---
208
232
 
233
+ ## Multi-step Flows
234
+
235
+ The interaction commands drive a page step by step; `run` turns a sequence into a versionable script:
236
+
237
+ ```bash
238
+ cssprobe-cli open https://example.com/editor
239
+ cssprobe-cli wait-for .editor-canvas
240
+ cssprobe-cli click ".layout-switch" --nth 1
241
+ cssprobe-cli type ".block-title input" "Hello" --press Enter
242
+ cssprobe-cli inspect .block-grid --json
243
+ cssprobe-cli close
244
+ ```
245
+
246
+ For repeatable flows, write a script and run it with one command. The script runs inside the daemon session (full Playwright `page` access):
247
+
248
+ ```js
249
+ // flow.js
250
+ export default async ({ page, context, frames, newPage, click, waitFor, type, inspect, screenshot, expect, log }) => {
251
+ await page.goto('https://example.com/editor', { waitUntil: 'domcontentloaded' });
252
+ await waitFor('.editor-canvas', { state: 'visible', timeout: 10000 });
253
+
254
+ await click('.layout-switch', { nth: 1 });
255
+ await waitFor({ js: "() => document.body.dataset.layout === 'two-col'" });
256
+
257
+ await type('.block-title input', 'Hello', { press: 'Enter', sequential: true, clear: true });
258
+ await waitFor({ networkIdle: true });
259
+
260
+ const { findings } = await inspect('.block-grid', { format: 'json' });
261
+ log('findings:', findings.length);
262
+ await screenshot({ path: './after-layout-switch.png' });
263
+
264
+ return { findings: findings.length };
265
+ };
266
+ ```
267
+
268
+ ```bash
269
+ cssprobe-cli run ./flow.js # --timeout ms for the whole script (default 300000, 0 = unlimited)
270
+ ```
271
+
272
+ Scripts must `default-export` an async function receiving the API object: `page`, `context`, `frames()`, `newPage()`, `click`, `hover`, `type`, `waitFor`, `inspect`, `screenshot`, `expect`, `log`. 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.
273
+
274
+ > Scripts are executed by the local daemon with full Node privileges — only run trusted scripts.
275
+
276
+ ---
277
+
278
+ ## Scenario Suites (Regression)
279
+
280
+ 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.
281
+
282
+ ```bash
283
+ cssprobe-cli scenario run suites/ticket-26571394.json \
284
+ --state ~/.cssprobe-cli/states/preprod.json \
285
+ --artifacts out/ticket-26571394 \
286
+ --junit out/junit.xml
287
+ ```
288
+
289
+ ```json
290
+ {
291
+ "name": "ticket 26571394 preprod",
292
+ "defaults": { "viewport": { "width": 1280, "height": 800 } },
293
+ "cases": [
294
+ {
295
+ "name": "refresh keeps the hash section aligned",
296
+ "url": "https://example.com/#listen-to-tcmi",
297
+ "steps": [
298
+ { "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" },
299
+ { "do": "assert", "label": "hash retained", "on": "timeline", "path": "summary.last.value.hash", "eq": "#listen-to-tcmi" },
300
+ { "do": "assert", "label": "anchor stable", "on": "timeline", "path": "summary.tail.min", "min": 70 },
301
+ { "do": "assert", "label": "anchor stable", "on": "timeline", "path": "summary.tail.max", "max": 90 }
302
+ ]
303
+ },
304
+ {
305
+ "name": "deployment fingerprint",
306
+ "url": "https://example.com/",
307
+ "steps": [
308
+ { "do": "waitFor", "js": "() => !!window.DEBUG?.NavigatorStore", "timeout": 20000 },
309
+ { "do": "assert", "globals": { "DEBUG.NavigatorStore.setHashRestoreProtected": "function" }, "absent": ["DEBUG.NavigatorStore.navigateToSectionId"] }
310
+ ]
311
+ }
312
+ ]
313
+ }
314
+ ```
315
+
316
+ Steps: `goto`, `reload`, `eval`, `sample`, `waitStable`, `click`, `waitFor`, `screenshot`, `assert`. `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.
317
+
209
318
  ## Confidence Model
210
319
 
211
320
  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
@@ -0,0 +1,49 @@
1
+ import type { Page } from 'playwright-core';
2
+ export interface ElementDescriptor {
3
+ tag: string;
4
+ classes: string[];
5
+ text: string;
6
+ }
7
+ export interface TargetResult {
8
+ matches: number;
9
+ nth: number;
10
+ descriptor: ElementDescriptor | null;
11
+ }
12
+ export type WaitState = 'visible' | 'hidden' | 'attached' | 'detached';
13
+ export type WaitTarget = {
14
+ kind: 'selector';
15
+ selector: string;
16
+ state: WaitState;
17
+ } | {
18
+ kind: 'js';
19
+ expression: string;
20
+ } | {
21
+ kind: 'networkIdle';
22
+ };
23
+ export declare function clickElement(page: Page, selector: string, opts?: {
24
+ nth?: number;
25
+ force?: boolean;
26
+ timeout?: number;
27
+ }): Promise<TargetResult>;
28
+ export declare function hoverElement(page: Page, selector: string, opts?: {
29
+ nth?: number;
30
+ force?: boolean;
31
+ timeout?: number;
32
+ }): Promise<TargetResult>;
33
+ export interface TypeOptions {
34
+ press?: string;
35
+ sequential?: boolean;
36
+ clear?: boolean;
37
+ delay?: number;
38
+ nth?: number;
39
+ timeout?: number;
40
+ }
41
+ export declare function typeInto(page: Page, selector: string, text: string, opts?: TypeOptions): Promise<void>;
42
+ export declare function describeWaitTarget(target: WaitTarget): string;
43
+ export declare function resolveWaitTarget(selector: string | undefined, opts: {
44
+ js?: string;
45
+ networkIdle?: boolean;
46
+ state?: string;
47
+ }): WaitTarget;
48
+ export declare function normalizePredicateExpression(expression: string): string;
49
+ export declare function waitForCondition(page: Page, target: WaitTarget, timeout: number): Promise<number>;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ // Interaction primitives shared by daemon commands and the run-script API.
3
+ // Strict mode is preserved when no nth is given: multiple matches fail the action.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.clickElement = clickElement;
6
+ exports.hoverElement = hoverElement;
7
+ exports.typeInto = typeInto;
8
+ exports.describeWaitTarget = describeWaitTarget;
9
+ exports.resolveWaitTarget = resolveWaitTarget;
10
+ exports.normalizePredicateExpression = normalizePredicateExpression;
11
+ exports.waitForCondition = waitForCondition;
12
+ const DEFAULT_TIMEOUT = 30_000;
13
+ function resolveLocator(page, selector, nth) {
14
+ const base = page.locator(selector);
15
+ return nth ? base.nth(nth - 1) : base;
16
+ }
17
+ async function describeTarget(locator) {
18
+ return await locator
19
+ .evaluate(el => ({
20
+ tag: el.tagName.toLowerCase(),
21
+ classes: Array.from(el.classList),
22
+ text: (el.textContent ?? '').trim().slice(0, 60),
23
+ }))
24
+ .catch(() => null);
25
+ }
26
+ async function interactTarget(page, selector, opts, action) {
27
+ const base = page.locator(selector);
28
+ const locator = resolveLocator(page, selector, opts.nth);
29
+ const matches = await base.count();
30
+ const descriptor = await describeTarget(locator);
31
+ await action(locator, { force: opts.force, timeout: opts.timeout ?? DEFAULT_TIMEOUT });
32
+ return { matches, nth: opts.nth ?? 1, descriptor };
33
+ }
34
+ async function clickElement(page, selector, opts = {}) {
35
+ return interactTarget(page, selector, opts, (locator, o) => locator.click({ force: o.force, timeout: o.timeout }));
36
+ }
37
+ async function hoverElement(page, selector, opts = {}) {
38
+ return interactTarget(page, selector, opts, (locator, o) => locator.hover({ force: o.force, timeout: o.timeout }));
39
+ }
40
+ async function typeInto(page, selector, text, opts = {}) {
41
+ const locator = resolveLocator(page, selector, opts.nth);
42
+ const timeout = opts.timeout ?? DEFAULT_TIMEOUT;
43
+ if (opts.sequential) {
44
+ if (opts.clear)
45
+ await locator.fill('', { timeout });
46
+ await locator.pressSequentially(text, { delay: opts.delay ?? 0, timeout });
47
+ }
48
+ else {
49
+ await locator.fill(text, { timeout });
50
+ }
51
+ if (opts.press)
52
+ await locator.press(opts.press, { timeout });
53
+ }
54
+ function describeWaitTarget(target) {
55
+ switch (target.kind) {
56
+ case 'selector':
57
+ return `${target.selector} (state: ${target.state})`;
58
+ case 'js':
59
+ return `JS predicate: ${target.expression.slice(0, 80)}`;
60
+ case 'networkIdle':
61
+ return 'network idle';
62
+ }
63
+ }
64
+ function resolveWaitTarget(selector, opts) {
65
+ const modeCount = [selector, opts.js, opts.networkIdle].filter(Boolean).length;
66
+ if (modeCount !== 1)
67
+ throw new Error('wait-for requires exactly one of: <selector>, --js "<expression>", --network-idle');
68
+ if (selector)
69
+ return { kind: 'selector', selector, state: (opts.state ?? 'visible') };
70
+ if (opts.js)
71
+ return { kind: 'js', expression: opts.js };
72
+ return { kind: 'networkIdle' };
73
+ }
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(page, target, timeout) {
82
+ const started = Date.now();
83
+ switch (target.kind) {
84
+ case 'selector':
85
+ await page.waitForSelector(target.selector, { state: target.state, timeout });
86
+ break;
87
+ case 'js':
88
+ await page.waitForFunction(normalizePredicateExpression(target.expression), null, { timeout });
89
+ break;
90
+ case 'networkIdle':
91
+ await page.waitForLoadState('networkidle', { timeout });
92
+ break;
93
+ }
94
+ return Date.now() - started;
95
+ }
96
+ //# sourceMappingURL=interactions.js.map
@@ -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. */