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.
- package/AGENTS.md +39 -7
- package/CHANGELOG.md +30 -0
- package/README.md +58 -5
- package/dist/browser/frames.d.ts +15 -0
- package/dist/browser/frames.js +60 -0
- package/dist/browser/interactions.d.ts +3 -2
- package/dist/browser/interactions.js +16 -4
- package/dist/browser/launcher.d.ts +30 -2
- package/dist/browser/launcher.js +141 -34
- package/dist/browser/sampler.d.ts +43 -0
- package/dist/browser/sampler.js +111 -0
- package/dist/cli/commands.d.ts +3 -0
- package/dist/cli/commands.js +93 -5
- package/dist/cli/input.d.ts +6 -0
- package/dist/cli/input.js +24 -0
- package/dist/cli/program.js +152 -1
- package/dist/cli/session-ops.d.ts +8 -0
- package/dist/cli/session-ops.js +24 -1
- package/dist/cli/state-check.d.ts +17 -0
- package/dist/cli/state-check.js +43 -0
- package/dist/cli/url-guard.d.ts +2 -0
- package/dist/cli/url-guard.js +24 -0
- package/dist/daemon/daemon.js +70 -20
- package/dist/daemon/expect.d.ts +8 -0
- package/dist/daemon/expect.js +47 -0
- package/dist/daemon/flowRunner.d.ts +13 -1
- package/dist/daemon/flowRunner.js +28 -3
- package/dist/engine/evalSerializer.js +2 -2
- package/dist/help.json +96 -17
- package/dist/scenario/assertions.d.ts +13 -0
- package/dist/scenario/assertions.js +111 -0
- package/dist/scenario/reporters.d.ts +13 -0
- package/dist/scenario/reporters.js +79 -0
- package/dist/scenario/runner.d.ts +10 -0
- package/dist/scenario/runner.js +251 -0
- package/dist/scenario/schema.d.ts +1270 -0
- package/dist/scenario/schema.js +151 -0
- package/docs/guide/installation.md +1 -1
- package/llms.txt +1 -1
- 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,
|
|
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,
|
|
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
|
-
- **
|
|
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
|
-
- `
|
|
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
|
|
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
|
-
|
|
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
|
|
89
|
+
await host.waitForSelector(target.selector, { state: target.state, timeout });
|
|
78
90
|
break;
|
|
79
91
|
case 'js':
|
|
80
|
-
await
|
|
92
|
+
await host.waitForFunction(normalizePredicateExpression(target.expression), null, { timeout });
|
|
81
93
|
break;
|
|
82
94
|
case 'networkIdle':
|
|
83
|
-
await
|
|
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. */
|