waygraph 0.12.39 → 0.12.40

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/cli.js CHANGED
@@ -808,59 +808,11 @@ async function installOverlay(page, title) {
808
808
  const focus = !!opts.focus;
809
809
  const color = opts.color || "";
810
810
  const zoom = opts.zoom != null && opts.zoom > 1 ? opts.zoom : 1;
811
- const followMouse = opts.followMouse !== false && zoom > 1;
812
811
  const camMs = opts.camMs != null ? opts.camMs : 700;
813
- let settled = zoom <= 1;
814
- const settleAt = Date.now() + camMs;
815
812
  const style = { size, weight, focus, color };
816
- // Screen Studio / Screenify: dead zone follow (see followTick).
817
- const deadFrac = 0.5;
818
- let lastPanAt = 0;
819
- const readCursorViewport = () => {
820
- const cursor = document.getElementById("wg-cursor");
821
- if (cursor && cursor.style.opacity !== "0") {
822
- const r = cursor.getBoundingClientRect();
823
- if (r.width || r.height || r.left || r.top) {
824
- return { x: r.left, y: r.top };
825
- }
826
- }
827
- if (window.__wgCursorPos) return window.__wgCursorPos;
828
- return null;
829
- };
830
- const followTick = () => {
831
- if (!followMouse || !settled || window.__wgCameraLock) return;
832
- if (!window.__wgSetCamera || !window.__wgViewportToDoc) return;
833
- const cam = window.__wgCamera;
834
- if (!cam || !(cam.scale > 1.001)) return;
835
- // Skip while the demo cursor is mid CSS travel - reading a
836
- // mid-lerp rect makes the camera chase noise and teleport.
837
- if (window.__wgCursorTravelUntil && Date.now() < window.__wgCursorTravelUntil) {
838
- return;
839
- }
840
- const pos = readCursorViewport();
841
- if (!pos) return;
842
- const vw = window.innerWidth;
843
- const vh = window.innerHeight;
844
- const dx = pos.x - vw / 2;
845
- const dy = pos.y - vh / 2;
846
- const deadX = (vw * deadFrac) / 2;
847
- const deadY = (vh * deadFrac) / 2;
848
- let ox = 0;
849
- let oy = 0;
850
- if (dx > deadX) ox = dx - deadX;
851
- else if (dx < -deadX) ox = dx + deadX;
852
- if (dy > deadY) oy = dy - deadY;
853
- else if (dy < -deadY) oy = dy + deadY;
854
- if (ox === 0 && oy === 0) return;
855
- const now = Date.now();
856
- if (now - lastPanAt < 120) return;
857
- lastPanAt = now;
858
- // Partial gain - never yank the full overflow in one pan.
859
- const gain = 0.35;
860
- const nx = cam.cx + (ox / cam.scale) * gain;
861
- const ny = cam.cy + (oy / cam.scale) * gain;
862
- window.__wgSetCamera(nx, ny, cam.scale, 280);
863
- };
813
+ // Auto-center only. Never chase the host mouse (that fights the
814
+ // camera). Playwright's real mouse is driven separately by the CLI.
815
+ window.__wgIgnoreHostMouse = true;
864
816
  const tick = () => {
865
817
  const el = document.querySelector(sel);
866
818
  if (el && window.__wgPositionRing) {
@@ -872,8 +824,6 @@ async function installOverlay(page, title) {
872
824
  style,
873
825
  );
874
826
  }
875
- if (!settled && Date.now() >= settleAt) settled = true;
876
- followTick();
877
827
  window.__wgRingRaf = requestAnimationFrame(tick);
878
828
  };
879
829
  if (zoom > 1 && window.__wgSetCamera) {
@@ -882,41 +832,25 @@ async function installOverlay(page, title) {
882
832
  const already =
883
833
  window.__wgCamera && window.__wgCamera.scale > 1.001;
884
834
  const dur = already ? Math.min(camMs, 420) : camMs;
885
- // Focus must be document-space, not raw getBoundingClientRect
886
- // (viewport) - otherwise already-zoomed pans fight the cursor.
887
835
  const pt = window.__wgElDocPoint
888
836
  ? window.__wgElDocPoint(el0)
889
837
  : {
890
838
  x: el0.getBoundingClientRect().left + el0.getBoundingClientRect().width / 2,
891
839
  y: el0.getBoundingClientRect().top + el0.getBoundingClientRect().height / 2,
892
840
  };
841
+ // Lock for the whole ring - host mouse cannot steer.
893
842
  window.__wgCameraLock = true;
894
843
  window.__wgSetCamera(pt.x, pt.y, zoom, dur);
895
- // Unlock for dead-zone follow only when explicitly allowed.
896
- // zoomOut:false / cart-edge targets stay locked through dwell+click.
897
- if (followMouse && opts.allowFollow) {
898
- setTimeout(() => {
899
- window.__wgCameraLock = false;
900
- }, dur + 40);
901
- }
902
844
  }
903
845
  }
904
846
  window.__wgRingRaf = requestAnimationFrame(tick);
905
- // Real mouse only seeds cursor pos; dead-zone follow runs in RAF.
906
- const onMove = (ev) => {
907
- window.__wgCursorPos = { x: ev.clientX, y: ev.clientY };
908
- };
909
- window.addEventListener("mousemove", onMove, { passive: true });
910
- window.__wgRingTrack = onMove;
847
+ // Do NOT listen to host mousemove - it was steering the camera.
848
+ window.__wgRingTrack = null;
911
849
  window.__wgStopHighlightTrack = () => {
912
850
  if (window.__wgRingRaf) {
913
851
  cancelAnimationFrame(window.__wgRingRaf);
914
852
  window.__wgRingRaf = null;
915
853
  }
916
- if (window.__wgRingTrack) {
917
- window.removeEventListener("mousemove", window.__wgRingTrack);
918
- window.__wgRingTrack = null;
919
- }
920
854
  window.__wgStopHighlightTrack = null;
921
855
  };
922
856
  };
@@ -1653,18 +1587,10 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1653
1587
  }
1654
1588
  const z = h.zoom != null && Number(h.zoom) > 1 ? Number(h.zoom) : 1;
1655
1589
  const camMs = 700;
1656
- const followMouse =
1657
- h.followMouse !== undefined
1658
- ? !!h.followMouse
1659
- : h.follow !== undefined
1660
- ? !!h.follow
1661
- : z > 1;
1662
- // Track/zoom first, then park the demo cursor on the LIVE rect.
1663
- // Placing the cursor on a pre-zoom boundingBox leaves it stranded after
1664
- // body scale (html cursor is outside the transform).
1590
+ // Camera auto-centers on the ring target. Host mouse never steers.
1665
1591
  await page
1666
1592
  .evaluate(
1667
- ({ sel, label, tone, size, weight, focus, color, zoom, followMouse, camMs, allowFollow }) => {
1593
+ ({ sel, label, tone, size, weight, focus, color, zoom, camMs }) => {
1668
1594
  if (window.__wgStartHighlightTrack) {
1669
1595
  window.__wgStartHighlightTrack({
1670
1596
  sel,
@@ -1675,9 +1601,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1675
1601
  focus,
1676
1602
  color,
1677
1603
  zoom,
1678
- followMouse,
1679
1604
  camMs,
1680
- allowFollow,
1681
1605
  });
1682
1606
  }
1683
1607
  },
@@ -1690,11 +1614,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1690
1614
  focus: !!h.focus,
1691
1615
  color: h.color || h.labelColor || h.textColor || "",
1692
1616
  zoom: z,
1693
- followMouse,
1694
1617
  camMs,
1695
- // Follow only while zoomed-out-between-rings is OK. zoomOut:false
1696
- // means hold the frame for the upcoming click - no chase.
1697
- allowFollow: followMouse && h.zoomOut !== false,
1698
1618
  },
1699
1619
  )
1700
1620
  .catch(() => {});
@@ -1704,19 +1624,18 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1704
1624
  const waitMs = alreadyZoomed ? Math.min(camMs, 420) : camMs;
1705
1625
  await new Promise((res) => setTimeout(res, waitMs));
1706
1626
  if (h.cursor !== false) {
1707
- await page
1708
- .evaluate(({ sel, ms }) => {
1627
+ // Park Playwright real mouse + demo cursor on the LIVE centered target.
1628
+ const live = await page
1629
+ .evaluate((sel) => {
1709
1630
  const el = document.querySelector(sel);
1710
- if (!el || !window.__wgMoveCursorTo) return;
1711
- window.__wgCameraLock = true;
1631
+ if (!el) return null;
1712
1632
  const r = el.getBoundingClientRect();
1713
- window.__wgMoveCursorTo(r.left + r.width / 2, r.top + r.height / 2, ms, false);
1714
- setTimeout(() => {
1715
- window.__wgCameraLock = false;
1716
- }, ms + 40);
1717
- }, { sel: h.selector, ms: Math.min(280, camMs) })
1718
- .catch(() => {});
1719
- await new Promise((res) => setTimeout(res, Math.min(280, camMs)));
1633
+ return { x: r.left, y: r.top, width: r.width, height: r.height };
1634
+ }, h.selector)
1635
+ .catch(() => null);
1636
+ if (live) {
1637
+ await moveCursorTo(page, live, Math.min(280, camMs), { keepLock: true });
1638
+ }
1720
1639
  }
1721
1640
  // Appear settle
1722
1641
  await new Promise((res) => setTimeout(res, fadeMs));
@@ -2489,19 +2408,27 @@ async function moveCursorTo(page, box, ms, opts) {
2489
2408
  const y = box.y + box.height / 2;
2490
2409
  const travel = Math.max(0, Number(ms) || 0);
2491
2410
  const keepLock = !!(opts && opts.keepLock);
2492
- // Freeze camera follow while the demo cursor travels - otherwise dead-zone
2493
- // pans fight the click target (esp. right-edge cart chrome).
2411
+ // Camera stays locked; demo cursor + Playwright real mouse move together.
2412
+ // Host OS mouse is ignored (no mousemove camera steer).
2494
2413
  await page
2495
2414
  .evaluate(({ x, y, ms }) => {
2496
2415
  window.__wgCameraLock = true;
2416
+ window.__wgIgnoreHostMouse = true;
2497
2417
  if (window.__wgMoveCursorTo) window.__wgMoveCursorTo(x, y, ms);
2498
2418
  }, { x, y, ms: travel })
2499
2419
  .catch(() => {});
2500
- // CSS transition is async - wait the full travel so the cursor is ON the
2501
- // target before ring/pulse/real click (NavBlock click nav was skipping
2502
- // this and looked like a teleport).
2420
+ try {
2421
+ if (travel > 0) {
2422
+ const steps = Math.max(1, Math.min(30, Math.round(travel / 20)));
2423
+ await page.mouse.move(x, y, { steps });
2424
+ } else {
2425
+ await page.mouse.move(x, y);
2426
+ }
2427
+ } catch {
2428
+ /* headed / detached - demo cursor still moved */
2429
+ }
2503
2430
  if (travel > 0) {
2504
- await new Promise((res) => setTimeout(res, travel));
2431
+ await new Promise((res) => setTimeout(res, Math.min(80, travel)));
2505
2432
  }
2506
2433
  if (!keepLock) {
2507
2434
  await page
@@ -2513,6 +2440,16 @@ async function moveCursorTo(page, box, ms, opts) {
2513
2440
  return { x, y };
2514
2441
  }
2515
2442
 
2443
+ /** Live viewport center of a locator (after camera transform) - no scroll. */
2444
+ async function liveCenterOf(locator) {
2445
+ return locator
2446
+ .evaluate((el) => {
2447
+ const r = el.getBoundingClientRect();
2448
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2, w: r.width, h: r.height };
2449
+ })
2450
+ .catch(() => null);
2451
+ }
2452
+
2516
2453
  async function clickPulseAt(page, x, y, tone) {
2517
2454
  await page
2518
2455
  .evaluate(({ x, y, tone }) => {
@@ -2640,29 +2577,44 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2640
2577
  let keptCameraLock = false;
2641
2578
  try {
2642
2579
  await installOverlay(page);
2643
- // Wait until the target is visible BEFORE measuring - otherwise
2644
- // defineNavBlock({ click }) (and any slow-to-appear control) skips
2645
- // the cursor entirely: boundingBox was null, then originalClick
2646
- // waited and clicked with no demo animation.
2647
2580
  await this.waitFor({
2648
2581
  state: "visible",
2649
2582
  timeout: (options && options.timeout) || 30000,
2650
2583
  }).catch(() => {});
2651
- // Body-scale camera + Playwright scrollIntoView = teleport storm.
2652
- // Lock the camera for the whole click when zoomed; force-click so
2653
- // Playwright does not try to scroll the transformed page.
2654
2584
  const zoomed = await page
2655
2585
  .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
2656
2586
  .catch(() => false);
2657
2587
  if (zoomed) {
2588
+ // Re-auto-center on this target, then lock - no host mouse, no scroll.
2658
2589
  await page
2659
2590
  .evaluate(() => {
2660
2591
  window.__wgCameraLock = true;
2592
+ window.__wgIgnoreHostMouse = true;
2661
2593
  })
2662
2594
  .catch(() => {});
2663
2595
  keptCameraLock = true;
2596
+ const handle = await this.elementHandle().catch(() => null);
2597
+ if (handle) {
2598
+ await page
2599
+ .evaluate(
2600
+ (el) => {
2601
+ if (!el || !window.__wgSetCamera || !window.__wgElDocPoint) return;
2602
+ const cam = window.__wgCamera || { scale: 1.4 };
2603
+ const pt = window.__wgElDocPoint(el);
2604
+ const s = cam.scale > 1.001 ? cam.scale : 1.4;
2605
+ window.__wgSetCamera(pt.x, pt.y, s, 280);
2606
+ },
2607
+ handle,
2608
+ )
2609
+ .catch(() => {});
2610
+ await handle.dispose().catch(() => {});
2611
+ await new Promise((res) => setTimeout(res, 300));
2612
+ }
2664
2613
  }
2665
- const box = await this.boundingBox();
2614
+ const live = await liveCenterOf(this);
2615
+ const box = live
2616
+ ? { x: live.x - live.w / 2, y: live.y - live.h / 2, width: live.w, height: live.h }
2617
+ : await this.boundingBox();
2666
2618
  const narrated = box ? await wasJustNarrated(page) : false;
2667
2619
  if (box && !narrated) {
2668
2620
  let fallback = "click";
@@ -2670,10 +2622,8 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2670
2622
  const text = (await this.textContent())?.trim();
2671
2623
  if (text && text.length > 0 && text.length <= 30) fallback = text;
2672
2624
  } catch {
2673
- // element has no simple text (an icon button, say) - generic label is fine
2625
+ /* ignore */
2674
2626
  }
2675
- // Prefer "nav: …" when this locator is a NavBlock click target
2676
- // (set on page by runStepMode just before act).
2677
2627
  try {
2678
2628
  const navLabel = await page.evaluate(() => window.__wgPendingNavClickLabel || null);
2679
2629
  if (navLabel) fallback = String(navLabel);
@@ -2683,13 +2633,13 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2683
2633
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
2684
2634
  const cap = await captionForLocator(page, this, stubs, fallback);
2685
2635
  clickTone = cap.tone;
2686
- // Already centered from stubBefore zoom: short cursor, keep lock.
2687
- const travel = zoomed ? cursorMs(160) : cursorMs(600);
2688
- clickPoint = await moveCursorTo(page, box, travel, {
2689
- keepLock: zoomed,
2690
- });
2691
- // Re-measure after cursor settle - stale pre-travel box teleports the ring.
2692
- const liveBox = (await this.boundingBox()) || box;
2636
+ const travel = zoomed ? cursorMs(200) : cursorMs(600);
2637
+ const again = (await liveCenterOf(this)) || live;
2638
+ const aim = again
2639
+ ? { x: again.x - again.w / 2, y: again.y - again.h / 2, width: again.w, height: again.h }
2640
+ : box;
2641
+ clickPoint = await moveCursorTo(page, aim, travel, { keepLock: zoomed });
2642
+ const liveBox = aim;
2693
2643
  clickPoint = {
2694
2644
  x: liveBox.x + liveBox.width / 2,
2695
2645
  y: liveBox.y + liveBox.height / 2,
@@ -2699,30 +2649,17 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2699
2649
  weight: cap.weight,
2700
2650
  });
2701
2651
  await dwellMatchedStub(page, this, stubs, pacing);
2702
- // "pop for a few seconds" - Dan's own phrase, matching the
2703
- // zsign demo-engine's ring-before-click pattern in
2704
- // services/help-center-clip-engine's video-pipeline.
2705
2652
  await new Promise((res) => setTimeout(res, clickPrePop()));
2706
2653
  } else if (box) {
2707
- // Ring/caption already handled by narrate() - still move the
2708
- // cursor + pulse the click point, just skip re-showing the ring.
2709
2654
  const travel = zoomed ? cursorMs(120) : cursorMs(200);
2710
- clickPoint = await moveCursorTo(page, box, travel, {
2711
- keepLock: zoomed,
2712
- });
2655
+ clickPoint = await moveCursorTo(page, box, travel, { keepLock: zoomed });
2713
2656
  clickTone = "planned";
2714
2657
  }
2715
2658
  } catch {
2716
- // best-effort - the real click below still runs either way
2659
+ /* best-effort */
2717
2660
  }
2718
2661
  if (clickPoint) {
2719
2662
  await clickPulseAt(page, clickPoint.x, clickPoint.y, clickTone);
2720
- // clickPulseAt only triggers the CSS animation class - it doesn't
2721
- // wait for it. Without a pause here, the real click (and any
2722
- // resulting navigation/DOM change) fires while the ripple is still
2723
- // mid-animation, sometimes cutting it off before it's even visible.
2724
- // Wait out the same .5s the "@keyframes wg-pulse" rule uses, so the
2725
- // mock click visually completes before the real one fires.
2726
2663
  await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 500));
2727
2664
  }
2728
2665
  const zoomedClick = await page
@@ -2731,21 +2668,29 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2731
2668
  try {
2732
2669
  let result;
2733
2670
  if (zoomedClick || keptCameraLock) {
2734
- // Playwright scrollIntoView + body-scale camera = "outside viewport"
2735
- // retry storm. Click the element in-page with no scrolling.
2736
- const handle = await this.elementHandle().catch(() => null);
2737
- if (handle) {
2738
- await handle.evaluate((el) => {
2739
- if (el && typeof el.click === "function") el.click();
2740
- });
2741
- await handle.dispose().catch(() => {});
2671
+ // Real Playwright mouse at live visual center - never scrollIntoView.
2672
+ const pt =
2673
+ clickPoint ||
2674
+ (await liveCenterOf(this)) ||
2675
+ null;
2676
+ if (pt && Number.isFinite(pt.x) && Number.isFinite(pt.y)) {
2677
+ await page.mouse.click(pt.x, pt.y);
2742
2678
  result = undefined;
2743
2679
  } else {
2744
- result = await originalClick.call(this, {
2745
- ...(options || {}),
2746
- force: true,
2747
- timeout: (options && options.timeout) || 5000,
2748
- });
2680
+ const handle = await this.elementHandle().catch(() => null);
2681
+ if (handle) {
2682
+ await handle.evaluate((el) => {
2683
+ if (el && typeof el.click === "function") el.click();
2684
+ });
2685
+ await handle.dispose().catch(() => {});
2686
+ result = undefined;
2687
+ } else {
2688
+ result = await originalClick.call(this, {
2689
+ ...(options || {}),
2690
+ force: true,
2691
+ timeout: (options && options.timeout) || 5000,
2692
+ });
2693
+ }
2749
2694
  }
2750
2695
  } else {
2751
2696
  result = await originalClick.call(this, options);
@@ -97,12 +97,11 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
97
97
  */
98
98
  focus?: boolean;
99
99
  /**
100
- * After camera zoom settles, pan with the real mouse pointer (Screen Studio).
101
- * Default: on when `zoom` > 1. Set false to lock the camera on the target.
102
- * Alias: `follow`.
100
+ * Deprecated no-op. Camera auto-centers on the ring target; host mouse is
101
+ * ignored. Playwright's real mouse is driven to the centered target instead.
103
102
  */
104
103
  followMouse?: boolean;
105
- /** Alias for {@link WaygraphHighlightStub.followMouse}. */
104
+ /** Alias for {@link WaygraphHighlightStub.followMouse} (no-op). */
106
105
  follow?: boolean;
107
106
  /**
108
107
  * Caption text color (CSS color). Overrides tone label color when set.
@@ -49,7 +49,6 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
49
49
  color: "#c9a6ff",
50
50
  zoom: 1.45,
51
51
  zoomOut: false,
52
- followMouse: true,
53
52
  },
54
53
  add: {
55
54
  selector: InventorySel.addBtn(DEMO_PRODUCT_ID),
@@ -75,7 +74,6 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
75
74
  color: "#86efac",
76
75
  zoom: 1.6,
77
76
  zoomOut: false,
78
- followMouse: true,
79
77
  duration: true,
80
78
  });
81
79
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.39",
3
+ "version": "0.12.40",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -49,7 +49,6 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
49
49
  color: "#c9a6ff",
50
50
  zoom: 1.45,
51
51
  zoomOut: false,
52
- followMouse: true,
53
52
  },
54
53
  add: {
55
54
  selector: InventorySel.addBtn(DEMO_PRODUCT_ID),
@@ -75,7 +74,6 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
75
74
  color: "#86efac",
76
75
  zoom: 1.6,
77
76
  zoomOut: false,
78
- followMouse: true,
79
77
  duration: true,
80
78
  });
81
79
  },