waygraph 0.12.32 → 0.12.34

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
@@ -413,19 +413,11 @@ const RING_CSS =
413
413
  "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
414
414
  "border:2.5px solid #7C3AED;border-radius:10px;" +
415
415
  "box-shadow:0 0 0 4px rgba(124,58,237,.16);" +
416
- "transition:opacity .32s ease,left .28s ease,top .28s ease,width .28s ease,height .28s ease,border-color .15s,box-shadow .32s ease;}" +
417
- // Unquoted [data-focus=1]: this lives inside CHAIN_RUNNER_SCRIPT's template
418
- // literal - a \\" here would be eaten by the outer template and break the
419
- // generated .mjs. CSS allows unquoted attr values for identifiers.
420
- "#wg-ring[data-focus=1]{box-shadow:0 0 0 4px rgba(124,58,237,.2),0 0 0 9999px rgba(15,6,32,.55);}" +
421
- "#wg-ring[data-tone=auto][data-focus=1]{box-shadow:0 0 0 4px rgba(156,163,175,.28),0 0 0 9999px rgba(15,6,32,.55);}" +
422
- "#wg-ring[data-tone=info][data-focus=1]{box-shadow:0 0 0 4px rgba(59,130,246,.22),0 0 0 9999px rgba(15,6,32,.55);}" +
423
- "#wg-ring[data-tone=warning][data-focus=1]{box-shadow:0 0 0 4px rgba(234,179,8,.22),0 0 0 9999px rgba(15,6,32,.55);}" +
424
- "#wg-ring[data-tone=danger][data-focus=1]{box-shadow:0 0 0 4px rgba(239,68,68,.22),0 0 0 9999px rgba(15,6,32,.55);}" +
425
- "#wg-ring[data-tone=success][data-focus=1]{box-shadow:0 0 0 4px rgba(34,197,94,.22),0 0 0 9999px rgba(15,6,32,.55);}" +
426
- // Planned (stubs / fixtures / YAP / instruction.highlights) = purple.
427
- // Automation (verify fallback, unmatched fill/click) = gray - operators
428
- // can ignore engine checks and watch purple + semantic tones.
416
+ "transition:opacity .32s ease,border-color .15s;}" +
417
+ "#wg-focus-veil{position:fixed;z-index:2147483645;pointer-events:none;opacity:0;" +
418
+ "border-radius:10px;box-shadow:0 0 0 9999px rgba(12,6,28,.62);" +
419
+ "transition:opacity .28s ease;}" +
420
+ "#wg-focus-veil.wg-focus-on{opacity:1;}" +
429
421
  "#wg-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}" +
430
422
  "#wg-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}" +
431
423
  "#wg-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
@@ -441,7 +433,7 @@ const RING_CSS =
441
433
  // window.__wgPositionRing in installOverlay.
442
434
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
443
435
  "max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
444
- "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .32s ease,left .28s ease,top .28s ease,background .15s,color .15s;}" +
436
+ "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .32s ease,background .15s,color .15s;}" +
445
437
  "#wg-ring-label[data-tone=auto]{background:#6B7280;color:#fff;}" +
446
438
  "#wg-ring-label[data-tone=info]{background:#2563EB;color:#fff;}" +
447
439
  "#wg-ring-label[data-tone=warning]{background:#EAB308;color:#1c1917;}" +
@@ -683,6 +675,11 @@ async function installOverlay(page, title) {
683
675
  } catch {
684
676
  /* private mode / blocked storage - falls back to manual gating */
685
677
  }
678
+ if (!document.getElementById("wg-focus-veil")) {
679
+ const veil = document.createElement("div");
680
+ veil.id = "wg-focus-veil";
681
+ document.documentElement.appendChild(veil);
682
+ }
686
683
  if (!document.getElementById("wg-ring")) {
687
684
  const ring = document.createElement("div");
688
685
  ring.id = "wg-ring";
@@ -733,6 +730,7 @@ async function installOverlay(page, title) {
733
730
  window.__wgPositionRing = (box, label, tone, style) => {
734
731
  const ring = document.getElementById("wg-ring");
735
732
  const ringLabel = document.getElementById("wg-ring-label");
733
+ const veil = document.getElementById("wg-focus-veil");
736
734
  if (!ring || !ringLabel) return;
737
735
  const raw = (tone || "planned") + "";
738
736
  const t =
@@ -757,8 +755,8 @@ async function installOverlay(page, title) {
757
755
  const vh = window.innerHeight;
758
756
  let left = box.x - pad;
759
757
  let top = box.y - pad;
760
- const width = box.width + pad * 2;
761
- const height = box.height + pad * 2;
758
+ const width = Math.max(0, box.width + pad * 2);
759
+ const height = Math.max(0, box.height + pad * 2);
762
760
  if (left < margin) left = margin;
763
761
  if (top < margin) top = margin;
764
762
  if (left + width > vw - margin) left = Math.max(margin, vw - margin - width);
@@ -768,24 +766,100 @@ async function installOverlay(page, title) {
768
766
  ring.style.width = width + "px";
769
767
  ring.style.height = height + "px";
770
768
  ring.style.opacity = "1";
769
+ if (veil) {
770
+ veil.style.left = left + "px";
771
+ veil.style.top = top + "px";
772
+ veil.style.width = width + "px";
773
+ veil.style.height = height + "px";
774
+ const br = size === "sm" ? "8px" : size === "lg" ? "12px" : "10px";
775
+ veil.style.borderRadius = br;
776
+ if (st.focus) veil.classList.add("wg-focus-on");
777
+ else veil.classList.remove("wg-focus-on");
778
+ }
771
779
  ringLabel.textContent = label;
772
780
  ringLabel.style.opacity = "1";
773
781
  const capColor = st.color || st.labelColor || st.textColor;
774
782
  if (capColor) ringLabel.style.color = String(capColor);
775
783
  else ringLabel.style.removeProperty("color");
776
- // Measure the label's own natural width/height (its real
777
- // rendered size, unchanged) before deciding where it fits.
778
784
  const lw = ringLabel.offsetWidth;
779
785
  const lh = ringLabel.offsetHeight;
780
786
  let labelLeft = left;
781
787
  let labelTop = top + height + 8;
782
- if (labelTop + lh > vh - margin) labelTop = top - lh - 8; // flip above if it'd overflow the bottom
788
+ if (labelTop + lh > vh - margin) labelTop = top - lh - 8;
783
789
  if (labelTop < margin) labelTop = margin;
784
790
  if (labelLeft + lw > vw - margin) labelLeft = Math.max(margin, vw - margin - lw);
785
791
  if (labelLeft < margin) labelLeft = margin;
786
792
  ringLabel.style.left = labelLeft + "px";
787
793
  ringLabel.style.top = labelTop + "px";
788
794
  };
795
+ window.__wgHideFocusVeil = () => {
796
+ const veil = document.getElementById("wg-focus-veil");
797
+ if (veil) veil.classList.remove("wg-focus-on");
798
+ };
799
+ window.__wgStartHighlightTrack = (opts) => {
800
+ if (window.__wgStopHighlightTrack) window.__wgStopHighlightTrack();
801
+ const sel = opts.sel;
802
+ const label = opts.label;
803
+ const tone = opts.tone || "planned";
804
+ const size = opts.size || "md";
805
+ const weight = opts.weight || "normal";
806
+ const focus = !!opts.focus;
807
+ const color = opts.color || "";
808
+ const zoom = opts.zoom != null && opts.zoom > 1 ? opts.zoom : 1;
809
+ const followMouse = opts.followMouse !== false && zoom > 1;
810
+ const camMs = opts.camMs != null ? opts.camMs : 700;
811
+ let settled = zoom <= 1;
812
+ const settleAt = Date.now() + camMs;
813
+ const style = { size, weight, focus, color };
814
+ const tick = () => {
815
+ const el = document.querySelector(sel);
816
+ if (el && window.__wgPositionRing) {
817
+ const r = el.getBoundingClientRect();
818
+ window.__wgPositionRing(
819
+ { x: r.left, y: r.top, width: r.width, height: r.height },
820
+ label,
821
+ tone,
822
+ style,
823
+ );
824
+ }
825
+ if (!settled && Date.now() >= settleAt) settled = true;
826
+ window.__wgRingRaf = requestAnimationFrame(tick);
827
+ };
828
+ if (zoom > 1 && window.__wgSetCamera) {
829
+ const el0 = document.querySelector(sel);
830
+ if (el0) {
831
+ const r0 = el0.getBoundingClientRect();
832
+ window.__wgSetCamera(
833
+ r0.left + r0.width / 2,
834
+ r0.top + r0.height / 2,
835
+ zoom,
836
+ camMs,
837
+ );
838
+ }
839
+ }
840
+ window.__wgRingRaf = requestAnimationFrame(tick);
841
+ const onMove = (ev) => {
842
+ if (!followMouse || !settled) return;
843
+ if (!window.__wgSetCamera) return;
844
+ const cam = window.__wgCamera;
845
+ const scale = (cam && cam.scale > 1 ? cam.scale : zoom) || zoom;
846
+ if (!(scale > 1)) return;
847
+ window.__wgSetCamera(ev.clientX, ev.clientY, scale, 140);
848
+ };
849
+ window.addEventListener("mousemove", onMove, { passive: true });
850
+ window.__wgRingTrack = onMove;
851
+ window.__wgStopHighlightTrack = () => {
852
+ if (window.__wgRingRaf) {
853
+ cancelAnimationFrame(window.__wgRingRaf);
854
+ window.__wgRingRaf = null;
855
+ }
856
+ if (window.__wgRingTrack) {
857
+ window.removeEventListener("mousemove", window.__wgRingTrack);
858
+ window.__wgRingTrack = null;
859
+ }
860
+ window.__wgStopHighlightTrack = null;
861
+ };
862
+ };
789
863
  window.__wgClickPulse = (x, y, tone) => {
790
864
  const pulse = document.getElementById("wg-click-pulse");
791
865
  if (!pulse) return;
@@ -810,6 +884,7 @@ async function installOverlay(page, title) {
810
884
  const ringLabel = document.getElementById("wg-ring-label");
811
885
  if (ring) ring.style.opacity = "0";
812
886
  if (ringLabel) ringLabel.style.opacity = "0";
887
+ if (window.__wgHideFocusVeil) window.__wgHideFocusVeil();
813
888
  if (window.__wgClearZoom) window.__wgClearZoom();
814
889
  };
815
890
  // Screen Studio-style camera: scale <body>, keep overlays on <html> fixed.
@@ -1431,15 +1506,18 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1431
1506
  const stopLiveFollow = async () => {
1432
1507
  await page
1433
1508
  .evaluate(() => {
1509
+ if (window.__wgStopHighlightTrack) window.__wgStopHighlightTrack();
1434
1510
  if (window.__wgRingRaf) {
1435
1511
  cancelAnimationFrame(window.__wgRingRaf);
1436
1512
  window.__wgRingRaf = null;
1437
1513
  }
1438
1514
  if (window.__wgRingTrack) {
1515
+ window.removeEventListener("mousemove", window.__wgRingTrack);
1439
1516
  window.removeEventListener("resize", window.__wgRingTrack);
1440
1517
  window.removeEventListener("scroll", window.__wgRingTrack, true);
1441
1518
  window.__wgRingTrack = null;
1442
1519
  }
1520
+ if (window.__wgHideFocusVeil) window.__wgHideFocusVeil();
1443
1521
  })
1444
1522
  .catch(() => {});
1445
1523
  };
@@ -1462,12 +1540,16 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1462
1540
  } else if (h.todos && h.todos.length) {
1463
1541
  await setDemoTodos(page, normalizeTodos(h.todos, h.todoIndex));
1464
1542
  }
1465
- const cx = box.x + box.width / 2;
1466
- const cy = box.y + box.height / 2;
1467
1543
  const z = h.zoom != null && Number(h.zoom) > 1 ? Number(h.zoom) : 1;
1468
1544
  const camMs = 700;
1469
- // One-shot camera: zoom + center together (no zoom-then-align).
1470
- await applyCameraZoom(page, box, z, camMs);
1545
+ const followMouse =
1546
+ h.followMouse !== undefined
1547
+ ? !!h.followMouse
1548
+ : h.follow !== undefined
1549
+ ? !!h.follow
1550
+ : z > 1;
1551
+ const cx = box.x + box.width / 2;
1552
+ const cy = box.y + box.height / 2;
1471
1553
  if (h.cursor !== false) {
1472
1554
  await page
1473
1555
  .evaluate(({ cx, cy, camMs }) => {
@@ -1475,41 +1557,40 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1475
1557
  }, { cx, cy, camMs })
1476
1558
  .catch(() => {});
1477
1559
  }
1478
- await showRing(page, box, h.label, h.tone || "planned", {
1479
- size: h.size,
1480
- weight: h.weight,
1481
- focus: !!h.focus,
1482
- color: h.color || h.labelColor || h.textColor,
1483
- });
1484
- await new Promise((res) => setTimeout(res, camMs));
1485
- // Live ring track only - camera stays locked (no per-frame re-pan).
1560
+ // Track first: ring rides visual rect through the zoom (no re-adjust).
1486
1561
  await page
1487
- .evaluate(({ sel, label, tone, size, weight, focus, color }) => {
1488
- const tick = () => {
1489
- const el = document.querySelector(sel);
1490
- if (!el) {
1491
- window.__wgRingRaf = requestAnimationFrame(tick);
1492
- return;
1562
+ .evaluate(
1563
+ ({ sel, label, tone, size, weight, focus, color, zoom, followMouse, camMs }) => {
1564
+ if (window.__wgStartHighlightTrack) {
1565
+ window.__wgStartHighlightTrack({
1566
+ sel,
1567
+ label,
1568
+ tone,
1569
+ size,
1570
+ weight,
1571
+ focus,
1572
+ color,
1573
+ zoom,
1574
+ followMouse,
1575
+ camMs,
1576
+ });
1493
1577
  }
1494
- const r = el.getBoundingClientRect();
1495
- const box = { x: r.left, y: r.top, width: r.width, height: r.height };
1496
- if (window.__wgPositionRing) {
1497
- window.__wgPositionRing(box, label, tone || "planned", { size, weight, focus, color });
1498
- }
1499
- window.__wgRingRaf = requestAnimationFrame(tick);
1500
- };
1501
- window.__wgRingRaf = requestAnimationFrame(tick);
1502
- window.__wgRingTrack = () => {};
1503
- }, {
1504
- sel: h.selector,
1505
- label: h.label,
1506
- tone: h.tone || "planned",
1507
- size: h.size || "md",
1508
- weight: h.weight || "normal",
1509
- focus: !!h.focus,
1510
- color: h.color || h.labelColor || h.textColor || "",
1511
- })
1578
+ },
1579
+ {
1580
+ sel: h.selector,
1581
+ label: h.label,
1582
+ tone: h.tone || "planned",
1583
+ size: h.size || "md",
1584
+ weight: h.weight || "normal",
1585
+ focus: !!h.focus,
1586
+ color: h.color || h.labelColor || h.textColor || "",
1587
+ zoom: z,
1588
+ followMouse,
1589
+ camMs,
1590
+ },
1591
+ )
1512
1592
  .catch(() => {});
1593
+ await new Promise((res) => setTimeout(res, camMs));
1513
1594
 
1514
1595
  // Appear settle
1515
1596
  await new Promise((res) => setTimeout(res, fadeMs));
@@ -2255,7 +2336,7 @@ async function teardownOverlay(page) {
2255
2336
  window.__wgRingTrack = null;
2256
2337
  }
2257
2338
  window.__wgNarrateOwnsRing = false;
2258
- for (const id of ["wg-panel", "wg-ring", "wg-ring-label", "wg-cursor", "wg-click-pulse", "wg-banner", "wg-todo-modal"]) {
2339
+ for (const id of ["wg-panel", "wg-ring", "wg-ring-label", "wg-cursor", "wg-click-pulse", "wg-banner", "wg-todo-modal", "wg-focus-veil"]) {
2259
2340
  const el = document.getElementById(id);
2260
2341
  if (el) el.remove();
2261
2342
  }
@@ -89,6 +89,14 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
89
89
  * Spotlight: dim the rest of the page, keep this target as the focal point.
90
90
  */
91
91
  focus?: boolean;
92
+ /**
93
+ * After camera zoom settles, pan with the real mouse pointer (Screen Studio).
94
+ * Default: on when `zoom` > 1. Set false to lock the camera on the target.
95
+ * Alias: `follow`.
96
+ */
97
+ followMouse?: boolean;
98
+ /** Alias for {@link WaygraphHighlightStub.followMouse}. */
99
+ follow?: boolean;
92
100
  /**
93
101
  * Caption text color (CSS color). Overrides tone label color when set.
94
102
  * Aliases accepted at runtime: `color`, `labelColor`, `textColor`.
@@ -101,7 +109,7 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
101
109
  cursor?: boolean;
102
110
  }
103
111
  /** Flow fixture patch: label required; selector optional (inherits from stub). */
104
- export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag" | "tone" | "size" | "weight" | "zoom" | "focus" | "color" | "cursor"> & FixtureDurationFields;
112
+ export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag" | "tone" | "size" | "weight" | "zoom" | "focus" | "followMouse" | "follow" | "color" | "cursor"> & FixtureDurationFields;
105
113
  export type HighlightStubPhase = Record<string, WaygraphHighlightStub>;
106
114
  /**
107
115
  * Open stub lifecycle context. Authors set rings + episode fixtures freely —
@@ -8,9 +8,9 @@
8
8
  "typecheck": "tsc --noEmit",
9
9
  "test": "playwright test",
10
10
  "flows": "waygraph list",
11
- "demo": "waygraph demo --blocks shopFlow",
12
- "demo:step": "waygraph demo --blocks shopFlow --title \"Shop demo\"",
13
- "demo:auto-next": "waygraph demo --blocks shopFlow --auto-next --title \"Shop demo\"",
11
+ "demo": "waygraph demo --blocks \"loginFlow then shopFlow\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\"",
12
+ "demo:step": "waygraph demo --blocks \"loginFlow then shopFlow\" --title \"Shop demo\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\"",
13
+ "demo:auto-next": "waygraph demo --blocks \"loginFlow then shopFlow\" --auto-next --title \"Product then cart\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\"",
14
14
  "auto": "waygraph auto",
15
15
  "auto:cli": "waygraph auto --cli --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
16
16
  "prove:auto-cli": "node scripts/drive-auto-cli.mjs",
@@ -1,5 +1,6 @@
1
1
  import { defineNavClickBlock, Trait } from "waygraph";
2
2
  import type { CartPage } from "../../../states/checkout.states.js";
3
+ import { InventorySel } from "../inventory/methods/inventory-items.js";
3
4
 
4
5
  /**
5
6
  * Kind: Nav
@@ -14,4 +15,36 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
14
15
  checkpoint: "CartPage",
15
16
  click: ".shopping_cart_link",
16
17
  verify: [Trait.url({ pathname: "/cart.html" })],
18
+ // Demo: zoom cart chrome, then the cart line items after landing.
19
+ stubBefore: (ctx) => {
20
+ ctx.title("Open cart");
21
+ ctx.todos(["Find the product", "Add to cart", "Open cart"]);
22
+ ctx.todoIndex(2);
23
+ ctx.zoom(1.4);
24
+ ctx.highlights({
25
+ cart: {
26
+ selector: ".shopping_cart_link",
27
+ label: "Cart",
28
+ detail: "Zoom cart chrome",
29
+ focus: true,
30
+ color: "#c9a6ff",
31
+ zoom: 1.55,
32
+ followMouse: true,
33
+ weight: "bold",
34
+ },
35
+ });
36
+ },
37
+ stubAfter: (ctx) => {
38
+ ctx.banner("Your cart");
39
+ ctx.ring("items", {
40
+ selector: InventorySel.cartItem,
41
+ label: "Cart items",
42
+ detail: "Product landed in cart",
43
+ focus: true,
44
+ color: "#86efac",
45
+ zoom: 1.4,
46
+ followMouse: true,
47
+ duration: true,
48
+ });
49
+ },
17
50
  });
@@ -3,6 +3,9 @@ import type { ItemInCart } from "../../../../states/checkout.states.js";
3
3
  import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
4
4
  import { collectAddableItems, InventorySel } from "./inventory-items.js";
5
5
 
6
+ /** Showcase product for headed demos (matches shopFlow --data selectedItem). */
7
+ const DEMO_PRODUCT_ID = "sauce-labs-backpack";
8
+
6
9
  /**
7
10
  * Kind: Effect
8
11
  * Helper: defineEffectBlock
@@ -30,13 +33,46 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
30
33
  },
31
34
  },
32
35
  ],
33
- stubBefore: {},
34
- stubAfter: {
35
- badge: {
36
+ // Demo path: zoom the product, then the add button, then (after) the cart badge.
37
+ stubBefore: (ctx) => {
38
+ ctx.title("Pick a product");
39
+ ctx.todos(["Find the product", "Add to cart", "Open cart"]);
40
+ ctx.todoIndex(0);
41
+ ctx.zoom(1.35);
42
+ ctx.highlights({
43
+ product: {
44
+ selector: InventorySel.title(DEMO_PRODUCT_ID),
45
+ label: "Sauce Labs Backpack",
46
+ detail: "Zoom the product",
47
+ focus: true,
48
+ color: "#c9a6ff",
49
+ zoom: 1.45,
50
+ followMouse: true,
51
+ },
52
+ add: {
53
+ selector: InventorySel.addBtn(DEMO_PRODUCT_ID),
54
+ label: "Add to cart",
55
+ focus: true,
56
+ color: "#86efac",
57
+ zoom: 1.5,
58
+ weight: "bold",
59
+ },
60
+ });
61
+ },
62
+ stubAfter: (ctx) => {
63
+ ctx.banner("In the cart");
64
+ ctx.todos(["Find the product", "Add to cart", "Open cart"]);
65
+ ctx.todoIndex(1);
66
+ ctx.ring("badge", {
36
67
  selector: InventorySel.cartBadge,
37
68
  label: "Cart badge",
38
- detail: "Item is in the cart",
39
- },
69
+ detail: "Item count updated",
70
+ focus: true,
71
+ color: "#86efac",
72
+ zoom: 1.6,
73
+ followMouse: true,
74
+ duration: true,
75
+ });
40
76
  },
41
77
  },
42
78
  async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
@@ -81,14 +81,14 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
81
81
  ctx.banner("Inventory");
82
82
  ctx.todos(["Enter username", "Enter password", "Click Login"]);
83
83
  ctx.todoIndex(2);
84
- // Fleece sits lower - demo smooth-scrolls + Screen Studio zooms to it.
85
- ctx.ring("fleece", {
86
- selector: '[data-test="add-to-cart-sauce-labs-fleece-jacket"]',
87
- label: "Fleece jacket",
88
- detail: "Smooth scroll + camera zoom",
84
+ // Landed on inventory - hand off to shopFlow for product -> cart zoom.
85
+ ctx.ring("shelf", {
86
+ selector: ".inventory_list",
87
+ label: "Product shelf",
88
+ detail: "Ready to shop",
89
89
  focus: true,
90
- color: "#86efac",
91
- zoom: 1.45,
90
+ color: "#c9a6ff",
91
+ zoom: 1.25,
92
92
  duration: true,
93
93
  });
94
94
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.32",
3
+ "version": "0.12.34",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -1,5 +1,6 @@
1
1
  import { defineNavClickBlock, Trait } from "waygraph";
2
2
  import type { CartPage } from "../../../states/checkout.states.js";
3
+ import { InventorySel } from "../inventory/methods/inventory-items.js";
3
4
 
4
5
  /**
5
6
  * Kind: Nav
@@ -14,4 +15,36 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
14
15
  checkpoint: "CartPage",
15
16
  click: ".shopping_cart_link",
16
17
  verify: [Trait.url({ pathname: "/cart.html" })],
18
+ // Demo: zoom cart chrome, then the cart line items after landing.
19
+ stubBefore: (ctx) => {
20
+ ctx.title("Open cart");
21
+ ctx.todos(["Find the product", "Add to cart", "Open cart"]);
22
+ ctx.todoIndex(2);
23
+ ctx.zoom(1.4);
24
+ ctx.highlights({
25
+ cart: {
26
+ selector: ".shopping_cart_link",
27
+ label: "Cart",
28
+ detail: "Zoom cart chrome",
29
+ focus: true,
30
+ color: "#c9a6ff",
31
+ zoom: 1.55,
32
+ followMouse: true,
33
+ weight: "bold",
34
+ },
35
+ });
36
+ },
37
+ stubAfter: (ctx) => {
38
+ ctx.banner("Your cart");
39
+ ctx.ring("items", {
40
+ selector: InventorySel.cartItem,
41
+ label: "Cart items",
42
+ detail: "Product landed in cart",
43
+ focus: true,
44
+ color: "#86efac",
45
+ zoom: 1.4,
46
+ followMouse: true,
47
+ duration: true,
48
+ });
49
+ },
17
50
  });
@@ -3,6 +3,9 @@ import type { ItemInCart } from "../../../../states/checkout.states.js";
3
3
  import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
4
4
  import { collectAddableItems, InventorySel } from "./inventory-items.js";
5
5
 
6
+ /** Showcase product for headed demos (matches shopFlow --data selectedItem). */
7
+ const DEMO_PRODUCT_ID = "sauce-labs-backpack";
8
+
6
9
  /**
7
10
  * Kind: Effect
8
11
  * Helper: defineEffectBlock
@@ -30,13 +33,46 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
30
33
  },
31
34
  },
32
35
  ],
33
- stubBefore: {},
34
- stubAfter: {
35
- badge: {
36
+ // Demo path: zoom the product, then the add button, then (after) the cart badge.
37
+ stubBefore: (ctx) => {
38
+ ctx.title("Pick a product");
39
+ ctx.todos(["Find the product", "Add to cart", "Open cart"]);
40
+ ctx.todoIndex(0);
41
+ ctx.zoom(1.35);
42
+ ctx.highlights({
43
+ product: {
44
+ selector: InventorySel.title(DEMO_PRODUCT_ID),
45
+ label: "Sauce Labs Backpack",
46
+ detail: "Zoom the product",
47
+ focus: true,
48
+ color: "#c9a6ff",
49
+ zoom: 1.45,
50
+ followMouse: true,
51
+ },
52
+ add: {
53
+ selector: InventorySel.addBtn(DEMO_PRODUCT_ID),
54
+ label: "Add to cart",
55
+ focus: true,
56
+ color: "#86efac",
57
+ zoom: 1.5,
58
+ weight: "bold",
59
+ },
60
+ });
61
+ },
62
+ stubAfter: (ctx) => {
63
+ ctx.banner("In the cart");
64
+ ctx.todos(["Find the product", "Add to cart", "Open cart"]);
65
+ ctx.todoIndex(1);
66
+ ctx.ring("badge", {
36
67
  selector: InventorySel.cartBadge,
37
68
  label: "Cart badge",
38
- detail: "Item is in the cart",
39
- },
69
+ detail: "Item count updated",
70
+ focus: true,
71
+ color: "#86efac",
72
+ zoom: 1.6,
73
+ followMouse: true,
74
+ duration: true,
75
+ });
40
76
  },
41
77
  },
42
78
  async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
@@ -81,14 +81,14 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
81
81
  ctx.banner("Inventory");
82
82
  ctx.todos(["Enter username", "Enter password", "Click Login"]);
83
83
  ctx.todoIndex(2);
84
- // Fleece sits lower - demo smooth-scrolls + Screen Studio zooms to it.
85
- ctx.ring("fleece", {
86
- selector: '[data-test="add-to-cart-sauce-labs-fleece-jacket"]',
87
- label: "Fleece jacket",
88
- detail: "Smooth scroll + camera zoom",
84
+ // Landed on inventory - hand off to shopFlow for product -> cart zoom.
85
+ ctx.ring("shelf", {
86
+ selector: ".inventory_list",
87
+ label: "Product shelf",
88
+ detail: "Ready to shop",
89
89
  focus: true,
90
- color: "#86efac",
91
- zoom: 1.45,
90
+ color: "#c9a6ff",
91
+ zoom: 1.25,
92
92
  duration: true,
93
93
  });
94
94
  },