cssprobe-cli 0.3.1 → 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 +35 -7
- package/CHANGELOG.md +21 -0
- package/README.md +54 -4
- package/dist/browser/frames.d.ts +15 -0
- package/dist/browser/frames.js +60 -0
- package/dist/browser/interactions.d.ts +1 -0
- package/dist/browser/interactions.js +9 -1
- 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 +92 -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 +67 -19
- package/dist/daemon/expect.d.ts +8 -0
- package/dist/daemon/expect.js +47 -0
- package/dist/daemon/flowRunner.d.ts +12 -1
- package/dist/daemon/flowRunner.js +22 -0
- package/dist/engine/evalSerializer.js +2 -2
- package/dist/help.json +94 -16
- 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/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,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,7 @@ src/
|
|
|
52
63
|
- **Output strategy**: `Output` interface → `TextOutput` (Markdown for inspect, tables for lists) / `JsonOutput` (machine-readable JSON).
|
|
53
64
|
- **Config priority**: CLI flags → env vars → `~/.cssprobe-clirc` profiles. Switch with `-p <profile>`.
|
|
54
65
|
- **State management**: `state-import` converts Netscape cookies; `state-save` persists the current session's cookies + localStorage after manual login. Session persists browser state until `close`.
|
|
55
|
-
- **Browser discovery**: `findBrowserExecutable()` scans
|
|
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.
|
|
56
67
|
- **Error format**: `Error: <message>` → JSON mode: `{ isError: true, error: "<message>" }` → `process.exit(1)`
|
|
57
68
|
|
|
58
69
|
## Commands
|
|
@@ -62,7 +73,9 @@ src/
|
|
|
62
73
|
- `--viewport <WxH>` — Set viewport size (e.g. `--viewport 1280x720`)
|
|
63
74
|
- `--state <file>` — Load saved state (cookies + localStorage)
|
|
64
75
|
- `close` — Close the browser session
|
|
65
|
-
- `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
|
|
66
79
|
|
|
67
80
|
### CSS Inspection
|
|
68
81
|
- `inspect <selector>` — Inspect runtime CSS of a page element
|
|
@@ -78,6 +91,9 @@ src/
|
|
|
78
91
|
- `eval <expression>` — Evaluate JavaScript (browser context)
|
|
79
92
|
- Returns raw string values (not JSON-quoted) for string results
|
|
80
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
|
|
81
97
|
- `playwright <call>` — Execute Playwright API call (Node.js context)
|
|
82
98
|
- Returns `{ ok: true, value: null, note: ... }` when the call returns no value; throws a clear error message on failure
|
|
83
99
|
- `screenshot` — Take a screenshot
|
|
@@ -93,13 +109,22 @@ src/
|
|
|
93
109
|
- Default: `fill()` (replaces the value); `--sequential` for real key events (custom editors), `--clear` clears first
|
|
94
110
|
- `--press <key>` — press a key after typing (e.g. `Enter`)
|
|
95
111
|
- `--delay <ms>` — keystroke delay for `--sequential`
|
|
96
|
-
- `
|
|
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
|
|
97
115
|
- `wait-for <selector>` — selector state: `--state visible|hidden|attached|detached` (default visible)
|
|
98
116
|
- `wait-for --js "<predicate>"` — wait until a JS predicate is truthy
|
|
99
117
|
- `wait-for --network-idle` — wait until the page is network-idle
|
|
100
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`
|
|
101
125
|
- `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)
|
|
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[]`
|
|
103
128
|
- `--timeout <ms>` — whole-script guard (default 300000, 0 = unlimited); failure exits 1 and keeps the session alive
|
|
104
129
|
- `screenshot` relative paths resolve against the script's directory; logs collected and returned
|
|
105
130
|
|
|
@@ -110,6 +135,9 @@ src/
|
|
|
110
135
|
- `--merge <file>` — Existing state file to merge into
|
|
111
136
|
- `state-save` — Save current browser session state (cookies + localStorage)
|
|
112
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
|
|
113
141
|
|
|
114
142
|
### Config
|
|
115
143
|
- `config-show` / `config-set` / `config-list` / `config-use` / `config-new` / `config-path`
|
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,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
|
|
|
@@ -196,6 +203,8 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
|
|
|
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
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`) |
|
|
199
208
|
| `run <file>` | Run a multi-step flow script |
|
|
200
209
|
|
|
201
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.
|
|
@@ -206,6 +215,7 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
|
|
|
206
215
|
|---------|-------------|
|
|
207
216
|
| `state-import <file>` | Import cookies from Netscape format |
|
|
208
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`) |
|
|
209
219
|
|
|
210
220
|
### Configuration
|
|
211
221
|
|
|
@@ -237,7 +247,7 @@ For repeatable flows, write a script and run it with one command. The script run
|
|
|
237
247
|
|
|
238
248
|
```js
|
|
239
249
|
// flow.js
|
|
240
|
-
export default async ({ page, click, waitFor, type, inspect, screenshot, log }) => {
|
|
250
|
+
export default async ({ page, context, frames, newPage, click, waitFor, type, inspect, screenshot, expect, log }) => {
|
|
241
251
|
await page.goto('https://example.com/editor', { waitUntil: 'domcontentloaded' });
|
|
242
252
|
await waitFor('.editor-canvas', { state: 'visible', timeout: 10000 });
|
|
243
253
|
|
|
@@ -259,12 +269,52 @@ export default async ({ page, click, waitFor, type, inspect, screenshot, log })
|
|
|
259
269
|
cssprobe-cli run ./flow.js # --timeout ms for the whole script (default 300000, 0 = unlimited)
|
|
260
270
|
```
|
|
261
271
|
|
|
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.
|
|
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.
|
|
263
273
|
|
|
264
274
|
> Scripts are executed by the local daemon with full Node privileges — only run trusted scripts.
|
|
265
275
|
|
|
266
276
|
---
|
|
267
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
|
+
|
|
268
318
|
## Confidence Model
|
|
269
319
|
|
|
270
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
|
|
@@ -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 normalizePredicateExpression(expression: string): string;
|
|
48
49
|
export declare function waitForCondition(page: Page, 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,6 +71,13 @@ function resolveWaitTarget(selector, opts) {
|
|
|
70
71
|
return { kind: 'js', expression: opts.js };
|
|
71
72
|
return { kind: 'networkIdle' };
|
|
72
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
|
+
}
|
|
73
81
|
async function waitForCondition(page, target, timeout) {
|
|
74
82
|
const started = Date.now();
|
|
75
83
|
switch (target.kind) {
|
|
@@ -77,7 +85,7 @@ async function waitForCondition(page, target, timeout) {
|
|
|
77
85
|
await page.waitForSelector(target.selector, { state: target.state, timeout });
|
|
78
86
|
break;
|
|
79
87
|
case 'js':
|
|
80
|
-
await page.waitForFunction(target.expression, null, { timeout });
|
|
88
|
+
await page.waitForFunction(normalizePredicateExpression(target.expression), null, { timeout });
|
|
81
89
|
break;
|
|
82
90
|
case 'networkIdle':
|
|
83
91
|
await page.waitForLoadState('networkidle', { timeout });
|
|
@@ -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. */
|
package/dist/browser/launcher.js
CHANGED
|
@@ -6,6 +6,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
};
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.BrowserLauncher = void 0;
|
|
9
|
+
exports.browserCacheRoots = browserCacheRoots;
|
|
10
|
+
exports.resolveExecutableIn = resolveExecutableIn;
|
|
11
|
+
exports.listBrowserInstalls = listBrowserInstalls;
|
|
12
|
+
exports.expectedBrowsers = expectedBrowsers;
|
|
13
|
+
exports.diagnoseBrowser = diagnoseBrowser;
|
|
14
|
+
exports.findBrowserExecutable = findBrowserExecutable;
|
|
9
15
|
const fs_1 = __importDefault(require("fs"));
|
|
10
16
|
const path_1 = __importDefault(require("path"));
|
|
11
17
|
const os_1 = __importDefault(require("os"));
|
|
@@ -21,39 +27,133 @@ function loadBundle() {
|
|
|
21
27
|
bundleContent = fs_1.default.readFileSync(COLLECTOR_BUNDLE_PATH, 'utf-8');
|
|
22
28
|
return bundleContent;
|
|
23
29
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
30
|
+
function browserCacheRoots() {
|
|
31
|
+
const fromEnv = process.env.PLAYWRIGHT_BROWSERS_PATH;
|
|
32
|
+
if (fromEnv)
|
|
33
|
+
return [fromEnv];
|
|
34
|
+
if (process.platform === 'darwin') {
|
|
35
|
+
return [path_1.default.join(os_1.default.homedir(), 'Library', 'Caches', 'ms-playwright')];
|
|
36
|
+
}
|
|
37
|
+
if (process.platform === 'win32') {
|
|
38
|
+
const localAppData = process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Local');
|
|
39
|
+
return [path_1.default.join(localAppData, 'ms-playwright')];
|
|
40
|
+
}
|
|
41
|
+
return [path_1.default.join(os_1.default.homedir(), '.cache', 'ms-playwright')];
|
|
42
|
+
}
|
|
43
|
+
function resolveExecutableIn(dir, platform) {
|
|
44
|
+
const candidates = [];
|
|
45
|
+
if (platform === 'darwin') {
|
|
46
|
+
candidates.push(path_1.default.join(dir, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'));
|
|
47
|
+
candidates.push(path_1.default.join(dir, 'chrome-mac', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'));
|
|
48
|
+
}
|
|
49
|
+
else if (platform === 'linux') {
|
|
50
|
+
candidates.push(path_1.default.join(dir, 'chrome-linux', 'chrome'));
|
|
51
|
+
candidates.push(path_1.default.join(dir, 'chrome-linux64', 'chrome'));
|
|
52
|
+
}
|
|
53
|
+
else if (platform === 'win32') {
|
|
54
|
+
candidates.push(path_1.default.join(dir, 'chrome-win', 'chrome.exe'));
|
|
55
|
+
candidates.push(path_1.default.join(dir, 'chrome-win64', 'chrome.exe'));
|
|
56
|
+
}
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
if (fs_1.default.existsSync(candidate))
|
|
59
|
+
return candidate;
|
|
54
60
|
}
|
|
55
61
|
return null;
|
|
56
62
|
}
|
|
63
|
+
function listBrowserInstalls(cacheRoots = browserCacheRoots()) {
|
|
64
|
+
const installs = [];
|
|
65
|
+
for (const root of cacheRoots) {
|
|
66
|
+
if (!fs_1.default.existsSync(root))
|
|
67
|
+
continue;
|
|
68
|
+
for (const entry of fs_1.default.readdirSync(root)) {
|
|
69
|
+
const match = /^(chromium|chromium_headless_shell)-(\d+)$/.exec(entry);
|
|
70
|
+
if (!match)
|
|
71
|
+
continue;
|
|
72
|
+
const dir = path_1.default.join(root, entry);
|
|
73
|
+
installs.push({
|
|
74
|
+
kind: match[1] === 'chromium' ? 'chromium' : 'chromium-headless-shell',
|
|
75
|
+
revision: parseInt(match[2], 10),
|
|
76
|
+
dir,
|
|
77
|
+
executable: resolveExecutableIn(dir, process.platform),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return installs.sort((a, b) => b.revision - a.revision);
|
|
82
|
+
}
|
|
83
|
+
function expectedBrowsers() {
|
|
84
|
+
try {
|
|
85
|
+
const pkgPath = require.resolve('playwright-core/package.json');
|
|
86
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
87
|
+
const browsersPath = path_1.default.join(path_1.default.dirname(pkgPath), 'browsers.json');
|
|
88
|
+
const browsers = JSON.parse(fs_1.default.readFileSync(browsersPath, 'utf-8'));
|
|
89
|
+
const chromium = browsers.browsers?.find(browser => browser.name === 'chromium');
|
|
90
|
+
const headlessShell = browsers.browsers?.find(browser => browser.name === 'chromium-headless-shell');
|
|
91
|
+
return {
|
|
92
|
+
chromium: chromium ? parseInt(chromium.revision, 10) : undefined,
|
|
93
|
+
headlessShell: headlessShell ? parseInt(headlessShell.revision, 10) : undefined,
|
|
94
|
+
playwrightVersion: pkg.version,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function installCommand(expected) {
|
|
102
|
+
return expected.playwrightVersion
|
|
103
|
+
? `npx playwright@${expected.playwrightVersion} install chromium`
|
|
104
|
+
: 'npx playwright install chromium';
|
|
105
|
+
}
|
|
106
|
+
function diagnoseBrowser(engine = 'chromium', cacheRoots = browserCacheRoots()) {
|
|
107
|
+
const expected = expectedBrowsers();
|
|
108
|
+
const installs = listBrowserInstalls(cacheRoots);
|
|
109
|
+
const chromiumInstalls = installs.filter(install => install.kind === 'chromium' && install.executable);
|
|
110
|
+
const exact = expected.chromium
|
|
111
|
+
? chromiumInstalls.some(install => install.revision === expected.chromium)
|
|
112
|
+
: chromiumInstalls.length > 0;
|
|
113
|
+
if (engine !== 'chromium') {
|
|
114
|
+
return {
|
|
115
|
+
platform: process.platform,
|
|
116
|
+
cacheRoots,
|
|
117
|
+
expected,
|
|
118
|
+
installs,
|
|
119
|
+
status: 'missing',
|
|
120
|
+
hint: `Automatic discovery only covers chromium; run "npx playwright install ${engine}" if launch fails.`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (exact) {
|
|
124
|
+
return { platform: process.platform, cacheRoots, expected, installs, status: 'exact', hint: null };
|
|
125
|
+
}
|
|
126
|
+
if (chromiumInstalls.length > 0) {
|
|
127
|
+
const found = chromiumInstalls.map(install => install.revision).join(', ');
|
|
128
|
+
return {
|
|
129
|
+
platform: process.platform,
|
|
130
|
+
cacheRoots,
|
|
131
|
+
expected,
|
|
132
|
+
installs,
|
|
133
|
+
status: 'fallback',
|
|
134
|
+
hint: `Found chromium revision(s) ${found}, expected ${expected.chromium ?? 'unknown'}. Launch may work through the fallback. To install the expected build: ${installCommand(expected)}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
platform: process.platform,
|
|
139
|
+
cacheRoots,
|
|
140
|
+
expected,
|
|
141
|
+
installs,
|
|
142
|
+
status: 'missing',
|
|
143
|
+
hint: `No chromium install found. Install it with: ${installCommand(expected)}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** Try to find an existing browser executable in the Playwright cache. */
|
|
147
|
+
function findBrowserExecutable(engine, cacheRoots = browserCacheRoots()) {
|
|
148
|
+
if (engine !== 'chromium')
|
|
149
|
+
return null;
|
|
150
|
+
const installs = listBrowserInstalls(cacheRoots).filter(install => install.kind === 'chromium' && install.executable);
|
|
151
|
+
if (!installs.length)
|
|
152
|
+
return null;
|
|
153
|
+
const expected = expectedBrowsers().chromium;
|
|
154
|
+
const exact = installs.find(install => install.revision === expected);
|
|
155
|
+
return (exact ?? installs[0]).executable;
|
|
156
|
+
}
|
|
57
157
|
class BrowserLauncher {
|
|
58
158
|
options;
|
|
59
159
|
browser = null;
|
|
@@ -74,7 +174,13 @@ class BrowserLauncher {
|
|
|
74
174
|
if (execPath) {
|
|
75
175
|
launchOptions.executablePath = execPath;
|
|
76
176
|
}
|
|
77
|
-
|
|
177
|
+
try {
|
|
178
|
+
this.browser = await factory.launch(launchOptions);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const diagnosis = diagnoseBrowser(engine);
|
|
182
|
+
throw new Error(`${error.message}\nBrowser diagnosis: ${diagnosis.status}. Run "cssprobe-cli doctor" for details.`);
|
|
183
|
+
}
|
|
78
184
|
// Load saved state if provided
|
|
79
185
|
const vp = this.options.viewport || { width: 1280, height: 720 };
|
|
80
186
|
const contextOptions = { viewport: vp };
|
|
@@ -98,12 +204,13 @@ class BrowserLauncher {
|
|
|
98
204
|
await this.page.goto(`file://${resolved}`, { waitUntil: 'load', timeout: 30000 });
|
|
99
205
|
}
|
|
100
206
|
}
|
|
101
|
-
async collect(cfg) {
|
|
207
|
+
async collect(cfg, target) {
|
|
102
208
|
if (!this.page)
|
|
103
209
|
throw new Error('Browser not opened. Call open() first.');
|
|
210
|
+
const host = target ?? this.page;
|
|
104
211
|
const bundle = loadBundle();
|
|
105
|
-
await
|
|
106
|
-
const snapshot = await
|
|
212
|
+
await host.addScriptTag({ content: bundle });
|
|
213
|
+
const snapshot = await host.evaluate((config) => {
|
|
107
214
|
return window.__cssprobe_cli.collect(config);
|
|
108
215
|
}, cfg);
|
|
109
216
|
return snapshot;
|