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.
- package/AGENTS.md +59 -6
- package/CHANGELOG.md +21 -0
- package/README.md +111 -2
- package/dist/browser/frames.d.ts +15 -0
- package/dist/browser/frames.js +60 -0
- package/dist/browser/interactions.d.ts +49 -0
- package/dist/browser/interactions.js +96 -0
- 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 +162 -4
- package/dist/cli/input.d.ts +6 -0
- package/dist/cli/input.js +24 -0
- package/dist/cli/program.js +171 -1
- package/dist/cli/session-ops.d.ts +10 -1
- package/dist/cli/session-ops.js +27 -56
- 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 +162 -18
- package/dist/daemon/expect.d.ts +8 -0
- package/dist/daemon/expect.js +47 -0
- package/dist/daemon/flowRunner.d.ts +73 -0
- package/dist/daemon/flowRunner.js +177 -0
- package/dist/engine/evalSerializer.d.ts +1 -0
- package/dist/engine/evalSerializer.js +43 -0
- package/dist/help.json +156 -16
- package/dist/installer/skill-template.js +43 -0
- 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 +244 -0
- package/dist/scenario/schema.d.ts +1210 -0
- package/dist/scenario/schema.js +145 -0
- package/dist/utils/screenshot.d.ts +1 -0
- package/dist/utils/screenshot.js +58 -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, 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
|
-
│
|
|
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
|
-
|
|
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
|
|
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. */
|