cssprobe-cli 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +39 -7
  2. package/CHANGELOG.md +30 -0
  3. package/README.md +58 -5
  4. package/dist/browser/frames.d.ts +15 -0
  5. package/dist/browser/frames.js +60 -0
  6. package/dist/browser/interactions.d.ts +3 -2
  7. package/dist/browser/interactions.js +16 -4
  8. package/dist/browser/launcher.d.ts +30 -2
  9. package/dist/browser/launcher.js +141 -34
  10. package/dist/browser/sampler.d.ts +43 -0
  11. package/dist/browser/sampler.js +111 -0
  12. package/dist/cli/commands.d.ts +3 -0
  13. package/dist/cli/commands.js +93 -5
  14. package/dist/cli/input.d.ts +6 -0
  15. package/dist/cli/input.js +24 -0
  16. package/dist/cli/program.js +152 -1
  17. package/dist/cli/session-ops.d.ts +8 -0
  18. package/dist/cli/session-ops.js +24 -1
  19. package/dist/cli/state-check.d.ts +17 -0
  20. package/dist/cli/state-check.js +43 -0
  21. package/dist/cli/url-guard.d.ts +2 -0
  22. package/dist/cli/url-guard.js +24 -0
  23. package/dist/daemon/daemon.js +70 -20
  24. package/dist/daemon/expect.d.ts +8 -0
  25. package/dist/daemon/expect.js +47 -0
  26. package/dist/daemon/flowRunner.d.ts +13 -1
  27. package/dist/daemon/flowRunner.js +28 -3
  28. package/dist/engine/evalSerializer.js +2 -2
  29. package/dist/help.json +96 -17
  30. package/dist/scenario/assertions.d.ts +13 -0
  31. package/dist/scenario/assertions.js +111 -0
  32. package/dist/scenario/reporters.d.ts +13 -0
  33. package/dist/scenario/reporters.js +79 -0
  34. package/dist/scenario/runner.d.ts +10 -0
  35. package/dist/scenario/runner.js +251 -0
  36. package/dist/scenario/schema.d.ts +1270 -0
  37. package/dist/scenario/schema.js +151 -0
  38. package/docs/guide/installation.md +1 -1
  39. package/llms.txt +1 -1
  40. package/package.json +1 -1
@@ -15,6 +15,8 @@ const analyzer_1 = require("../engine/analyzer");
15
15
  const renderer_1 = require("../engine/renderer");
16
16
  const evalSerializer_1 = require("../engine/evalSerializer");
17
17
  const screenshot_1 = require("../utils/screenshot");
18
+ const frames_1 = require("../browser/frames");
19
+ const expect_1 = require("./expect");
18
20
  const FLOW_DEFAULT_TIMEOUT = 300_000;
19
21
  const CAPTURED_CONSOLE_METHODS = ['log', 'info', 'warn', 'error'];
20
22
  // tsc compiles `import()` to `require()` under module=commonjs, which cannot
@@ -64,20 +66,23 @@ function requirePage(launcher) {
64
66
  return page;
65
67
  }
66
68
  async function flowWaitFor(page, target, opts) {
69
+ const host = opts?.frame ? (0, frames_1.resolveFrame)(page, opts.frame) : page;
67
70
  if (typeof target === 'string') {
68
71
  const state = opts?.state ?? 'visible';
69
72
  const timeout = opts?.timeout ?? 30_000;
70
- const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'selector', selector: target, state }, timeout);
73
+ const elapsedMs = await (0, interactions_1.waitForCondition)(host, { kind: 'selector', selector: target, state }, timeout);
71
74
  return { waitedFor: 'selector', state, elapsedMs };
72
75
  }
73
76
  if ('js' in target) {
74
77
  const timeout = target.timeout ?? 30_000;
75
- const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'js', expression: target.js }, timeout);
78
+ const elapsedMs = await (0, interactions_1.waitForCondition)(host, { kind: 'js', expression: target.js }, timeout);
76
79
  return { waitedFor: 'js', elapsedMs };
77
80
  }
78
81
  if (target.networkIdle) {
82
+ if (opts?.frame)
83
+ throw new Error('networkIdle waits are page-level; remove opts.frame');
79
84
  const timeout = target.timeout ?? 30_000;
80
- const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'networkIdle' }, timeout);
85
+ const elapsedMs = await (0, interactions_1.waitForCondition)(host, { kind: 'networkIdle' }, timeout);
81
86
  return { waitedFor: 'networkidle', elapsedMs };
82
87
  }
83
88
  throw new Error('waitFor requires a selector string, { js }, or { networkIdle: true }');
@@ -102,6 +107,10 @@ function buildFlowApi(launcher, logs, baseDir) {
102
107
  return (0, renderer_1.renderMarkdown)(snapshot, findings);
103
108
  return { snapshot, findings };
104
109
  },
110
+ context: page.context(),
111
+ frames: () => (0, frames_1.describeFrames)(page),
112
+ newPage: () => page.context().newPage(),
113
+ expect: (actual) => (0, expect_1.expect)(actual),
105
114
  screenshot: async (opts) => {
106
115
  const buffer = await page.screenshot({ fullPage: opts?.fullPage });
107
116
  const out = opts?.path ? path_1.default.resolve(baseDir, opts.path) : undefined;
@@ -122,6 +131,10 @@ function renderEnvelope(env) {
122
131
  else {
123
132
  lines.push(`Flow failed in ${env.elapsedMs}ms: ${env.error}`);
124
133
  }
134
+ if (env.pageErrors?.length) {
135
+ lines.push('', `Page errors (${env.pageErrors.length}):`);
136
+ lines.push(...env.pageErrors.map(error => ` - ${error.message}`));
137
+ }
125
138
  if (env.logs.length) {
126
139
  lines.push('', 'Logs:');
127
140
  lines.push(...env.logs.map(line => ` ${line}`));
@@ -132,6 +145,15 @@ async function runFlow(launcher, absPath, opts = {}) {
132
145
  const logs = [];
133
146
  const started = Date.now();
134
147
  const restore = captureConsole(logs);
148
+ const pageErrors = [];
149
+ const page = launcher.getPage();
150
+ const onPageError = (error) => {
151
+ pageErrors.push({
152
+ message: error.message,
153
+ stack: error.stack?.split('\n').slice(0, 6).join('\n'),
154
+ });
155
+ };
156
+ page?.on('pageerror', onPageError);
135
157
  let env;
136
158
  try {
137
159
  const fn = await loadFlowScript(absPath);
@@ -145,8 +167,11 @@ async function runFlow(launcher, absPath, opts = {}) {
145
167
  env = { ok: false, logs, error: e.message, elapsedMs: Date.now() - started };
146
168
  }
147
169
  finally {
170
+ page?.off('pageerror', onPageError);
148
171
  restore();
149
172
  }
173
+ if (pageErrors.length)
174
+ env.pageErrors = pageErrors;
150
175
  return {
151
176
  text: opts.json ? JSON.stringify(env, null, 2) : renderEnvelope(env),
152
177
  exitCode: env.ok ? 0 : 1,
@@ -32,10 +32,10 @@ function serializeEvalResult(value) {
32
32
  return '[Depth limit]';
33
33
  seen.add(v);
34
34
  if (Array.isArray(v))
35
- return v.map(item => walk(item, depth + 1));
35
+ return Array.from(v, item => (item === undefined ? null : walk(item, depth + 1)));
36
36
  const out = {};
37
37
  for (const [key, val] of Object.entries(v))
38
- out[key] = walk(val, depth + 1);
38
+ out[key] = val === undefined ? null : walk(val, depth + 1);
39
39
  return out;
40
40
  };
41
41
  return walk(value, 0);
package/dist/help.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
- "global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nSession Management:\n open [url] open browser in session mode (non-blocking)\n close close the browser session\n status show current session status\n resize <width> <height> resize the browser viewport\n eval <expression> evaluate javascript expression on the page (browser context)\n playwright <call> execute playwright api call (node.js context)\n click <selector> click an element with real mouse events (works with custom react components)\n hover <selector> hover an element with real mouse events\n type <selector> <text> type text into an element (fill by default; --sequential for real key events)\n wait-for [selector] wait for a condition: selector state, js predicate, or network idle\n run <file> run a multi-step flow script (default-exports an async function receiving the api object)\n state-import [file] import cookies from netscape format file\n state-save save current browser session state (cookies + localstorage)\n mcp start mcp server (stdio transport) for ai clients\n mcp-install install mcp server config into ai client configs\n mcp-uninstall remove mcp server config from ai client configs\n\nCSS Inspection:\n inspect <selector> inspect the runtime css of a page element\n tree <selector> show dom tree structure\n layout <selector> show ascii layout diagram\n findings <selector> show only issues/warnings/errors\n inject-css <css> inject css into the current page\n\nExport:\n screenshot take a screenshot of the current page\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
2
+ "global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nSession Management:\n open [url] open browser in session mode (non-blocking)\n close close the browser session\n status show current session status\n frames list frames in the current page\n doctor diagnose browser installation and playwright revision mismatch\n scenario <action> <file> run a json scenario suite (regression cases, artifacts, junit)\n resize <width> <height> resize the browser viewport\n eval <expression> evaluate javascript expression on the page (browser context)\n playwright <call> execute playwright api call (node.js context)\n click <selector> click an element with real mouse events (works with custom react components)\n hover <selector> hover an element with real mouse events\n type <selector> <text> type text into an element (fill by default; --sequential for real key events)\n wait-for [selector] wait for a condition: selector state, js predicate, or network idle\n sample <expression> sample a js expression over time and return a timeline with summary stats\n wait-stable <expression> wait until a js expression stops changing (tolerance + stable window)\n run <file> run a multi-step flow script (default-exports an async function receiving the api object)\n state-import [file] import cookies from netscape format file\n state-save save current browser session state (cookies + localstorage)\n state-check validate a saved state file: cookies, expiry, and target domain coverage\n mcp start mcp server (stdio transport) for ai clients\n mcp-install install mcp server config into ai client configs\n mcp-uninstall remove mcp server config from ai client configs\n\nCSS Inspection:\n inspect <selector> inspect the runtime css of a page element\n tree <selector> show dom tree structure\n layout <selector> show ascii layout diagram\n findings <selector> show only issues/warnings/errors\n inject-css <css> inject css into the current page\n\nExport:\n screenshot take a screenshot of the current page\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
3
3
  "commands": {
4
4
  "open": {
5
- "help": "cssprobe-cli open [url]\n\nOpen browser in session mode (non-blocking)\n\nArguments:\n [url] url, file:// path, or local html file path\nOptions:\n --browser browser engine (default chromium)\n --headed show the browser window\n --viewport viewport size as wxh (e.g. 1280x720)\n --state path to saved state file (cookies + localstorage)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
5
+ "help": "cssprobe-cli open [url]\n\nOpen browser in session mode (non-blocking)\n\nArguments:\n [url] url, file:// path, or local html file path\nOptions:\n --browser browser engine (default chromium)\n --headed show the browser window\n --viewport viewport size as wxh (e.g. 1280x720)\n --state path to saved state file (cookies + localstorage)\n --expect-url regex the final page url must match\n --fail-on-login fail when the page redirects to a login url\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
6
6
  "flags": {
7
7
  "browser": "string",
8
8
  "headed": "boolean",
9
9
  "viewport": "string",
10
- "state": "string"
10
+ "state": "string",
11
+ "expect-url": "string",
12
+ "fail-on-login": "boolean"
11
13
  },
12
14
  "args": [
13
15
  "url"
@@ -25,44 +27,80 @@
25
27
  "flags": {},
26
28
  "args": []
27
29
  },
30
+ "frames": {
31
+ "help": "cssprobe-cli frames \n\nList frames in the current page\n\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
32
+ "flags": {},
33
+ "args": []
34
+ },
35
+ "doctor": {
36
+ "help": "cssprobe-cli doctor \n\nDiagnose browser installation and Playwright revision mismatch\n\nOptions:\n --engine browser engine: chromium, firefox, webkit (default: chromium)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
37
+ "flags": {
38
+ "engine": "string"
39
+ },
40
+ "args": []
41
+ },
42
+ "scenario": {
43
+ "help": "cssprobe-cli scenario <action> <file>\n\nRun a JSON scenario suite (regression cases, artifacts, JUnit)\n\nArguments:\n <action> only \"run\" is supported\n <file> scenario suite json path\nOptions:\n --state fallback state file when the suite/case does not set one\n --artifacts artifact directory (default: ~/.cssprobe-cli/scenario-runs/<suite>-<timestamp>)\n --junit write a junit xml report to this path\n --bail stop after the first failing case\n --headed show the browser window\n --browser browser engine (default chromium)\n --viewport viewport size as wxh (default: suite defaults or 1280x720)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
44
+ "flags": {
45
+ "state": "string",
46
+ "artifacts": "string",
47
+ "junit": "string",
48
+ "bail": "boolean",
49
+ "headed": "boolean",
50
+ "browser": "string",
51
+ "viewport": "string"
52
+ },
53
+ "args": [
54
+ "action",
55
+ "file"
56
+ ]
57
+ },
28
58
  "inspect": {
29
- "help": "cssprobe-cli inspect <selector>\n\nInspect the runtime CSS of a page element\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --json output structured json instead of markdown\n --brief compact output: tree sketch + warnings/errors only\n --layout ascii layout diagram showing element positions and sizes\n --depth dom tree depth (default: auto, max 20)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
59
+ "help": "cssprobe-cli inspect <selector>\n\nInspect the runtime CSS of a page element\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --json output structured json instead of markdown\n --brief compact output: tree sketch + warnings/errors only\n --layout ascii layout diagram showing element positions and sizes\n --depth dom tree depth (default: auto, max 20)\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
30
60
  "flags": {
31
61
  "json": "boolean",
32
62
  "brief": "boolean",
33
63
  "layout": "boolean",
34
- "depth": "string"
64
+ "depth": "string",
65
+ "frame": "string"
35
66
  },
36
67
  "args": [
37
68
  "selector"
38
69
  ]
39
70
  },
40
71
  "tree": {
41
- "help": "cssprobe-cli tree <selector>\n\nShow DOM tree structure\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --depth tree depth (default: auto, max 20)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
72
+ "help": "cssprobe-cli tree <selector>\n\nShow DOM tree structure\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --depth tree depth (default: auto, max 20)\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
42
73
  "flags": {
43
- "depth": "string"
74
+ "depth": "string",
75
+ "frame": "string"
44
76
  },
45
77
  "args": [
46
78
  "selector"
47
79
  ]
48
80
  },
49
81
  "layout": {
50
- "help": "cssprobe-cli layout <selector>\n\nShow ASCII layout diagram\n\nArguments:\n <selector> css selector for the root element\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
51
- "flags": {},
82
+ "help": "cssprobe-cli layout <selector>\n\nShow ASCII layout diagram\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
83
+ "flags": {
84
+ "frame": "string"
85
+ },
52
86
  "args": [
53
87
  "selector"
54
88
  ]
55
89
  },
56
90
  "findings": {
57
- "help": "cssprobe-cli findings <selector>\n\nShow only issues/warnings/errors\n\nArguments:\n <selector> css selector for the root element\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
58
- "flags": {},
91
+ "help": "cssprobe-cli findings <selector>\n\nShow only issues/warnings/errors\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
92
+ "flags": {
93
+ "frame": "string"
94
+ },
59
95
  "args": [
60
96
  "selector"
61
97
  ]
62
98
  },
63
99
  "inject-css": {
64
- "help": "cssprobe-cli inject-css <css>\n\nInject CSS into the current page\n\nArguments:\n <css> css code to inject\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
65
- "flags": {},
100
+ "help": "cssprobe-cli inject-css <css>\n\nInject CSS into the current page\n\nArguments:\n <css> css code to inject\nOptions:\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
101
+ "flags": {
102
+ "frame": "string"
103
+ },
66
104
  "args": [
67
105
  "css"
68
106
  ]
@@ -76,14 +114,16 @@
76
114
  ]
77
115
  },
78
116
  "eval": {
79
- "help": "cssprobe-cli eval <expression>\n\nEvaluate JavaScript expression on the page (browser context)\n\nArguments:\n <expression> javascript expression to evaluate\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
80
- "flags": {},
117
+ "help": "cssprobe-cli eval <expression>\n\nEvaluate JavaScript expression on the page (browser context)\n\nArguments:\n <expression> javascript expression, or @file / - (stdin) for long scripts\nOptions:\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
118
+ "flags": {
119
+ "frame": "string"
120
+ },
81
121
  "args": [
82
122
  "expression"
83
123
  ]
84
124
  },
85
125
  "playwright": {
86
- "help": "cssprobe-cli playwright <call>\n\nExecute Playwright API call (Node.js context)\n\nArguments:\n <call> playwright api call, e.g. \"page.setviewportsize({width: 1280, height: 720})\"\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
126
+ "help": "cssprobe-cli playwright <call>\n\nExecute Playwright API call (Node.js context)\n\nArguments:\n <call> playwright api call, or @file / - (stdin); e.g. \"page.setviewportsize({width: 1280, height: 720})\"\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
87
127
  "flags": {},
88
128
  "args": [
89
129
  "call"
@@ -135,17 +175,45 @@
135
175
  ]
136
176
  },
137
177
  "wait-for": {
138
- "help": "cssprobe-cli wait-for [selector]\n\nWait for a condition: selector state, JS predicate, or network idle\n\nArguments:\n [selector] css selector to wait for\nOptions:\n --state selector state to wait for (default visible)\n --js js predicate, e.g. \"() => document.queryselector('.x')?.offsetheight > 100\"\n --network-idle wait until the page is network-idle\n --timeout timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
178
+ "help": "cssprobe-cli wait-for [selector]\n\nWait for a condition: selector state, JS predicate, or network idle\n\nArguments:\n [selector] css selector to wait for\nOptions:\n --state selector state to wait for (default visible)\n --js js predicate, e.g. \"() => document.queryselector('.x')?.offsetheight > 100\"\n --network-idle wait until the page is network-idle\n --frame wait inside a frame: index, name, or url substring\n --timeout timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
139
179
  "flags": {
140
180
  "state": "string",
141
181
  "js": "string",
142
182
  "network-idle": "boolean",
183
+ "frame": "string",
143
184
  "timeout": "string"
144
185
  },
145
186
  "args": [
146
187
  "selector"
147
188
  ]
148
189
  },
190
+ "sample": {
191
+ "help": "cssprobe-cli sample <expression>\n\nSample a JS expression over time and return a timeline with summary stats\n\nArguments:\n <expression> js expression sampled on an interval\nOptions:\n --interval sampling interval in ms (default 250)\n --duration total sampling duration in ms (default 5000)\n --key dot path for numeric extraction, e.g. \"top\" or \"box.y\"\n --tail-from tail starts at this elapsed ms (default: last 5000ms)\n --ignore-errors record evaluation errors as sample points instead of aborting\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
192
+ "flags": {
193
+ "interval": "string",
194
+ "duration": "string",
195
+ "key": "string",
196
+ "tail-from": "string",
197
+ "ignore-errors": "boolean",
198
+ "frame": "string"
199
+ },
200
+ "args": [
201
+ "expression"
202
+ ]
203
+ },
204
+ "wait-stable": {
205
+ "help": "cssprobe-cli wait-stable <expression>\n\nWait until a JS expression stops changing (tolerance + stable window)\n\nArguments:\n <expression> js expression polled until stable\nOptions:\n --tolerance absolute tolerance for numeric values (default 0)\n --window stable window in ms (default 1500)\n --timeout timeout in ms (default 30000)\n --interval polling interval in ms (default 200)\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
206
+ "flags": {
207
+ "tolerance": "string",
208
+ "window": "string",
209
+ "timeout": "string",
210
+ "interval": "string",
211
+ "frame": "string"
212
+ },
213
+ "args": [
214
+ "expression"
215
+ ]
216
+ },
149
217
  "run": {
150
218
  "help": "cssprobe-cli run <file>\n\nRun a multi-step flow script (default-exports an async function receiving the API object)\n\nArguments:\n <file> flow script path (.js / .mjs / .cjs)\nOptions:\n --timeout whole-script timeout in ms (default 300000, 0 = unlimited)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
151
219
  "flags": {
@@ -173,6 +241,14 @@
173
241
  },
174
242
  "args": []
175
243
  },
244
+ "state-check": {
245
+ "help": "cssprobe-cli state-check \n\nValidate a saved state file: cookies, expiry, and target domain coverage\n\nOptions:\n --state state file path (default: ~/.cssprobe-cli/states/session.json)\n --url target url whose hostname must be covered by the cookies\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
246
+ "flags": {
247
+ "state": "string",
248
+ "url": "string"
249
+ },
250
+ "args": []
251
+ },
176
252
  "config-show": {
177
253
  "help": "cssprobe-cli config-show \n\nShow current configuration\n\nOptions:\n --profile profile name",
178
254
  "flags": {
@@ -256,7 +332,9 @@
256
332
  },
257
333
  "booleanOptions": [
258
334
  "headed",
335
+ "fail-on-login",
259
336
  "all",
337
+ "bail",
260
338
  "json",
261
339
  "brief",
262
340
  "layout",
@@ -265,6 +343,7 @@
265
343
  "sequential",
266
344
  "clear",
267
345
  "network-idle",
346
+ "ignore-errors",
268
347
  "local"
269
348
  ]
270
349
  }
@@ -0,0 +1,13 @@
1
+ import type { AssertStep } from './schema';
2
+ export interface AssertionState {
3
+ url: string;
4
+ title: string;
5
+ frames: number;
6
+ }
7
+ export interface AssertionContext {
8
+ vars: Record<string, unknown>;
9
+ state: AssertionState;
10
+ globals: Record<string, string>;
11
+ }
12
+ export declare function resolvePath(value: unknown, path?: string): unknown;
13
+ export declare function evaluateAssertion(assertion: AssertStep, context: AssertionContext): string | null;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvePath = resolvePath;
4
+ exports.evaluateAssertion = evaluateAssertion;
5
+ function resolvePath(value, path) {
6
+ if (!path)
7
+ return value;
8
+ let current = value;
9
+ for (const part of path.split('.')) {
10
+ if (current === null || current === undefined)
11
+ return undefined;
12
+ current = current[part];
13
+ }
14
+ return current;
15
+ }
16
+ function deepEqual(a, b) {
17
+ if (a === b)
18
+ return true;
19
+ if (typeof a !== typeof b)
20
+ return false;
21
+ if (a === null || b === null)
22
+ return false;
23
+ if (Array.isArray(a) || Array.isArray(b)) {
24
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
25
+ return false;
26
+ return a.every((item, index) => deepEqual(item, b[index]));
27
+ }
28
+ if (typeof a === 'object' && typeof b === 'object') {
29
+ const aKeys = Object.keys(a).sort();
30
+ const bKeys = Object.keys(b).sort();
31
+ if (aKeys.length !== bKeys.length || aKeys.some((key, index) => key !== bKeys[index]))
32
+ return false;
33
+ return aKeys.every(key => deepEqual(a[key], b[key]));
34
+ }
35
+ return false;
36
+ }
37
+ function label(assertion) {
38
+ return assertion.label ?? 'assert';
39
+ }
40
+ function evaluateAssertion(assertion, context) {
41
+ const errors = [];
42
+ if (assertion.url) {
43
+ let regex;
44
+ try {
45
+ regex = new RegExp(assertion.url);
46
+ }
47
+ catch {
48
+ return `${label(assertion)}: invalid url pattern ${assertion.url}`;
49
+ }
50
+ if (!regex.test(context.state.url)) {
51
+ errors.push(`url ${JSON.stringify(context.state.url)} does not match ${assertion.url}`);
52
+ }
53
+ }
54
+ for (const [name, expected] of Object.entries(assertion.globals ?? {})) {
55
+ const actual = context.globals[name] ?? 'undefined';
56
+ if (actual !== expected) {
57
+ errors.push(`global ${name}: expected ${expected}, received ${actual}`);
58
+ }
59
+ }
60
+ for (const name of assertion.absent ?? []) {
61
+ const actual = context.globals[name] ?? 'undefined';
62
+ if (actual !== 'undefined') {
63
+ errors.push(`global ${name}: expected absent, received ${actual}`);
64
+ }
65
+ }
66
+ const hasValueCheck = ['eq', 'ne', 'match', 'within', 'min', 'max'].some(key => key in assertion);
67
+ if (!hasValueCheck) {
68
+ return errors.length ? `${label(assertion)}: ${errors.join('; ')}` : null;
69
+ }
70
+ let value;
71
+ if (assertion.on) {
72
+ if (assertion.on === 'state') {
73
+ value = context.state;
74
+ }
75
+ else if (assertion.on in context.vars) {
76
+ value = context.vars[assertion.on];
77
+ }
78
+ else {
79
+ return `${label(assertion)}: unknown variable "${assertion.on}"`;
80
+ }
81
+ }
82
+ else {
83
+ value = context.state;
84
+ }
85
+ value = resolvePath(value, assertion.path);
86
+ if ('eq' in assertion && !deepEqual(value, assertion.eq)) {
87
+ errors.push(`expected ${JSON.stringify(assertion.eq)}, received ${JSON.stringify(value)}`);
88
+ }
89
+ if ('ne' in assertion && deepEqual(value, assertion.ne)) {
90
+ errors.push(`expected value different from ${JSON.stringify(assertion.ne)}`);
91
+ }
92
+ if (assertion.match !== undefined) {
93
+ const actual = typeof value === 'string' ? value : JSON.stringify(value);
94
+ if (!new RegExp(assertion.match).test(actual)) {
95
+ errors.push(`expected ${JSON.stringify(actual)} to match ${assertion.match}`);
96
+ }
97
+ }
98
+ if (assertion.within) {
99
+ if (typeof value !== 'number' || Math.abs(value - assertion.within.expected) > assertion.within.tolerance) {
100
+ errors.push(`expected ${assertion.within.expected} ±${assertion.within.tolerance}, received ${JSON.stringify(value)}`);
101
+ }
102
+ }
103
+ if (assertion.min !== undefined && (typeof value !== 'number' || value < assertion.min)) {
104
+ errors.push(`expected >= ${assertion.min}, received ${JSON.stringify(value)}`);
105
+ }
106
+ if (assertion.max !== undefined && (typeof value !== 'number' || value > assertion.max)) {
107
+ errors.push(`expected <= ${assertion.max}, received ${JSON.stringify(value)}`);
108
+ }
109
+ return errors.length ? `${label(assertion)}: ${errors.join('; ')}` : null;
110
+ }
111
+ //# sourceMappingURL=assertions.js.map
@@ -0,0 +1,13 @@
1
+ import type { CaseResult } from './schema';
2
+ export declare function slugify(value: string): string;
3
+ export interface ScenarioSummary {
4
+ total: number;
5
+ passed: number;
6
+ failed: number;
7
+ durationMs: number;
8
+ ok: boolean;
9
+ }
10
+ export declare function summarize(results: CaseResult[]): ScenarioSummary;
11
+ export declare function formatConsole(suiteName: string, results: CaseResult[]): string;
12
+ export declare function escapeXml(value: string): string;
13
+ export declare function toJunit(suiteName: string, results: CaseResult[]): string;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.slugify = slugify;
4
+ exports.summarize = summarize;
5
+ exports.formatConsole = formatConsole;
6
+ exports.escapeXml = escapeXml;
7
+ exports.toJunit = toJunit;
8
+ function slugify(value) {
9
+ return value
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, '-')
12
+ .replace(/^-+|-+$/g, '')
13
+ .slice(0, 60) || 'case';
14
+ }
15
+ function summarize(results) {
16
+ const passed = results.filter(result => result.ok).length;
17
+ const durationMs = results.reduce((total, result) => total + result.durationMs, 0);
18
+ return {
19
+ total: results.length,
20
+ passed,
21
+ failed: results.length - passed,
22
+ durationMs,
23
+ ok: passed === results.length,
24
+ };
25
+ }
26
+ function formatConsole(suiteName, results) {
27
+ const summary = summarize(results);
28
+ const lines = [];
29
+ for (const result of results) {
30
+ const marker = result.ok ? 'PASS' : result.skipped ? 'SKIP' : 'FAIL';
31
+ lines.push(`${marker} ${result.name} (${result.durationMs}ms)`);
32
+ if (result.error)
33
+ lines.push(` ${result.error}`);
34
+ for (const step of result.steps) {
35
+ if (!step.ok)
36
+ lines.push(` step ${step.index + 1} (${step.action}): ${step.error}`);
37
+ }
38
+ if (result.artifactsDir)
39
+ lines.push(` artifacts: ${result.artifactsDir}`);
40
+ }
41
+ lines.push('');
42
+ lines.push(`${suiteName}: ${summary.passed}/${summary.total} passed, ${summary.failed} failed in ${summary.durationMs}ms`);
43
+ return lines.join('\n');
44
+ }
45
+ function escapeXml(value) {
46
+ return value
47
+ .replace(/&/g, '&amp;')
48
+ .replace(/</g, '&lt;')
49
+ .replace(/>/g, '&gt;')
50
+ .replace(/"/g, '&quot;')
51
+ .replace(/'/g, '&apos;');
52
+ }
53
+ function toJunit(suiteName, results) {
54
+ const summary = summarize(results);
55
+ const cases = results
56
+ .map(result => {
57
+ const time = (result.durationMs / 1000).toFixed(3);
58
+ const failureDetails = [
59
+ result.error,
60
+ ...result.steps.filter(step => !step.ok).map(step => `step ${step.index + 1} (${step.action}): ${step.error}`),
61
+ ]
62
+ .filter(Boolean)
63
+ .join('\n');
64
+ const body = result.ok
65
+ ? ''
66
+ : `<failure message="${escapeXml(result.error ?? 'scenario case failed')}">${escapeXml(failureDetails)}</failure>`;
67
+ return ` <testcase classname="${escapeXml(suiteName)}" name="${escapeXml(result.name)}" time="${time}">${body}</testcase>`;
68
+ })
69
+ .join('\n');
70
+ return [
71
+ '<?xml version="1.0" encoding="UTF-8"?>',
72
+ `<testsuites tests="${summary.total}" failures="${summary.failed}" time="${(summary.durationMs / 1000).toFixed(3)}">`,
73
+ ` <testsuite name="${escapeXml(suiteName)}" tests="${summary.total}" failures="${summary.failed}" time="${(summary.durationMs / 1000).toFixed(3)}">`,
74
+ cases,
75
+ ' </testsuite>',
76
+ '</testsuites>',
77
+ ].join('\n');
78
+ }
79
+ //# sourceMappingURL=reporters.js.map
@@ -0,0 +1,10 @@
1
+ import { type ScenarioSummary } from './reporters';
2
+ import type { CaseResult, ScenarioRunOptions, Suite } from './schema';
3
+ export interface ScenarioRunResult {
4
+ suite: string;
5
+ ok: boolean;
6
+ summary: ScenarioSummary;
7
+ results: CaseResult[];
8
+ artifactsDir: string;
9
+ }
10
+ export declare function runScenario(suite: Suite, suitePath: string, options?: ScenarioRunOptions): Promise<ScenarioRunResult>;