jev-cdp 0.1.2 → 0.1.4
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 +6 -4
- package/dist/cli.js +249 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,8 +27,8 @@ This is an early experimental port. See [NOTICE.md](NOTICE.md) for source attrib
|
|
|
27
27
|
Run the published CLI without adding it to a project. Bun must be installed for either command:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
bunx jev-cdp@0.1.
|
|
31
|
-
npx -y jev-cdp@0.1.
|
|
30
|
+
bunx jev-cdp@0.1.3 help run
|
|
31
|
+
npx -y jev-cdp@0.1.3 help run
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Chrome with a CDP endpoint and `TYPESAFE_API_KEY` are required for browser runs. FFmpeg is required for `--recording`.
|
|
@@ -96,10 +96,12 @@ bun run run -- run \
|
|
|
96
96
|
--final-state
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
`--recording` captures Chrome's compositor screencast stream for the full goal and renders an H.264 MP4. Because Chrome's native pointer is not part of that stream, the adapter draws a high-contrast cursor that starts at the viewport center, glides to each target, and pulses on clicks. `--interaction-pauses` adds a deterministic delay
|
|
99
|
+
`--recording` captures Chrome's compositor screencast stream for the full goal and renders an H.264 MP4. Because Chrome's native pointer is not part of that stream, the adapter draws a high-contrast cursor that starts at the viewport center, glides to each target, and pulses on clicks. `--interaction-pauses` adds a deterministic delay after opening or loading a page, switching to an attached tab, or changing the URL within a page. Jev chooses during that delay, and the adapter waits only for any time left before acting. The same flag also pauses after moving to a click target and before pressing the mouse. Jev does not choose or observe these delays. `--screenshot` saves the final viewport after the goal stops; when recording is also enabled, it reuses the final screencast frame.
|
|
100
100
|
|
|
101
101
|
`--final-state` adds an AI-oriented semantic snapshot to the final JSON on standard output. It includes the final URL, title, visible text, viewport, scroll state, actionable elements, accessible labels, and control state such as `pressed`, `checked`, `selected`, and `expanded`. Progress remains on standard error, so a coding agent can parse standard output as one JSON object and choose the next bounded goal without another browser observation.
|
|
102
102
|
|
|
103
|
+
The snapshot includes visible controls in direct child iframes, including cross-origin frames. Clicking a link there uses its frame coordinates and checks that the observed control is still current. If that click opens a new tab, Jev switches to it, returns its target ID for the next goal, and keeps the recording and 1120×780 viewport consistent across the switch.
|
|
104
|
+
|
|
103
105
|
## Supply known field values without another LLM
|
|
104
106
|
|
|
105
107
|
For portable QA scenarios, let the planner provide exact test data instead of invoking the Luna fallback. Match a field by its observed accessible label:
|
|
@@ -181,7 +183,7 @@ The context is disposed after evidence capture by default. Combine it with `--ke
|
|
|
181
183
|
| `--tab TARGET_ID` | — | create a new tab | Attach to one exact existing Chrome page target |
|
|
182
184
|
| `tabs` | — | — | Print target IDs, titles, and URLs for open page tabs |
|
|
183
185
|
| `--recording PATH.mp4` | — | off | Record the complete goal with an animated cursor |
|
|
184
|
-
| `--interaction-pauses MS` | — | `0` |
|
|
186
|
+
| `--interaction-pauses MS` | — | `0` | Pause after page loads and before clicks, overlapping page pauses with Jev decisions |
|
|
185
187
|
| `--screenshot PATH.jpg` | — | off | Save the final browser viewport |
|
|
186
188
|
| `--final-state` | — | off | Include the final semantic page state in stdout JSON |
|
|
187
189
|
| `--field-value LABEL=VALUE` | — | Luna fallback | Type caller-provided test data into the exactly labeled field |
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "jev-cdp",
|
|
6
|
-
version: "0.1.
|
|
6
|
+
version: "0.1.4",
|
|
7
7
|
description: "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
|
|
8
8
|
type: "module",
|
|
9
9
|
license: "MIT",
|
|
@@ -257,6 +257,24 @@ var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1
|
|
|
257
257
|
|
|
258
258
|
// src/browser.ts
|
|
259
259
|
var MARKER = `(() => { const state=${snapshot_default}; return state?.marker ?? null; })()`;
|
|
260
|
+
var RECORDING_CURSOR_INIT = `(() => {
|
|
261
|
+
if (window !== window.top) return;
|
|
262
|
+
const mount = () => {
|
|
263
|
+
if (!document.documentElement) return false;
|
|
264
|
+
if (document.getElementById('__jev-recording-cursor')) return true;
|
|
265
|
+
const cursor = document.createElement('div');
|
|
266
|
+
cursor.id = '__jev-recording-cursor';
|
|
267
|
+
cursor.setAttribute('aria-hidden', 'true');
|
|
268
|
+
cursor.innerHTML = '<svg width="28" height="34" viewBox="0 0 28 34" xmlns="http://www.w3.org/2000/svg"><path d="M2 2v25l7-7 5 11 5-2-5-11h10z" fill="white" stroke="#111827" stroke-width="2.5" stroke-linejoin="round"/></svg>';
|
|
269
|
+
Object.assign(cursor.style, {position:'fixed',left:'50vw',top:'50vh',width:'28px',height:'34px',zIndex:'2147483647',pointerEvents:'none',filter:'drop-shadow(0 2px 2px rgba(0,0,0,.35))',transition:'left 180ms cubic-bezier(.2,.8,.2,1), top 180ms cubic-bezier(.2,.8,.2,1)',transform:'translate(-3px,-3px)'});
|
|
270
|
+
document.documentElement.append(cursor);
|
|
271
|
+
return true;
|
|
272
|
+
};
|
|
273
|
+
if (!mount()) {
|
|
274
|
+
const observer = new MutationObserver(() => { if (mount()) observer.disconnect(); });
|
|
275
|
+
observer.observe(document, { childList: true });
|
|
276
|
+
}
|
|
277
|
+
})()`;
|
|
260
278
|
|
|
261
279
|
class StalePageError extends Error {
|
|
262
280
|
}
|
|
@@ -294,8 +312,20 @@ class Browser {
|
|
|
294
312
|
#recordingSequence = 0;
|
|
295
313
|
#recordingWrites = Promise.resolve();
|
|
296
314
|
#stopRecordingEvents;
|
|
315
|
+
#stopPageEvents = [];
|
|
316
|
+
#mainFrameId;
|
|
317
|
+
#pageLoadPromise = null;
|
|
318
|
+
#resolvePageLoad;
|
|
319
|
+
#pauseUntil = 0;
|
|
320
|
+
#frameSessions = new Map;
|
|
321
|
+
#frameContexts = new Map;
|
|
322
|
+
#knownTargets = new Set;
|
|
323
|
+
#openedTabs = [];
|
|
324
|
+
#cdpUrl;
|
|
325
|
+
#switchOnPopup = false;
|
|
297
326
|
constructor(cdp, sessionId, targetId, ownsTarget, browserContextId, options) {
|
|
298
327
|
this.#cdp = cdp;
|
|
328
|
+
this.#cdpUrl = options.cdpUrl;
|
|
299
329
|
this.#sessionId = sessionId;
|
|
300
330
|
this.#targetId = targetId;
|
|
301
331
|
this.#ownsTarget = ownsTarget;
|
|
@@ -353,7 +383,20 @@ class Browser {
|
|
|
353
383
|
flatten: true
|
|
354
384
|
});
|
|
355
385
|
const browser = new Browser(cdp, attached.sessionId, targetId, ownsTarget, browserContextId, options);
|
|
386
|
+
browser.#knownTargets = new Set((await listChromeTargets(options.cdpUrl)).map((target) => target.id));
|
|
356
387
|
try {
|
|
388
|
+
browser.#stopPageEvents.push(cdp.on("Target.attachedToTarget", browser.#sessionId, (params) => {
|
|
389
|
+
const info = params.targetInfo;
|
|
390
|
+
if (info?.type === "iframe" && info.targetId && typeof params.sessionId === "string")
|
|
391
|
+
browser.#frameSessions.set(info.targetId, params.sessionId);
|
|
392
|
+
}));
|
|
393
|
+
browser.#stopPageEvents.push(cdp.on("Page.frameNavigated", browser.#sessionId, (params) => {
|
|
394
|
+
const frame = params.frame;
|
|
395
|
+
if (frame?.id)
|
|
396
|
+
browser.#frameContexts.delete(frame.id);
|
|
397
|
+
}));
|
|
398
|
+
await browser.call("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
399
|
+
await browser.call("Page.enable");
|
|
357
400
|
await browser.call("Emulation.setDeviceMetricsOverride", {
|
|
358
401
|
width: 1120,
|
|
359
402
|
height: 780,
|
|
@@ -361,9 +404,11 @@ class Browser {
|
|
|
361
404
|
mobile: false
|
|
362
405
|
});
|
|
363
406
|
await browser.call("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
407
|
+
await browser.watchPageLoads();
|
|
364
408
|
if (options.url)
|
|
365
409
|
await browser.call("Page.navigate", { url: options.url });
|
|
366
410
|
await browser.waitForReady();
|
|
411
|
+
browser.#pauseUntil = performance.now() + browser.#interactionPauses;
|
|
367
412
|
await browser.startRecording();
|
|
368
413
|
return browser;
|
|
369
414
|
} catch (error) {
|
|
@@ -377,23 +422,83 @@ class Browser {
|
|
|
377
422
|
get targetId() {
|
|
378
423
|
return this.#targetId;
|
|
379
424
|
}
|
|
425
|
+
async watchPageLoads() {
|
|
426
|
+
if (!this.#interactionPauses)
|
|
427
|
+
return;
|
|
428
|
+
await this.call("Page.enable");
|
|
429
|
+
const tree = await this.call("Page.getFrameTree");
|
|
430
|
+
this.#mainFrameId = tree.frameTree.frame.id;
|
|
431
|
+
const loading = (params) => {
|
|
432
|
+
if (params.frameId !== this.#mainFrameId || this.#pageLoadPromise)
|
|
433
|
+
return;
|
|
434
|
+
this.#pageLoadPromise = new Promise((resolve2) => {
|
|
435
|
+
this.#resolvePageLoad = resolve2;
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
const loaded = () => {
|
|
439
|
+
if (!this.#pageLoadPromise)
|
|
440
|
+
return;
|
|
441
|
+
this.#pauseUntil = performance.now() + this.#interactionPauses;
|
|
442
|
+
this.#resolvePageLoad?.();
|
|
443
|
+
this.#resolvePageLoad = undefined;
|
|
444
|
+
this.#pageLoadPromise = null;
|
|
445
|
+
};
|
|
446
|
+
this.#stopPageEvents.push(this.#cdp.on("Page.frameStartedLoading", this.#sessionId, loading), this.#cdp.on("Page.frameNavigated", this.#sessionId, (params) => {
|
|
447
|
+
const frame = params.frame;
|
|
448
|
+
if (frame && frame.id === this.#mainFrameId && !frame.parentId)
|
|
449
|
+
loading({ frameId: frame.id });
|
|
450
|
+
}), this.#cdp.on("Page.loadEventFired", this.#sessionId, loaded), this.#cdp.on("Page.frameStoppedLoading", this.#sessionId, (params) => {
|
|
451
|
+
if (params.frameId === this.#mainFrameId)
|
|
452
|
+
loaded();
|
|
453
|
+
}), this.#cdp.on("Page.navigatedWithinDocument", this.#sessionId, (params) => {
|
|
454
|
+
if (params.frameId === this.#mainFrameId) {
|
|
455
|
+
this.#pauseUntil = performance.now() + this.#interactionPauses;
|
|
456
|
+
this.resetRecordingCursor().catch(() => {
|
|
457
|
+
return;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
async waitForInteractionPause() {
|
|
463
|
+
if (!this.#interactionPauses)
|
|
464
|
+
return;
|
|
465
|
+
while (true) {
|
|
466
|
+
const pageLoad = this.#pageLoadPromise;
|
|
467
|
+
if (pageLoad) {
|
|
468
|
+
await Promise.race([
|
|
469
|
+
pageLoad,
|
|
470
|
+
Bun.sleep(15000).then(() => {
|
|
471
|
+
throw new Error("Page did not finish loading within 15 seconds");
|
|
472
|
+
})
|
|
473
|
+
]);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const remaining = this.#pauseUntil - performance.now();
|
|
477
|
+
if (remaining <= 0)
|
|
478
|
+
return;
|
|
479
|
+
await Bun.sleep(remaining);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
380
482
|
async startRecording() {
|
|
381
483
|
if (!this.#recordingPath)
|
|
382
484
|
return;
|
|
383
485
|
if (!Bun.which("ffmpeg"))
|
|
384
486
|
throw new Error("--recording requires ffmpeg on PATH");
|
|
385
|
-
this.#recordingDirectory
|
|
487
|
+
const firstSegment = !this.#recordingDirectory;
|
|
488
|
+
if (firstSegment)
|
|
489
|
+
this.#recordingDirectory = await mkdtemp(join(tmpdir(), "jev-cdp-recording-"));
|
|
386
490
|
await this.call("Page.enable");
|
|
491
|
+
await this.call("Page.addScriptToEvaluateOnNewDocument", { source: RECORDING_CURSOR_INIT });
|
|
387
492
|
const viewport = await this.evaluate("({width: innerWidth, height: innerHeight})");
|
|
388
493
|
if (!viewport)
|
|
389
494
|
throw new Error("Could not read the recording viewport");
|
|
390
495
|
await this.animateCursor(viewport.width / 2, viewport.height / 2);
|
|
391
496
|
const initial = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 70 });
|
|
392
|
-
const initialPath = join(this.#recordingDirectory, "
|
|
497
|
+
const initialPath = join(this.#recordingDirectory, `${String(this.#recordingSequence++).padStart(6, "0")}.jpg`);
|
|
393
498
|
await Bun.write(initialPath, Buffer.from(initial.data, "base64"));
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
this.#
|
|
499
|
+
if (firstSegment)
|
|
500
|
+
this.#recordingStartedAt = performance.now();
|
|
501
|
+
this.#recordingFrames.push({ path: initialPath, elapsedMs: firstSegment ? 0 : performance.now() - this.#recordingStartedAt });
|
|
397
502
|
this.#stopRecordingEvents = this.#cdp.on("Page.screencastFrame", this.#sessionId, (params) => {
|
|
398
503
|
const frame = params;
|
|
399
504
|
this.call("Page.screencastFrameAck", { sessionId: frame.sessionId }).catch(() => {
|
|
@@ -485,25 +590,37 @@ class Browser {
|
|
|
485
590
|
async animateCursor(x, y, click = false) {
|
|
486
591
|
if (!this.#recordingPath)
|
|
487
592
|
return;
|
|
593
|
+
await this.evaluate(RECORDING_CURSOR_INIT);
|
|
488
594
|
await this.evaluate(`(point => {
|
|
489
|
-
|
|
490
|
-
if (!cursor)
|
|
491
|
-
cursor=document.createElement('div');
|
|
492
|
-
cursor.id='__jev-recording-cursor'; cursor.setAttribute('aria-hidden','true');
|
|
493
|
-
cursor.innerHTML='<svg width="28" height="34" viewBox="0 0 28 34" xmlns="http://www.w3.org/2000/svg"><path d="M2 2v25l7-7 5 11 5-2-5-11h10z" fill="white" stroke="#111827" stroke-width="2.5" stroke-linejoin="round"/></svg>';
|
|
494
|
-
Object.assign(cursor.style,{position:'fixed',left:'50vw',top:'50vh',width:'28px',height:'34px',zIndex:'2147483647',pointerEvents:'none',filter:'drop-shadow(0 2px 2px rgba(0,0,0,.35))',transition:'left 180ms cubic-bezier(.2,.8,.2,1), top 180ms cubic-bezier(.2,.8,.2,1)',transform:'translate(-3px,-3px)'});
|
|
495
|
-
document.documentElement.append(cursor);
|
|
496
|
-
}
|
|
595
|
+
const cursor=document.getElementById('__jev-recording-cursor');
|
|
596
|
+
if (!cursor) return;
|
|
497
597
|
cursor.style.left=point.x+'px'; cursor.style.top=point.y+'px';
|
|
498
598
|
if (point.click) cursor.animate([{transform:'translate(-3px,-3px) scale(1)'},{transform:'translate(-3px,-3px) scale(.72)'},{transform:'translate(-3px,-3px) scale(1)'}],{duration:260,easing:'ease-out'});
|
|
499
599
|
})(${JSON.stringify({ x, y, click })})`);
|
|
500
600
|
await Bun.sleep(click ? 80 : 200);
|
|
501
601
|
}
|
|
602
|
+
async resetRecordingCursor() {
|
|
603
|
+
if (!this.#recordingPath)
|
|
604
|
+
return;
|
|
605
|
+
await this.evaluate(`${RECORDING_CURSOR_INIT}; (() => {
|
|
606
|
+
const cursor = document.getElementById('__jev-recording-cursor');
|
|
607
|
+
if (!cursor) return;
|
|
608
|
+
cursor.style.transition = 'none';
|
|
609
|
+
cursor.style.left = '50vw';
|
|
610
|
+
cursor.style.top = '50vh';
|
|
611
|
+
requestAnimationFrame(() => { cursor.style.transition = 'left 180ms cubic-bezier(.2,.8,.2,1), top 180ms cubic-bezier(.2,.8,.2,1)'; });
|
|
612
|
+
})()`);
|
|
613
|
+
}
|
|
502
614
|
async waitForReady() {
|
|
503
615
|
const deadline = Date.now() + 15000;
|
|
504
616
|
while (Date.now() < deadline) {
|
|
505
|
-
|
|
506
|
-
|
|
617
|
+
try {
|
|
618
|
+
if (!this.#pageLoadPromise && await this.evaluate("document.readyState") === "complete")
|
|
619
|
+
return;
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (!(error instanceof StalePageError))
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
507
624
|
await Bun.sleep(20);
|
|
508
625
|
}
|
|
509
626
|
throw new Error("Page did not finish loading within 15 seconds");
|
|
@@ -514,6 +631,64 @@ class Browser {
|
|
|
514
631
|
throw new StalePageError("Document changed during evaluation");
|
|
515
632
|
return response.result?.value;
|
|
516
633
|
}
|
|
634
|
+
async evaluateFrame(frameId, expression) {
|
|
635
|
+
const session = this.#frameSessions.get(frameId);
|
|
636
|
+
let contextId = this.#frameContexts.get(frameId);
|
|
637
|
+
if (!session && !contextId) {
|
|
638
|
+
const context = await this.call("Page.createIsolatedWorld", { frameId });
|
|
639
|
+
contextId = context.executionContextId;
|
|
640
|
+
this.#frameContexts.set(frameId, contextId);
|
|
641
|
+
}
|
|
642
|
+
const response = await this.#cdp.command("Runtime.evaluate", { expression, returnByValue: true, ...contextId ? { contextId } : {} }, session ?? this.#sessionId);
|
|
643
|
+
if (response.exceptionDetails)
|
|
644
|
+
throw new StalePageError("Frame changed during evaluation");
|
|
645
|
+
return response.result?.value;
|
|
646
|
+
}
|
|
647
|
+
async frameOffset(frameId) {
|
|
648
|
+
const owner = await this.call("DOM.getFrameOwner", { frameId });
|
|
649
|
+
const box = await this.call("DOM.getBoxModel", { backendNodeId: owner.backendNodeId });
|
|
650
|
+
return { x: box.model.content[0], y: box.model.content[1] };
|
|
651
|
+
}
|
|
652
|
+
async childFrames() {
|
|
653
|
+
const tree = await this.call("Page.getFrameTree");
|
|
654
|
+
return (tree.frameTree.childFrames ?? []).map((child) => child.frame.id);
|
|
655
|
+
}
|
|
656
|
+
async discoverTabs() {
|
|
657
|
+
if (!this.#switchOnPopup)
|
|
658
|
+
return;
|
|
659
|
+
for (const target of await listChromeTargets(this.#cdpUrl)) {
|
|
660
|
+
if (target.type !== "page" || this.#knownTargets.has(target.id))
|
|
661
|
+
continue;
|
|
662
|
+
this.#knownTargets.add(target.id);
|
|
663
|
+
const attached = await this.#cdp.command("Target.attachToTarget", { targetId: target.id, flatten: true });
|
|
664
|
+
try {
|
|
665
|
+
await this.#cdp.command("Emulation.setDeviceMetricsOverride", {
|
|
666
|
+
width: 1120,
|
|
667
|
+
height: 780,
|
|
668
|
+
deviceScaleFactor: 1,
|
|
669
|
+
mobile: false
|
|
670
|
+
}, attached.sessionId);
|
|
671
|
+
} finally {
|
|
672
|
+
if (this.#switchOnPopup) {
|
|
673
|
+
if (this.#recordingPath) {
|
|
674
|
+
await this.call("Page.stopScreencast").catch(() => {
|
|
675
|
+
return;
|
|
676
|
+
});
|
|
677
|
+
this.#stopRecordingEvents?.();
|
|
678
|
+
this.#stopRecordingEvents = undefined;
|
|
679
|
+
await this.#recordingWrites;
|
|
680
|
+
}
|
|
681
|
+
this.#sessionId = attached.sessionId;
|
|
682
|
+
this.#targetId = target.id;
|
|
683
|
+
this.#switchOnPopup = false;
|
|
684
|
+
await this.startRecording();
|
|
685
|
+
} else {
|
|
686
|
+
await this.#cdp.command("Target.detachFromTarget", { sessionId: attached.sessionId });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
this.#openedTabs.push({ id: target.id, url: target.url, title: target.title });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
517
692
|
async settleAfterInput() {
|
|
518
693
|
const action = this.#afterInput;
|
|
519
694
|
this.#afterInput = null;
|
|
@@ -549,6 +724,7 @@ class Browser {
|
|
|
549
724
|
}
|
|
550
725
|
async observe(screenshot = this.#screenshots) {
|
|
551
726
|
await this.settleAfterInput();
|
|
727
|
+
await this.discoverTabs();
|
|
552
728
|
let info;
|
|
553
729
|
for (let attempt = 0;attempt < 10; attempt++) {
|
|
554
730
|
try {
|
|
@@ -563,6 +739,36 @@ class Browser {
|
|
|
563
739
|
}
|
|
564
740
|
if (!info)
|
|
565
741
|
throw new StalePageError("Document is navigating");
|
|
742
|
+
if (this.#openedTabs.length) {
|
|
743
|
+
const targets = await listChromeTargets(this.#cdpUrl);
|
|
744
|
+
this.#openedTabs = this.#openedTabs.map((tab) => {
|
|
745
|
+
const current = targets.find((target) => target.id === tab.id);
|
|
746
|
+
return current ? { ...tab, url: current.url, title: current.title } : tab;
|
|
747
|
+
});
|
|
748
|
+
info.text = `${info.text}
|
|
749
|
+
${this.#openedTabs.map((tab) => `Opened new tab: ${tab.title} ${tab.url} (target ${tab.id})`).join(`
|
|
750
|
+
`)}`;
|
|
751
|
+
}
|
|
752
|
+
for (const frameId of await this.childFrames()) {
|
|
753
|
+
try {
|
|
754
|
+
const offset = await this.frameOffset(frameId);
|
|
755
|
+
const child = await this.evaluateFrame(frameId, snapshot_default);
|
|
756
|
+
if (!child)
|
|
757
|
+
continue;
|
|
758
|
+
info.text = `${info.text}
|
|
759
|
+
${child.text}`.slice(0, 6000);
|
|
760
|
+
for (const action of child.actions) {
|
|
761
|
+
if (!action.node || !action.rect)
|
|
762
|
+
continue;
|
|
763
|
+
const rect = { ...action.rect, x: action.rect.x + offset.x, y: action.rect.y + offset.y };
|
|
764
|
+
if (rect.x < 0 || rect.y < 0 || rect.x >= info.w || rect.y >= info.h)
|
|
765
|
+
continue;
|
|
766
|
+
info.actions.push({ ...action, frameId, rect, id: `e${info.actions.length + 1}` });
|
|
767
|
+
info.guards[`${frameId}:${action.node}`] = child.guards[String(action.node)];
|
|
768
|
+
}
|
|
769
|
+
info.marker = [info.marker, frameId, child.marker];
|
|
770
|
+
} catch {}
|
|
771
|
+
}
|
|
566
772
|
const page = { ...info, fingerprint: fingerprint(info) };
|
|
567
773
|
if (screenshot) {
|
|
568
774
|
const capture = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 72 });
|
|
@@ -571,6 +777,12 @@ class Browser {
|
|
|
571
777
|
return page;
|
|
572
778
|
}
|
|
573
779
|
async fresh(page, action) {
|
|
780
|
+
if (action?.frameId && typeof action.node === "number") {
|
|
781
|
+
const current = await this.evaluateFrame(action.frameId, `(() => {
|
|
782
|
+
const c=window.__jevFast; return c ? c.guard(c.nodes.get(${action.node})) : null;
|
|
783
|
+
})()`);
|
|
784
|
+
return stableStringify(current) === stableStringify(page.guards[`${action.frameId}:${action.node}`]);
|
|
785
|
+
}
|
|
574
786
|
if (action && (action.kind === "click" || action.kind === "select")) {
|
|
575
787
|
if (typeof action.node !== "number")
|
|
576
788
|
return false;
|
|
@@ -603,7 +815,11 @@ class Browser {
|
|
|
603
815
|
}
|
|
604
816
|
if (typeof action.node !== "number")
|
|
605
817
|
throw new Error("Invalid observed node");
|
|
606
|
-
const target = await this.
|
|
818
|
+
const target = await (action.frameId ? this.evaluateFrame(action.frameId, `(action => {
|
|
819
|
+
const e=window.__jevFast?.nodes.get(action.node);
|
|
820
|
+
if (!e?.isConnected || !e.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return null;
|
|
821
|
+
const r=e.getBoundingClientRect(); return {x:r.x+r.width/2,y:r.y+r.height/2};
|
|
822
|
+
})(${JSON.stringify(action)})`) : this.evaluate(`(action => {
|
|
607
823
|
const e=window.__jevFast?.nodes.get(action.node);
|
|
608
824
|
if (!e?.isConnected || e.matches(':disabled') || e.closest('[aria-disabled="true"],[inert]') ||
|
|
609
825
|
!e.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return null;
|
|
@@ -619,12 +835,17 @@ class Browser {
|
|
|
619
835
|
e.dispatchEvent(new Event('change',{bubbles:true}));
|
|
620
836
|
}
|
|
621
837
|
return {x,y};
|
|
622
|
-
})(${JSON.stringify(action)})`);
|
|
838
|
+
})(${JSON.stringify(action)})`));
|
|
623
839
|
if (!target) {
|
|
624
840
|
if (action.kind === "select")
|
|
625
841
|
throw new Error("Dropdown execution was not confirmed");
|
|
626
842
|
throw new StalePageError("Target changed or is covered");
|
|
627
843
|
}
|
|
844
|
+
if (action.frameId) {
|
|
845
|
+
const offset = await this.frameOffset(action.frameId);
|
|
846
|
+
target.x += offset.x;
|
|
847
|
+
target.y += offset.y;
|
|
848
|
+
}
|
|
628
849
|
await this.animateCursor(target.x, target.y);
|
|
629
850
|
if (action.kind !== "select") {
|
|
630
851
|
if (action.kind === "click" && this.#interactionPauses > 0) {
|
|
@@ -665,11 +886,18 @@ class Browser {
|
|
|
665
886
|
}
|
|
666
887
|
}
|
|
667
888
|
this.#afterInput = action;
|
|
889
|
+
if (action.frameId && action.kind === "click") {
|
|
890
|
+
this.#switchOnPopup = true;
|
|
891
|
+
await Bun.sleep(650);
|
|
892
|
+
}
|
|
668
893
|
}
|
|
669
894
|
async close() {
|
|
670
895
|
if (this.#closed)
|
|
671
896
|
return;
|
|
672
897
|
this.#closed = true;
|
|
898
|
+
for (const stop of this.#stopPageEvents)
|
|
899
|
+
stop();
|
|
900
|
+
this.#stopPageEvents = [];
|
|
673
901
|
let failure;
|
|
674
902
|
try {
|
|
675
903
|
if (this.#recordingPath)
|
|
@@ -1150,6 +1378,7 @@ class Agent {
|
|
|
1150
1378
|
this.#textCalls.push({ ...helper, field: action.label, value: text });
|
|
1151
1379
|
}
|
|
1152
1380
|
}
|
|
1381
|
+
await this.#browser.waitForInteractionPause();
|
|
1153
1382
|
await this.#browser.act(action, page, text ?? undefined);
|
|
1154
1383
|
this.#pendingText = null;
|
|
1155
1384
|
const entry = {
|
|
@@ -1279,7 +1508,8 @@ Browser behavior:
|
|
|
1279
1508
|
[env: JEV_BROWSER_VISIBLE=1]
|
|
1280
1509
|
--keep-open Leave a runner-created tab or context open.
|
|
1281
1510
|
[env: JEV_BROWSER_KEEP_OPEN=1]
|
|
1282
|
-
--interaction-pauses <ms> Pause after
|
|
1511
|
+
--interaction-pauses <ms> Pause after page loads and before clicks. Jev decisions
|
|
1512
|
+
run during page pauses, so only remaining time is waited.
|
|
1283
1513
|
|
|
1284
1514
|
Known field values:
|
|
1285
1515
|
--field-value <label=value> Type an exact non-secret value when that accessible
|