@crouton-kit/capture 1.3.8 → 1.3.10
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/bin/capture +29841 -8539
- package/package.json +6 -3
- package/src/capture.ts +86 -173
- package/src/cdp/a11y.ts +113 -14
- package/src/cdp/args.ts +300 -205
- package/src/cdp/bridge/protocol.ts +93 -6
- package/src/cdp/bridge/server.ts +118 -53
- package/src/cdp/bridge/spawn.ts +53 -6
- package/src/cdp/client.ts +13 -3
- package/src/cdp/commands/bridge-serve.ts +20 -14
- package/src/cdp/commands/cdp.ts +259 -90
- package/src/cdp/commands/gate-guard.ts +43 -0
- package/src/cdp/commands/lib.ts +339 -213
- package/src/cdp/commands/measure/census.ts +76 -20
- package/src/cdp/commands/measure/check.ts +100 -26
- package/src/cdp/commands/measure/diff.ts +109 -21
- package/src/cdp/commands/measure/explain.ts +121 -19
- package/src/cdp/commands/measure/index.ts +31 -39
- package/src/cdp/commands/measure/map-ax.ts +52 -0
- package/src/cdp/commands/measure/map-focus.ts +41 -13
- package/src/cdp/commands/measure/map-layers.ts +49 -15
- package/src/cdp/commands/measure/map-scroll.ts +85 -15
- package/src/cdp/commands/measure/snap.ts +405 -25
- package/src/cdp/commands/measure/sweep.ts +217 -20
- package/src/cdp/commands/motion/index.ts +18 -17
- package/src/cdp/commands/motion/jank.ts +119 -9
- package/src/cdp/commands/motion/mask.ts +81 -12
- package/src/cdp/commands/motion/rec.ts +520 -19
- package/src/cdp/commands/motion/response.ts +65 -15
- package/src/cdp/commands/motion/timeline.ts +139 -14
- package/src/cdp/commands/page/click.ts +194 -0
- package/src/cdp/commands/page/elements.ts +154 -0
- package/src/cdp/commands/page/exec.ts +317 -0
- package/src/cdp/commands/page/index.ts +72 -0
- package/src/cdp/commands/page/navigate.ts +330 -0
- package/src/cdp/commands/page/scroll.ts +107 -0
- package/src/cdp/commands/page/shot.ts +208 -0
- package/src/cdp/commands/page/type.ts +104 -0
- package/src/cdp/commands/tab/index.ts +59 -0
- package/src/cdp/commands/tab/list.ts +167 -0
- package/src/cdp/commands/tab/network.ts +162 -0
- package/src/cdp/commands/tab/open.ts +90 -0
- package/src/cdp/commands/tab/reset.ts +160 -0
- package/src/cdp/connection.ts +368 -37
- package/src/cdp/detect.ts +42 -13
- package/src/cdp/dispatch.ts +74 -28
- package/src/cdp/har-recorder.ts +648 -115
- package/src/cdp/leaf-grammar.ts +99 -0
- package/src/cdp/measure/census.ts +205 -0
- package/src/cdp/measure/check.ts +318 -0
- package/src/cdp/measure/collectors/animation.ts +385 -0
- package/src/cdp/measure/collectors/ax.ts +278 -0
- package/src/cdp/measure/collectors/focus.ts +1041 -0
- package/src/cdp/measure/collectors/forms.ts +905 -0
- package/src/cdp/measure/collectors/geometry.ts +1038 -0
- package/src/cdp/measure/collectors/hittest.ts +835 -0
- package/src/cdp/measure/collectors/layers.ts +697 -0
- package/src/cdp/measure/collectors/media.ts +522 -0
- package/src/cdp/measure/collectors/pixels.ts +1584 -0
- package/src/cdp/measure/collectors/queries.ts +464 -0
- package/src/cdp/measure/collectors/scroll.ts +599 -0
- package/src/cdp/measure/collectors/states.ts +992 -0
- package/src/cdp/measure/collectors/style-provenance.ts +643 -0
- package/src/cdp/measure/collectors/styles.ts +355 -0
- package/src/cdp/measure/collectors/text.ts +713 -0
- package/src/cdp/measure/diff.ts +470 -0
- package/src/cdp/measure/explain.ts +498 -0
- package/src/cdp/measure/map-ax.ts +633 -0
- package/src/cdp/measure/map-focus.ts +188 -0
- package/src/cdp/measure/map-layers.ts +177 -0
- package/src/cdp/measure/map-scroll.ts +317 -0
- package/src/cdp/measure/redaction.ts +51 -0
- package/src/cdp/measure/settle.ts +1051 -0
- package/src/cdp/measure/snapshot.ts +390 -0
- package/src/cdp/measure/sweep.ts +330 -0
- package/src/cdp/measure/types.ts +220 -0
- package/src/cdp/measure/v2/collectors.ts +19 -0
- package/src/cdp/measure/v2/orchestrator.ts +46 -0
- package/src/cdp/measure/v2/snap-selection.ts +33 -0
- package/src/cdp/measure/v2/target-lock.ts +15 -0
- package/src/cdp/measure/v2/target.ts +16 -0
- package/src/cdp/motion/jank.ts +433 -0
- package/src/cdp/motion/mask.ts +421 -0
- package/src/cdp/motion/recorder.ts +764 -0
- package/src/cdp/motion/response.ts +356 -0
- package/src/cdp/motion/timeline.ts +215 -0
- package/src/cdp/record.ts +5 -212
- package/src/cdp/recorder-bridge.ts +1478 -105
- package/src/cdp/recorder-client.ts +326 -0
- package/src/cdp/scope-lock.ts +73 -0
- package/src/cdp/screenshot.ts +199 -66
- package/src/cdp/targets.ts +21 -1
- package/src/cdp/timing.ts +15 -10
- package/src/cdp/types.ts +22 -7
- package/src/cdp/viewport.ts +21 -0
- package/src/cdp.ts +12 -58
- package/src/command-help.ts +29 -0
- package/src/command-resolution.ts +22 -0
- package/src/command-walker.ts +35 -0
- package/src/contracts/a11y.ts +33 -0
- package/src/contracts/cursor.ts +35 -0
- package/src/contracts/index.ts +15 -0
- package/src/contracts/motion.ts +19 -0
- package/src/contracts/neutral.ts +56 -0
- package/src/contracts/primitives.ts +205 -0
- package/src/contracts/release.ts +17 -0
- package/src/contracts/results.ts +208 -0
- package/src/contracts/routes.ts +140 -0
- package/src/contracts/snapshot.ts +92 -0
- package/src/cursor/index.ts +13 -0
- package/src/cursor/service.ts +310 -0
- package/src/errors.ts +71 -0
- package/src/har-manager.ts +992 -41
- package/src/interact.ts +410 -98
- package/src/output/artifact-lifecycle.ts +371 -0
- package/src/output/artifact.ts +6 -0
- package/src/output/exact-raw.ts +114 -0
- package/src/output/render.ts +396 -2
- package/src/output/structured.ts +211 -0
- package/src/registry.ts +208 -0
- package/src/session/artifacts.ts +452 -200
- package/src/session/commands.ts +1162 -397
- package/src/session/coordinator.ts +251 -0
- package/src/session/har-filter.ts +57 -0
- package/src/session/log-tailer.ts +653 -0
- package/src/session-context.ts +279 -51
- package/src/vault/bundle.ts +32 -17
- package/src/cdp/commands/exec.ts +0 -105
- package/src/cdp/commands/har.ts +0 -128
- package/src/cdp/commands/tabs.ts +0 -183
- package/src/cdp/commands/traffic.ts +0 -121
- package/src/cdp/commands/ui.ts +0 -172
- package/src/cdp/execute.ts +0 -113
- package/src/cdp/har-output.ts +0 -47
- package/src/cdp/usage.ts +0 -79
- package/src/tab-session.ts +0 -91
package/src/cdp/commands/har.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createHarRecording,
|
|
3
|
-
readHarRecording as readHarFile,
|
|
4
|
-
deleteHarRecording,
|
|
5
|
-
harFilePath,
|
|
6
|
-
} from '../../har-manager.js';
|
|
7
|
-
import { getActiveSession } from '../../session-context.js';
|
|
8
|
-
import { type ParsedArgs } from '../types.js';
|
|
9
|
-
|
|
10
|
-
export async function cmdHar(parsed: ParsedArgs, args: string[]): Promise<void> {
|
|
11
|
-
if (parsed.help) {
|
|
12
|
-
console.log(
|
|
13
|
-
'Usage: capture har <create|read|delete>\n\n' +
|
|
14
|
-
' create Create a new HAR recording (returns id)\n' +
|
|
15
|
-
' read [id] Read accumulated HAR entries (id optional in active session)\n' +
|
|
16
|
-
' delete <id> Delete a HAR recording\n\n' +
|
|
17
|
-
'Filters (read):\n' +
|
|
18
|
-
' --filter-url <pattern> Substring or regex match on URL\n' +
|
|
19
|
-
' --filter-status <code> Status code, prefix (e.g. 4), or range (e.g. 400-499)\n' +
|
|
20
|
-
' --filter-method <method> HTTP method (GET, POST, ...)\n' +
|
|
21
|
-
' --limit <N> Return only the first N matching entries\n\n' +
|
|
22
|
-
'Pass --har <id> to exec/navigate commands to append traffic to a recording.',
|
|
23
|
-
);
|
|
24
|
-
process.exit(0);
|
|
25
|
-
}
|
|
26
|
-
const subcommand = args[1];
|
|
27
|
-
|
|
28
|
-
if (subcommand === 'create') {
|
|
29
|
-
// Generate a short random ID
|
|
30
|
-
const { id, path: harPath } = createHarRecording();
|
|
31
|
-
console.log(JSON.stringify({ id, path: harPath }, null, 2));
|
|
32
|
-
console.error(
|
|
33
|
-
`\nPass --har ${id} to subsequent commands to record traffic.`,
|
|
34
|
-
);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (subcommand === 'read') {
|
|
39
|
-
// ID is optional — fall back to active session's harId
|
|
40
|
-
let id = args[2];
|
|
41
|
-
if (id && id.startsWith('--')) id = undefined as unknown as string;
|
|
42
|
-
if (!id) {
|
|
43
|
-
const session = getActiveSession();
|
|
44
|
-
if (session?.harId) {
|
|
45
|
-
id = session.harId;
|
|
46
|
-
} else {
|
|
47
|
-
console.error('Usage: capture har read [id]\n' +
|
|
48
|
-
' ID is optional when a capture session is active.');
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const har = readHarFile(id);
|
|
54
|
-
if (!har) {
|
|
55
|
-
console.error(`No HAR recording found for id: ${id}`);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let entries = har.log.entries;
|
|
60
|
-
const total = entries.length;
|
|
61
|
-
const filters: string[] = [];
|
|
62
|
-
|
|
63
|
-
if (parsed.filterUrl) {
|
|
64
|
-
const pattern = parsed.filterUrl;
|
|
65
|
-
let re: RegExp | null = null;
|
|
66
|
-
try { re = new RegExp(pattern, 'i'); } catch { re = null; }
|
|
67
|
-
entries = entries.filter((e) =>
|
|
68
|
-
re ? re.test(e.request.url) : e.request.url.toLowerCase().includes(pattern.toLowerCase()),
|
|
69
|
-
);
|
|
70
|
-
filters.push(`url~${pattern}`);
|
|
71
|
-
}
|
|
72
|
-
if (parsed.filterStatus) {
|
|
73
|
-
const s = parsed.filterStatus;
|
|
74
|
-
let matcher: (code: number) => boolean;
|
|
75
|
-
if (/^\d+-\d+$/.test(s)) {
|
|
76
|
-
const [lo, hi] = s.split('-').map((n) => parseInt(n, 10));
|
|
77
|
-
matcher = (c) => c >= lo && c <= hi;
|
|
78
|
-
} else if (/^\d+$/.test(s)) {
|
|
79
|
-
if (s.length < 3) {
|
|
80
|
-
matcher = (c) => String(c).startsWith(s);
|
|
81
|
-
} else {
|
|
82
|
-
const n = parseInt(s, 10);
|
|
83
|
-
matcher = (c) => c === n;
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
matcher = () => true;
|
|
87
|
-
}
|
|
88
|
-
entries = entries.filter((e) => matcher(e.response.status));
|
|
89
|
-
filters.push(`status=${s}`);
|
|
90
|
-
}
|
|
91
|
-
if (parsed.filterMethod) {
|
|
92
|
-
const m = parsed.filterMethod;
|
|
93
|
-
entries = entries.filter((e) => e.request.method.toUpperCase() === m);
|
|
94
|
-
filters.push(`method=${m}`);
|
|
95
|
-
}
|
|
96
|
-
if (typeof parsed.limit === 'number' && parsed.limit > 0) {
|
|
97
|
-
entries = entries.slice(0, parsed.limit);
|
|
98
|
-
filters.push(`limit=${parsed.limit}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const filtersLabel = filters.length > 0 ? ` [${filters.join(', ')}]` : '';
|
|
102
|
-
console.error(
|
|
103
|
-
`HAR: ${harFilePath(id)} (${entries.length}/${total} entries)${filtersLabel}`,
|
|
104
|
-
);
|
|
105
|
-
console.log(JSON.stringify({ log: { entries } }, null, 2));
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (subcommand === 'delete') {
|
|
110
|
-
const id = args[2];
|
|
111
|
-
if (!id) {
|
|
112
|
-
console.error('Usage: capture har delete <id>');
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
deleteHarRecording(id);
|
|
117
|
-
console.log(JSON.stringify({ deleted: true, id }, null, 2));
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
console.error(
|
|
122
|
-
'Usage: capture har <create|read|delete>\n' +
|
|
123
|
-
' create Create a new HAR recording (returns id)\n' +
|
|
124
|
-
' read <id> Read accumulated HAR entries\n' +
|
|
125
|
-
' delete <id> Delete a HAR recording',
|
|
126
|
-
);
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
package/src/cdp/commands/tabs.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { detectCdpPortsAsync, detectCdpPort, getDefaultBrowserId, pickPreferredEndpoint } from '../detect.js';
|
|
2
|
-
import { listTargets, openTab } from '../targets.js';
|
|
3
|
-
import { navigateAndWait } from '../record.js';
|
|
4
|
-
import { CDPClient } from '../client.js';
|
|
5
|
-
import { updateActiveSession } from '../../session-context.js';
|
|
6
|
-
import { type ParsedArgs } from '../types.js';
|
|
7
|
-
|
|
8
|
-
export async function cmdDetect(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
9
|
-
if (parsed.help) {
|
|
10
|
-
console.log(
|
|
11
|
-
'Usage: capture detect\n\n' +
|
|
12
|
-
'Detect CDP port (prioritizes default browser).\n' +
|
|
13
|
-
'Returns JSON with port, app name, and all discovered endpoints.',
|
|
14
|
-
);
|
|
15
|
-
process.exit(0);
|
|
16
|
-
}
|
|
17
|
-
const endpoints = await detectCdpPortsAsync();
|
|
18
|
-
if (endpoints.length === 0) {
|
|
19
|
-
console.error(
|
|
20
|
-
'ERROR: No browser with CDP found.\n\n' +
|
|
21
|
-
'Fix: Start a browser with remote debugging enabled:\n' +
|
|
22
|
-
' Arc: Already enabled by default\n' +
|
|
23
|
-
' Chrome: --remote-debugging-port=9222\n' +
|
|
24
|
-
' Electron apps expose CDP automatically',
|
|
25
|
-
);
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
const preferred = pickPreferredEndpoint(endpoints, getDefaultBrowserId());
|
|
29
|
-
console.log(
|
|
30
|
-
JSON.stringify(
|
|
31
|
-
{
|
|
32
|
-
port: preferred.port,
|
|
33
|
-
app: preferred.app,
|
|
34
|
-
isElectron: preferred.isElectron,
|
|
35
|
-
all: endpoints.map((e) => ({
|
|
36
|
-
port: e.port,
|
|
37
|
-
app: e.app,
|
|
38
|
-
isElectron: e.isElectron,
|
|
39
|
-
})),
|
|
40
|
-
},
|
|
41
|
-
null,
|
|
42
|
-
2,
|
|
43
|
-
),
|
|
44
|
-
);
|
|
45
|
-
console.error(
|
|
46
|
-
`\nFound ${endpoints.length} CDP endpoint${endpoints.length > 1 ? 's' : ''}. Default: port ${preferred.port} (${preferred.app}).` +
|
|
47
|
-
`\n\nNext (most tasks): capture session start --url <your app url>` +
|
|
48
|
-
`\n (one-off): capture list --port ${preferred.port}`,
|
|
49
|
-
);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function cmdList(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
54
|
-
if (parsed.help) {
|
|
55
|
-
console.log(
|
|
56
|
-
'Usage: capture list [--port <port>]\n\n' +
|
|
57
|
-
'List all browser tabs across all CDP endpoints (or a specific port).\n' +
|
|
58
|
-
'Returns JSON array of {id, title, url, port, app}.',
|
|
59
|
-
);
|
|
60
|
-
process.exit(0);
|
|
61
|
-
}
|
|
62
|
-
if (parsed.port) {
|
|
63
|
-
// Single port mode
|
|
64
|
-
const targets = await listTargets(parsed.port);
|
|
65
|
-
const pages = targets.filter((t) => t.type === 'page');
|
|
66
|
-
console.log(JSON.stringify(pages.map((t) => ({
|
|
67
|
-
id: t.id,
|
|
68
|
-
title: t.title,
|
|
69
|
-
url: t.url,
|
|
70
|
-
})), null, 2));
|
|
71
|
-
console.error(
|
|
72
|
-
`\n${pages.length} tab${pages.length !== 1 ? 's' : ''} on port ${parsed.port}.` +
|
|
73
|
-
`\n\nTarget a tab with: --target <id prefix> (first 8 chars sufficient)`,
|
|
74
|
-
);
|
|
75
|
-
} else {
|
|
76
|
-
// All endpoints mode
|
|
77
|
-
const endpoints = await detectCdpPortsAsync();
|
|
78
|
-
if (endpoints.length === 0) {
|
|
79
|
-
console.error('No CDP endpoints found.');
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
const allPages: { id: string; title: string; url: string; port: number; app: string }[] = [];
|
|
83
|
-
await Promise.all(endpoints.map(async (ep) => {
|
|
84
|
-
try {
|
|
85
|
-
const targets = await listTargets(ep.port);
|
|
86
|
-
const pages = targets.filter((t) => t.type === 'page');
|
|
87
|
-
for (const t of pages) {
|
|
88
|
-
allPages.push({ id: t.id, title: t.title, url: t.url, port: ep.port, app: ep.app });
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
// Skip endpoints that fail to respond
|
|
92
|
-
}
|
|
93
|
-
}));
|
|
94
|
-
console.log(JSON.stringify(allPages, null, 2));
|
|
95
|
-
console.error(
|
|
96
|
-
`\n${allPages.length} tab${allPages.length !== 1 ? 's' : ''} across ${endpoints.length} CDP endpoint${endpoints.length !== 1 ? 's' : ''}.` +
|
|
97
|
-
`\n\nTarget a tab with: --target <id prefix> (first 8 chars sufficient)`,
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export async function cmdOpen(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
104
|
-
if (parsed.help) {
|
|
105
|
-
console.log(
|
|
106
|
-
'Usage: capture open <url> [--new] [--port <port>]\n\n' +
|
|
107
|
-
'Open URL in browser (returns tab ID).\n\n' +
|
|
108
|
-
'Example: capture open "https://app.example.com" --new',
|
|
109
|
-
);
|
|
110
|
-
process.exit(0);
|
|
111
|
-
}
|
|
112
|
-
const url = parsed.positional[0];
|
|
113
|
-
if (!url) {
|
|
114
|
-
console.error(
|
|
115
|
-
'ERROR: Missing URL.\n\n' +
|
|
116
|
-
'Usage: capture open <url> [--new] [--port <port>]\n\n' +
|
|
117
|
-
'Example: capture open "https://app.example.com" --new',
|
|
118
|
-
);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
const p = parsed.port ?? (await detectCdpPort());
|
|
122
|
-
const tab = await navigateAndWait(p, url, { forceNew: parsed.new });
|
|
123
|
-
console.log(JSON.stringify({
|
|
124
|
-
id: tab.id,
|
|
125
|
-
title: tab.title,
|
|
126
|
-
url: tab.url,
|
|
127
|
-
port: p,
|
|
128
|
-
}, null, 2));
|
|
129
|
-
console.error(
|
|
130
|
-
`\nOpened: ${tab.title || url}` +
|
|
131
|
-
`\n\nFor a multi-step validation, prefer a session (auto-targets this kind of tab):` +
|
|
132
|
-
`\n capture session start --url "${url}"` +
|
|
133
|
-
`\n\nOr interact one-off against this tab:` +
|
|
134
|
-
`\n capture a11y --port ${p} --target ${tab.id.slice(0, 8)} --interactive` +
|
|
135
|
-
`\n capture screenshot --port ${p} --target ${tab.id.slice(0, 8)}` +
|
|
136
|
-
`\n capture exec "<js>" --port ${p} --target ${tab.id.slice(0, 8)}`,
|
|
137
|
-
);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function cmdResetTab(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
142
|
-
if (parsed.help) {
|
|
143
|
-
console.log(
|
|
144
|
-
'Usage: capture reset-tab <url> [--port <port>]\n\n' +
|
|
145
|
-
'Abandon a stuck/unresponsive tab and open a fresh one.\n' +
|
|
146
|
-
'Updates session context so subsequent commands auto-target the new tab.\n\n' +
|
|
147
|
-
'Example: capture reset-tab "https://www.instagram.com/"',
|
|
148
|
-
);
|
|
149
|
-
process.exit(0);
|
|
150
|
-
}
|
|
151
|
-
const url = parsed.positional[0];
|
|
152
|
-
if (!url) {
|
|
153
|
-
console.error(
|
|
154
|
-
'Usage: capture reset-tab <url> [--port <port>]',
|
|
155
|
-
);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
const p = parsed.port ?? (await detectCdpPort());
|
|
159
|
-
const tab = await openTab(p, url);
|
|
160
|
-
|
|
161
|
-
// Wait for page load
|
|
162
|
-
if (tab.webSocketDebuggerUrl) {
|
|
163
|
-
const client = new CDPClient(tab.webSocketDebuggerUrl);
|
|
164
|
-
await client.waitReady();
|
|
165
|
-
await client.send('Page.enable');
|
|
166
|
-
await new Promise<void>((resolve) => {
|
|
167
|
-
const timer = setTimeout(() => resolve(), 10000);
|
|
168
|
-
client.on('Page.loadEventFired', () => {
|
|
169
|
-
clearTimeout(timer);
|
|
170
|
-
resolve();
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
client.close();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Update session context so subsequent commands auto-target the new tab
|
|
177
|
-
updateActiveSession({ targetId: tab.id });
|
|
178
|
-
|
|
179
|
-
console.log(JSON.stringify({ id: tab.id, url: tab.url, port: p }, null, 2));
|
|
180
|
-
console.error(`\nNew tab opened. Session context updated.`);
|
|
181
|
-
console.error(`Use --target ${tab.id.slice(0, 8)} if passing target explicitly.`);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { recordTraffic, navigateAndRecord } from '../record.js';
|
|
2
|
-
import { withConnection } from '../connection.js';
|
|
3
|
-
import { appendToHarRecording as appendToHar } from '../../har-manager.js';
|
|
4
|
-
import { type ParsedArgs } from '../types.js';
|
|
5
|
-
|
|
6
|
-
export async function cmdRecord(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
7
|
-
if (parsed.help) {
|
|
8
|
-
console.log(
|
|
9
|
-
'Usage: capture record --target <id> [--duration <secs>] [--har-out <path>]\n\n' +
|
|
10
|
-
'Passive HAR recording for the specified duration (default: 10s).',
|
|
11
|
-
);
|
|
12
|
-
process.exit(0);
|
|
13
|
-
}
|
|
14
|
-
if (!parsed.target) {
|
|
15
|
-
console.error(
|
|
16
|
-
'Usage: capture record --target <id> [--duration <secs>] [--har-out <path>]',
|
|
17
|
-
);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const result = await recordTraffic({
|
|
22
|
-
port: parsed.port,
|
|
23
|
-
targetId: parsed.target,
|
|
24
|
-
duration: parsed.duration,
|
|
25
|
-
harOutPath: parsed.harOut,
|
|
26
|
-
});
|
|
27
|
-
console.log(
|
|
28
|
-
JSON.stringify(
|
|
29
|
-
{ entryCount: result.entryCount, harPath: result.harPath },
|
|
30
|
-
null,
|
|
31
|
-
2,
|
|
32
|
-
),
|
|
33
|
-
);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function cmdNavigate(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
38
|
-
if (parsed.help) {
|
|
39
|
-
console.log(
|
|
40
|
-
'Usage: capture navigate <url> [--har-out <path>] [--settle <ms>] [--har <id>]\n\n' +
|
|
41
|
-
'Navigate to URL and record HAR. Appends to session HAR if --har is set.\n\n' +
|
|
42
|
-
'Example: capture navigate "https://app.example.com/dashboard" --settle 3000',
|
|
43
|
-
);
|
|
44
|
-
process.exit(0);
|
|
45
|
-
}
|
|
46
|
-
const url = parsed.positional[0];
|
|
47
|
-
if (!url) {
|
|
48
|
-
console.error(
|
|
49
|
-
'Usage: capture navigate <url> [--har-out <path>] [--settle <ms>]',
|
|
50
|
-
);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const result = await navigateAndRecord({
|
|
55
|
-
port: parsed.port,
|
|
56
|
-
url,
|
|
57
|
-
targetId: parsed.target,
|
|
58
|
-
harOutPath: parsed.har ? undefined : parsed.harOut,
|
|
59
|
-
settle: parsed.settle,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Append to session HAR if active
|
|
63
|
-
if (parsed.har && result.har.log.entries.length > 0) {
|
|
64
|
-
appendToHar(parsed.har, result.har.log.entries);
|
|
65
|
-
console.error(` [har:${parsed.har}] +${result.har.log.entries.length} entries`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
console.log(
|
|
69
|
-
JSON.stringify(
|
|
70
|
-
{
|
|
71
|
-
entryCount: result.entryCount,
|
|
72
|
-
harPath: result.harPath,
|
|
73
|
-
tabUrl: result.tab.url,
|
|
74
|
-
timedOut: result.timedOut,
|
|
75
|
-
},
|
|
76
|
-
null,
|
|
77
|
-
2,
|
|
78
|
-
),
|
|
79
|
-
);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function cmdNetwork(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
84
|
-
if (parsed.help) {
|
|
85
|
-
console.log(
|
|
86
|
-
'Usage: capture network <offline|online> [--target <id>]\n\n' +
|
|
87
|
-
'Toggle network connectivity for a tab. Use "offline" to simulate\n' +
|
|
88
|
-
'network failure (kills WebSocket connections, blocks HTTP requests).\n' +
|
|
89
|
-
'Use "online" to restore connectivity.',
|
|
90
|
-
);
|
|
91
|
-
process.exit(0);
|
|
92
|
-
}
|
|
93
|
-
const mode = parsed.positional[0];
|
|
94
|
-
if (!mode || !['offline', 'online'].includes(mode)) {
|
|
95
|
-
console.error('Usage: capture network <offline|online> [--target <id>]');
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
const offline = mode === 'offline';
|
|
99
|
-
const result = await withConnection(
|
|
100
|
-
parsed,
|
|
101
|
-
async (client) => {
|
|
102
|
-
await client.send('Network.enable');
|
|
103
|
-
await client.send('Network.emulateNetworkConditions', {
|
|
104
|
-
offline,
|
|
105
|
-
latency: offline ? -1 : 0,
|
|
106
|
-
downloadThroughput: offline ? 0 : -1,
|
|
107
|
-
uploadThroughput: offline ? 0 : -1,
|
|
108
|
-
});
|
|
109
|
-
return { network: mode, offline };
|
|
110
|
-
},
|
|
111
|
-
{ settle: 0 },
|
|
112
|
-
);
|
|
113
|
-
console.log(JSON.stringify(result, null, 2));
|
|
114
|
-
if (offline) {
|
|
115
|
-
console.error('\nNetwork disabled. WebSocket connections will drop.');
|
|
116
|
-
console.error('Restore with: capture network online');
|
|
117
|
-
} else {
|
|
118
|
-
console.error('\nNetwork restored.');
|
|
119
|
-
}
|
|
120
|
-
return;
|
|
121
|
-
}
|
package/src/cdp/commands/ui.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import { withConnection } from '../connection.js';
|
|
3
|
-
import { captureScreenshot, autoScreenshot } from '../screenshot.js';
|
|
4
|
-
import { getAccessibilityTree, flattenA11yTree } from '../a11y.js';
|
|
5
|
-
import { clickByName, typeText, focusAndType } from '../../interact.js';
|
|
6
|
-
import { nextStepPath } from '../../session-context.js';
|
|
7
|
-
import { type ParsedArgs } from '../types.js';
|
|
8
|
-
|
|
9
|
-
export async function cmdScreenshot(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
10
|
-
if (parsed.help) {
|
|
11
|
-
console.log(
|
|
12
|
-
'Usage: capture screenshot [--target <id> | --url <pattern>] [--out <path>] [--viewport <preset>] [--height <px>] [--full-page]\n\n' +
|
|
13
|
-
'Capture a screenshot of the targeted tab. Saves to --out or auto-generates a path.\n\n' +
|
|
14
|
-
'Options:\n' +
|
|
15
|
-
' --viewport <preset> Viewport preset (default: desktop)\n' +
|
|
16
|
-
' desktop-wide 1920x1080\n' +
|
|
17
|
-
' desktop 1280x800\n' +
|
|
18
|
-
' tablet 768x1024\n' +
|
|
19
|
-
' mobile 390x844\n' +
|
|
20
|
-
' --height <px> Override viewport height (e.g. 1600)\n' +
|
|
21
|
-
' --full-page Capture the entire scrollable page',
|
|
22
|
-
);
|
|
23
|
-
process.exit(0);
|
|
24
|
-
}
|
|
25
|
-
const VIEWPORTS: Record<string, { width: number; height: number }> = {
|
|
26
|
-
'desktop-wide': { width: 1920, height: 1080 },
|
|
27
|
-
'desktop': { width: 1280, height: 800 },
|
|
28
|
-
'tablet': { width: 768, height: 1024 },
|
|
29
|
-
'mobile': { width: 390, height: 844 },
|
|
30
|
-
};
|
|
31
|
-
const viewportName = parsed.viewport ?? 'desktop';
|
|
32
|
-
const viewport = VIEWPORTS[viewportName];
|
|
33
|
-
if (!viewport) {
|
|
34
|
-
console.error(`Unknown viewport "${viewportName}". Options: ${Object.keys(VIEWPORTS).join(', ')}`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
// Allow --height to override the viewport preset height
|
|
38
|
-
if (parsed.height) {
|
|
39
|
-
viewport.height = parsed.height;
|
|
40
|
-
}
|
|
41
|
-
const result = await withConnection(
|
|
42
|
-
parsed,
|
|
43
|
-
async (client) => {
|
|
44
|
-
const png = await captureScreenshot(client, viewport, { fullPage: parsed.fullPage });
|
|
45
|
-
const outPath = parsed.out
|
|
46
|
-
? parsed.out
|
|
47
|
-
: (nextStepPath('screenshot', 'manual') ?? `/tmp/capture-screenshot-${Date.now()}.png`);
|
|
48
|
-
fs.writeFileSync(outPath, png);
|
|
49
|
-
return { path: outPath, bytes: png.length };
|
|
50
|
-
},
|
|
51
|
-
{ settle: 0 },
|
|
52
|
-
);
|
|
53
|
-
console.log(JSON.stringify(result, null, 2));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function cmdClick(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
58
|
-
if (parsed.help) {
|
|
59
|
-
console.log(
|
|
60
|
-
'Usage: capture click "name" [--role button|link|...] [--no-screenshot]\n\n' +
|
|
61
|
-
'Click an element by its accessible name. Auto-captures a screenshot after clicking.',
|
|
62
|
-
);
|
|
63
|
-
process.exit(0);
|
|
64
|
-
}
|
|
65
|
-
const name = parsed.positional[0];
|
|
66
|
-
if (!name) {
|
|
67
|
-
console.error('Usage: capture click "name" [--role button|link|...] [--no-screenshot]');
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
const clickSettle = parsed.settle !== undefined ? parsed.settle : (parsed.har ? 2500 : 1000);
|
|
71
|
-
const result = await withConnection(
|
|
72
|
-
parsed,
|
|
73
|
-
async (client) => {
|
|
74
|
-
const clickResult = await clickByName(client, name, parsed.role);
|
|
75
|
-
const screenshot = await autoScreenshot(client, 'click', name, parsed.noScreenshot);
|
|
76
|
-
return { ...clickResult, screenshot };
|
|
77
|
-
},
|
|
78
|
-
{ settle: clickSettle },
|
|
79
|
-
);
|
|
80
|
-
console.log(JSON.stringify(result, null, 2));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function cmdType(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
85
|
-
if (parsed.help) {
|
|
86
|
-
console.log(
|
|
87
|
-
'Usage: capture type "text" [--into "field name"] [--no-screenshot]\n\n' +
|
|
88
|
-
'Type text into the focused element, or into a named field with --into.',
|
|
89
|
-
);
|
|
90
|
-
process.exit(0);
|
|
91
|
-
}
|
|
92
|
-
const text = parsed.positional[0];
|
|
93
|
-
if (!text) {
|
|
94
|
-
console.error('Usage: capture type "text" [--into "field name"] [--no-screenshot]');
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
const result = await withConnection(
|
|
98
|
-
parsed,
|
|
99
|
-
async (client) => {
|
|
100
|
-
let field: string | null = null;
|
|
101
|
-
if (parsed.into) {
|
|
102
|
-
await focusAndType(client, parsed.into, text, parsed.role);
|
|
103
|
-
field = parsed.into;
|
|
104
|
-
} else {
|
|
105
|
-
await typeText(client, text);
|
|
106
|
-
}
|
|
107
|
-
const label = field ?? text;
|
|
108
|
-
const screenshot = await autoScreenshot(client, 'type', label, parsed.noScreenshot);
|
|
109
|
-
return { typed: text, field, screenshot };
|
|
110
|
-
},
|
|
111
|
-
{ settle: parsed.settle !== undefined ? parsed.settle : (parsed.har ? 1500 : 500) },
|
|
112
|
-
);
|
|
113
|
-
console.log(JSON.stringify(result, null, 2));
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export async function cmdA11y(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
118
|
-
if (parsed.help) {
|
|
119
|
-
console.log(
|
|
120
|
-
'Usage: capture a11y [--target <id>] [--json] [--interactive]\n\n' +
|
|
121
|
-
'Get the accessibility tree. Use --interactive for interactive elements only.\n' +
|
|
122
|
-
'Use --json for structured JSON output.',
|
|
123
|
-
);
|
|
124
|
-
process.exit(0);
|
|
125
|
-
}
|
|
126
|
-
const result = await withConnection(
|
|
127
|
-
parsed,
|
|
128
|
-
async (client) => {
|
|
129
|
-
const tree = await getAccessibilityTree(client);
|
|
130
|
-
|
|
131
|
-
if (parsed.json) {
|
|
132
|
-
return tree;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Flat text output
|
|
136
|
-
const lines = flattenA11yTree(tree, {
|
|
137
|
-
interactive: parsed.interactive,
|
|
138
|
-
});
|
|
139
|
-
return lines.join('\n');
|
|
140
|
-
},
|
|
141
|
-
{ settle: 0 },
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
if (parsed.json) {
|
|
145
|
-
console.log(JSON.stringify(result, null, 2));
|
|
146
|
-
} else {
|
|
147
|
-
console.log(result);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Hint when a11y tree is sparse
|
|
151
|
-
if (!parsed.json) {
|
|
152
|
-
const lineCount = typeof result === 'string' ? result.split('\n').filter(Boolean).length : 0;
|
|
153
|
-
if (lineCount === 0) {
|
|
154
|
-
console.error(
|
|
155
|
-
'\nNo elements found.' +
|
|
156
|
-
(parsed.interactive ? ' Try without --interactive to see all nodes.' : '') +
|
|
157
|
-
'\n\nIf this page is part of your project, consider adding ARIA attributes:\n' +
|
|
158
|
-
' - role="button|link|textbox" on interactive elements\n' +
|
|
159
|
-
' - aria-label="..." on elements without visible text\n' +
|
|
160
|
-
' - Semantic HTML (<button>, <input>, <nav>) provides roles automatically',
|
|
161
|
-
);
|
|
162
|
-
} else if (parsed.interactive && lineCount < 5) {
|
|
163
|
-
console.error(
|
|
164
|
-
`\nOnly ${lineCount} interactive element${lineCount === 1 ? '' : 's'} found. ` +
|
|
165
|
-
'If this seems low, the page may lack ARIA markup.\n' +
|
|
166
|
-
'Consider adding role and aria-label attributes to interactive elements in your frontend code.',
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return;
|
|
172
|
-
}
|