@monoes/monobrowse 1.0.9 → 1.0.12
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/README.md +1 -1
- package/dist/src/__tests__/adapters.test.js +5 -2
- package/dist/src/__tests__/adapters.test.js.map +1 -1
- package/dist/src/__tests__/batch-eval-parsing.test.js +4 -2
- package/dist/src/__tests__/batch-eval-parsing.test.js.map +1 -1
- package/dist/src/__tests__/box-model.test.js +9 -5
- package/dist/src/__tests__/box-model.test.js.map +1 -1
- package/dist/src/__tests__/browser-launch.test.d.ts +10 -0
- package/dist/src/__tests__/browser-launch.test.d.ts.map +1 -1
- package/dist/src/__tests__/browser-launch.test.js +17 -10
- package/dist/src/__tests__/browser-launch.test.js.map +1 -1
- package/dist/src/__tests__/cdp-client.test.js +9 -3
- package/dist/src/__tests__/cdp-client.test.js.map +1 -1
- package/dist/src/__tests__/close-browser.test.d.ts +13 -0
- package/dist/src/__tests__/close-browser.test.d.ts.map +1 -1
- package/dist/src/__tests__/close-browser.test.js +40 -26
- package/dist/src/__tests__/close-browser.test.js.map +1 -1
- package/dist/src/__tests__/console-log.test.js +17 -6
- package/dist/src/__tests__/console-log.test.js.map +1 -1
- package/dist/src/__tests__/find.test.js +26 -6
- package/dist/src/__tests__/find.test.js.map +1 -1
- package/dist/src/__tests__/frame-command.test.js +9 -3
- package/dist/src/__tests__/frame-command.test.js.map +1 -1
- package/dist/src/__tests__/open-url-navigate-error.test.js +1 -3
- package/dist/src/__tests__/open-url-navigate-error.test.js.map +1 -1
- package/dist/src/__tests__/output.test.js +1 -1
- package/dist/src/__tests__/output.test.js.map +1 -1
- package/dist/src/__tests__/reap-idle-browser.test.d.ts +15 -0
- package/dist/src/__tests__/reap-idle-browser.test.d.ts.map +1 -0
- package/dist/src/__tests__/reap-idle-browser.test.js +238 -0
- package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -0
- package/dist/src/__tests__/ref-cache.test.js +18 -10
- package/dist/src/__tests__/ref-cache.test.js.map +1 -1
- package/dist/src/__tests__/session-state.test.d.ts +8 -0
- package/dist/src/__tests__/session-state.test.d.ts.map +1 -1
- package/dist/src/__tests__/session-state.test.js +10 -10
- package/dist/src/__tests__/session-state.test.js.map +1 -1
- package/dist/src/__tests__/snapshot.test.js +45 -15
- package/dist/src/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/__tests__/storage-eval.test.js +14 -6
- package/dist/src/__tests__/storage-eval.test.js.map +1 -1
- package/dist/src/__tests__/tabs.test.js +25 -9
- package/dist/src/__tests__/tabs.test.js.map +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts +1 -1
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -1
- package/dist/src/browser/action-builder/analyzer.js +25 -9
- package/dist/src/browser/action-builder/analyzer.js.map +1 -1
- package/dist/src/browser/action-builder/types.d.ts.map +1 -1
- package/dist/src/browser/actions.d.ts +1 -1
- package/dist/src/browser/actions.d.ts.map +1 -1
- package/dist/src/browser/actions.js +16 -6
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/adapters/gemini.d.ts.map +1 -1
- package/dist/src/browser/adapters/gemini.js.map +1 -1
- package/dist/src/browser/adapters/google.d.ts.map +1 -1
- package/dist/src/browser/adapters/google.js.map +1 -1
- package/dist/src/browser/adapters/index.d.ts.map +1 -1
- package/dist/src/browser/adapters/index.js +3 -3
- package/dist/src/browser/adapters/index.js.map +1 -1
- package/dist/src/browser/adapters/instagram.d.ts.map +1 -1
- package/dist/src/browser/adapters/instagram.js.map +1 -1
- package/dist/src/browser/adapters/linkedin.d.ts.map +1 -1
- package/dist/src/browser/adapters/linkedin.js.map +1 -1
- package/dist/src/browser/adapters/microsoft.d.ts.map +1 -1
- package/dist/src/browser/adapters/microsoft.js.map +1 -1
- package/dist/src/browser/adapters/x.d.ts.map +1 -1
- package/dist/src/browser/adapters/x.js.map +1 -1
- package/dist/src/browser/browser.d.ts +30 -0
- package/dist/src/browser/browser.d.ts.map +1 -1
- package/dist/src/browser/browser.js +122 -15
- package/dist/src/browser/browser.js.map +1 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +21 -8
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/console-log.d.ts.map +1 -1
- package/dist/src/browser/console-log.js +2 -2
- package/dist/src/browser/console-log.js.map +1 -1
- package/dist/src/browser/dashboard/server.d.ts.map +1 -1
- package/dist/src/browser/dashboard/server.js +29 -19
- package/dist/src/browser/dashboard/server.js.map +1 -1
- package/dist/src/browser/dashboard/ui.html +153 -165
- package/dist/src/browser/dialog.d.ts.map +1 -1
- package/dist/src/browser/dialog.js +3 -1
- package/dist/src/browser/dialog.js.map +1 -1
- package/dist/src/browser/emulation.d.ts.map +1 -1
- package/dist/src/browser/emulation.js +21 -6
- package/dist/src/browser/emulation.js.map +1 -1
- package/dist/src/browser/find.d.ts +4 -4
- package/dist/src/browser/find.d.ts.map +1 -1
- package/dist/src/browser/find.js +14 -6
- package/dist/src/browser/find.js.map +1 -1
- package/dist/src/browser/har.d.ts.map +1 -1
- package/dist/src/browser/har.js +14 -7
- package/dist/src/browser/har.js.map +1 -1
- package/dist/src/browser/index.d.ts +16 -16
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +16 -16
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/network.d.ts.map +1 -1
- package/dist/src/browser/network.js +14 -4
- package/dist/src/browser/network.js.map +1 -1
- package/dist/src/browser/pdf.d.ts.map +1 -1
- package/dist/src/browser/pdf.js +3 -3
- package/dist/src/browser/pdf.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +10 -5
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/record.d.ts +1 -1
- package/dist/src/browser/record.d.ts.map +1 -1
- package/dist/src/browser/record.js +18 -7
- package/dist/src/browser/record.js.map +1 -1
- package/dist/src/browser/ref-cache.d.ts.map +1 -1
- package/dist/src/browser/ref-cache.js +9 -4
- package/dist/src/browser/ref-cache.js.map +1 -1
- package/dist/src/browser/screenshot.d.ts.map +1 -1
- package/dist/src/browser/screenshot.js +11 -7
- package/dist/src/browser/screenshot.js.map +1 -1
- package/dist/src/browser/session.d.ts +1 -1
- package/dist/src/browser/session.d.ts.map +1 -1
- package/dist/src/browser/session.js +27 -8
- package/dist/src/browser/session.js.map +1 -1
- package/dist/src/browser/snapshot.d.ts +1 -1
- package/dist/src/browser/snapshot.d.ts.map +1 -1
- package/dist/src/browser/snapshot.js +8 -4
- package/dist/src/browser/snapshot.js.map +1 -1
- package/dist/src/browser/storage.d.ts.map +1 -1
- package/dist/src/browser/storage.js.map +1 -1
- package/dist/src/browser/tabs.d.ts.map +1 -1
- package/dist/src/browser/tabs.js +9 -3
- package/dist/src/browser/tabs.js.map +1 -1
- package/dist/src/browser/trace.d.ts.map +1 -1
- package/dist/src/browser/trace.js +3 -3
- package/dist/src/browser/trace.js.map +1 -1
- package/dist/src/browser/types.d.ts.map +1 -1
- package/dist/src/browser/types.js +23 -4
- package/dist/src/browser/types.js.map +1 -1
- package/dist/src/browser/vitals.d.ts.map +1 -1
- package/dist/src/browser/vitals.js +1 -1
- package/dist/src/browser/vitals.js.map +1 -1
- package/dist/src/browser/wait.d.ts.map +1 -1
- package/dist/src/browser/wait.js +11 -5
- package/dist/src/browser/wait.js.map +1 -1
- package/dist/src/cli/action.d.ts.map +1 -1
- package/dist/src/cli/action.js +14 -11
- package/dist/src/cli/action.js.map +1 -1
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +453 -159
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/output.js +5 -5
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/cli/platform.d.ts.map +1 -1
- package/dist/src/cli/platform.js +15 -11
- package/dist/src/cli/platform.js.map +1 -1
- package/dist/src/cli/types.d.ts +1 -1
- package/dist/src/cli/types.d.ts.map +1 -1
- package/dist/src/cli.js +21 -9
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapters.test.ts +15 -8
- package/src/__tests__/batch-eval-parsing.test.ts +4 -2
- package/src/__tests__/box-model.test.ts +9 -5
- package/src/__tests__/browser-launch.test.ts +26 -15
- package/src/__tests__/cdp-client.test.ts +25 -9
- package/src/__tests__/close-browser.test.ts +49 -26
- package/src/__tests__/console-log.test.ts +36 -13
- package/src/__tests__/find.test.ts +58 -20
- package/src/__tests__/frame-command.test.ts +12 -6
- package/src/__tests__/open-url-navigate-error.test.ts +2 -4
- package/src/__tests__/output.test.ts +1 -1
- package/src/__tests__/reap-idle-browser.test.ts +276 -0
- package/src/__tests__/ref-cache.test.ts +25 -12
- package/src/__tests__/session-state.test.ts +40 -28
- package/src/__tests__/snapshot.test.ts +61 -22
- package/src/__tests__/storage-eval.test.ts +30 -16
- package/src/__tests__/tabs.test.ts +26 -10
- package/src/browser/action-builder/analyzer.ts +39 -15
- package/src/browser/action-builder/types.ts +45 -8
- package/src/browser/actions.ts +288 -123
- package/src/browser/adapters/gemini.ts +7 -3
- package/src/browser/adapters/google.ts +5 -3
- package/src/browser/adapters/index.ts +5 -4
- package/src/browser/adapters/instagram.ts +4 -2
- package/src/browser/adapters/linkedin.ts +4 -2
- package/src/browser/adapters/microsoft.ts +9 -5
- package/src/browser/adapters/x.ts +7 -3
- package/src/browser/browser.ts +188 -42
- package/src/browser/cdp.ts +43 -11
- package/src/browser/console-log.ts +24 -7
- package/src/browser/dashboard/server.ts +44 -25
- package/src/browser/dashboard/ui.html +153 -165
- package/src/browser/dialog.ts +25 -8
- package/src/browser/emulation.ts +86 -36
- package/src/browser/find.ts +110 -47
- package/src/browser/har.ts +58 -26
- package/src/browser/index.ts +16 -16
- package/src/browser/network.ts +147 -46
- package/src/browser/pdf.ts +4 -4
- package/src/browser/profiler.ts +23 -10
- package/src/browser/record.ts +38 -19
- package/src/browser/ref-cache.ts +38 -17
- package/src/browser/screenshot.ts +58 -32
- package/src/browser/session.ts +51 -18
- package/src/browser/snapshot.ts +34 -20
- package/src/browser/storage.ts +73 -17
- package/src/browser/tabs.ts +29 -17
- package/src/browser/trace.ts +14 -10
- package/src/browser/types.ts +23 -4
- package/src/browser/vitals.ts +33 -16
- package/src/browser/wait.ts +47 -21
- package/src/cli/action.ts +52 -21
- package/src/cli/commands.ts +1093 -385
- package/src/cli/output.ts +5 -5
- package/src/cli/platform.ts +44 -23
- package/src/cli/types.ts +1 -1
- package/src/cli.ts +34 -18
- package/src/index.ts +2 -2
|
@@ -6,23 +6,21 @@
|
|
|
6
6
|
* validation logic: the CdpClient is a stub that answers the handful of CDP
|
|
7
7
|
* commands the round trip issues.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
|
|
10
|
+
import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
14
|
import type { CdpClient } from '../browser/cdp.js';
|
|
14
|
-
import {
|
|
15
|
-
saveStateFile,
|
|
16
|
-
loadStateFile,
|
|
17
|
-
saveSession,
|
|
18
|
-
loadSession,
|
|
19
|
-
listSessions,
|
|
20
|
-
} from '../browser/session.js';
|
|
15
|
+
import { loadSession, loadStateFile, saveSession, saveStateFile } from '../browser/session.js';
|
|
21
16
|
|
|
22
17
|
const COOKIES = [{ name: 'sid', value: 'abc', domain: 'x.test', path: '/' }];
|
|
23
18
|
|
|
24
19
|
/** Records every CDP command and answers the ones session.ts depends on. */
|
|
25
|
-
function stubClient(): {
|
|
20
|
+
function stubClient(): {
|
|
21
|
+
client: CdpClient;
|
|
22
|
+
calls: Array<{ method: string; params: unknown; sid?: string }>;
|
|
23
|
+
} {
|
|
26
24
|
const calls: Array<{ method: string; params: unknown; sid?: string }> = [];
|
|
27
25
|
const client = {
|
|
28
26
|
send: vi.fn(async (method: string, params: unknown, sid?: string) => {
|
|
@@ -30,8 +28,10 @@ function stubClient(): { client: CdpClient; calls: Array<{ method: string; param
|
|
|
30
28
|
if (method === 'Network.getCookies') return { cookies: COOKIES };
|
|
31
29
|
if (method === 'Runtime.evaluate') {
|
|
32
30
|
const expr = (params as { expression: string }).expression;
|
|
33
|
-
if (expr.includes('Object.entries(localStorage)'))
|
|
34
|
-
|
|
31
|
+
if (expr.includes('Object.entries(localStorage)'))
|
|
32
|
+
return { result: { value: '{"k":"v"}' } };
|
|
33
|
+
if (expr.includes('Object.entries(sessionStorage)'))
|
|
34
|
+
return { result: { value: '{"s":"1"}' } };
|
|
35
35
|
}
|
|
36
36
|
return {};
|
|
37
37
|
}),
|
|
@@ -69,7 +69,7 @@ describe('validateSessionName (via saveSession/loadSession)', () => {
|
|
|
69
69
|
for (const [label, name] of bad) {
|
|
70
70
|
it(`rejects ${label}`, async () => {
|
|
71
71
|
await expect(saveSession(client, 'S1', 'T1', name, 'https://x.test', 'T')).rejects.toThrow(
|
|
72
|
-
/Invalid session name
|
|
72
|
+
/Invalid session name/,
|
|
73
73
|
);
|
|
74
74
|
await expect(loadSession(client, 'S1', name)).rejects.toThrow(/Invalid session name/);
|
|
75
75
|
});
|
|
@@ -77,7 +77,7 @@ describe('validateSessionName (via saveSession/loadSession)', () => {
|
|
|
77
77
|
|
|
78
78
|
it('accepts an ordinary name (fails later, on the missing file, not on validation)', async () => {
|
|
79
79
|
await expect(loadSession(client, 'S1', 'no-such-session-xyz')).rejects.toThrow(
|
|
80
|
-
'Session not found: no-such-session-xyz'
|
|
80
|
+
'Session not found: no-such-session-xyz',
|
|
81
81
|
);
|
|
82
82
|
});
|
|
83
83
|
|
|
@@ -103,7 +103,7 @@ describe('validateFilePath (via saveStateFile/loadStateFile)', () => {
|
|
|
103
103
|
for (const [label, filePath] of bad) {
|
|
104
104
|
it(`rejects ${label}`, async () => {
|
|
105
105
|
await expect(
|
|
106
|
-
saveStateFile(client, 'S1', 'T1', filePath, 'https://x.test', 'T')
|
|
106
|
+
saveStateFile(client, 'S1', 'T1', filePath, 'https://x.test', 'T'),
|
|
107
107
|
).rejects.toThrow(/Invalid file path/);
|
|
108
108
|
await expect(loadStateFile(client, 'S1', filePath)).rejects.toThrow(/Invalid file path/);
|
|
109
109
|
});
|
|
@@ -111,7 +111,9 @@ describe('validateFilePath (via saveStateFile/loadStateFile)', () => {
|
|
|
111
111
|
|
|
112
112
|
it('allows a plain absolute path', async () => {
|
|
113
113
|
const p = join(dir, 'state.json');
|
|
114
|
-
await expect(
|
|
114
|
+
await expect(
|
|
115
|
+
saveStateFile(client, 'S1', 'T1', p, 'https://x.test', 'T'),
|
|
116
|
+
).resolves.toBeUndefined();
|
|
115
117
|
});
|
|
116
118
|
});
|
|
117
119
|
|
|
@@ -164,38 +166,41 @@ describe('state file round trip', () => {
|
|
|
164
166
|
it('rejects a state file whose cookies field is not an array', async () => {
|
|
165
167
|
const { client } = stubClient();
|
|
166
168
|
const p = join(dir, 'bad.json');
|
|
167
|
-
const { writeFile } = await import('fs/promises');
|
|
169
|
+
const { writeFile } = await import('node:fs/promises');
|
|
168
170
|
await writeFile(p, JSON.stringify({ cookies: { name: 'sid' } }));
|
|
169
171
|
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
170
|
-
'Invalid state file: cookies is not an array'
|
|
172
|
+
'Invalid state file: cookies is not an array',
|
|
171
173
|
);
|
|
172
174
|
});
|
|
173
175
|
|
|
174
176
|
it('rejects a state file whose localStorage is an array rather than an object', async () => {
|
|
175
177
|
const { client } = stubClient();
|
|
176
178
|
const p = join(dir, 'bad2.json');
|
|
177
|
-
const { writeFile } = await import('fs/promises');
|
|
179
|
+
const { writeFile } = await import('node:fs/promises');
|
|
178
180
|
await writeFile(p, JSON.stringify({ cookies: [], localStorage: ['nope'] }));
|
|
179
181
|
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
180
|
-
'Invalid state file: localStorage is not a plain object'
|
|
182
|
+
'Invalid state file: localStorage is not a plain object',
|
|
181
183
|
);
|
|
182
184
|
});
|
|
183
185
|
|
|
184
186
|
it('rejects a state file whose sessionStorage is an array', async () => {
|
|
185
187
|
const { client } = stubClient();
|
|
186
188
|
const p = join(dir, 'bad3.json');
|
|
187
|
-
const { writeFile } = await import('fs/promises');
|
|
189
|
+
const { writeFile } = await import('node:fs/promises');
|
|
188
190
|
await writeFile(p, JSON.stringify({ cookies: [], sessionStorage: ['nope'] }));
|
|
189
191
|
await expect(loadStateFile(client, 'S1', p)).rejects.toThrow(
|
|
190
|
-
'Invalid state file: sessionStorage is not a plain object'
|
|
192
|
+
'Invalid state file: sessionStorage is not a plain object',
|
|
191
193
|
);
|
|
192
194
|
});
|
|
193
195
|
|
|
194
196
|
it('accepts a state file with neither storage map present', async () => {
|
|
195
197
|
const { client, calls } = stubClient();
|
|
196
198
|
const p = join(dir, 'minimal.json');
|
|
197
|
-
const { writeFile } = await import('fs/promises');
|
|
198
|
-
await writeFile(
|
|
199
|
+
const { writeFile } = await import('node:fs/promises');
|
|
200
|
+
await writeFile(
|
|
201
|
+
p,
|
|
202
|
+
JSON.stringify({ targetId: 'T', sessionId: 'S', url: '', title: '', cookies: [] }),
|
|
203
|
+
);
|
|
199
204
|
await expect(loadStateFile(client, 'S1', p)).resolves.toMatchObject({ cookies: [] });
|
|
200
205
|
expect(calls.filter((c) => c.method === 'Runtime.evaluate')).toEqual([]);
|
|
201
206
|
});
|
|
@@ -224,7 +229,7 @@ describe('listSessions', () => {
|
|
|
224
229
|
it('lists only .json files, with the extension stripped', async () => {
|
|
225
230
|
const sessionDir = join(dir, '.monomind', 'browser-sessions');
|
|
226
231
|
await mkdir(sessionDir, { recursive: true });
|
|
227
|
-
const { writeFile } = await import('fs/promises');
|
|
232
|
+
const { writeFile } = await import('node:fs/promises');
|
|
228
233
|
await writeFile(join(sessionDir, 'alpha.json'), '{}');
|
|
229
234
|
await writeFile(join(sessionDir, 'beta.json'), '{}');
|
|
230
235
|
await writeFile(join(sessionDir, 'notes.txt'), 'ignore me');
|
|
@@ -236,7 +241,14 @@ describe('listSessions', () => {
|
|
|
236
241
|
it('saveSession writes into the session dir and loadSession reads it back', async () => {
|
|
237
242
|
const mod = await withHome(dir);
|
|
238
243
|
const saver = stubClient();
|
|
239
|
-
const filePath = await mod.saveSession(
|
|
244
|
+
const filePath = await mod.saveSession(
|
|
245
|
+
saver.client,
|
|
246
|
+
'S1',
|
|
247
|
+
'T1',
|
|
248
|
+
'my-session',
|
|
249
|
+
'https://x.test',
|
|
250
|
+
'T',
|
|
251
|
+
);
|
|
240
252
|
expect(filePath).toBe(join(dir, '.monomind', 'browser-sessions', 'my-session.json'));
|
|
241
253
|
expect((await stat(filePath)).mode & 0o777).toBe(0o600);
|
|
242
254
|
expect((await stat(join(dir, '.monomind', 'browser-sessions'))).mode & 0o777).toBe(0o700);
|
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
* resolves against. That transform is pure — the only browser interaction is
|
|
5
5
|
* the AX-tree fetch, which a stub answers from a literal node array.
|
|
6
6
|
*/
|
|
7
|
-
import { describe,
|
|
7
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
8
8
|
import type { CdpClient } from '../browser/cdp.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
captureSnapshot,
|
|
11
|
+
getElementBox,
|
|
12
|
+
getObjectIdForRef,
|
|
13
|
+
resolveRef,
|
|
14
|
+
} from '../browser/snapshot.js';
|
|
10
15
|
import type { ElementRef } from '../browser/types.js';
|
|
11
16
|
|
|
12
17
|
interface AXProp {
|
|
@@ -29,7 +34,7 @@ interface AXNode {
|
|
|
29
34
|
/** Client that serves a fixed AX tree plus location.href / document.title. */
|
|
30
35
|
function axClient(
|
|
31
36
|
nodes: AXNode[],
|
|
32
|
-
extra: { url?: string; title?: string; partial?: AXNode[]; selectorNodeId?: number } = {}
|
|
37
|
+
extra: { url?: string; title?: string; partial?: AXNode[]; selectorNodeId?: number } = {},
|
|
33
38
|
): { client: CdpClient; calls: string[] } {
|
|
34
39
|
const calls: string[] = [];
|
|
35
40
|
const client = {
|
|
@@ -49,8 +54,8 @@ function axClient(
|
|
|
49
54
|
result: {
|
|
50
55
|
value:
|
|
51
56
|
(params as { expression: string }).expression === 'location.href'
|
|
52
|
-
? extra.url ?? 'https://x.test/'
|
|
53
|
-
: extra.title ?? 'Test Page',
|
|
57
|
+
? (extra.url ?? 'https://x.test/')
|
|
58
|
+
: (extra.title ?? 'Test Page'),
|
|
54
59
|
},
|
|
55
60
|
};
|
|
56
61
|
default:
|
|
@@ -80,10 +85,14 @@ describe('captureSnapshot — tree shape', () => {
|
|
|
80
85
|
expect(snap.text).toBe(
|
|
81
86
|
'RootWebArea "Test Page" [ref=e1]\n' +
|
|
82
87
|
' form "Login" [ref=e2]\n' +
|
|
83
|
-
' button "Submit" [ref=e3]'
|
|
88
|
+
' button "Submit" [ref=e3]',
|
|
84
89
|
);
|
|
85
90
|
expect([...snap.refs.keys()]).toEqual(['e1', 'e2', 'e3']);
|
|
86
|
-
expect(snap.refs.get('e3')).toMatchObject({
|
|
91
|
+
expect(snap.refs.get('e3')).toMatchObject({
|
|
92
|
+
role: 'button',
|
|
93
|
+
name: 'Submit',
|
|
94
|
+
backendDOMNodeId: 30,
|
|
95
|
+
});
|
|
87
96
|
expect(snap).toMatchObject({ url: 'https://x.test/', title: 'Test Page' });
|
|
88
97
|
});
|
|
89
98
|
|
|
@@ -157,13 +166,13 @@ describe('captureSnapshot — options', () => {
|
|
|
157
166
|
});
|
|
158
167
|
|
|
159
168
|
it('compact drops indentation and descriptions', async () => {
|
|
160
|
-
const { client } = axClient([
|
|
161
|
-
button(1, 'Go', { description: { value: 'primary action' } }),
|
|
162
|
-
]);
|
|
169
|
+
const { client } = axClient([button(1, 'Go', { description: { value: 'primary action' } })]);
|
|
163
170
|
const spaced = await captureSnapshot(client, 'S1');
|
|
164
171
|
expect(spaced.text).toBe('button "Go" (primary action) [ref=e1]');
|
|
165
172
|
|
|
166
|
-
const { client: c2 } = axClient([
|
|
173
|
+
const { client: c2 } = axClient([
|
|
174
|
+
button(1, 'Go', { description: { value: 'primary action' } }),
|
|
175
|
+
]);
|
|
167
176
|
const compact = await captureSnapshot(c2, 'S1', { compact: true });
|
|
168
177
|
expect(compact.text).toBe('button "Go" [ref=e1]');
|
|
169
178
|
// The description is still available on the ref even when not printed.
|
|
@@ -198,7 +207,13 @@ describe('captureSnapshot — options', () => {
|
|
|
198
207
|
describe('captureSnapshot — property extraction and rendering', () => {
|
|
199
208
|
async function snapOf(props: AXProp[], over: Partial<AXNode> = {}) {
|
|
200
209
|
const { client } = axClient([
|
|
201
|
-
{
|
|
210
|
+
{
|
|
211
|
+
nodeId: 1,
|
|
212
|
+
role: { value: 'textbox' },
|
|
213
|
+
name: { value: 'Email' },
|
|
214
|
+
properties: props,
|
|
215
|
+
...over,
|
|
216
|
+
},
|
|
202
217
|
]);
|
|
203
218
|
return captureSnapshot(client, 'S1');
|
|
204
219
|
}
|
|
@@ -210,14 +225,18 @@ describe('captureSnapshot — property extraction and rendering', () => {
|
|
|
210
225
|
{ name: 'required', value: { value: true } },
|
|
211
226
|
]);
|
|
212
227
|
expect(snap.text).toBe(
|
|
213
|
-
'textbox "Email" [ref=e1, value="a@b.c", placeholder="you@example.com", required]'
|
|
228
|
+
'textbox "Email" [ref=e1, value="a@b.c", placeholder="you@example.com", required]',
|
|
214
229
|
);
|
|
215
230
|
expect(snap.refs.get('e1')).toMatchObject({ value: 'a@b.c', placeholder: 'you@example.com' });
|
|
216
231
|
});
|
|
217
232
|
|
|
218
233
|
it('renders disabled as a bare flag only when true', async () => {
|
|
219
|
-
expect((await snapOf([{ name: 'disabled', value: { value: true } }])).text).toContain(
|
|
220
|
-
|
|
234
|
+
expect((await snapOf([{ name: 'disabled', value: { value: true } }])).text).toContain(
|
|
235
|
+
', disabled]',
|
|
236
|
+
);
|
|
237
|
+
expect((await snapOf([{ name: 'disabled', value: { value: false } }])).text).not.toContain(
|
|
238
|
+
'disabled',
|
|
239
|
+
);
|
|
221
240
|
});
|
|
222
241
|
|
|
223
242
|
it('accepts checked/expanded as either the boolean or the string "true"', async () => {
|
|
@@ -276,16 +295,18 @@ describe('captureSnapshot — property extraction and rendering', () => {
|
|
|
276
295
|
});
|
|
277
296
|
|
|
278
297
|
describe('resolveRef', () => {
|
|
279
|
-
it('returns the ref when present', () => {
|
|
298
|
+
it('returns the ref when present', async () => {
|
|
280
299
|
const refs = new Map<string, ElementRef>([
|
|
281
300
|
['e1', { ref: 'e1', role: 'button', name: 'Go', nodeId: 1 }],
|
|
282
301
|
]);
|
|
283
|
-
expect(resolveRef({} as CdpClient, 'S1', refs, 'e1')).resolves.toMatchObject({
|
|
302
|
+
await expect(resolveRef({} as CdpClient, 'S1', refs, 'e1')).resolves.toMatchObject({
|
|
303
|
+
ref: 'e1',
|
|
304
|
+
});
|
|
284
305
|
});
|
|
285
306
|
|
|
286
307
|
it('throws a snapshot-first hint when the ref is unknown', async () => {
|
|
287
308
|
await expect(resolveRef({} as CdpClient, 'S1', new Map(), 'e9')).rejects.toThrow(
|
|
288
|
-
'Element ref @e9 not found. Run snapshot first.'
|
|
309
|
+
'Element ref @e9 not found. Run snapshot first.',
|
|
289
310
|
);
|
|
290
311
|
});
|
|
291
312
|
});
|
|
@@ -295,7 +316,7 @@ describe('getObjectIdForRef', () => {
|
|
|
295
316
|
const send = vi.fn();
|
|
296
317
|
const client = { send } as unknown as CdpClient;
|
|
297
318
|
await expect(
|
|
298
|
-
getObjectIdForRef(client, 'S1', { ref: 'e1', role: 'button', name: '', nodeId: 1 })
|
|
319
|
+
getObjectIdForRef(client, 'S1', { ref: 'e1', role: 'button', name: '', nodeId: 1 }),
|
|
299
320
|
).resolves.toBeNull();
|
|
300
321
|
expect(send).not.toHaveBeenCalled();
|
|
301
322
|
});
|
|
@@ -304,7 +325,13 @@ describe('getObjectIdForRef', () => {
|
|
|
304
325
|
const send = vi.fn(async () => ({ object: { objectId: 'OBJ' } }));
|
|
305
326
|
const client = { send } as unknown as CdpClient;
|
|
306
327
|
await expect(
|
|
307
|
-
getObjectIdForRef(client, 'S1', {
|
|
328
|
+
getObjectIdForRef(client, 'S1', {
|
|
329
|
+
ref: 'e1',
|
|
330
|
+
role: 'button',
|
|
331
|
+
name: '',
|
|
332
|
+
nodeId: 1,
|
|
333
|
+
backendDOMNodeId: 77,
|
|
334
|
+
}),
|
|
308
335
|
).resolves.toBe('OBJ');
|
|
309
336
|
expect(send.mock.calls[0]).toEqual(['DOM.resolveNode', { backendNodeId: 77 }, 'S1']);
|
|
310
337
|
});
|
|
@@ -312,7 +339,13 @@ describe('getObjectIdForRef', () => {
|
|
|
312
339
|
it('returns null when the node no longer resolves to an object', async () => {
|
|
313
340
|
const client = { send: vi.fn(async () => ({ object: {} })) } as unknown as CdpClient;
|
|
314
341
|
await expect(
|
|
315
|
-
getObjectIdForRef(client, 'S1', {
|
|
342
|
+
getObjectIdForRef(client, 'S1', {
|
|
343
|
+
ref: 'e1',
|
|
344
|
+
role: 'button',
|
|
345
|
+
name: '',
|
|
346
|
+
nodeId: 1,
|
|
347
|
+
backendDOMNodeId: 77,
|
|
348
|
+
}),
|
|
316
349
|
).resolves.toBeNull();
|
|
317
350
|
});
|
|
318
351
|
});
|
|
@@ -325,7 +358,13 @@ describe('getElementBox', () => {
|
|
|
325
358
|
})),
|
|
326
359
|
} as unknown as CdpClient;
|
|
327
360
|
await expect(
|
|
328
|
-
getElementBox(client, 'S1', {
|
|
361
|
+
getElementBox(client, 'S1', {
|
|
362
|
+
ref: 'e1',
|
|
363
|
+
role: 'button',
|
|
364
|
+
name: '',
|
|
365
|
+
nodeId: 1,
|
|
366
|
+
backendDOMNodeId: 5,
|
|
367
|
+
}),
|
|
329
368
|
).resolves.toEqual({ x: 60, y: 40, width: 100, height: 40 });
|
|
330
369
|
});
|
|
331
370
|
});
|
|
@@ -5,24 +5,27 @@
|
|
|
5
5
|
* evaluateJs's error/timeout contract is what this file pins down.
|
|
6
6
|
* Fake client throughout — no browser.
|
|
7
7
|
*/
|
|
8
|
-
import { describe,
|
|
9
|
-
import type { CdpClient } from '../browser/cdp.js';
|
|
8
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
9
|
import { evaluateJs } from '../browser/actions.js';
|
|
10
|
+
import type { CdpClient } from '../browser/cdp.js';
|
|
11
11
|
import {
|
|
12
|
-
getLocalStorageKey,
|
|
13
|
-
setLocalStorageKey,
|
|
14
|
-
removeLocalStorageKey,
|
|
15
12
|
clearLocalStorage,
|
|
13
|
+
clearSessionStorage,
|
|
16
14
|
getAllLocalStorage,
|
|
15
|
+
getAllSessionStorage,
|
|
16
|
+
getLocalStorageKey,
|
|
17
17
|
getSessionStorageKey,
|
|
18
|
-
|
|
18
|
+
removeLocalStorageKey,
|
|
19
19
|
removeSessionStorageKey,
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
setLocalStorageKey,
|
|
21
|
+
setSessionStorageKey,
|
|
22
22
|
} from '../browser/storage.js';
|
|
23
23
|
|
|
24
24
|
/** Client stub returning a fixed Runtime.evaluate value, recording expressions. */
|
|
25
|
-
function stubClient(
|
|
25
|
+
function stubClient(
|
|
26
|
+
value?: unknown,
|
|
27
|
+
exceptionDetails?: unknown,
|
|
28
|
+
): {
|
|
26
29
|
client: CdpClient;
|
|
27
30
|
exprs: string[];
|
|
28
31
|
sids: Array<string | undefined>;
|
|
@@ -33,7 +36,9 @@ function stubClient(value?: unknown, exceptionDetails?: unknown): {
|
|
|
33
36
|
send: vi.fn(async (_m: string, params: { expression: string }, sid?: string) => {
|
|
34
37
|
exprs.push(params.expression);
|
|
35
38
|
sids.push(sid);
|
|
36
|
-
return exceptionDetails
|
|
39
|
+
return exceptionDetails
|
|
40
|
+
? { result: {}, exceptionDetails }
|
|
41
|
+
: { result: { value, type: 'string' } };
|
|
37
42
|
}),
|
|
38
43
|
} as unknown as CdpClient;
|
|
39
44
|
return { client, exprs, sids };
|
|
@@ -58,19 +63,21 @@ describe('evaluateJs', () => {
|
|
|
58
63
|
exception: { description: 'ReferenceError: foo is not defined' },
|
|
59
64
|
});
|
|
60
65
|
await expect(evaluateJs(client, 'S1', 'foo')).rejects.toThrow(
|
|
61
|
-
'JS evaluation error: ReferenceError: foo is not defined'
|
|
66
|
+
'JS evaluation error: ReferenceError: foo is not defined',
|
|
62
67
|
);
|
|
63
68
|
});
|
|
64
69
|
|
|
65
70
|
it('falls back to exceptionDetails.text when there is no description', async () => {
|
|
66
71
|
const { client } = stubClient(undefined, { text: 'Uncaught SyntaxError' });
|
|
67
72
|
await expect(evaluateJs(client, 'S1', '{{')).rejects.toThrow(
|
|
68
|
-
'JS evaluation error: Uncaught SyntaxError'
|
|
73
|
+
'JS evaluation error: Uncaught SyntaxError',
|
|
69
74
|
);
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
it('returns undefined for an expression with no value', async () => {
|
|
73
|
-
const client = {
|
|
78
|
+
const client = {
|
|
79
|
+
send: vi.fn(async () => ({ result: { type: 'undefined' } })),
|
|
80
|
+
} as unknown as CdpClient;
|
|
74
81
|
await expect(evaluateJs(client, 'S1', 'void 0')).resolves.toBeUndefined();
|
|
75
82
|
});
|
|
76
83
|
|
|
@@ -79,14 +86,19 @@ describe('evaluateJs', () => {
|
|
|
79
86
|
// without the race; a 20ms budget proves the timer is armed.
|
|
80
87
|
const client = { send: vi.fn(() => new Promise(() => {})) } as unknown as CdpClient;
|
|
81
88
|
await expect(evaluateJs(client, 'S1', 'new Promise(() => {})', 20)).rejects.toThrow(
|
|
82
|
-
'JS evaluation timed out after 20ms'
|
|
89
|
+
'JS evaluation timed out after 20ms',
|
|
83
90
|
);
|
|
84
91
|
});
|
|
85
92
|
|
|
86
93
|
it('waits indefinitely when the timeout is disabled with 0', async () => {
|
|
87
94
|
let resolveSend: (v: unknown) => void = () => {};
|
|
88
95
|
const client = {
|
|
89
|
-
send: vi.fn(
|
|
96
|
+
send: vi.fn(
|
|
97
|
+
() =>
|
|
98
|
+
new Promise((r) => {
|
|
99
|
+
resolveSend = r;
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
90
102
|
} as unknown as CdpClient;
|
|
91
103
|
const p = evaluateJs(client, 'S1', 'slow()', 0);
|
|
92
104
|
resolveSend({ result: { value: 'done', type: 'string' } });
|
|
@@ -165,7 +177,9 @@ describe('sessionStorage helpers mirror the localStorage ones', () => {
|
|
|
165
177
|
});
|
|
166
178
|
|
|
167
179
|
it('getAll parses, and degrades to {} on garbage', async () => {
|
|
168
|
-
await expect(getAllSessionStorage(stubClient('{"s":"1"}').client, 'S1')).resolves.toEqual({
|
|
180
|
+
await expect(getAllSessionStorage(stubClient('{"s":"1"}').client, 'S1')).resolves.toEqual({
|
|
181
|
+
s: '1',
|
|
182
|
+
});
|
|
169
183
|
await expect(getAllSessionStorage(stubClient('<html>').client, 'S1')).resolves.toEqual({});
|
|
170
184
|
});
|
|
171
185
|
});
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* tabs.ts — target filtering and the CDP command sequences for
|
|
3
3
|
* close/activate/frame-switch. Fake fetch + fake client; no browser.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import type { CdpClient } from '../browser/cdp.js';
|
|
7
|
-
import {
|
|
7
|
+
import { activateTab, closeTab, listTabs, newTab, switchToFrame } from '../browser/tabs.js';
|
|
8
8
|
|
|
9
9
|
const realFetch = globalThis.fetch;
|
|
10
10
|
afterEach(() => {
|
|
@@ -55,7 +55,7 @@ describe('listTabs', () => {
|
|
|
55
55
|
|
|
56
56
|
it('propagates a failed /json/list', async () => {
|
|
57
57
|
globalThis.fetch = vi.fn(() =>
|
|
58
|
-
Promise.resolve(new Response('', { status: 502, statusText: 'Bad Gateway' }))
|
|
58
|
+
Promise.resolve(new Response('', { status: 502, statusText: 'Bad Gateway' })),
|
|
59
59
|
) as unknown as typeof fetch;
|
|
60
60
|
await expect(listTabs(9222)).rejects.toThrow('Failed to fetch targets: Bad Gateway');
|
|
61
61
|
});
|
|
@@ -66,7 +66,7 @@ describe('newTab', () => {
|
|
|
66
66
|
const fn = stubFetch({ id: 'T1', type: 'page' });
|
|
67
67
|
await newTab(9222);
|
|
68
68
|
expect(fn.mock.calls[0]![0]).toBe(
|
|
69
|
-
|
|
69
|
+
`http://127.0.0.1:9222/json/new?${encodeURIComponent('about:blank')}`,
|
|
70
70
|
);
|
|
71
71
|
});
|
|
72
72
|
|
|
@@ -81,7 +81,9 @@ describe('closeTab', () => {
|
|
|
81
81
|
it('sends Target.closeTarget for the given targetId', async () => {
|
|
82
82
|
const { client, calls } = stubClient();
|
|
83
83
|
await closeTab(client, 'S1', 'T9');
|
|
84
|
-
expect(calls).toEqual([
|
|
84
|
+
expect(calls).toEqual([
|
|
85
|
+
{ method: 'Target.closeTarget', params: { targetId: 'T9' }, sid: undefined },
|
|
86
|
+
]);
|
|
85
87
|
});
|
|
86
88
|
});
|
|
87
89
|
|
|
@@ -129,7 +131,10 @@ describe('switchToFrame', () => {
|
|
|
129
131
|
'Runtime.evaluate': { result: { result: { value: null } } },
|
|
130
132
|
'Page.getFrameTree': { frameTree: {} },
|
|
131
133
|
});
|
|
132
|
-
await expect(switchToFrame(client, 'S1', 'div.not-a-frame')).resolves.toEqual({
|
|
134
|
+
await expect(switchToFrame(client, 'S1', 'div.not-a-frame')).resolves.toEqual({
|
|
135
|
+
url: null,
|
|
136
|
+
sessionId: null,
|
|
137
|
+
});
|
|
133
138
|
// The selector is embedded as a JSON string literal, never interpolated raw.
|
|
134
139
|
const expr = (calls[0]!.params as { expression: string }).expression;
|
|
135
140
|
expect(expr).toContain('document.querySelector("div.not-a-frame")');
|
|
@@ -149,9 +154,14 @@ describe('switchToFrame', () => {
|
|
|
149
154
|
const { client, calls } = stubClient({
|
|
150
155
|
'Runtime.evaluate': { result: { result: { value: 'https://f.test/frame' } } },
|
|
151
156
|
'Page.getFrameTree': frameTreeWith('https://f.test/frame'),
|
|
152
|
-
'Target.getTargets': {
|
|
157
|
+
'Target.getTargets': {
|
|
158
|
+
targetInfos: [{ targetId: 'T1', type: 'page', url: 'https://x.test' }],
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({
|
|
162
|
+
url: 'https://f.test/frame',
|
|
163
|
+
sessionId: null,
|
|
153
164
|
});
|
|
154
|
-
await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({ url: 'https://f.test/frame', sessionId: null });
|
|
155
165
|
expect(calls.map((c) => c.method)).not.toContain('Target.attachToTarget');
|
|
156
166
|
});
|
|
157
167
|
|
|
@@ -167,7 +177,10 @@ describe('switchToFrame', () => {
|
|
|
167
177
|
},
|
|
168
178
|
'Target.attachToTarget': { sessionId: 'S-FRAME' },
|
|
169
179
|
});
|
|
170
|
-
await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({
|
|
180
|
+
await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({
|
|
181
|
+
url: 'https://f.test/frame',
|
|
182
|
+
sessionId: 'S-FRAME',
|
|
183
|
+
});
|
|
171
184
|
const attach = calls.find((c) => c.method === 'Target.attachToTarget');
|
|
172
185
|
expect(attach!.params).toEqual({ targetId: 'T-OOPIF', flatten: true });
|
|
173
186
|
});
|
|
@@ -177,7 +190,10 @@ describe('switchToFrame', () => {
|
|
|
177
190
|
'Runtime.evaluate': { result: { result: { value: 'https://f.test/frame' } } },
|
|
178
191
|
'Page.getFrameTree': frameTreeWith('https://other.test/elsewhere'),
|
|
179
192
|
});
|
|
180
|
-
await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({
|
|
193
|
+
await expect(switchToFrame(client, 'S1', 'iframe')).resolves.toEqual({
|
|
194
|
+
url: 'https://f.test/frame',
|
|
195
|
+
sessionId: null,
|
|
196
|
+
});
|
|
181
197
|
expect(calls.map((c) => c.method)).not.toContain('Target.getTargets');
|
|
182
198
|
});
|
|
183
199
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/browser/action-builder/analyzer.ts
|
|
2
|
-
import { spawn } from 'child_process';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
3
|
import type { ActionDef } from './types.js';
|
|
4
4
|
|
|
5
5
|
export interface AnalyzerPage {
|
|
@@ -45,7 +45,15 @@ function claudeCliCall(prompt: string): Promise<string> {
|
|
|
45
45
|
return new Promise((resolve, reject) => {
|
|
46
46
|
const child = spawn(
|
|
47
47
|
'claude',
|
|
48
|
-
[
|
|
48
|
+
[
|
|
49
|
+
'--print',
|
|
50
|
+
'--model',
|
|
51
|
+
'haiku',
|
|
52
|
+
'--strict-mcp-config',
|
|
53
|
+
'--no-session-persistence',
|
|
54
|
+
'--',
|
|
55
|
+
prompt,
|
|
56
|
+
],
|
|
49
57
|
{ stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true },
|
|
50
58
|
);
|
|
51
59
|
let stdout = '';
|
|
@@ -54,11 +62,19 @@ function claudeCliCall(prompt: string): Promise<string> {
|
|
|
54
62
|
const timer = setTimeout(() => {
|
|
55
63
|
if (settled) return;
|
|
56
64
|
settled = true;
|
|
57
|
-
try {
|
|
65
|
+
try {
|
|
66
|
+
child.kill('SIGTERM');
|
|
67
|
+
} catch {
|
|
68
|
+
/* ignore */
|
|
69
|
+
}
|
|
58
70
|
reject(new Error('claude --print timed out after 60s'));
|
|
59
71
|
}, 60_000);
|
|
60
|
-
child.stdout.on('data', (d: Buffer) => {
|
|
61
|
-
|
|
72
|
+
child.stdout.on('data', (d: Buffer) => {
|
|
73
|
+
stdout += d.toString();
|
|
74
|
+
});
|
|
75
|
+
child.stderr.on('data', (d: Buffer) => {
|
|
76
|
+
stderr += d.toString();
|
|
77
|
+
});
|
|
62
78
|
child.on('close', (code) => {
|
|
63
79
|
if (settled) return;
|
|
64
80
|
settled = true;
|
|
@@ -70,7 +86,11 @@ function claudeCliCall(prompt: string): Promise<string> {
|
|
|
70
86
|
if (settled) return;
|
|
71
87
|
settled = true;
|
|
72
88
|
clearTimeout(timer);
|
|
73
|
-
reject(
|
|
89
|
+
reject(
|
|
90
|
+
new Error(
|
|
91
|
+
`claude CLI not found — is Claude Code installed? (${(err as NodeJS.ErrnoException).code})`,
|
|
92
|
+
),
|
|
93
|
+
);
|
|
74
94
|
});
|
|
75
95
|
});
|
|
76
96
|
}
|
|
@@ -103,7 +123,7 @@ ActionDef schema:
|
|
|
103
123
|
export async function analyzePageForAction(
|
|
104
124
|
page: AnalyzerPage,
|
|
105
125
|
task: string,
|
|
106
|
-
|
|
126
|
+
_options: AnalyzerOptions = {},
|
|
107
127
|
): Promise<ActionDef> {
|
|
108
128
|
const url = await page.url();
|
|
109
129
|
const title = await page.evaluate<string>('document.title');
|
|
@@ -119,13 +139,15 @@ export async function analyzePageForAction(
|
|
|
119
139
|
const domContext = `URL: ${url}
|
|
120
140
|
Title: ${title}
|
|
121
141
|
Interactive elements (${elements.length}):
|
|
122
|
-
${elements
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
142
|
+
${elements
|
|
143
|
+
.map((el, i) => {
|
|
144
|
+
const attrs = Object.entries(el.attrs)
|
|
145
|
+
.filter(([, v]) => v)
|
|
146
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
147
|
+
.join(' ');
|
|
148
|
+
return `${i + 1}. <${el.tag}${attrs ? ` ${attrs}` : ''}>${el.text}</${el.tag}>`;
|
|
149
|
+
})
|
|
150
|
+
.join('\n')}`;
|
|
129
151
|
|
|
130
152
|
const fullPrompt = `${SYSTEM_PROMPT}\n\nTask: ${task}\n\nPage context:\n${domContext}`;
|
|
131
153
|
const responseText = await claudeCliCall(fullPrompt);
|
|
@@ -138,7 +160,9 @@ ${elements.map((el, i) => {
|
|
|
138
160
|
}
|
|
139
161
|
|
|
140
162
|
if (!actionDef.id || !actionDef.steps || !Array.isArray(actionDef.steps)) {
|
|
141
|
-
throw new Error(
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Claude returned invalid ActionDef: missing id or steps\n${responseText.slice(0, 500)}`,
|
|
165
|
+
);
|
|
142
166
|
}
|
|
143
167
|
|
|
144
168
|
return actionDef;
|