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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface StorageCookie {
|
|
2
|
+
name: string;
|
|
3
|
+
domain: string;
|
|
4
|
+
expires?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface StateReport {
|
|
7
|
+
cookies: number;
|
|
8
|
+
domains: string[];
|
|
9
|
+
expired: string[];
|
|
10
|
+
sessionCookies: number;
|
|
11
|
+
targetMatches: boolean | null;
|
|
12
|
+
warnings: string[];
|
|
13
|
+
ok: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function analyzeState(state: {
|
|
16
|
+
cookies?: StorageCookie[];
|
|
17
|
+
}, targetUrl?: string, nowMs?: number): StateReport;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeState = analyzeState;
|
|
4
|
+
function normalizeDomain(domain) {
|
|
5
|
+
return domain.replace(/^\./, '').toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
function analyzeState(state, targetUrl, nowMs = Date.now()) {
|
|
8
|
+
const cookies = state.cookies ?? [];
|
|
9
|
+
const expired = [];
|
|
10
|
+
let sessionCookies = 0;
|
|
11
|
+
for (const cookie of cookies) {
|
|
12
|
+
if (cookie.expires === undefined || cookie.expires <= 0) {
|
|
13
|
+
sessionCookies++;
|
|
14
|
+
}
|
|
15
|
+
else if (cookie.expires * 1000 <= nowMs) {
|
|
16
|
+
expired.push(cookie.name);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const domains = Array.from(new Set(cookies.map(cookie => normalizeDomain(cookie.domain)))).sort();
|
|
20
|
+
let targetMatches = null;
|
|
21
|
+
if (targetUrl) {
|
|
22
|
+
try {
|
|
23
|
+
const host = new URL(targetUrl).hostname.toLowerCase();
|
|
24
|
+
targetMatches = cookies.some(cookie => {
|
|
25
|
+
const domain = normalizeDomain(cookie.domain);
|
|
26
|
+
return host === domain || host.endsWith(`.${domain}`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error(`Invalid --url value: ${targetUrl}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const warnings = [];
|
|
34
|
+
if (!cookies.length)
|
|
35
|
+
warnings.push('no cookies in state file');
|
|
36
|
+
if (expired.length)
|
|
37
|
+
warnings.push(`expired cookies: ${expired.join(', ')}`);
|
|
38
|
+
if (targetMatches === false)
|
|
39
|
+
warnings.push(`no cookies match ${targetUrl}`);
|
|
40
|
+
const ok = cookies.length > 0 && expired.length === 0 && targetMatches !== false;
|
|
41
|
+
return { cookies: cookies.length, domains, expired, sessionCookies, targetMatches, warnings, ok };
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=state-check.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.looksLikeLoginUrl = looksLikeLoginUrl;
|
|
4
|
+
exports.matchesUrlPattern = matchesUrlPattern;
|
|
5
|
+
function looksLikeLoginUrl(raw) {
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(raw);
|
|
8
|
+
return /\/(?:s\/)?login(?:\/|$)/.test(url.pathname);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function matchesUrlPattern(pattern, raw) {
|
|
15
|
+
let regex;
|
|
16
|
+
try {
|
|
17
|
+
regex = new RegExp(pattern);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error(`Invalid --expect-url pattern: ${pattern}`);
|
|
21
|
+
}
|
|
22
|
+
return regex.test(raw);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=url-guard.js.map
|
package/dist/daemon/daemon.js
CHANGED
|
@@ -20,6 +20,8 @@ const dom_tree_1 = require("../engine/renderers/dom-tree");
|
|
|
20
20
|
const interactions_1 = require("../browser/interactions");
|
|
21
21
|
const evalSerializer_1 = require("../engine/evalSerializer");
|
|
22
22
|
const flowRunner_1 = require("./flowRunner");
|
|
23
|
+
const frames_1 = require("../browser/frames");
|
|
24
|
+
const sampler_1 = require("../browser/sampler");
|
|
23
25
|
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
24
26
|
async function startDaemon(sessionName, clientInfo, options) {
|
|
25
27
|
const sessionConfig = createSessionConfig(clientInfo, sessionName, options);
|
|
@@ -77,6 +79,18 @@ async function executeCommand(launcher, args) {
|
|
|
77
79
|
const command = args._[0];
|
|
78
80
|
const params = args._.slice(1);
|
|
79
81
|
switch (command) {
|
|
82
|
+
case 'frames': {
|
|
83
|
+
const frames = (0, frames_1.describeFrames)(requirePage(launcher));
|
|
84
|
+
if (args.json)
|
|
85
|
+
return { text: JSON.stringify(frames, null, 2) };
|
|
86
|
+
return { text: (0, frames_1.formatFrames)(frames) };
|
|
87
|
+
}
|
|
88
|
+
case 'page-info': {
|
|
89
|
+
const page = requirePage(launcher);
|
|
90
|
+
return {
|
|
91
|
+
text: JSON.stringify({ url: page.url(), title: await page.title(), frames: (0, frames_1.describeFrames)(page).length }, null, 2),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
80
94
|
case 'goto': {
|
|
81
95
|
const url = params[0] || 'about:blank';
|
|
82
96
|
await launcher.open(url);
|
|
@@ -91,7 +105,7 @@ async function executeCommand(launcher, args) {
|
|
|
91
105
|
downDepth: args.depth || 6,
|
|
92
106
|
maxNodes: 60,
|
|
93
107
|
upTo: 'html',
|
|
94
|
-
});
|
|
108
|
+
}, (0, frames_1.resolveFrame)(requirePage(launcher), args.frame));
|
|
95
109
|
const findings = (0, analyzer_1.analyze)(snapshot);
|
|
96
110
|
if (args.json) {
|
|
97
111
|
return { text: JSON.stringify((0, renderer_1.renderJSON)(snapshot, findings, args.brief), null, 2) };
|
|
@@ -109,7 +123,7 @@ async function executeCommand(launcher, args) {
|
|
|
109
123
|
downDepth: args.depth || 6,
|
|
110
124
|
maxNodes: 60,
|
|
111
125
|
upTo: 'html',
|
|
112
|
-
});
|
|
126
|
+
}, (0, frames_1.resolveFrame)(requirePage(launcher), args.frame));
|
|
113
127
|
// Render tree only
|
|
114
128
|
const lines = [];
|
|
115
129
|
lines.push(`## DOM tree (${snapshot.downDepth} levels deep)`);
|
|
@@ -127,7 +141,7 @@ async function executeCommand(launcher, args) {
|
|
|
127
141
|
downDepth: 6,
|
|
128
142
|
maxNodes: 60,
|
|
129
143
|
upTo: 'html',
|
|
130
|
-
});
|
|
144
|
+
}, (0, frames_1.resolveFrame)(requirePage(launcher), args.frame));
|
|
131
145
|
const findings = (0, analyzer_1.analyze)(snapshot);
|
|
132
146
|
return { text: (0, renderer_1.renderMarkdown)(snapshot, findings, false, true) };
|
|
133
147
|
}
|
|
@@ -140,7 +154,7 @@ async function executeCommand(launcher, args) {
|
|
|
140
154
|
downDepth: 6,
|
|
141
155
|
maxNodes: 60,
|
|
142
156
|
upTo: 'html',
|
|
143
|
-
});
|
|
157
|
+
}, (0, frames_1.resolveFrame)(requirePage(launcher), args.frame));
|
|
144
158
|
const findings = (0, analyzer_1.analyze)(snapshot);
|
|
145
159
|
const important = findings.filter(f => f.level === 'warning' || f.level === 'error');
|
|
146
160
|
if (important.length === 0) {
|
|
@@ -155,11 +169,9 @@ async function executeCommand(launcher, args) {
|
|
|
155
169
|
const css = params[0];
|
|
156
170
|
if (!css)
|
|
157
171
|
throw new Error('CSS is required for inject-css command');
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
await page.addStyleTag({ content: css });
|
|
162
|
-
return { text: JSON.stringify({ success: true }) };
|
|
172
|
+
const frame = (0, frames_1.resolveFrame)(requirePage(launcher), args.frame);
|
|
173
|
+
await frame.addStyleTag({ content: css });
|
|
174
|
+
return { text: JSON.stringify({ success: true, frame: frame.name() || frame.url() }) };
|
|
163
175
|
}
|
|
164
176
|
case 'screenshot': {
|
|
165
177
|
const page = launcher.getPage();
|
|
@@ -173,17 +185,21 @@ async function executeCommand(launcher, args) {
|
|
|
173
185
|
const expression = params[0];
|
|
174
186
|
if (!expression)
|
|
175
187
|
throw new Error('Expression is required for eval command');
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
|
|
179
|
-
const handle = await page.evaluateHandle(expression);
|
|
188
|
+
const frame = (0, frames_1.resolveFrame)(requirePage(launcher), args.frame);
|
|
189
|
+
const handle = await frame.evaluateHandle(expression);
|
|
180
190
|
try {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
191
|
+
const resolved = await handle.evaluateHandle((value) => Promise.resolve(value));
|
|
192
|
+
try {
|
|
193
|
+
const result = await resolved.evaluate(evalSerializer_1.serializeEvalResult);
|
|
194
|
+
if (typeof result === 'string')
|
|
195
|
+
return { text: result };
|
|
196
|
+
if (result === undefined)
|
|
197
|
+
return { text: 'undefined' };
|
|
198
|
+
return { text: JSON.stringify(result, null, 2) };
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
await resolved.dispose();
|
|
202
|
+
}
|
|
187
203
|
}
|
|
188
204
|
finally {
|
|
189
205
|
await handle.dispose();
|
|
@@ -282,6 +298,38 @@ async function executeCommand(launcher, args) {
|
|
|
282
298
|
const label = target.kind === 'selector' ? `${selector} (${target.state})` : (0, interactions_1.describeWaitTarget)(target);
|
|
283
299
|
return { text: `Wait satisfied: ${label} (${elapsedMs}ms)` };
|
|
284
300
|
}
|
|
301
|
+
case 'sample': {
|
|
302
|
+
const expression = params[0];
|
|
303
|
+
if (!expression)
|
|
304
|
+
throw new Error('Expression is required for sample command');
|
|
305
|
+
const interval = args.interval ?? 250;
|
|
306
|
+
const duration = args.duration ?? 5000;
|
|
307
|
+
if (duration <= 0)
|
|
308
|
+
throw new Error('--duration must be greater than 0');
|
|
309
|
+
if (interval <= 0)
|
|
310
|
+
throw new Error('--interval must be greater than 0');
|
|
311
|
+
const frame = (0, frames_1.resolveFrame)(requirePage(launcher), args.frame);
|
|
312
|
+
const points = await (0, sampler_1.sampleFrame)(frame, expression, {
|
|
313
|
+
intervalMs: interval,
|
|
314
|
+
durationMs: duration,
|
|
315
|
+
ignoreErrors: !!args['ignore-errors'],
|
|
316
|
+
});
|
|
317
|
+
const summary = (0, sampler_1.summarizeSamples)(points, args.key, 5000, args['tail-from']);
|
|
318
|
+
return { text: JSON.stringify({ expression, intervalMs: interval, summary, series: points }, null, 2) };
|
|
319
|
+
}
|
|
320
|
+
case 'wait-stable': {
|
|
321
|
+
const expression = params[0];
|
|
322
|
+
if (!expression)
|
|
323
|
+
throw new Error('Expression is required for wait-stable command');
|
|
324
|
+
const frame = (0, frames_1.resolveFrame)(requirePage(launcher), args.frame);
|
|
325
|
+
const result = await (0, sampler_1.waitForStableValue)(frame, expression, {
|
|
326
|
+
tolerance: args.tolerance,
|
|
327
|
+
windowMs: args.window,
|
|
328
|
+
timeoutMs: args.timeout,
|
|
329
|
+
intervalMs: args.interval,
|
|
330
|
+
});
|
|
331
|
+
return { text: JSON.stringify({ ok: true, expression, ...result }, null, 2) };
|
|
332
|
+
}
|
|
285
333
|
case 'run': {
|
|
286
334
|
const absPath = params[0];
|
|
287
335
|
if (!absPath)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface Matchers {
|
|
2
|
+
toBe(expected: unknown, label?: string): void;
|
|
3
|
+
toBeWithin(expected: number, tolerance: number, label?: string): void;
|
|
4
|
+
toMatch(pattern: RegExp | string, label?: string): void;
|
|
5
|
+
toHaveHash(hash: string, label?: string): void;
|
|
6
|
+
toBeSelected(index: number, label?: string): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function expect(actual: unknown): Matchers;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.expect = expect;
|
|
4
|
+
function labelSuffix(label) {
|
|
5
|
+
return label ? ` (${label})` : '';
|
|
6
|
+
}
|
|
7
|
+
function fail(label, message) {
|
|
8
|
+
throw new Error(`expect${labelSuffix(label)}: ${message}`);
|
|
9
|
+
}
|
|
10
|
+
function expect(actual) {
|
|
11
|
+
return {
|
|
12
|
+
toBe(expected, label) {
|
|
13
|
+
if (actual !== expected) {
|
|
14
|
+
fail(label, `expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}`);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
toBeWithin(expected, tolerance, label) {
|
|
18
|
+
if (typeof actual !== 'number' || !Number.isFinite(actual)) {
|
|
19
|
+
fail(label, `expected a finite number near ${expected}, received ${JSON.stringify(actual)}`);
|
|
20
|
+
}
|
|
21
|
+
if (Math.abs(actual - expected) > tolerance) {
|
|
22
|
+
fail(label, `expected ${expected} ±${tolerance}, received ${actual}`);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
toMatch(pattern, label) {
|
|
26
|
+
if (typeof actual !== 'string') {
|
|
27
|
+
fail(label, `expected a string, received ${JSON.stringify(actual)}`);
|
|
28
|
+
}
|
|
29
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
30
|
+
if (!regex.test(actual)) {
|
|
31
|
+
fail(label, `expected ${JSON.stringify(actual)} to match ${regex}`);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
toHaveHash(hash, label) {
|
|
35
|
+
const expected = hash.startsWith('#') ? hash : `#${hash}`;
|
|
36
|
+
if (actual !== expected) {
|
|
37
|
+
fail(label, `expected hash ${expected}, received ${JSON.stringify(actual)}`);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
toBeSelected(index, label) {
|
|
41
|
+
if (actual !== index) {
|
|
42
|
+
fail(label, `expected selected index ${index}, received ${JSON.stringify(actual)}`);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=expect.js.map
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import type { Page } from 'playwright-core';
|
|
1
|
+
import type { BrowserContext, Page } from 'playwright-core';
|
|
2
2
|
import type { BrowserLauncher } from '../browser/launcher';
|
|
3
3
|
import { type TargetResult, type TypeOptions, type WaitState } from '../browser/interactions';
|
|
4
4
|
import type { Finding, Snapshot } from '../engine/types';
|
|
5
|
+
import { type FrameInfo } from '../browser/frames';
|
|
6
|
+
import { type Matchers } from './expect';
|
|
7
|
+
export interface FlowPageError {
|
|
8
|
+
message: string;
|
|
9
|
+
stack?: string;
|
|
10
|
+
}
|
|
5
11
|
export interface FlowEnvelope {
|
|
6
12
|
ok: boolean;
|
|
7
13
|
logs: string[];
|
|
8
14
|
result?: unknown;
|
|
9
15
|
error?: string;
|
|
10
16
|
elapsedMs: number;
|
|
17
|
+
pageErrors?: FlowPageError[];
|
|
11
18
|
}
|
|
12
19
|
export interface WaitForOptions {
|
|
13
20
|
state?: WaitState;
|
|
@@ -20,6 +27,10 @@ export interface WaitOutcome {
|
|
|
20
27
|
}
|
|
21
28
|
export interface FlowApi {
|
|
22
29
|
page: Page;
|
|
30
|
+
context: BrowserContext;
|
|
31
|
+
frames(): FrameInfo[];
|
|
32
|
+
newPage(): Promise<Page>;
|
|
33
|
+
expect(actual: unknown): Matchers;
|
|
23
34
|
click(selector: string, opts?: {
|
|
24
35
|
nth?: number;
|
|
25
36
|
force?: boolean;
|
|
@@ -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
|
|
@@ -102,6 +104,10 @@ function buildFlowApi(launcher, logs, baseDir) {
|
|
|
102
104
|
return (0, renderer_1.renderMarkdown)(snapshot, findings);
|
|
103
105
|
return { snapshot, findings };
|
|
104
106
|
},
|
|
107
|
+
context: page.context(),
|
|
108
|
+
frames: () => (0, frames_1.describeFrames)(page),
|
|
109
|
+
newPage: () => page.context().newPage(),
|
|
110
|
+
expect: (actual) => (0, expect_1.expect)(actual),
|
|
105
111
|
screenshot: async (opts) => {
|
|
106
112
|
const buffer = await page.screenshot({ fullPage: opts?.fullPage });
|
|
107
113
|
const out = opts?.path ? path_1.default.resolve(baseDir, opts.path) : undefined;
|
|
@@ -122,6 +128,10 @@ function renderEnvelope(env) {
|
|
|
122
128
|
else {
|
|
123
129
|
lines.push(`Flow failed in ${env.elapsedMs}ms: ${env.error}`);
|
|
124
130
|
}
|
|
131
|
+
if (env.pageErrors?.length) {
|
|
132
|
+
lines.push('', `Page errors (${env.pageErrors.length}):`);
|
|
133
|
+
lines.push(...env.pageErrors.map(error => ` - ${error.message}`));
|
|
134
|
+
}
|
|
125
135
|
if (env.logs.length) {
|
|
126
136
|
lines.push('', 'Logs:');
|
|
127
137
|
lines.push(...env.logs.map(line => ` ${line}`));
|
|
@@ -132,6 +142,15 @@ async function runFlow(launcher, absPath, opts = {}) {
|
|
|
132
142
|
const logs = [];
|
|
133
143
|
const started = Date.now();
|
|
134
144
|
const restore = captureConsole(logs);
|
|
145
|
+
const pageErrors = [];
|
|
146
|
+
const page = launcher.getPage();
|
|
147
|
+
const onPageError = (error) => {
|
|
148
|
+
pageErrors.push({
|
|
149
|
+
message: error.message,
|
|
150
|
+
stack: error.stack?.split('\n').slice(0, 6).join('\n'),
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
page?.on('pageerror', onPageError);
|
|
135
154
|
let env;
|
|
136
155
|
try {
|
|
137
156
|
const fn = await loadFlowScript(absPath);
|
|
@@ -145,8 +164,11 @@ async function runFlow(launcher, absPath, opts = {}) {
|
|
|
145
164
|
env = { ok: false, logs, error: e.message, elapsedMs: Date.now() - started };
|
|
146
165
|
}
|
|
147
166
|
finally {
|
|
167
|
+
page?.off('pageerror', onPageError);
|
|
148
168
|
restore();
|
|
149
169
|
}
|
|
170
|
+
if (pageErrors.length)
|
|
171
|
+
env.pageErrors = pageErrors;
|
|
150
172
|
return {
|
|
151
173
|
text: opts.json ? JSON.stringify(env, null, 2) : renderEnvelope(env),
|
|
152
174
|
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
|
|
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
|
|
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"
|
|
@@ -146,6 +186,33 @@
|
|
|
146
186
|
"selector"
|
|
147
187
|
]
|
|
148
188
|
},
|
|
189
|
+
"sample": {
|
|
190
|
+
"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",
|
|
191
|
+
"flags": {
|
|
192
|
+
"interval": "string",
|
|
193
|
+
"duration": "string",
|
|
194
|
+
"key": "string",
|
|
195
|
+
"tail-from": "string",
|
|
196
|
+
"ignore-errors": "boolean",
|
|
197
|
+
"frame": "string"
|
|
198
|
+
},
|
|
199
|
+
"args": [
|
|
200
|
+
"expression"
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
"wait-stable": {
|
|
204
|
+
"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",
|
|
205
|
+
"flags": {
|
|
206
|
+
"tolerance": "string",
|
|
207
|
+
"window": "string",
|
|
208
|
+
"timeout": "string",
|
|
209
|
+
"interval": "string",
|
|
210
|
+
"frame": "string"
|
|
211
|
+
},
|
|
212
|
+
"args": [
|
|
213
|
+
"expression"
|
|
214
|
+
]
|
|
215
|
+
},
|
|
149
216
|
"run": {
|
|
150
217
|
"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
218
|
"flags": {
|
|
@@ -173,6 +240,14 @@
|
|
|
173
240
|
},
|
|
174
241
|
"args": []
|
|
175
242
|
},
|
|
243
|
+
"state-check": {
|
|
244
|
+
"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",
|
|
245
|
+
"flags": {
|
|
246
|
+
"state": "string",
|
|
247
|
+
"url": "string"
|
|
248
|
+
},
|
|
249
|
+
"args": []
|
|
250
|
+
},
|
|
176
251
|
"config-show": {
|
|
177
252
|
"help": "cssprobe-cli config-show \n\nShow current configuration\n\nOptions:\n --profile profile name",
|
|
178
253
|
"flags": {
|
|
@@ -256,7 +331,9 @@
|
|
|
256
331
|
},
|
|
257
332
|
"booleanOptions": [
|
|
258
333
|
"headed",
|
|
334
|
+
"fail-on-login",
|
|
259
335
|
"all",
|
|
336
|
+
"bail",
|
|
260
337
|
"json",
|
|
261
338
|
"brief",
|
|
262
339
|
"layout",
|
|
@@ -265,6 +342,7 @@
|
|
|
265
342
|
"sequential",
|
|
266
343
|
"clear",
|
|
267
344
|
"network-idle",
|
|
345
|
+
"ignore-errors",
|
|
268
346
|
"local"
|
|
269
347
|
]
|
|
270
348
|
}
|
|
@@ -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;
|