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,43 @@
|
|
|
1
|
+
import type { Frame } from 'playwright-core';
|
|
2
|
+
export interface SamplePoint {
|
|
3
|
+
t: number;
|
|
4
|
+
value: unknown;
|
|
5
|
+
visibility: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SampleSummary {
|
|
9
|
+
samples: number;
|
|
10
|
+
durationMs: number;
|
|
11
|
+
first: SamplePoint | null;
|
|
12
|
+
last: SamplePoint | null;
|
|
13
|
+
min: number | null;
|
|
14
|
+
max: number | null;
|
|
15
|
+
numeric: Array<[number, number]>;
|
|
16
|
+
changes: number;
|
|
17
|
+
hiddenMs: number;
|
|
18
|
+
tail: {
|
|
19
|
+
fromT: number;
|
|
20
|
+
min: number | null;
|
|
21
|
+
max: number | null;
|
|
22
|
+
windowMs: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare function extractNumber(value: unknown, key?: string): number | null;
|
|
26
|
+
export declare function summarizeSamples(points: SamplePoint[], key?: string, tailWindowMs?: number, tailFromMs?: number): SampleSummary;
|
|
27
|
+
export declare function isWithinTolerance(previous: unknown, next: unknown, tolerance: number): boolean;
|
|
28
|
+
export declare function sampleFrame(frame: Frame, expression: string, opts: {
|
|
29
|
+
intervalMs: number;
|
|
30
|
+
durationMs: number;
|
|
31
|
+
ignoreErrors?: boolean;
|
|
32
|
+
}): Promise<SamplePoint[]>;
|
|
33
|
+
export declare function waitForStableValue(frame: Frame, expression: string, opts: {
|
|
34
|
+
toleranceMs?: number;
|
|
35
|
+
windowMs?: number;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
intervalMs?: number;
|
|
38
|
+
tolerance?: number;
|
|
39
|
+
}): Promise<{
|
|
40
|
+
elapsedMs: number;
|
|
41
|
+
value: unknown;
|
|
42
|
+
samples: number;
|
|
43
|
+
}>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractNumber = extractNumber;
|
|
4
|
+
exports.summarizeSamples = summarizeSamples;
|
|
5
|
+
exports.isWithinTolerance = isWithinTolerance;
|
|
6
|
+
exports.sampleFrame = sampleFrame;
|
|
7
|
+
exports.waitForStableValue = waitForStableValue;
|
|
8
|
+
function extractNumber(value, key) {
|
|
9
|
+
if (key) {
|
|
10
|
+
let current = value;
|
|
11
|
+
for (const part of key.split('.')) {
|
|
12
|
+
if (current === null || current === undefined)
|
|
13
|
+
return null;
|
|
14
|
+
current = current[part];
|
|
15
|
+
}
|
|
16
|
+
return typeof current === 'number' && Number.isFinite(current) ? current : null;
|
|
17
|
+
}
|
|
18
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
19
|
+
}
|
|
20
|
+
function summarizeSamples(points, key, tailWindowMs = 5000, tailFromMs) {
|
|
21
|
+
const numeric = [];
|
|
22
|
+
for (const point of points) {
|
|
23
|
+
const n = extractNumber(point.value, key);
|
|
24
|
+
if (n !== null)
|
|
25
|
+
numeric.push([point.t, n]);
|
|
26
|
+
}
|
|
27
|
+
const tailFrom = Math.max(0, tailFromMs ?? (points.length ? points[points.length - 1].t - tailWindowMs : 0));
|
|
28
|
+
const tailNumeric = numeric.filter(([t]) => t >= tailFrom).map(([, value]) => value);
|
|
29
|
+
let changes = 0;
|
|
30
|
+
let hiddenMs = 0;
|
|
31
|
+
for (let i = 0; i < points.length; i++) {
|
|
32
|
+
if (i > 0 && JSON.stringify(points[i].value) !== JSON.stringify(points[i - 1].value))
|
|
33
|
+
changes++;
|
|
34
|
+
if (i < points.length - 1 && points[i].visibility !== 'visible') {
|
|
35
|
+
hiddenMs += points[i + 1].t - points[i].t;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
samples: points.length,
|
|
40
|
+
durationMs: points.length ? points[points.length - 1].t : 0,
|
|
41
|
+
first: points[0] ?? null,
|
|
42
|
+
last: points[points.length - 1] ?? null,
|
|
43
|
+
min: numeric.length ? Math.min(...numeric.map(item => item[1])) : null,
|
|
44
|
+
max: numeric.length ? Math.max(...numeric.map(item => item[1])) : null,
|
|
45
|
+
numeric,
|
|
46
|
+
changes,
|
|
47
|
+
hiddenMs,
|
|
48
|
+
tail: {
|
|
49
|
+
fromT: tailFrom,
|
|
50
|
+
min: tailNumeric.length ? Math.min(...tailNumeric) : null,
|
|
51
|
+
max: tailNumeric.length ? Math.max(...tailNumeric) : null,
|
|
52
|
+
windowMs: tailWindowMs,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isWithinTolerance(previous, next, tolerance) {
|
|
57
|
+
if (typeof previous === 'number' && typeof next === 'number') {
|
|
58
|
+
if (Number.isNaN(previous) || Number.isNaN(next))
|
|
59
|
+
return false;
|
|
60
|
+
return Math.abs(next - previous) <= tolerance;
|
|
61
|
+
}
|
|
62
|
+
return JSON.stringify(previous) === JSON.stringify(next);
|
|
63
|
+
}
|
|
64
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
65
|
+
async function readValue(frame, expression) {
|
|
66
|
+
return frame.evaluate(`(async () => ({ value: await (${expression}), visibility: document.visibilityState }))()`);
|
|
67
|
+
}
|
|
68
|
+
async function sampleFrame(frame, expression, opts) {
|
|
69
|
+
const intervalMs = Math.max(1, Math.floor(opts.intervalMs));
|
|
70
|
+
const started = Date.now();
|
|
71
|
+
const points = [];
|
|
72
|
+
while (Date.now() - started < opts.durationMs) {
|
|
73
|
+
const t = Date.now() - started;
|
|
74
|
+
try {
|
|
75
|
+
const { value, visibility } = await readValue(frame, expression);
|
|
76
|
+
points.push({ t, value, visibility });
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (!opts.ignoreErrors)
|
|
80
|
+
throw error;
|
|
81
|
+
points.push({ t, value: null, visibility: 'unknown', error: error.message });
|
|
82
|
+
}
|
|
83
|
+
await sleep(intervalMs);
|
|
84
|
+
}
|
|
85
|
+
return points;
|
|
86
|
+
}
|
|
87
|
+
async function waitForStableValue(frame, expression, opts) {
|
|
88
|
+
const intervalMs = Math.max(1, opts.intervalMs ?? 200);
|
|
89
|
+
const windowMs = Math.max(1, opts.windowMs ?? 1500);
|
|
90
|
+
const timeoutMs = Math.max(1, opts.timeoutMs ?? 30000);
|
|
91
|
+
const tolerance = opts.tolerance ?? 0;
|
|
92
|
+
const started = Date.now();
|
|
93
|
+
let previous;
|
|
94
|
+
let stableSince = started;
|
|
95
|
+
let samples = 0;
|
|
96
|
+
while (Date.now() - started < timeoutMs) {
|
|
97
|
+
const { value } = await readValue(frame, expression);
|
|
98
|
+
samples++;
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
if (samples === 1 || !isWithinTolerance(previous, value, tolerance)) {
|
|
101
|
+
previous = value;
|
|
102
|
+
stableSince = now;
|
|
103
|
+
}
|
|
104
|
+
else if (now - stableSince >= windowMs) {
|
|
105
|
+
return { elapsedMs: now - started, value, samples };
|
|
106
|
+
}
|
|
107
|
+
await sleep(intervalMs);
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`wait-stable timed out after ${timeoutMs}ms; last value: ${JSON.stringify(previous)}`);
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=sampler.js.map
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -14,14 +14,17 @@ export declare const inspectCommand: import("./command").CommandSchema<z.ZodObje
|
|
|
14
14
|
brief: z.ZodOptional<z.ZodBoolean>;
|
|
15
15
|
layout: z.ZodOptional<z.ZodBoolean>;
|
|
16
16
|
depth: z.ZodOptional<z.ZodEffects<z.ZodNumber, number, unknown>>;
|
|
17
|
+
frame: z.ZodOptional<z.ZodString>;
|
|
17
18
|
}, "strip", z.ZodTypeAny, {
|
|
18
19
|
json?: boolean | undefined;
|
|
19
20
|
brief?: boolean | undefined;
|
|
20
21
|
layout?: boolean | undefined;
|
|
21
22
|
depth?: number | undefined;
|
|
23
|
+
frame?: string | undefined;
|
|
22
24
|
}, {
|
|
23
25
|
json?: boolean | undefined;
|
|
24
26
|
brief?: boolean | undefined;
|
|
25
27
|
layout?: boolean | undefined;
|
|
26
28
|
depth?: unknown;
|
|
29
|
+
frame?: string | undefined;
|
|
27
30
|
}>>;
|
package/dist/cli/commands.js
CHANGED
|
@@ -57,6 +57,8 @@ const open = (0, command_1.declareCommand)({
|
|
|
57
57
|
headed: z.boolean().optional().describe('show the browser window'),
|
|
58
58
|
viewport: z.string().optional().describe('viewport size as WxH (e.g. 1280x720)'),
|
|
59
59
|
state: z.string().optional().describe('path to saved state file (cookies + localStorage)'),
|
|
60
|
+
'expect-url': z.string().optional().describe('regex the final page URL must match'),
|
|
61
|
+
'fail-on-login': z.boolean().optional().describe('fail when the page redirects to a login URL'),
|
|
60
62
|
}),
|
|
61
63
|
});
|
|
62
64
|
const close = (0, command_1.declareCommand)({
|
|
@@ -72,6 +74,37 @@ const status = (0, command_1.declareCommand)({
|
|
|
72
74
|
category: 'core',
|
|
73
75
|
description: 'Show current session status',
|
|
74
76
|
});
|
|
77
|
+
const framesCmd = (0, command_1.declareCommand)({
|
|
78
|
+
name: 'frames',
|
|
79
|
+
category: 'core',
|
|
80
|
+
description: 'List frames in the current page',
|
|
81
|
+
});
|
|
82
|
+
const scenarioCmd = (0, command_1.declareCommand)({
|
|
83
|
+
name: 'scenario',
|
|
84
|
+
category: 'core',
|
|
85
|
+
description: 'Run a JSON scenario suite (regression cases, artifacts, JUnit)',
|
|
86
|
+
args: z.object({
|
|
87
|
+
action: z.literal('run').describe('only "run" is supported'),
|
|
88
|
+
file: z.string().describe('scenario suite JSON path'),
|
|
89
|
+
}),
|
|
90
|
+
options: z.object({
|
|
91
|
+
state: z.string().optional().describe('fallback state file when the suite/case does not set one'),
|
|
92
|
+
artifacts: z.string().optional().describe('artifact directory (default: ~/.cssprobe-cli/scenario-runs/<suite>-<timestamp>)'),
|
|
93
|
+
junit: z.string().optional().describe('write a JUnit XML report to this path'),
|
|
94
|
+
bail: z.boolean().optional().describe('stop after the first failing case'),
|
|
95
|
+
headed: z.boolean().optional().describe('show the browser window'),
|
|
96
|
+
browser: z.enum(['chromium', 'firefox', 'webkit']).optional().describe('browser engine (default chromium)'),
|
|
97
|
+
viewport: z.string().optional().describe('viewport size as WxH (default: suite defaults or 1280x720)'),
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
const doctor = (0, command_1.declareCommand)({
|
|
101
|
+
name: 'doctor',
|
|
102
|
+
category: 'core',
|
|
103
|
+
description: 'Diagnose browser installation and Playwright revision mismatch',
|
|
104
|
+
options: z.object({
|
|
105
|
+
engine: z.string().optional().describe('browser engine: chromium, firefox, webkit (default: chromium)'),
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
75
108
|
// ── cssprobe commands ──
|
|
76
109
|
const inspect = (0, command_1.declareCommand)({
|
|
77
110
|
name: 'inspect',
|
|
@@ -85,6 +118,7 @@ const inspect = (0, command_1.declareCommand)({
|
|
|
85
118
|
brief: z.boolean().optional().describe('compact output: tree sketch + warnings/errors only'),
|
|
86
119
|
layout: z.boolean().optional().describe('ASCII layout diagram showing element positions and sizes'),
|
|
87
120
|
depth: numberArg.optional().describe('DOM tree depth (default: auto, max 20)'),
|
|
121
|
+
frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
|
|
88
122
|
}),
|
|
89
123
|
});
|
|
90
124
|
const tree = (0, command_1.declareCommand)({
|
|
@@ -96,6 +130,7 @@ const tree = (0, command_1.declareCommand)({
|
|
|
96
130
|
}),
|
|
97
131
|
options: z.object({
|
|
98
132
|
depth: numberArg.optional().describe('tree depth (default: auto, max 20)'),
|
|
133
|
+
frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
|
|
99
134
|
}),
|
|
100
135
|
});
|
|
101
136
|
const layout = (0, command_1.declareCommand)({
|
|
@@ -105,6 +140,9 @@ const layout = (0, command_1.declareCommand)({
|
|
|
105
140
|
args: z.object({
|
|
106
141
|
selector: z.string().describe('CSS selector for the root element'),
|
|
107
142
|
}),
|
|
143
|
+
options: z.object({
|
|
144
|
+
frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
|
|
145
|
+
}),
|
|
108
146
|
});
|
|
109
147
|
const findings = (0, command_1.declareCommand)({
|
|
110
148
|
name: 'findings',
|
|
@@ -113,6 +151,9 @@ const findings = (0, command_1.declareCommand)({
|
|
|
113
151
|
args: z.object({
|
|
114
152
|
selector: z.string().describe('CSS selector for the root element'),
|
|
115
153
|
}),
|
|
154
|
+
options: z.object({
|
|
155
|
+
frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
|
|
156
|
+
}),
|
|
116
157
|
});
|
|
117
158
|
const injectCss = (0, command_1.declareCommand)({
|
|
118
159
|
name: 'inject-css',
|
|
@@ -121,6 +162,9 @@ const injectCss = (0, command_1.declareCommand)({
|
|
|
121
162
|
args: z.object({
|
|
122
163
|
css: z.string().describe('CSS code to inject'),
|
|
123
164
|
}),
|
|
165
|
+
options: z.object({
|
|
166
|
+
frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
|
|
167
|
+
}),
|
|
124
168
|
});
|
|
125
169
|
// ── browser commands ──
|
|
126
170
|
const screenshot = (0, command_1.declareCommand)({
|
|
@@ -146,7 +190,10 @@ const evalCmd = (0, command_1.declareCommand)({
|
|
|
146
190
|
category: 'core',
|
|
147
191
|
description: 'Evaluate JavaScript expression on the page (browser context)',
|
|
148
192
|
args: z.object({
|
|
149
|
-
expression: z.string().describe('JavaScript expression
|
|
193
|
+
expression: z.string().describe('JavaScript expression, or @file / - (stdin) for long scripts'),
|
|
194
|
+
}),
|
|
195
|
+
options: z.object({
|
|
196
|
+
frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
|
|
150
197
|
}),
|
|
151
198
|
});
|
|
152
199
|
const playwrightCmd = (0, command_1.declareCommand)({
|
|
@@ -154,7 +201,7 @@ const playwrightCmd = (0, command_1.declareCommand)({
|
|
|
154
201
|
category: 'core',
|
|
155
202
|
description: 'Execute Playwright API call (Node.js context)',
|
|
156
203
|
args: z.object({
|
|
157
|
-
call: z.string().describe('Playwright API call, e.g. "page.setViewportSize({width: 1280, height: 720})"'),
|
|
204
|
+
call: z.string().describe('Playwright API call, or @file / - (stdin); e.g. "page.setViewportSize({width: 1280, height: 720})"'),
|
|
158
205
|
}),
|
|
159
206
|
});
|
|
160
207
|
// ── interaction ──
|
|
@@ -215,6 +262,37 @@ const waitForCmd = (0, command_1.declareCommand)({
|
|
|
215
262
|
timeout: numberArg.optional().describe('timeout in ms (default 30000)'),
|
|
216
263
|
}),
|
|
217
264
|
});
|
|
265
|
+
const sampleCmd = (0, command_1.declareCommand)({
|
|
266
|
+
name: 'sample',
|
|
267
|
+
category: 'core',
|
|
268
|
+
description: 'Sample a JS expression over time and return a timeline with summary stats',
|
|
269
|
+
args: z.object({
|
|
270
|
+
expression: z.string().describe('JS expression sampled on an interval'),
|
|
271
|
+
}),
|
|
272
|
+
options: z.object({
|
|
273
|
+
interval: numberArg.optional().describe('sampling interval in ms (default 250)'),
|
|
274
|
+
duration: numberArg.optional().describe('total sampling duration in ms (default 5000)'),
|
|
275
|
+
key: z.string().optional().describe('dot path for numeric extraction, e.g. "top" or "box.y"'),
|
|
276
|
+
'tail-from': numberArg.optional().describe('tail starts at this elapsed ms (default: last 5000ms)'),
|
|
277
|
+
'ignore-errors': z.boolean().optional().describe('record evaluation errors as sample points instead of aborting'),
|
|
278
|
+
frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
const waitStableCmd = (0, command_1.declareCommand)({
|
|
282
|
+
name: 'wait-stable',
|
|
283
|
+
category: 'core',
|
|
284
|
+
description: 'Wait until a JS expression stops changing (tolerance + stable window)',
|
|
285
|
+
args: z.object({
|
|
286
|
+
expression: z.string().describe('JS expression polled until stable'),
|
|
287
|
+
}),
|
|
288
|
+
options: z.object({
|
|
289
|
+
tolerance: numberArg.optional().describe('absolute tolerance for numeric values (default 0)'),
|
|
290
|
+
window: numberArg.optional().describe('stable window in ms (default 1500)'),
|
|
291
|
+
timeout: numberArg.optional().describe('timeout in ms (default 30000)'),
|
|
292
|
+
interval: numberArg.optional().describe('polling interval in ms (default 200)'),
|
|
293
|
+
frame: z.string().optional().describe('target frame: index, name, or URL substring (default: main frame)'),
|
|
294
|
+
}),
|
|
295
|
+
});
|
|
218
296
|
const runCmd = (0, command_1.declareCommand)({
|
|
219
297
|
name: 'run',
|
|
220
298
|
category: 'core',
|
|
@@ -248,6 +326,15 @@ const stateSave = (0, command_1.declareCommand)({
|
|
|
248
326
|
name: z.string().optional().describe('state file name (default: session)'),
|
|
249
327
|
}),
|
|
250
328
|
});
|
|
329
|
+
const stateCheck = (0, command_1.declareCommand)({
|
|
330
|
+
name: 'state-check',
|
|
331
|
+
category: 'core',
|
|
332
|
+
description: 'Validate a saved state file: cookies, expiry, and target domain coverage',
|
|
333
|
+
options: z.object({
|
|
334
|
+
state: z.string().optional().describe('state file path (default: ~/.cssprobe-cli/states/session.json)'),
|
|
335
|
+
url: z.string().optional().describe('target URL whose hostname must be covered by the cookies'),
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
251
338
|
// ── config ──
|
|
252
339
|
const configShow = (0, command_1.declareCommand)({
|
|
253
340
|
name: 'config-show',
|
|
@@ -343,15 +430,15 @@ const mcpUninstall = (0, command_1.declareCommand)({
|
|
|
343
430
|
// ── export ──
|
|
344
431
|
const commandsArray = [
|
|
345
432
|
// session management
|
|
346
|
-
open, close, status,
|
|
433
|
+
open, close, status, framesCmd, doctor, scenarioCmd,
|
|
347
434
|
// cssprobe
|
|
348
435
|
inspect, tree, layout, findings, injectCss,
|
|
349
436
|
// browser
|
|
350
437
|
resize, evalCmd, playwrightCmd, screenshot,
|
|
351
438
|
// interaction
|
|
352
|
-
click, hover, typeCmd, waitForCmd, runCmd,
|
|
439
|
+
click, hover, typeCmd, waitForCmd, sampleCmd, waitStableCmd, runCmd,
|
|
353
440
|
// state
|
|
354
|
-
stateImport, stateSave,
|
|
441
|
+
stateImport, stateSave, stateCheck,
|
|
355
442
|
// config
|
|
356
443
|
configShow, configSet, configList, configUse, configNew, configPath,
|
|
357
444
|
// skill
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveInputArg = resolveInputArg;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function resolveInputArg(value, source = {}) {
|
|
10
|
+
const exists = source.exists ?? fs_1.default.existsSync;
|
|
11
|
+
const readFile = source.readFile ?? ((file) => fs_1.default.readFileSync(file, 'utf-8'));
|
|
12
|
+
if (value === '-') {
|
|
13
|
+
const readStdin = source.readStdin ?? (() => fs_1.default.readFileSync(0, 'utf-8'));
|
|
14
|
+
return readStdin();
|
|
15
|
+
}
|
|
16
|
+
if (value.startsWith('@')) {
|
|
17
|
+
const file = path_1.default.resolve(value.slice(1));
|
|
18
|
+
if (!exists(file))
|
|
19
|
+
throw new Error(`File not found: ${file}`);
|
|
20
|
+
return readFile(file);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=input.js.map
|
package/dist/cli/program.js
CHANGED
|
@@ -9,11 +9,17 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const os_1 = __importDefault(require("os"));
|
|
10
10
|
const minimist_1 = require("./minimist");
|
|
11
11
|
const command_1 = require("./command");
|
|
12
|
+
const input_1 = require("./input");
|
|
12
13
|
const commands_1 = require("./commands");
|
|
13
14
|
const output_1 = require("./output");
|
|
14
15
|
const config_1 = require("../config/config");
|
|
15
16
|
const session_1 = require("../daemon/session");
|
|
16
17
|
const session_ops_1 = require("./session-ops");
|
|
18
|
+
const state_check_1 = require("./state-check");
|
|
19
|
+
const launcher_1 = require("../browser/launcher");
|
|
20
|
+
const schema_1 = require("../scenario/schema");
|
|
21
|
+
const runner_1 = require("../scenario/runner");
|
|
22
|
+
const reporters_1 = require("../scenario/reporters");
|
|
17
23
|
const mcp_1 = require("../mcp");
|
|
18
24
|
const globalOptions = ['json', 'raw', 'help', 'h', 'version', 'v', 'p', 'profile'];
|
|
19
25
|
const booleanGlobalOptions = ['help', 'json', 'raw', 'version', 'v', 'h'];
|
|
@@ -52,15 +58,25 @@ async function program() {
|
|
|
52
58
|
case 'close':
|
|
53
59
|
await handleClose(command, rawArgs, output);
|
|
54
60
|
return;
|
|
61
|
+
case 'doctor':
|
|
62
|
+
handleDoctor(command, rawArgs, output);
|
|
63
|
+
return;
|
|
64
|
+
case 'scenario':
|
|
65
|
+
await handleScenario(command, rawArgs, output);
|
|
66
|
+
return;
|
|
55
67
|
case 'status':
|
|
56
68
|
await handleStatus(output);
|
|
57
69
|
return;
|
|
58
70
|
case 'state-import':
|
|
59
71
|
await handleStateImport(command, rawArgs, output);
|
|
60
72
|
return;
|
|
73
|
+
case 'state-check':
|
|
74
|
+
handleStateCheck(command, rawArgs, output);
|
|
75
|
+
return;
|
|
61
76
|
case 'state-save':
|
|
62
77
|
await handleStateSave(command, rawArgs, output);
|
|
63
78
|
return;
|
|
79
|
+
case 'frames':
|
|
64
80
|
case 'inspect':
|
|
65
81
|
case 'tree':
|
|
66
82
|
case 'layout':
|
|
@@ -74,6 +90,8 @@ async function program() {
|
|
|
74
90
|
case 'hover':
|
|
75
91
|
case 'type':
|
|
76
92
|
case 'wait-for':
|
|
93
|
+
case 'sample':
|
|
94
|
+
case 'wait-stable':
|
|
77
95
|
await handleSessionCommand(commandName, command, rawArgs, output);
|
|
78
96
|
return;
|
|
79
97
|
case 'run':
|
|
@@ -148,6 +166,8 @@ async function handleOpen(command, args, output) {
|
|
|
148
166
|
headed: !!parsed.headed,
|
|
149
167
|
viewport,
|
|
150
168
|
state: parsed.state,
|
|
169
|
+
expectUrl: parsed['expect-url'],
|
|
170
|
+
failOnLogin: !!parsed['fail-on-login'],
|
|
151
171
|
});
|
|
152
172
|
if (output.json) {
|
|
153
173
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -183,11 +203,100 @@ async function handleClose(command, args, output) {
|
|
|
183
203
|
output.error(e instanceof Error ? e.message : String(e));
|
|
184
204
|
}
|
|
185
205
|
}
|
|
206
|
+
async function handleScenario(command, args, output) {
|
|
207
|
+
try {
|
|
208
|
+
const cmdArgs = splitArgs(args);
|
|
209
|
+
const parsed = (0, command_1.parseCommand)(command, cmdArgs);
|
|
210
|
+
const file = path_1.default.resolve(parsed.file);
|
|
211
|
+
if (!fs_1.default.existsSync(file))
|
|
212
|
+
output.error(`Scenario suite not found: ${file}`);
|
|
213
|
+
const raw = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
214
|
+
const validation = schema_1.suiteSchema.safeParse(raw);
|
|
215
|
+
if (!validation.success) {
|
|
216
|
+
const issues = validation.error.issues
|
|
217
|
+
.map(issue => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
218
|
+
.join('\n');
|
|
219
|
+
output.error(`Invalid scenario suite:\n${issues}`);
|
|
220
|
+
}
|
|
221
|
+
let viewport;
|
|
222
|
+
const viewportStr = parsed.viewport;
|
|
223
|
+
if (viewportStr) {
|
|
224
|
+
const parts = viewportStr.split('x');
|
|
225
|
+
const width = parseInt(parts[0], 10);
|
|
226
|
+
const height = parseInt(parts[1], 10);
|
|
227
|
+
if (parts.length !== 2 || isNaN(width) || isNaN(height)) {
|
|
228
|
+
output.error('Invalid viewport format. Use WxH (e.g. 1280x720)');
|
|
229
|
+
}
|
|
230
|
+
viewport = { width, height };
|
|
231
|
+
}
|
|
232
|
+
const result = await (0, runner_1.runScenario)(validation.data, file, {
|
|
233
|
+
artifactsDir: parsed.artifacts,
|
|
234
|
+
junitPath: parsed.junit,
|
|
235
|
+
bail: !!parsed.bail,
|
|
236
|
+
headed: !!parsed.headed,
|
|
237
|
+
browser: parsed.browser,
|
|
238
|
+
viewport,
|
|
239
|
+
state: parsed.state,
|
|
240
|
+
});
|
|
241
|
+
if (output.json) {
|
|
242
|
+
console.log(JSON.stringify(result, null, 2));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
console.log((0, reporters_1.formatConsole)(result.suite, result.results));
|
|
246
|
+
console.log(`Artifacts: ${result.artifactsDir}`);
|
|
247
|
+
if (parsed.junit)
|
|
248
|
+
console.log(`JUnit: ${path_1.default.resolve(parsed.junit)}`);
|
|
249
|
+
}
|
|
250
|
+
if (!result.ok)
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
output.error(e instanceof Error ? e.message : String(e));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function handleDoctor(command, args, output) {
|
|
258
|
+
try {
|
|
259
|
+
const cmdArgs = splitArgs(args);
|
|
260
|
+
const parsed = (0, command_1.parseCommand)(command, cmdArgs);
|
|
261
|
+
const diagnosis = (0, launcher_1.diagnoseBrowser)(parsed.engine);
|
|
262
|
+
if (output.json) {
|
|
263
|
+
console.log(JSON.stringify(diagnosis, null, 2));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
console.log(`Platform: ${diagnosis.platform}`);
|
|
267
|
+
console.log(`Cache roots: ${diagnosis.cacheRoots.join(', ')}`);
|
|
268
|
+
console.log(`Expected: chromium ${diagnosis.expected.chromium ?? 'unknown'}, headless-shell ${diagnosis.expected.headlessShell ?? 'unknown'} (playwright-core ${diagnosis.expected.playwrightVersion ?? 'unknown'})`);
|
|
269
|
+
if (diagnosis.installs.length) {
|
|
270
|
+
console.log('Installed:');
|
|
271
|
+
for (const install of diagnosis.installs) {
|
|
272
|
+
console.log(` ${install.kind}-${install.revision}${install.executable ? '' : ' (no executable)'} ${install.dir}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
console.log('Installed: none');
|
|
277
|
+
}
|
|
278
|
+
console.log(`Status: ${diagnosis.status}`);
|
|
279
|
+
if (diagnosis.hint)
|
|
280
|
+
console.log(`Hint: ${diagnosis.hint}`);
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
output.error(e instanceof Error ? e.message : String(e));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
186
286
|
async function handleStatus(output) {
|
|
187
287
|
try {
|
|
188
288
|
const result = await (0, session_ops_1.getSessionStatus)();
|
|
289
|
+
let page = null;
|
|
290
|
+
if (result.alive) {
|
|
291
|
+
try {
|
|
292
|
+
page = await (0, session_ops_1.getPageInfo)();
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
page = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
189
298
|
if (output.json) {
|
|
190
|
-
console.log(JSON.stringify(result, null, 2));
|
|
299
|
+
console.log(JSON.stringify({ ...result, page }, null, 2));
|
|
191
300
|
return;
|
|
192
301
|
}
|
|
193
302
|
if (!result.config) {
|
|
@@ -198,7 +307,42 @@ async function handleStatus(output) {
|
|
|
198
307
|
console.log(`Status: ${result.alive ? 'alive' : 'dead'}`);
|
|
199
308
|
if (result.alive) {
|
|
200
309
|
console.log(`Browser: ${result.config.browser.browserName}`);
|
|
310
|
+
if (page) {
|
|
311
|
+
console.log(`URL: ${page.url}`);
|
|
312
|
+
console.log(`Title: ${page.title}`);
|
|
313
|
+
console.log(`Frames: ${page.frames}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
output.error(e instanceof Error ? e.message : String(e));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function handleStateCheck(command, args, output) {
|
|
322
|
+
try {
|
|
323
|
+
const cmdArgs = splitArgs(args);
|
|
324
|
+
const parsed = (0, command_1.parseCommand)(command, cmdArgs);
|
|
325
|
+
const file = path_1.default.resolve(parsed.state ??
|
|
326
|
+
path_1.default.join(os_1.default.homedir(), '.cssprobe-cli', 'states', 'session.json'));
|
|
327
|
+
if (!fs_1.default.existsSync(file))
|
|
328
|
+
output.error(`State file not found: ${file}`);
|
|
329
|
+
const raw = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
330
|
+
if (!raw || !Array.isArray(raw.cookies))
|
|
331
|
+
throw new Error(`Invalid state file (missing cookies): ${file}`);
|
|
332
|
+
const report = (0, state_check_1.analyzeState)(raw, parsed.url);
|
|
333
|
+
if (output.json) {
|
|
334
|
+
console.log(JSON.stringify({ file, ...report }, null, 2));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
console.log(`State: ${file}`);
|
|
338
|
+
console.log(`Cookies: ${report.cookies} (${report.sessionCookies} session)`);
|
|
339
|
+
console.log(`Domains: ${report.domains.join(', ') || '(none)'}`);
|
|
340
|
+
if (report.targetMatches !== null) {
|
|
341
|
+
console.log(`Target match: ${report.targetMatches ? 'yes' : 'no'}`);
|
|
201
342
|
}
|
|
343
|
+
console.log(`Status: ${report.ok ? 'ok' : 'attention'}`);
|
|
344
|
+
for (const warning of report.warnings)
|
|
345
|
+
console.log(`Warning: ${warning}`);
|
|
202
346
|
}
|
|
203
347
|
catch (e) {
|
|
204
348
|
output.error(e instanceof Error ? e.message : String(e));
|
|
@@ -217,6 +361,13 @@ async function handleRun(command, args, output) {
|
|
|
217
361
|
async function handleSessionCommand(commandName, command, args, output) {
|
|
218
362
|
try {
|
|
219
363
|
const cmdArgs = splitArgs(args);
|
|
364
|
+
if (commandName === 'eval' || commandName === 'playwright') {
|
|
365
|
+
const input = cmdArgs._[1];
|
|
366
|
+
if (typeof input === 'string') {
|
|
367
|
+
cmdArgs._ = cmdArgs._.slice();
|
|
368
|
+
cmdArgs._[1] = (0, input_1.resolveInputArg)(input);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
220
371
|
const parsed = (0, command_1.parseCommand)(command, cmdArgs);
|
|
221
372
|
const result = await (0, session_ops_1.runSessionCommand)(commandName, command, parsed, !!args.json);
|
|
222
373
|
// Screenshot: save the base64 data URL to a file and report the path
|
|
@@ -11,6 +11,8 @@ export interface OpenOptions {
|
|
|
11
11
|
height: number;
|
|
12
12
|
};
|
|
13
13
|
state?: string;
|
|
14
|
+
expectUrl?: string;
|
|
15
|
+
failOnLogin?: boolean;
|
|
14
16
|
}
|
|
15
17
|
export interface OpenResult {
|
|
16
18
|
sessionId: string;
|
|
@@ -21,6 +23,12 @@ export interface OpenResult {
|
|
|
21
23
|
height: number;
|
|
22
24
|
};
|
|
23
25
|
}
|
|
26
|
+
export interface PageInfo {
|
|
27
|
+
url: string;
|
|
28
|
+
title: string;
|
|
29
|
+
frames: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function getPageInfo(): Promise<PageInfo>;
|
|
24
32
|
export declare function openSession(opts: OpenOptions): Promise<OpenResult>;
|
|
25
33
|
export declare function waitForSession(timeoutMs: number): Promise<Session | undefined>;
|
|
26
34
|
export interface CloseResult {
|
package/dist/cli/session-ops.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.saveScreenshotFile = void 0;
|
|
4
|
+
exports.getPageInfo = getPageInfo;
|
|
4
5
|
exports.openSession = openSession;
|
|
5
6
|
exports.waitForSession = waitForSession;
|
|
6
7
|
exports.closeSession = closeSession;
|
|
@@ -8,8 +9,16 @@ exports.getSessionStatus = getSessionStatus;
|
|
|
8
9
|
exports.buildDaemonArgs = buildDaemonArgs;
|
|
9
10
|
exports.runSessionCommand = runSessionCommand;
|
|
10
11
|
const session_1 = require("../daemon/session");
|
|
12
|
+
const url_guard_1 = require("./url-guard");
|
|
11
13
|
var screenshot_1 = require("../utils/screenshot");
|
|
12
14
|
Object.defineProperty(exports, "saveScreenshotFile", { enumerable: true, get: function () { return screenshot_1.saveScreenshotFile; } });
|
|
15
|
+
async function getPageInfo() {
|
|
16
|
+
const session = await (0, session_1.loadSession)();
|
|
17
|
+
if (!session)
|
|
18
|
+
throw new Error('No active session. Run: cssprobe-cli open <url>');
|
|
19
|
+
const result = await session.run({ _: ['page-info'] });
|
|
20
|
+
return JSON.parse(result.text);
|
|
21
|
+
}
|
|
13
22
|
async function openSession(opts) {
|
|
14
23
|
const url = opts.url || 'about:blank';
|
|
15
24
|
const browser = opts.browser || 'chromium';
|
|
@@ -24,10 +33,24 @@ async function openSession(opts) {
|
|
|
24
33
|
if (!session)
|
|
25
34
|
throw new Error('Daemon failed to become ready within 5s. Check the daemon log for errors.');
|
|
26
35
|
await session.run({ _: ['goto', url] });
|
|
36
|
+
const info = await getPageInfo();
|
|
37
|
+
const guardError = (() => {
|
|
38
|
+
if (opts.failOnLogin && (0, url_guard_1.looksLikeLoginUrl)(info.url)) {
|
|
39
|
+
return `Login required: page redirected to ${info.url}. Refresh the state file or log in manually.`;
|
|
40
|
+
}
|
|
41
|
+
if (opts.expectUrl && !(0, url_guard_1.matchesUrlPattern)(opts.expectUrl, info.url)) {
|
|
42
|
+
return `Expected URL to match ${opts.expectUrl}, got ${info.url}`;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
})();
|
|
46
|
+
if (guardError) {
|
|
47
|
+
await closeSession();
|
|
48
|
+
throw new Error(guardError);
|
|
49
|
+
}
|
|
27
50
|
return {
|
|
28
51
|
sessionId: 'default',
|
|
29
52
|
pid,
|
|
30
|
-
url,
|
|
53
|
+
url: info.url,
|
|
31
54
|
viewport: opts.viewport || { width: 1280, height: 720 },
|
|
32
55
|
};
|
|
33
56
|
}
|