waygraph 0.12.37 → 0.12.39

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
@@ -712,6 +712,7 @@ async function installOverlay(page, title) {
712
712
  if (!cursor) return;
713
713
  const travel = ms || 600;
714
714
  window.__wgCursorPos = { x: Number(x) || 0, y: Number(y) || 0 };
715
+ window.__wgCursorTravelUntil = Date.now() + (instant ? 0 : travel) + 40;
715
716
  cursor.style.setProperty("--wg-cursor-ms", travel + "ms");
716
717
  if (instant) {
717
718
  const prev = cursor.style.transition;
@@ -812,10 +813,8 @@ async function installOverlay(page, title) {
812
813
  let settled = zoom <= 1;
813
814
  const settleAt = Date.now() + camMs;
814
815
  const style = { size, weight, focus, color };
815
- // Screen Studio / Screenify: dead zone = center ~40% of viewport.
816
- // Pan only when the demo cursor leaves that zone - never hard-center
817
- // on every mousemove (that fights like same-pole magnets).
818
- const deadFrac = 0.4;
816
+ // Screen Studio / Screenify: dead zone follow (see followTick).
817
+ const deadFrac = 0.5;
819
818
  let lastPanAt = 0;
820
819
  const readCursorViewport = () => {
821
820
  const cursor = document.getElementById("wg-cursor");
@@ -833,6 +832,11 @@ async function installOverlay(page, title) {
833
832
  if (!window.__wgSetCamera || !window.__wgViewportToDoc) return;
834
833
  const cam = window.__wgCamera;
835
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
+ }
836
840
  const pos = readCursorViewport();
837
841
  if (!pos) return;
838
842
  const vw = window.innerWidth;
@@ -849,12 +853,13 @@ async function installOverlay(page, title) {
849
853
  else if (dy < -deadY) oy = dy + deadY;
850
854
  if (ox === 0 && oy === 0) return;
851
855
  const now = Date.now();
852
- if (now - lastPanAt < 80) return;
856
+ if (now - lastPanAt < 120) return;
853
857
  lastPanAt = now;
854
- // Viewport overflow -> document delta (divide by scale).
855
- const nx = cam.cx + ox / cam.scale;
856
- const ny = cam.cy + oy / cam.scale;
857
- window.__wgSetCamera(nx, ny, cam.scale, 220);
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);
858
863
  };
859
864
  const tick = () => {
860
865
  const el = document.querySelector(sel);
@@ -887,9 +892,13 @@ async function installOverlay(page, title) {
887
892
  };
888
893
  window.__wgCameraLock = true;
889
894
  window.__wgSetCamera(pt.x, pt.y, zoom, dur);
890
- setTimeout(() => {
891
- window.__wgCameraLock = false;
892
- }, dur + 40);
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
+ }
893
902
  }
894
903
  }
895
904
  window.__wgRingRaf = requestAnimationFrame(tick);
@@ -1413,6 +1422,25 @@ async function renderBeforeStep(page, info) {
1413
1422
 
1414
1423
  async function ensureSelectorInView(page, selector) {
1415
1424
  try {
1425
+ // Body-scale camera + scrollIntoView = teleport / "outside viewport" loop.
1426
+ // When zoomed, pan the camera to the target instead of scrolling the document.
1427
+ const zoomed = await page
1428
+ .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
1429
+ .catch(() => false);
1430
+ if (zoomed) {
1431
+ await page
1432
+ .evaluate((sel) => {
1433
+ const el = document.querySelector(sel);
1434
+ if (!el || !window.__wgSetCamera || !window.__wgElDocPoint) return;
1435
+ const cam = window.__wgCamera;
1436
+ const pt = window.__wgElDocPoint(el);
1437
+ window.__wgCameraLock = true;
1438
+ window.__wgSetCamera(pt.x, pt.y, cam.scale > 1 ? cam.scale : 1.4, 320);
1439
+ }, selector)
1440
+ .catch(() => {});
1441
+ await new Promise((res) => setTimeout(res, 340));
1442
+ return;
1443
+ }
1416
1444
  const loc = page.locator(selector).first();
1417
1445
  await loc.scrollIntoViewIfNeeded().catch(() => {});
1418
1446
  await page
@@ -1636,7 +1664,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1636
1664
  // body scale (html cursor is outside the transform).
1637
1665
  await page
1638
1666
  .evaluate(
1639
- ({ sel, label, tone, size, weight, focus, color, zoom, followMouse, camMs }) => {
1667
+ ({ sel, label, tone, size, weight, focus, color, zoom, followMouse, camMs, allowFollow }) => {
1640
1668
  if (window.__wgStartHighlightTrack) {
1641
1669
  window.__wgStartHighlightTrack({
1642
1670
  sel,
@@ -1649,6 +1677,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1649
1677
  zoom,
1650
1678
  followMouse,
1651
1679
  camMs,
1680
+ allowFollow,
1652
1681
  });
1653
1682
  }
1654
1683
  },
@@ -1663,6 +1692,9 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1663
1692
  zoom: z,
1664
1693
  followMouse,
1665
1694
  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,
1666
1698
  },
1667
1699
  )
1668
1700
  .catch(() => {});
@@ -1708,6 +1740,14 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1708
1740
  const last = list.length > 0 ? list[list.length - 1] : null;
1709
1741
  if (!last || last.zoomOut !== false) {
1710
1742
  await resetCameraZoom(page);
1743
+ } else {
1744
+ // Hold lock into the upcoming act click so follow/scroll cannot yank
1745
+ // the camera off the already-centered target (cart teleport bug).
1746
+ await page
1747
+ .evaluate(() => {
1748
+ window.__wgCameraLock = true;
1749
+ })
1750
+ .catch(() => {});
1711
1751
  }
1712
1752
  if (list.length === 0) {
1713
1753
  await hideRing(page);
@@ -2444,10 +2484,11 @@ async function teardownOverlay(page) {
2444
2484
  .catch(() => {});
2445
2485
  }
2446
2486
 
2447
- async function moveCursorTo(page, box, ms) {
2487
+ async function moveCursorTo(page, box, ms, opts) {
2448
2488
  const x = box.x + box.width / 2;
2449
2489
  const y = box.y + box.height / 2;
2450
2490
  const travel = Math.max(0, Number(ms) || 0);
2491
+ const keepLock = !!(opts && opts.keepLock);
2451
2492
  // Freeze camera follow while the demo cursor travels - otherwise dead-zone
2452
2493
  // pans fight the click target (esp. right-edge cart chrome).
2453
2494
  await page
@@ -2462,11 +2503,13 @@ async function moveCursorTo(page, box, ms) {
2462
2503
  if (travel > 0) {
2463
2504
  await new Promise((res) => setTimeout(res, travel));
2464
2505
  }
2465
- await page
2466
- .evaluate(() => {
2467
- window.__wgCameraLock = false;
2468
- })
2469
- .catch(() => {});
2506
+ if (!keepLock) {
2507
+ await page
2508
+ .evaluate(() => {
2509
+ window.__wgCameraLock = false;
2510
+ })
2511
+ .catch(() => {});
2512
+ }
2470
2513
  return { x, y };
2471
2514
  }
2472
2515
 
@@ -2594,6 +2637,7 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2594
2637
  proto.click = async function (options) {
2595
2638
  let clickPoint = null;
2596
2639
  let clickTone = "auto";
2640
+ let keptCameraLock = false;
2597
2641
  try {
2598
2642
  await installOverlay(page);
2599
2643
  // Wait until the target is visible BEFORE measuring - otherwise
@@ -2604,6 +2648,20 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2604
2648
  state: "visible",
2605
2649
  timeout: (options && options.timeout) || 30000,
2606
2650
  }).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
+ const zoomed = await page
2655
+ .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
2656
+ .catch(() => false);
2657
+ if (zoomed) {
2658
+ await page
2659
+ .evaluate(() => {
2660
+ window.__wgCameraLock = true;
2661
+ })
2662
+ .catch(() => {});
2663
+ keptCameraLock = true;
2664
+ }
2607
2665
  const box = await this.boundingBox();
2608
2666
  const narrated = box ? await wasJustNarrated(page) : false;
2609
2667
  if (box && !narrated) {
@@ -2625,8 +2683,18 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2625
2683
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
2626
2684
  const cap = await captionForLocator(page, this, stubs, fallback);
2627
2685
  clickTone = cap.tone;
2628
- clickPoint = await moveCursorTo(page, box, cursorMs(600));
2629
- await showRing(page, box, cap.label, 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;
2693
+ clickPoint = {
2694
+ x: liveBox.x + liveBox.width / 2,
2695
+ y: liveBox.y + liveBox.height / 2,
2696
+ };
2697
+ await showRing(page, liveBox, cap.label, cap.tone, {
2630
2698
  size: cap.size,
2631
2699
  weight: cap.weight,
2632
2700
  });
@@ -2638,7 +2706,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2638
2706
  } else if (box) {
2639
2707
  // Ring/caption already handled by narrate() - still move the
2640
2708
  // cursor + pulse the click point, just skip re-showing the ring.
2641
- clickPoint = await moveCursorTo(page, box, cursorMs(200));
2709
+ const travel = zoomed ? cursorMs(120) : cursorMs(200);
2710
+ clickPoint = await moveCursorTo(page, box, travel, {
2711
+ keepLock: zoomed,
2712
+ });
2642
2713
  clickTone = "planned";
2643
2714
  }
2644
2715
  } catch {
@@ -2654,10 +2725,43 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2654
2725
  // mock click visually completes before the real one fires.
2655
2726
  await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 500));
2656
2727
  }
2657
- const result = await originalClick.call(this, options);
2658
- await new Promise((res) => setTimeout(res, clickPostPop()));
2659
- await hideRing(page);
2660
- return result;
2728
+ const zoomedClick = await page
2729
+ .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
2730
+ .catch(() => false);
2731
+ try {
2732
+ let result;
2733
+ 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(() => {});
2742
+ result = undefined;
2743
+ } else {
2744
+ result = await originalClick.call(this, {
2745
+ ...(options || {}),
2746
+ force: true,
2747
+ timeout: (options && options.timeout) || 5000,
2748
+ });
2749
+ }
2750
+ } else {
2751
+ result = await originalClick.call(this, options);
2752
+ }
2753
+ await new Promise((res) => setTimeout(res, clickPostPop()));
2754
+ await hideRing(page);
2755
+ return result;
2756
+ } finally {
2757
+ if (keptCameraLock) {
2758
+ await page
2759
+ .evaluate(() => {
2760
+ window.__wgCameraLock = false;
2761
+ })
2762
+ .catch(() => {});
2763
+ }
2764
+ }
2661
2765
  };
2662
2766
  }
2663
2767
 
@@ -30,7 +30,7 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
30
30
  color: "#c9a6ff",
31
31
  zoom: 1.55,
32
32
  zoomOut: false,
33
- followMouse: true,
33
+ followMouse: false,
34
34
  weight: "bold",
35
35
  },
36
36
  });
@@ -45,7 +45,7 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
45
45
  focus: true,
46
46
  color: "#86efac",
47
47
  zoom: 1.4,
48
- followMouse: true,
48
+ followMouse: false,
49
49
  duration: true,
50
50
  });
51
51
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.37",
3
+ "version": "0.12.39",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
30
30
  color: "#c9a6ff",
31
31
  zoom: 1.55,
32
32
  zoomOut: false,
33
- followMouse: true,
33
+ followMouse: false,
34
34
  weight: "bold",
35
35
  },
36
36
  });
@@ -45,7 +45,7 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
45
45
  focus: true,
46
46
  color: "#86efac",
47
47
  zoom: 1.4,
48
- followMouse: true,
48
+ followMouse: false,
49
49
  duration: true,
50
50
  });
51
51
  },