waygraph 0.12.33 → 0.12.35

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
@@ -829,11 +829,15 @@ async function installOverlay(page, title) {
829
829
  const el0 = document.querySelector(sel);
830
830
  if (el0) {
831
831
  const r0 = el0.getBoundingClientRect();
832
+ const already =
833
+ window.__wgCamera && window.__wgCamera.scale > 1.001;
834
+ // Already zoomed (zoomOut:false chain): short pan, not full zoom-in.
835
+ const dur = already ? Math.min(camMs, 420) : camMs;
832
836
  window.__wgSetCamera(
833
837
  r0.left + r0.width / 2,
834
838
  r0.top + r0.height / 2,
835
839
  zoom,
836
- camMs,
840
+ dur,
837
841
  );
838
842
  }
839
843
  }
@@ -1590,7 +1594,11 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1590
1594
  },
1591
1595
  )
1592
1596
  .catch(() => {});
1593
- await new Promise((res) => setTimeout(res, camMs));
1597
+ const alreadyZoomed = await page
1598
+ .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
1599
+ .catch(() => false);
1600
+ const waitMs = alreadyZoomed ? Math.min(camMs, 420) : camMs;
1601
+ await new Promise((res) => setTimeout(res, waitMs));
1594
1602
 
1595
1603
  // Appear settle
1596
1604
  await new Promise((res) => setTimeout(res, fadeMs));
@@ -1599,11 +1607,12 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1599
1607
  const holdMs =
1600
1608
  authored != null ? authored : defaultHoldMs != null ? defaultHoldMs : legacyMs;
1601
1609
  await new Promise((res) => setTimeout(res, holdMs));
1602
- // Disappear before next (respect fade)
1610
+ // Disappear before next. zoomOut:false keeps camera for the next ring.
1603
1611
  await stopLiveFollow();
1604
1612
  await hideRing(page);
1605
- await resetCameraZoom(page);
1606
- await new Promise((res) => setTimeout(res, fadeMs));
1613
+ const doZoomOut = h.zoomOut !== false;
1614
+ if (doZoomOut) await resetCameraZoom(page);
1615
+ await new Promise((res) => setTimeout(res, doZoomOut ? fadeMs : 120));
1607
1616
  } catch {
1608
1617
  /* best-effort */
1609
1618
  }
@@ -2939,6 +2948,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2939
2948
  focus: styled.focus,
2940
2949
  color: styled.color,
2941
2950
  cursor: styled.cursor,
2951
+ zoomOut: styled.zoomOut,
2942
2952
  };
2943
2953
  });
2944
2954
  await cycleHighlightRings(page, beforeRings, !!pacing.gatesFast, {
@@ -3019,6 +3029,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3019
3029
  focus: styled.focus,
3020
3030
  color: styled.color,
3021
3031
  cursor: styled.cursor,
3032
+ zoomOut: styled.zoomOut,
3022
3033
  };
3023
3034
  });
3024
3035
  await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
@@ -3113,6 +3124,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3113
3124
  focus: styled.focus,
3114
3125
  color: styled.color,
3115
3126
  cursor: styled.cursor,
3127
+ zoomOut: styled.zoomOut,
3116
3128
  };
3117
3129
  });
3118
3130
  } else {
@@ -50,6 +50,8 @@ export type StubPhaseFixtures = {
50
50
  * cursor. Not element CSS zoom. Typical `1.25` .. `2`.
51
51
  */
52
52
  zoom?: number;
53
+ /** Default zoomOut for rings (false = keep camera between rings). */
54
+ zoomOut?: boolean;
53
55
  /** Top demo banner title (the purple "waygraph demo" card). */
54
56
  title?: string;
55
57
  };
@@ -81,10 +83,15 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
81
83
  weight?: HighlightWeight;
82
84
  /**
83
85
  * Camera zoom while this ring is up (Screen Studio style - page scales toward
84
- * the target / cursor). Typical `1.25` .. `2`. Cleared when the ring hides.
85
- * Episode default: {@link StubCtx.zoom}.
86
+ * the target / cursor). Typical `1.25` .. `2`. Cleared when the ring hides
87
+ * unless `zoomOut: false`. Episode default: {@link StubCtx.zoom}.
86
88
  */
87
89
  zoom?: number;
90
+ /**
91
+ * When false, keep the camera zoomed after this ring (pan to the next target
92
+ * instead of zooming out). Default true. Phase default: {@link StubCtx.zoomOut}.
93
+ */
94
+ zoomOut?: boolean;
88
95
  /**
89
96
  * Spotlight: dim the rest of the page, keep this target as the focal point.
90
97
  */
@@ -109,7 +116,7 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
109
116
  cursor?: boolean;
110
117
  }
111
118
  /** Flow fixture patch: label required; selector optional (inherits from stub). */
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;
119
+ export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag" | "tone" | "size" | "weight" | "zoom" | "zoomOut" | "focus" | "followMouse" | "follow" | "color" | "cursor"> & FixtureDurationFields;
113
120
  export type HighlightStubPhase = Record<string, WaygraphHighlightStub>;
114
121
  /**
115
122
  * Open stub lifecycle context. Authors set rings + episode fixtures freely —
@@ -147,6 +154,10 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
147
154
  todoIndex(n: number): void;
148
155
  /** Default camera zoom for rings without their own zoom. */
149
156
  zoom(n: number): void;
157
+ /**
158
+ * Default zoomOut for rings in this phase. `false` = keep camera between rings.
159
+ */
160
+ zoomOut(keep: boolean): void;
150
161
  /** Top banner title (waygraph demo card). */
151
162
  title(text: string): void;
152
163
  /** Alias for {@link StubCtx.title}. */
@@ -170,6 +181,7 @@ export type StubPhaseResult = {
170
181
  todos: WaygraphTodoItem[];
171
182
  todoIndex?: number;
172
183
  zoom?: number;
184
+ zoomOut?: boolean;
173
185
  title?: string;
174
186
  };
175
187
  /**
@@ -403,6 +403,9 @@ function createStubCtx(bag, opts) {
403
403
  if (Number.isFinite(n) && n > 0)
404
404
  bag.zoom = n;
405
405
  },
406
+ zoomOut(keep) {
407
+ bag.zoomOut = !!keep;
408
+ },
406
409
  title(text) {
407
410
  bag.title = String(text ?? "");
408
411
  },
@@ -419,6 +422,8 @@ function createStubCtx(bag, opts) {
419
422
  if (partial.zoom !== undefined && Number.isFinite(partial.zoom) && partial.zoom > 0) {
420
423
  bag.zoom = partial.zoom;
421
424
  }
425
+ if (partial.zoomOut !== undefined)
426
+ bag.zoomOut = !!partial.zoomOut;
422
427
  if (partial.title !== undefined)
423
428
  bag.title = String(partial.title ?? "");
424
429
  },
@@ -443,6 +448,11 @@ function applyDefaultZoom(slots, defaultZoom) {
443
448
  return slots;
444
449
  return slots.map((h) => (h.zoom !== undefined && h.zoom > 0 ? h : { ...h, zoom: defaultZoom }));
445
450
  }
451
+ function applyDefaultZoomOut(slots, defaultZoomOut) {
452
+ if (defaultZoomOut === undefined)
453
+ return slots;
454
+ return slots.map((h) => h.zoomOut !== undefined ? h : { ...h, zoomOut: defaultZoomOut });
455
+ }
446
456
  function mergePhaseMaps(phaseMap, fixturePhase) {
447
457
  const slotIds = new Set([...Object.keys(phaseMap), ...Object.keys(fixturePhase || {})]);
448
458
  const ordered = [...slotIds].sort((a, b) => {
@@ -546,12 +556,14 @@ export async function runStubPhase(block, phase, opts) {
546
556
  const fixturePhase = opts?.fixtures?.[block.name]?.[phase];
547
557
  let highlights = mergePhaseMaps(bag.highlights, fixturePhase);
548
558
  highlights = applyDefaultZoom(highlights, bag.zoom);
559
+ highlights = applyDefaultZoomOut(highlights, bag.zoomOut);
549
560
  const todos = normalizeTodos(bag.todos, bag.todoIndex);
550
561
  return {
551
562
  highlights,
552
563
  todos,
553
564
  ...(bag.todoIndex !== undefined ? { todoIndex: bag.todoIndex } : {}),
554
565
  ...(bag.zoom !== undefined ? { zoom: bag.zoom } : {}),
566
+ ...(bag.zoomOut !== undefined ? { zoomOut: bag.zoomOut } : {}),
555
567
  ...(bag.title !== undefined && bag.title !== "" ? { title: bag.title } : {}),
556
568
  };
557
569
  }
@@ -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,37 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
14
15
  checkpoint: "CartPage",
15
16
  click: ".shopping_cart_link",
16
17
  verify: [Trait.url({ pathname: "/cart.html" })],
18
+ stubBefore: (ctx) => {
19
+ ctx.title("Open cart");
20
+ ctx.todos(["Find the product", "Add to cart", "Open cart"]);
21
+ ctx.todoIndex(2);
22
+ ctx.zoom(1.4);
23
+ ctx.highlights({
24
+ cart: {
25
+ selector: ".shopping_cart_link",
26
+ label: "Cart",
27
+ detail: "Zoom cart chrome",
28
+ focus: true,
29
+ color: "#c9a6ff",
30
+ zoom: 1.55,
31
+ zoomOut: false,
32
+ followMouse: true,
33
+ weight: "bold",
34
+ },
35
+ });
36
+ },
37
+ stubAfter: (ctx) => {
38
+ ctx.banner("Your cart");
39
+ ctx.zoomOut(false);
40
+ ctx.ring("items", {
41
+ selector: InventorySel.cartItem,
42
+ label: "Cart items",
43
+ detail: "Product landed in cart",
44
+ focus: true,
45
+ color: "#86efac",
46
+ zoom: 1.4,
47
+ followMouse: true,
48
+ duration: true,
49
+ });
50
+ },
17
51
  });
@@ -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,49 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
30
33
  },
31
34
  },
32
35
  ],
33
- stubBefore: {},
34
- stubAfter: {
35
- badge: {
36
+ // Demo: zoom product -> add (stay zoomed), then badge after act.
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.zoomOut(false);
43
+ ctx.highlights({
44
+ product: {
45
+ selector: InventorySel.title(DEMO_PRODUCT_ID),
46
+ label: "Sauce Labs Backpack",
47
+ detail: "Zoom the product",
48
+ focus: true,
49
+ color: "#c9a6ff",
50
+ zoom: 1.45,
51
+ zoomOut: false,
52
+ followMouse: true,
53
+ },
54
+ add: {
55
+ selector: InventorySel.addBtn(DEMO_PRODUCT_ID),
56
+ label: "Add to cart",
57
+ focus: true,
58
+ color: "#86efac",
59
+ zoom: 1.5,
60
+ zoomOut: false,
61
+ weight: "bold",
62
+ },
63
+ });
64
+ },
65
+ stubAfter: (ctx) => {
66
+ ctx.banner("In the cart");
67
+ ctx.todos(["Find the product", "Add to cart", "Open cart"]);
68
+ ctx.todoIndex(1);
69
+ ctx.ring("badge", {
36
70
  selector: InventorySel.cartBadge,
37
71
  label: "Cart badge",
38
- detail: "Item is in the cart",
39
- },
72
+ detail: "Item count updated",
73
+ focus: true,
74
+ color: "#86efac",
75
+ zoom: 1.6,
76
+ followMouse: true,
77
+ duration: true,
78
+ });
40
79
  },
41
80
  },
42
81
  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.33",
3
+ "version": "0.12.35",
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,37 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
14
15
  checkpoint: "CartPage",
15
16
  click: ".shopping_cart_link",
16
17
  verify: [Trait.url({ pathname: "/cart.html" })],
18
+ stubBefore: (ctx) => {
19
+ ctx.title("Open cart");
20
+ ctx.todos(["Find the product", "Add to cart", "Open cart"]);
21
+ ctx.todoIndex(2);
22
+ ctx.zoom(1.4);
23
+ ctx.highlights({
24
+ cart: {
25
+ selector: ".shopping_cart_link",
26
+ label: "Cart",
27
+ detail: "Zoom cart chrome",
28
+ focus: true,
29
+ color: "#c9a6ff",
30
+ zoom: 1.55,
31
+ zoomOut: false,
32
+ followMouse: true,
33
+ weight: "bold",
34
+ },
35
+ });
36
+ },
37
+ stubAfter: (ctx) => {
38
+ ctx.banner("Your cart");
39
+ ctx.zoomOut(false);
40
+ ctx.ring("items", {
41
+ selector: InventorySel.cartItem,
42
+ label: "Cart items",
43
+ detail: "Product landed in cart",
44
+ focus: true,
45
+ color: "#86efac",
46
+ zoom: 1.4,
47
+ followMouse: true,
48
+ duration: true,
49
+ });
50
+ },
17
51
  });
@@ -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,49 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
30
33
  },
31
34
  },
32
35
  ],
33
- stubBefore: {},
34
- stubAfter: {
35
- badge: {
36
+ // Demo: zoom product -> add (stay zoomed), then badge after act.
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.zoomOut(false);
43
+ ctx.highlights({
44
+ product: {
45
+ selector: InventorySel.title(DEMO_PRODUCT_ID),
46
+ label: "Sauce Labs Backpack",
47
+ detail: "Zoom the product",
48
+ focus: true,
49
+ color: "#c9a6ff",
50
+ zoom: 1.45,
51
+ zoomOut: false,
52
+ followMouse: true,
53
+ },
54
+ add: {
55
+ selector: InventorySel.addBtn(DEMO_PRODUCT_ID),
56
+ label: "Add to cart",
57
+ focus: true,
58
+ color: "#86efac",
59
+ zoom: 1.5,
60
+ zoomOut: false,
61
+ weight: "bold",
62
+ },
63
+ });
64
+ },
65
+ stubAfter: (ctx) => {
66
+ ctx.banner("In the cart");
67
+ ctx.todos(["Find the product", "Add to cart", "Open cart"]);
68
+ ctx.todoIndex(1);
69
+ ctx.ring("badge", {
36
70
  selector: InventorySel.cartBadge,
37
71
  label: "Cart badge",
38
- detail: "Item is in the cart",
39
- },
72
+ detail: "Item count updated",
73
+ focus: true,
74
+ color: "#86efac",
75
+ zoom: 1.6,
76
+ followMouse: true,
77
+ duration: true,
78
+ });
40
79
  },
41
80
  },
42
81
  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
  },