@jobshimo/browser-link 0.34.1 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-instructions/content.js +9 -6
- package/dist/agent-instructions/content.js.map +1 -1
- package/dist/bridge/events.d.ts +1 -1
- package/dist/bridge/events.js +8 -1
- package/dist/bridge/events.js.map +1 -1
- package/dist/bridge/ws-bridge.d.ts +17 -0
- package/dist/bridge/ws-bridge.js +34 -0
- package/dist/bridge/ws-bridge.js.map +1 -1
- package/dist/cdp/inpage/builders.d.ts +18 -0
- package/dist/cdp/inpage/builders.js +21 -1
- package/dist/cdp/inpage/builders.js.map +1 -1
- package/dist/cdp/inpage/observer.d.ts +57 -0
- package/dist/cdp/inpage/observer.js +405 -0
- package/dist/cdp/inpage/observer.js.map +1 -0
- package/dist/cdp/observer-host.d.ts +137 -0
- package/dist/cdp/observer-host.js +188 -0
- package/dist/cdp/observer-host.js.map +1 -0
- package/dist/cdp/transport.js +160 -25
- package/dist/cdp/transport.js.map +1 -1
- package/dist/extension/background.js +182 -33
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/inpage/builders.d.ts +18 -0
- package/dist/extension/inpage/builders.js +20 -0
- package/dist/extension/inpage/builders.js.map +1 -1
- package/dist/extension/inpage/observer.d.ts +68 -0
- package/dist/extension/inpage/observer.js +416 -0
- package/dist/extension/inpage/observer.js.map +1 -0
- package/dist/extension/manifest.json +1 -1
- package/dist/extension/observer-host.d.ts +84 -0
- package/dist/extension/observer-host.js +172 -0
- package/dist/extension/observer-host.js.map +1 -0
- package/dist/tools/browser-definitions.js +3 -3
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/server-instructions.js +9 -5
- package/dist/tools/server-instructions.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { buildSnapshotJs, buildFindJs, buildClickResolveJs, buildTypeResolveJs, buildFocusJs, buildStateJs, buildDragProbeJs, buildElementRectJs, } from './inpage/builders.js';
|
|
1
|
+
import { buildSnapshotJs, buildFindJs, buildClickResolveJs, buildTypeResolveJs, buildFocusJs, buildStateJs, buildDragProbeJs, buildElementRectJs, buildWaitForSelectorJs, } from './inpage/builders.js';
|
|
2
|
+
import { OBSERVER_BINDING, buildObserverChangesJs, buildObserverInstallJs, buildObserverMarkJs, buildObserverTeardownJs, } from './inpage/observer.js';
|
|
3
|
+
import { TOP_DOCUMENT_PROBE_JS, applyObserverMessage, createObserverMirror, isTopDocumentMessage, mergeSettleChanges, parseObserverMessage, readObserverMark, } from './observer-host.js';
|
|
2
4
|
import { runScreenshot } from './screenshot.js';
|
|
3
5
|
import { buildKeyEventSequence, resolveKey, modifiersToBitmask, MODIFIER_BITS, } from './keymap.js';
|
|
4
6
|
import { resolveSettleParams, settleSafely } from './settle.js';
|
|
@@ -425,6 +427,171 @@ function sleep(ms) {
|
|
|
425
427
|
function runSettle(tabId, settle) {
|
|
426
428
|
return settleSafely((expression) => evaluateInTab(tabId, expression), settle);
|
|
427
429
|
}
|
|
430
|
+
const OBSERVER_UNAVAILABLE = { ok: false, reason: 'observer-unavailable' };
|
|
431
|
+
const OBSERVER_DOC_CHANGED = { ok: false, reason: 'doc-changed' };
|
|
432
|
+
/** Ceiling for one observer plumbing evaluation: a page frozen by a native
|
|
433
|
+
* dialog never answers `Runtime.evaluate`, and `cleanup` must still be
|
|
434
|
+
* able to detach. */
|
|
435
|
+
const OBSERVER_EVAL_TIMEOUT_MS = 2_000;
|
|
436
|
+
/** Race a CDP call against `OBSERVER_EVAL_TIMEOUT_MS`; rejects on timeout. */
|
|
437
|
+
async function withObserverTimeout(work) {
|
|
438
|
+
let timer;
|
|
439
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
440
|
+
timer = setTimeout(() => {
|
|
441
|
+
reject(new Error(`observer evaluation timed out after ${OBSERVER_EVAL_TIMEOUT_MS}ms`));
|
|
442
|
+
}, OBSERVER_EVAL_TIMEOUT_MS);
|
|
443
|
+
});
|
|
444
|
+
return Promise.race([work, timeout]).finally(() => {
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* `Runtime.evaluate` for page-observer plumbing (mark, changes, probes,
|
|
450
|
+
* teardown). Unlike `evaluateInTab` it passes no `userGesture` — these
|
|
451
|
+
* reads must never hand the page transient user activation it did not
|
|
452
|
+
* earn — and it rejects after `OBSERVER_EVAL_TIMEOUT_MS`.
|
|
453
|
+
*/
|
|
454
|
+
async function evaluateObserverJs(tabId, expression) {
|
|
455
|
+
const result = await withObserverTimeout(cdp(tabId, 'Runtime.evaluate', {
|
|
456
|
+
expression,
|
|
457
|
+
returnByValue: true,
|
|
458
|
+
}));
|
|
459
|
+
if (result.exceptionDetails) {
|
|
460
|
+
const ex = result.exceptionDetails;
|
|
461
|
+
throw new Error(ex.exception?.description ?? ex.text ?? 'Evaluation failed');
|
|
462
|
+
}
|
|
463
|
+
return result.result.value;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Take the page observer's action mark BEFORE an action dispatches (spec v4
|
|
467
|
+
* §5.4). `null` when settle is off for this call (no changes are reported
|
|
468
|
+
* then); an `observer-unavailable` marker when the observer was never
|
|
469
|
+
* installed on this tab or the mark could not be taken.
|
|
470
|
+
*/
|
|
471
|
+
async function takeObserverMark(tabId, settle) {
|
|
472
|
+
if (!settle)
|
|
473
|
+
return null;
|
|
474
|
+
if (tabStates.get(tabId)?.observer?.installed !== true)
|
|
475
|
+
return OBSERVER_UNAVAILABLE;
|
|
476
|
+
try {
|
|
477
|
+
return await evaluateObserverJs(tabId, buildObserverMarkJs());
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return OBSERVER_UNAVAILABLE;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Run the settle wait, then read what the page observer saw since `mark`
|
|
485
|
+
* and fold it into the settle result (`mergeSettleChanges`). Never throws:
|
|
486
|
+
* a changes read that rejects — typically because the action navigated and
|
|
487
|
+
* destroyed the context — is reported as `doc-changed`.
|
|
488
|
+
*/
|
|
489
|
+
async function settleWithChanges(tabId, settle, mark) {
|
|
490
|
+
const settleResult = await runSettle(tabId, settle);
|
|
491
|
+
if (!settleResult)
|
|
492
|
+
return undefined;
|
|
493
|
+
const parsed = readObserverMark(mark);
|
|
494
|
+
let changes = mark ?? OBSERVER_UNAVAILABLE;
|
|
495
|
+
if (parsed) {
|
|
496
|
+
try {
|
|
497
|
+
changes = await evaluateObserverJs(tabId, buildObserverChangesJs(parsed.mark, parsed.doc_id));
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
changes = OBSERVER_DOC_CHANGED;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return mergeSettleChanges(settleResult, changes) ?? undefined;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Handle one page-observer binding message. Summaries only update the
|
|
507
|
+
* mirror. `doc_start` / `nav` are first checked against the top document
|
|
508
|
+
* (the install script also runs in same-process child frames, and the
|
|
509
|
+
* binding is page-visible), then reported to the server as a
|
|
510
|
+
* `tab-navigated` bridge event so it can keep the tab's URL and title
|
|
511
|
+
* current.
|
|
512
|
+
*/
|
|
513
|
+
async function handleObserverMessage(state, payload) {
|
|
514
|
+
const mirror = state.observer;
|
|
515
|
+
if (!mirror)
|
|
516
|
+
return;
|
|
517
|
+
const message = parseObserverMessage(payload);
|
|
518
|
+
if (!message)
|
|
519
|
+
return;
|
|
520
|
+
if (message.type === 'summary') {
|
|
521
|
+
applyObserverMessage(mirror, message);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
let probe;
|
|
525
|
+
try {
|
|
526
|
+
probe = await evaluateObserverJs(state.tabId, TOP_DOCUMENT_PROBE_JS);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!isTopDocumentMessage(probe, message.doc_id))
|
|
532
|
+
return;
|
|
533
|
+
const outcome = applyObserverMessage(mirror, message);
|
|
534
|
+
if (outcome.kind !== 'doc_start' && outcome.kind !== 'nav')
|
|
535
|
+
return;
|
|
536
|
+
let title = '';
|
|
537
|
+
try {
|
|
538
|
+
title = (await chrome.tabs.get(state.tabId)).title ?? '';
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
// Best effort — the tab may be closing; the URL is still worth sending.
|
|
542
|
+
}
|
|
543
|
+
if (state.ws && state.ws.readyState === WebSocket.OPEN && state.serverTabId) {
|
|
544
|
+
send(state.ws, {
|
|
545
|
+
kind: 'bridge.event',
|
|
546
|
+
eventKind: 'tab-navigated',
|
|
547
|
+
tabId: state.serverTabId,
|
|
548
|
+
data: { url: outcome.url, title },
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Install the page observer on a freshly attached tab: the push binding,
|
|
554
|
+
* then the observer as a new-document script that also runs immediately in
|
|
555
|
+
* the current document. Non-fatal — on failure the tab connects as before
|
|
556
|
+
* and actions report `settle.changes: null` with `observer-unavailable`.
|
|
557
|
+
*/
|
|
558
|
+
async function installObserver(state) {
|
|
559
|
+
const mirror = createObserverMirror();
|
|
560
|
+
state.observer = mirror;
|
|
561
|
+
try {
|
|
562
|
+
await cdp(state.tabId, 'Runtime.addBinding', { name: OBSERVER_BINDING });
|
|
563
|
+
const added = await cdp(state.tabId, 'Page.addScriptToEvaluateOnNewDocument', { source: buildObserverInstallJs(), runImmediately: true });
|
|
564
|
+
if (typeof added?.identifier === 'string')
|
|
565
|
+
state.observerScriptId = added.identifier;
|
|
566
|
+
mirror.installed = true;
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
console.warn('browser-link: page observer not installed', err instanceof Error ? err.message : String(err));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/** Best-effort observer removal before the debugger detaches. */
|
|
573
|
+
async function uninstallObserver(state) {
|
|
574
|
+
if (!state.observer?.installed)
|
|
575
|
+
return;
|
|
576
|
+
state.observer.installed = false;
|
|
577
|
+
if (state.observerScriptId !== undefined) {
|
|
578
|
+
try {
|
|
579
|
+
await withObserverTimeout(cdp(state.tabId, 'Page.removeScriptToEvaluateOnNewDocument', {
|
|
580
|
+
identifier: state.observerScriptId,
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
// Best effort — the tab may already be gone.
|
|
585
|
+
}
|
|
586
|
+
state.observerScriptId = undefined;
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
await evaluateObserverJs(state.tabId, buildObserverTeardownJs());
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
// Best effort — the tab may already be gone.
|
|
593
|
+
}
|
|
594
|
+
}
|
|
428
595
|
/**
|
|
429
596
|
* Dispatch the CDP `Input.dispatchKeyEvent` sequence for one resolved key.
|
|
430
597
|
* The event shapes come from `buildKeyEventSequence` (keymap.ts) — pure and
|
|
@@ -472,34 +639,6 @@ function waitForCdpEvent(tabId, method, timeoutMs) {
|
|
|
472
639
|
chrome.debugger.onEvent.addListener(handler);
|
|
473
640
|
});
|
|
474
641
|
}
|
|
475
|
-
/**
|
|
476
|
-
* Build the JS expression that checks a single wait_for condition for a
|
|
477
|
-
* selector. Returns a boolean. Used by `case 'wait_for'` in handleTool.
|
|
478
|
-
*
|
|
479
|
-
* - visible: element exists AND non-zero box AND opacity > 0
|
|
480
|
-
* - hidden: element doesn't exist OR zero box OR opacity 0 OR display:none
|
|
481
|
-
* - attached: element exists in DOM (any state)
|
|
482
|
-
* - detached: element does NOT exist in DOM
|
|
483
|
-
*/
|
|
484
|
-
function buildWaitSelectorExpr(selector, condition) {
|
|
485
|
-
const sel = JSON.stringify(selector);
|
|
486
|
-
if (condition === 'attached') {
|
|
487
|
-
return `Boolean(document.querySelector(${sel}))`;
|
|
488
|
-
}
|
|
489
|
-
if (condition === 'detached') {
|
|
490
|
-
return `!document.querySelector(${sel})`;
|
|
491
|
-
}
|
|
492
|
-
// visible / hidden share the same probe — only the truthiness flips.
|
|
493
|
-
const want = condition === 'hidden' ? '!' : '';
|
|
494
|
-
return `(() => {
|
|
495
|
-
const el = document.querySelector(${sel});
|
|
496
|
-
if (!el) return ${condition === 'hidden' ? 'true' : 'false'};
|
|
497
|
-
const s = getComputedStyle(el);
|
|
498
|
-
if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') return ${condition === 'hidden' ? 'true' : 'false'};
|
|
499
|
-
const r = el.getBoundingClientRect();
|
|
500
|
-
return ${want}(r.width > 0 && r.height > 0);
|
|
501
|
-
})()`;
|
|
502
|
-
}
|
|
503
642
|
// Convert an arbitrary console arg (`unknown`) into something printable
|
|
504
643
|
// without leaking `[object Object]`. Used by the console buffer when CDP
|
|
505
644
|
// hands us values we don't statically know.
|
|
@@ -649,6 +788,11 @@ function attachDebuggerListener(state) {
|
|
|
649
788
|
}
|
|
650
789
|
return;
|
|
651
790
|
}
|
|
791
|
+
if (method === 'Runtime.bindingCalled' &&
|
|
792
|
+
params.name === OBSERVER_BINDING) {
|
|
793
|
+
void handleObserverMessage(state, params.payload ?? '');
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
652
796
|
if (method === 'Runtime.bindingCalled') {
|
|
653
797
|
// Fired for EVERY CDP binding on the tab, not just ours. Two gates
|
|
654
798
|
// before a payload is even parsed: the per-session binding name must
|
|
@@ -863,6 +1007,7 @@ async function performClick(tabId, params) {
|
|
|
863
1007
|
}
|
|
864
1008
|
return { ok: false, error: `Element not found: ${selector}` };
|
|
865
1009
|
}
|
|
1010
|
+
const mark = await takeObserverMark(tabId, settle);
|
|
866
1011
|
const baseline = await beginExpect(evaluate, expectSpec);
|
|
867
1012
|
await cdp(tabId, 'Input.dispatchMouseEvent', {
|
|
868
1013
|
type: 'mouseMoved',
|
|
@@ -883,7 +1028,7 @@ async function performClick(tabId, params) {
|
|
|
883
1028
|
button: 'left',
|
|
884
1029
|
clickCount: 1,
|
|
885
1030
|
});
|
|
886
|
-
const settleResult = await
|
|
1031
|
+
const settleResult = await settleWithChanges(tabId, settle, mark);
|
|
887
1032
|
const verdict = await verifyExpect(evaluate, expectSpec, baseline, settleResult);
|
|
888
1033
|
if (!verdict.ok)
|
|
889
1034
|
return { ok: false, error: verdict.error };
|
|
@@ -918,9 +1063,10 @@ async function performType(tabId, params) {
|
|
|
918
1063
|
return { ok: false, error: `Element not found: ${selector}` };
|
|
919
1064
|
}
|
|
920
1065
|
}
|
|
1066
|
+
const mark = await takeObserverMark(tabId, settle);
|
|
921
1067
|
const baseline = await beginExpect(evaluate, expectSpec);
|
|
922
1068
|
await cdp(tabId, 'Input.insertText', { text });
|
|
923
|
-
const settleResult = await
|
|
1069
|
+
const settleResult = await settleWithChanges(tabId, settle, mark);
|
|
924
1070
|
const verdict = await verifyExpect(evaluate, expectSpec, baseline, settleResult);
|
|
925
1071
|
if (!verdict.ok)
|
|
926
1072
|
return { ok: false, error: verdict.error };
|
|
@@ -959,9 +1105,10 @@ async function performPress(tabId, params) {
|
|
|
959
1105
|
return { ok: false, error: `Element not found: ${params.selector}` };
|
|
960
1106
|
}
|
|
961
1107
|
}
|
|
1108
|
+
const mark = await takeObserverMark(tabId, settle);
|
|
962
1109
|
const baseline = await beginExpect(evaluate, expectSpec);
|
|
963
1110
|
await dispatchKeyEvent(tabId, def, modifiers);
|
|
964
|
-
const settleResult = await
|
|
1111
|
+
const settleResult = await settleWithChanges(tabId, settle, mark);
|
|
965
1112
|
const verdict = await verifyExpect(evaluate, expectSpec, baseline, settleResult);
|
|
966
1113
|
if (!verdict.ok)
|
|
967
1114
|
return { ok: false, error: verdict.error };
|
|
@@ -1010,7 +1157,7 @@ shouldCancel) {
|
|
|
1010
1157
|
// null and we return matched=false immediately.
|
|
1011
1158
|
let check = null;
|
|
1012
1159
|
if (waitSelector !== undefined) {
|
|
1013
|
-
const expr =
|
|
1160
|
+
const expr = buildWaitForSelectorJs(waitSelector, waitCondition);
|
|
1014
1161
|
check = async () => {
|
|
1015
1162
|
try {
|
|
1016
1163
|
const result = await evaluateInTab(tabId, expr);
|
|
@@ -2229,6 +2376,7 @@ async function cleanup(tabId) {
|
|
|
2229
2376
|
}
|
|
2230
2377
|
}
|
|
2231
2378
|
if (state.debuggerAttached) {
|
|
2379
|
+
await uninstallObserver(state);
|
|
2232
2380
|
try {
|
|
2233
2381
|
await chrome.debugger.detach({ tabId });
|
|
2234
2382
|
}
|
|
@@ -2486,6 +2634,7 @@ async function doConnectTab(tabId) {
|
|
|
2486
2634
|
error: `Could not enable CDP: ${err instanceof Error ? err.message : String(err)}`,
|
|
2487
2635
|
};
|
|
2488
2636
|
}
|
|
2637
|
+
await installObserver(state);
|
|
2489
2638
|
const ws = new WebSocket(WS_URL);
|
|
2490
2639
|
state.ws = ws;
|
|
2491
2640
|
// Capture the fields we'll send over the wire NOW that we've already
|