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.
- package/AGENTS.md +39 -7
- package/CHANGELOG.md +30 -0
- package/README.md +58 -5
- package/dist/browser/frames.d.ts +15 -0
- package/dist/browser/frames.js +60 -0
- package/dist/browser/interactions.d.ts +3 -2
- package/dist/browser/interactions.js +16 -4
- 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 +93 -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 +70 -20
- package/dist/daemon/expect.d.ts +8 -0
- package/dist/daemon/expect.js +47 -0
- package/dist/daemon/flowRunner.d.ts +13 -1
- package/dist/daemon/flowRunner.js +28 -3
- package/dist/engine/evalSerializer.js +2 -2
- package/dist/help.json +96 -17
- 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 +251 -0
- package/dist/scenario/schema.d.ts +1270 -0
- package/dist/scenario/schema.js +151 -0
- package/docs/guide/installation.md +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
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
|
}
|
|
@@ -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();
|
|
@@ -262,9 +278,11 @@ async function executeCommand(launcher, args) {
|
|
|
262
278
|
networkIdle: args['network-idle'],
|
|
263
279
|
state: args.state,
|
|
264
280
|
});
|
|
281
|
+
const page = requirePage(launcher);
|
|
282
|
+
const waitHost = args.frame ? (0, frames_1.resolveFrame)(page, args.frame) : page;
|
|
265
283
|
let elapsedMs;
|
|
266
284
|
try {
|
|
267
|
-
elapsedMs = await (0, interactions_1.waitForCondition)(
|
|
285
|
+
elapsedMs = await (0, interactions_1.waitForCondition)(waitHost, target, timeout);
|
|
268
286
|
}
|
|
269
287
|
catch (e) {
|
|
270
288
|
throw new Error(`wait-for failed for ${(0, interactions_1.describeWaitTarget)(target)} within ${timeout}ms: ${e.message}`);
|
|
@@ -282,6 +300,38 @@ async function executeCommand(launcher, args) {
|
|
|
282
300
|
const label = target.kind === 'selector' ? `${selector} (${target.state})` : (0, interactions_1.describeWaitTarget)(target);
|
|
283
301
|
return { text: `Wait satisfied: ${label} (${elapsedMs}ms)` };
|
|
284
302
|
}
|
|
303
|
+
case 'sample': {
|
|
304
|
+
const expression = params[0];
|
|
305
|
+
if (!expression)
|
|
306
|
+
throw new Error('Expression is required for sample command');
|
|
307
|
+
const interval = args.interval ?? 250;
|
|
308
|
+
const duration = args.duration ?? 5000;
|
|
309
|
+
if (duration <= 0)
|
|
310
|
+
throw new Error('--duration must be greater than 0');
|
|
311
|
+
if (interval <= 0)
|
|
312
|
+
throw new Error('--interval must be greater than 0');
|
|
313
|
+
const frame = (0, frames_1.resolveFrame)(requirePage(launcher), args.frame);
|
|
314
|
+
const points = await (0, sampler_1.sampleFrame)(frame, expression, {
|
|
315
|
+
intervalMs: interval,
|
|
316
|
+
durationMs: duration,
|
|
317
|
+
ignoreErrors: !!args['ignore-errors'],
|
|
318
|
+
});
|
|
319
|
+
const summary = (0, sampler_1.summarizeSamples)(points, args.key, 5000, args['tail-from']);
|
|
320
|
+
return { text: JSON.stringify({ expression, intervalMs: interval, summary, series: points }, null, 2) };
|
|
321
|
+
}
|
|
322
|
+
case 'wait-stable': {
|
|
323
|
+
const expression = params[0];
|
|
324
|
+
if (!expression)
|
|
325
|
+
throw new Error('Expression is required for wait-stable command');
|
|
326
|
+
const frame = (0, frames_1.resolveFrame)(requirePage(launcher), args.frame);
|
|
327
|
+
const result = await (0, sampler_1.waitForStableValue)(frame, expression, {
|
|
328
|
+
tolerance: args.tolerance,
|
|
329
|
+
windowMs: args.window,
|
|
330
|
+
timeoutMs: args.timeout,
|
|
331
|
+
intervalMs: args.interval,
|
|
332
|
+
});
|
|
333
|
+
return { text: JSON.stringify({ ok: true, expression, ...result }, null, 2) };
|
|
334
|
+
}
|
|
285
335
|
case 'run': {
|
|
286
336
|
const absPath = params[0];
|
|
287
337
|
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,17 +1,25 @@
|
|
|
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;
|
|
14
21
|
timeout?: number;
|
|
22
|
+
frame?: string;
|
|
15
23
|
}
|
|
16
24
|
export interface WaitOutcome {
|
|
17
25
|
waitedFor: 'selector' | 'js' | 'networkidle';
|
|
@@ -20,6 +28,10 @@ export interface WaitOutcome {
|
|
|
20
28
|
}
|
|
21
29
|
export interface FlowApi {
|
|
22
30
|
page: Page;
|
|
31
|
+
context: BrowserContext;
|
|
32
|
+
frames(): FrameInfo[];
|
|
33
|
+
newPage(): Promise<Page>;
|
|
34
|
+
expect(actual: unknown): Matchers;
|
|
23
35
|
click(selector: string, opts?: {
|
|
24
36
|
nth?: number;
|
|
25
37
|
force?: boolean;
|