@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.
@@ -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)).scrollIntoView({
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.0",
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:dev": "vitepress dev 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",
@@ -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
- if (element instanceof HTMLElement) element.focus();
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)).scrollIntoView({
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) => {