@olimsaidov/icdp 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frame/index.mjs +30 -23
- package/package.json +7 -3
- package/src/frame/index.ts +43 -24
package/dist/frame/index.mjs
CHANGED
|
@@ -24588,23 +24588,6 @@ function wrapConsoleMethod(fn, type) {
|
|
|
24588
24588
|
Object.defineProperty(wrapped, consoleWrapped, { value: true });
|
|
24589
24589
|
return wrapped;
|
|
24590
24590
|
}
|
|
24591
|
-
function highlight(el) {
|
|
24592
|
-
const rect = el.getBoundingClientRect();
|
|
24593
|
-
const marker = document.createElement("div");
|
|
24594
|
-
marker.style.cssText = [
|
|
24595
|
-
"position:fixed",
|
|
24596
|
-
`left:${rect.left}px`,
|
|
24597
|
-
`top:${rect.top}px`,
|
|
24598
|
-
`width:${rect.width}px`,
|
|
24599
|
-
`height:${rect.height}px`,
|
|
24600
|
-
"z-index:2147483647",
|
|
24601
|
-
"pointer-events:none",
|
|
24602
|
-
"outline:2px solid #005fb8",
|
|
24603
|
-
"background:rgba(0,95,184,.08)"
|
|
24604
|
-
].join(";");
|
|
24605
|
-
document.documentElement.appendChild(marker);
|
|
24606
|
-
setTimeout(() => marker.remove(), 500);
|
|
24607
|
-
}
|
|
24608
24591
|
function setNativeValue(el, value) {
|
|
24609
24592
|
const proto = el instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
|
|
24610
24593
|
(Object.getOwnPropertyDescriptor(proto, "value")?.set)?.call(el, value);
|
|
@@ -24622,7 +24605,6 @@ function canSelectText(el) {
|
|
|
24622
24605
|
function insertText(text) {
|
|
24623
24606
|
const el = document.activeElement;
|
|
24624
24607
|
if (!el) return;
|
|
24625
|
-
highlight(el);
|
|
24626
24608
|
if (el instanceof HTMLInputElement && !canSelectText(el)) {
|
|
24627
24609
|
setNativeValue(el, text);
|
|
24628
24610
|
el.dispatchEvent(new InputEvent("input", {
|
|
@@ -24805,16 +24787,35 @@ function getOuterHTML(params) {
|
|
|
24805
24787
|
}
|
|
24806
24788
|
function focusNode(params) {
|
|
24807
24789
|
const element = elementForBackendId(Number(params.backendNodeId ?? params.nodeId));
|
|
24808
|
-
if (element instanceof HTMLElement) element.focus();
|
|
24790
|
+
if (element instanceof HTMLElement) element.focus({ preventScroll: true });
|
|
24809
24791
|
return {};
|
|
24810
24792
|
}
|
|
24811
24793
|
function scrollIntoViewIfNeeded(params) {
|
|
24812
|
-
elementForBackendId(Number(params.backendNodeId ?? params.nodeId))
|
|
24813
|
-
block: "center",
|
|
24814
|
-
inline: "center"
|
|
24815
|
-
});
|
|
24794
|
+
revealWithinFrame(elementForBackendId(Number(params.backendNodeId ?? params.nodeId)));
|
|
24816
24795
|
return {};
|
|
24817
24796
|
}
|
|
24797
|
+
/**
|
|
24798
|
+
* Bring an element into view by scrolling its own scroll containers and the
|
|
24799
|
+
* frame's viewport — never `Element.scrollIntoView`, which cascades across the
|
|
24800
|
+
* iframe boundary and yanks the embedding page to re-position the frame. Honours
|
|
24801
|
+
* "IfNeeded": a no-op when the element is already on screen, minimal scroll
|
|
24802
|
+
* otherwise.
|
|
24803
|
+
*/
|
|
24804
|
+
function revealWithinFrame(el) {
|
|
24805
|
+
const container = scrollableAncestor(el);
|
|
24806
|
+
if (container instanceof Element && container !== document.scrollingElement) {
|
|
24807
|
+
const c = container.getBoundingClientRect();
|
|
24808
|
+
const r = el.getBoundingClientRect();
|
|
24809
|
+
if (r.top < c.top) container.scrollTop += r.top - c.top;
|
|
24810
|
+
else if (r.bottom > c.bottom) container.scrollTop += r.bottom - c.bottom;
|
|
24811
|
+
if (r.left < c.left) container.scrollLeft += r.left - c.left;
|
|
24812
|
+
else if (r.right > c.right) container.scrollLeft += r.right - c.right;
|
|
24813
|
+
}
|
|
24814
|
+
const r = el.getBoundingClientRect();
|
|
24815
|
+
const dy = r.top < 0 ? r.top : r.bottom > window.innerHeight ? r.bottom - window.innerHeight : 0;
|
|
24816
|
+
const dx = r.left < 0 ? r.left : r.right > window.innerWidth ? r.right - window.innerWidth : 0;
|
|
24817
|
+
if (dx || dy) window.scrollBy(dx, dy);
|
|
24818
|
+
}
|
|
24818
24819
|
function requestChildNodes(params) {
|
|
24819
24820
|
const node = registry.nodeForBackendId(Number(params.nodeId));
|
|
24820
24821
|
if (!node) throw new Error(`No element for backendDOMNodeId=${params.nodeId}`);
|
|
@@ -25074,6 +25075,12 @@ function announce(allowed) {
|
|
|
25074
25075
|
function startFrameAgent(options) {
|
|
25075
25076
|
if (started || window.parent === window) return;
|
|
25076
25077
|
started = true;
|
|
25078
|
+
const revealOnly = function() {
|
|
25079
|
+
revealWithinFrame(this);
|
|
25080
|
+
};
|
|
25081
|
+
Element.prototype.scrollIntoView = revealOnly;
|
|
25082
|
+
const proto = Element.prototype;
|
|
25083
|
+
if (typeof proto.scrollIntoViewIfNeeded === "function") proto.scrollIntoViewIfNeeded = revealOnly;
|
|
25077
25084
|
const allowed = options.allowedParents;
|
|
25078
25085
|
window.addEventListener("message", (event) => {
|
|
25079
25086
|
if (event.source !== window.parent || !isHandshakeMessage(event.data)) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olimsaidov/icdp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Chrome DevTools Protocol over an iframe boundary: drive and inspect embedded (including cross-origin) apps with CDP tools, without a real browser debugging session.",
|
|
5
5
|
"homepage": "https://github.com/olimsaidov/icdp#readme",
|
|
6
6
|
"bugs": "https://github.com/olimsaidov/icdp/issues",
|
|
@@ -45,8 +45,9 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsdown",
|
|
47
47
|
"check": "tsc --noEmit && oxlint && oxfmt --check .",
|
|
48
|
-
"docs:build": "vitepress build docs",
|
|
49
|
-
"docs:
|
|
48
|
+
"docs:build": "node docs/scripts/build-demo-frame.mjs && vitepress build docs",
|
|
49
|
+
"docs:demo-frame": "node docs/scripts/build-demo-frame.mjs",
|
|
50
|
+
"docs:dev": "node docs/scripts/build-demo-frame.mjs && vitepress dev docs",
|
|
50
51
|
"docs:preview": "vitepress preview docs",
|
|
51
52
|
"fmt": "oxfmt .",
|
|
52
53
|
"gen:conformance": "node scripts/gen-conformance.ts",
|
|
@@ -63,9 +64,12 @@
|
|
|
63
64
|
"ws": "^8.21.0"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
67
|
+
"@olimsaidov/agent-browser-wasm": "^0.27.3",
|
|
66
68
|
"@types/aria-query": "^5.0.4",
|
|
67
69
|
"@types/node": "^24",
|
|
68
70
|
"@types/ws": "^8",
|
|
71
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
72
|
+
"@xterm/xterm": "^6.0.0",
|
|
69
73
|
"jsdom": "^29.1.1",
|
|
70
74
|
"mermaid": "^11.15.0",
|
|
71
75
|
"oxfmt": "^0.54.0",
|
package/src/frame/index.ts
CHANGED
|
@@ -382,24 +382,6 @@ function wrapConsoleMethod(fn: ConsoleMethod, type: string): ConsoleMethod {
|
|
|
382
382
|
return wrapped;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
function highlight(el: Element): void {
|
|
386
|
-
const rect = el.getBoundingClientRect();
|
|
387
|
-
const marker = document.createElement("div");
|
|
388
|
-
marker.style.cssText = [
|
|
389
|
-
"position:fixed",
|
|
390
|
-
`left:${rect.left}px`,
|
|
391
|
-
`top:${rect.top}px`,
|
|
392
|
-
`width:${rect.width}px`,
|
|
393
|
-
`height:${rect.height}px`,
|
|
394
|
-
"z-index:2147483647",
|
|
395
|
-
"pointer-events:none",
|
|
396
|
-
"outline:2px solid #005fb8",
|
|
397
|
-
"background:rgba(0,95,184,.08)",
|
|
398
|
-
].join(";");
|
|
399
|
-
document.documentElement.appendChild(marker);
|
|
400
|
-
setTimeout(() => marker.remove(), 500);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
385
|
function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): void {
|
|
404
386
|
const proto =
|
|
405
387
|
el instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
|
|
@@ -414,7 +396,6 @@ function canSelectText(el: HTMLInputElement): boolean {
|
|
|
414
396
|
function insertText(text: string): void {
|
|
415
397
|
const el = document.activeElement as HTMLElement | null;
|
|
416
398
|
if (!el) return;
|
|
417
|
-
highlight(el);
|
|
418
399
|
if (el instanceof HTMLInputElement && !canSelectText(el)) {
|
|
419
400
|
setNativeValue(el, text);
|
|
420
401
|
el.dispatchEvent(
|
|
@@ -665,20 +646,45 @@ function getOuterHTML(params: CdpParams<"DOM.getOuterHTML">): Protocol.DOM.GetOu
|
|
|
665
646
|
|
|
666
647
|
function focusNode(params: CdpParams<"DOM.focus">): Record<string, never> {
|
|
667
648
|
const element = elementForBackendId(Number(params.backendNodeId ?? params.nodeId));
|
|
668
|
-
|
|
649
|
+
// preventScroll: a default focus() scrolls the element into view across every
|
|
650
|
+
// ancestor — including the embedding page across the iframe boundary, yanking
|
|
651
|
+
// the host. Revealing the element is scrollIntoViewIfNeeded's job (and it stays
|
|
652
|
+
// within the frame); focus must not move the parent.
|
|
653
|
+
if (element instanceof HTMLElement) element.focus({ preventScroll: true });
|
|
669
654
|
return {};
|
|
670
655
|
}
|
|
671
656
|
|
|
672
657
|
function scrollIntoViewIfNeeded(
|
|
673
658
|
params: CdpParams<"DOM.scrollIntoViewIfNeeded">,
|
|
674
659
|
): Record<string, never> {
|
|
675
|
-
elementForBackendId(Number(params.backendNodeId ?? params.nodeId))
|
|
676
|
-
block: "center",
|
|
677
|
-
inline: "center",
|
|
678
|
-
});
|
|
660
|
+
revealWithinFrame(elementForBackendId(Number(params.backendNodeId ?? params.nodeId)));
|
|
679
661
|
return {};
|
|
680
662
|
}
|
|
681
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Bring an element into view by scrolling its own scroll containers and the
|
|
666
|
+
* frame's viewport — never `Element.scrollIntoView`, which cascades across the
|
|
667
|
+
* iframe boundary and yanks the embedding page to re-position the frame. Honours
|
|
668
|
+
* "IfNeeded": a no-op when the element is already on screen, minimal scroll
|
|
669
|
+
* otherwise.
|
|
670
|
+
*/
|
|
671
|
+
function revealWithinFrame(el: Element): void {
|
|
672
|
+
const container = scrollableAncestor(el);
|
|
673
|
+
if (container instanceof Element && container !== document.scrollingElement) {
|
|
674
|
+
const c = container.getBoundingClientRect();
|
|
675
|
+
const r = el.getBoundingClientRect();
|
|
676
|
+
if (r.top < c.top) container.scrollTop += r.top - c.top;
|
|
677
|
+
else if (r.bottom > c.bottom) container.scrollTop += r.bottom - c.bottom;
|
|
678
|
+
if (r.left < c.left) container.scrollLeft += r.left - c.left;
|
|
679
|
+
else if (r.right > c.right) container.scrollLeft += r.right - c.right;
|
|
680
|
+
}
|
|
681
|
+
// window.scrollBy scrolls this frame's own document, not the parent's.
|
|
682
|
+
const r = el.getBoundingClientRect();
|
|
683
|
+
const dy = r.top < 0 ? r.top : r.bottom > window.innerHeight ? r.bottom - window.innerHeight : 0;
|
|
684
|
+
const dx = r.left < 0 ? r.left : r.right > window.innerWidth ? r.right - window.innerWidth : 0;
|
|
685
|
+
if (dx || dy) window.scrollBy(dx, dy);
|
|
686
|
+
}
|
|
687
|
+
|
|
682
688
|
function requestChildNodes(params: CdpParams<"DOM.requestChildNodes">): Record<string, never> {
|
|
683
689
|
const node = registry.nodeForBackendId(Number(params.nodeId));
|
|
684
690
|
if (!node) throw new Error(`No element for backendDOMNodeId=${params.nodeId}`);
|
|
@@ -1022,6 +1028,19 @@ export function startFrameAgent(options: FrameAgentOptions): void {
|
|
|
1022
1028
|
if (started || window.parent === window) return;
|
|
1023
1029
|
started = true;
|
|
1024
1030
|
|
|
1031
|
+
// The embedded app shipped this agent to be automated, and automation must
|
|
1032
|
+
// never scroll the *host* page. Element.scrollIntoView cascades across the
|
|
1033
|
+
// iframe boundary — it scrolls every scrollable ancestor, including the
|
|
1034
|
+
// embedding document — and agent-browser evals it to position elements before
|
|
1035
|
+
// clicking. Replace it (and the legacy scrollIntoViewIfNeeded) with a
|
|
1036
|
+
// frame-local reveal so commands move the frame, never the page around it.
|
|
1037
|
+
const revealOnly = function (this: Element): void {
|
|
1038
|
+
revealWithinFrame(this);
|
|
1039
|
+
};
|
|
1040
|
+
Element.prototype.scrollIntoView = revealOnly as Element["scrollIntoView"];
|
|
1041
|
+
const proto = Element.prototype as Element & { scrollIntoViewIfNeeded?: () => void };
|
|
1042
|
+
if (typeof proto.scrollIntoViewIfNeeded === "function") proto.scrollIntoViewIfNeeded = revealOnly;
|
|
1043
|
+
|
|
1025
1044
|
const allowed = options.allowedParents;
|
|
1026
1045
|
|
|
1027
1046
|
window.addEventListener("message", (event) => {
|