waygraph 0.13.3 → 0.13.5

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.
Files changed (27) hide show
  1. package/dist/cli.js +1776 -124
  2. package/dist/highlights.d.ts +35 -3
  3. package/dist/highlights.js +55 -17
  4. package/dist/index.d.ts +1 -1
  5. package/examples/saucedemo/.tmp/device-demo/orientation-gestures.webm +0 -0
  6. package/examples/saucedemo/.tmp/device-demo/page@fe3808e393870b60560762cb58982a73.webm +0 -0
  7. package/examples/saucedemo/.tmp/device-showcase/page@01821f1be3025a1f33af7aeaa7c31453.webm +0 -0
  8. package/examples/saucedemo/.tmp/device-showcase/page@112894bf514e729c25426effd0465db0.webm +0 -0
  9. package/examples/saucedemo/.tmp/device-showcase/page@254d2f8bb34167fb9f128efc2aa9831f.webm +0 -0
  10. package/examples/saucedemo/.tmp/device-showcase/page@2fee43e36341108e0ee6e6a1360b14f9.webm +0 -0
  11. package/examples/saucedemo/.tmp/device-showcase/page@648144d90fdf2c65bd7b03faeadc2aeb.webm +0 -0
  12. package/examples/saucedemo/.tmp/device-showcase/page@a360145174b3bccffd6516db7d084a9c.webm +0 -0
  13. package/examples/saucedemo/.tmp/device-showcase/page@d045914b97b35d17147a940965a7a93f.webm +0 -0
  14. package/examples/saucedemo/.tmp/ts/page@6145fa8fc23d19870af85e0e111c0e30.webm +0 -0
  15. package/examples/saucedemo/.tmp/ts-sample/page@e2995031b7eef8fc0e3c1bdb60f58254.webm +0 -0
  16. package/examples/saucedemo/.tmp/ts2/page@1a450773bef74c91a68a89093fc41838.webm +0 -0
  17. package/examples/saucedemo/README.md +2 -0
  18. package/examples/saucedemo/package.json +2 -0
  19. package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-cart.block.ts +21 -13
  20. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +27 -18
  21. package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +40 -27
  22. package/examples/saucedemo/src/flows/device-showcase.flow.ts +28 -0
  23. package/package.json +1 -1
  24. package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +10 -4
  25. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +7 -5
  26. package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +6 -3
  27. package/templates/quickstart/src/flows/device-showcase.flow.ts +28 -0
@@ -88,6 +88,22 @@ export type TodoDockState = {
88
88
  groups: WaygraphTodoGroup[];
89
89
  pos?: "left" | "right";
90
90
  };
91
+ /** Options for compact {@link StubCtx.todos}(`id`, items, opts). */
92
+ export type WaygraphTodosOpts = {
93
+ /** Dock heading. */
94
+ title?: string;
95
+ /** Current item index (0-based). */
96
+ index?: number;
97
+ /** `sequential` | `checklist` | `bullets`. */
98
+ style?: TodoListStyle;
99
+ /** Dock side. */
100
+ pos?: "left" | "right";
101
+ /**
102
+ * Keep other docks with different ids (multi-todo). Default false —
103
+ * one checklist at a time (setting todos replaces the previous dock).
104
+ */
105
+ parallel?: boolean;
106
+ };
91
107
  /** Episode-level fixtures authored inside stubBefore/After/OnError(ctx). */
92
108
  export type StubPhaseFixtures = {
93
109
  todos?: readonly WaygraphTodoInput[];
@@ -242,16 +258,24 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
242
258
  * for authored stub queues; set false to keep cursor hidden).
243
259
  */
244
260
  cursor?: boolean;
261
+ /**
262
+ * Optional link to a todo item id. When set, Method fill/click advances that
263
+ * todo row instead of relying on stub array index (avoids password/submit/username
264
+ * alphabetical reorder bugs).
265
+ */
266
+ todo?: string;
267
+ /** Slot key from stubBefore/After map (e.g. "username") - set by resolver. */
268
+ slotId?: string;
245
269
  /**
246
270
  * Pointer feel when touchMode is on (0.13+):
247
271
  * - `tap` - short press via touchscreen (default in touch mode)
248
272
  * - `hold` - long-press (~600ms) then release
249
273
  * - `click` - mouse click even in touch mode
250
274
  */
251
- gesture?: "tap" | "hold" | "click";
275
+ gesture?: "tap" | "hold" | "click" | "swipe";
252
276
  }
253
277
  /** Flow fixture patch: label required; selector optional (inherits from stub). */
254
- 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;
278
+ 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" | "todo" | "gesture"> & FixtureDurationFields;
255
279
  export type HighlightStubPhase = Record<string, WaygraphHighlightStub>;
256
280
  /**
257
281
  * Open stub lifecycle context. Authors set rings + episode fixtures freely —
@@ -283,8 +307,14 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
283
307
  highlights(slots: HighlightStubPhase): void;
284
308
  /** Set / overwrite one named ring. */
285
309
  ring(id: string, stub: WaygraphHighlightStub): void;
286
- /** Episode checklist (demo panel). Single list; use {@link StubCtx.todoGroups} for FR/AC packs. */
310
+ /**
311
+ * Floating checklist. Prefer the compact form:
312
+ * ctx.todos("login", ["Enter email", "Enter password"], { title: "Sign in", index: 0 })
313
+ * Legacy: ctx.todos([...]); ctx.todoId(...); ctx.todoIndex(...); …
314
+ * Default = one dock (setting todos replaces). Pass `{ parallel: true }` to keep others.
315
+ */
287
316
  todos(items: readonly WaygraphTodoInput[]): void;
317
+ todos(id: string, items: readonly WaygraphTodoInput[], opts?: WaygraphTodosOpts): void;
288
318
  /** Current checklist index (bump for sequential plans). */
289
319
  todoIndex(n: number): void;
290
320
  /** Heading above the single {@link StubCtx.todos} list. */
@@ -392,6 +422,8 @@ export type StubPhaseResult = {
392
422
  zoomOut?: boolean;
393
423
  title?: string;
394
424
  todoPos?: "left" | "right";
425
+ /** When true, keep other docks (multi-todo). Default false = replace. */
426
+ todoParallel?: boolean;
395
427
  };
396
428
  /**
397
429
  * One yap/slide in a multi-step demo caption sequence.
@@ -475,12 +475,19 @@ function pickFxFields(base, patch) {
475
475
  const focus = patch?.focus !== undefined ? patch.focus : base?.focus;
476
476
  const color = patch?.color !== undefined ? patch.color : base?.color;
477
477
  const cursor = patch?.cursor !== undefined ? patch.cursor : base?.cursor;
478
+ const todo = patch?.todo !== undefined ? patch.todo : base?.todo;
479
+ const gesture = patch?.gesture !== undefined ? patch.gesture : base?.gesture;
478
480
  if (focus !== undefined)
479
481
  out.focus = !!focus;
480
482
  if (color !== undefined && String(color).trim())
481
483
  out.color = String(color).trim();
482
484
  if (cursor !== undefined)
483
485
  out.cursor = !!cursor;
486
+ if (todo !== undefined && String(todo).trim())
487
+ out.todo = String(todo).trim();
488
+ if (gesture === "tap" || gesture === "hold" || gesture === "click" || gesture === "swipe") {
489
+ out.gesture = gesture;
490
+ }
484
491
  return out;
485
492
  }
486
493
  function mergeSlot(base, patch) {
@@ -634,9 +641,7 @@ export function buildTodoDock(opts) {
634
641
  if (!items.length)
635
642
  return undefined;
636
643
  const group = { style, items };
637
- if (opts.todoTitle && String(opts.todoTitle).trim()) {
638
- group.title = String(opts.todoTitle).trim();
639
- }
644
+ // Title once on the dock - not also on the lone group (avoids duplicate headings).
640
645
  const dock = { style, groups: [group] };
641
646
  if (opts.todoTitle && String(opts.todoTitle).trim()) {
642
647
  dock.title = String(opts.todoTitle).trim();
@@ -738,13 +743,38 @@ function createStubCtx(bag, opts) {
738
743
  ring(id, stub) {
739
744
  bag.highlights = { ...bag.highlights, [id]: stub };
740
745
  },
741
- todos(items) {
746
+ todos(a, b, c) {
742
747
  bag.todoTouched = true;
743
748
  bag.hideTodos = false;
744
- bag.todos = items;
745
749
  delete bag.todoGroups;
746
- // Empty list = explicit clear (same as hideTodos).
747
- if (!items || !items.length)
750
+ if (typeof a === "string") {
751
+ const id = String(a).trim();
752
+ if (id)
753
+ bag.todoId = id;
754
+ else
755
+ delete bag.todoId;
756
+ const items = (Array.isArray(b) ? b : []);
757
+ bag.todos = items;
758
+ const opts = (c && typeof c === "object" ? c : {});
759
+ if (opts.title != null)
760
+ bag.todoTitle = String(opts.title);
761
+ if (opts.index != null)
762
+ bag.todoIndex = Number(opts.index);
763
+ const st = normalizeTodoStyle(opts.style);
764
+ if (st)
765
+ bag.todoStyle = st;
766
+ const pos = normalizeTodoPos(opts.pos);
767
+ if (pos)
768
+ bag.todoPos = pos;
769
+ bag.todoParallel = opts.parallel === true;
770
+ if (!items.length)
771
+ bag.hideTodos = true;
772
+ return;
773
+ }
774
+ bag.todos = a;
775
+ // Legacy list-only form: leave parallel unset (CLI treats as replace).
776
+ delete bag.todoParallel;
777
+ if (!a || !a.length)
748
778
  bag.hideTodos = true;
749
779
  },
750
780
  todoIndex(n) {
@@ -977,19 +1007,26 @@ function applyDefaultZoomOut(slots, defaultZoomOut) {
977
1007
  return slots.map((h) => h.zoomOut !== undefined ? h : { ...h, zoomOut: defaultZoomOut });
978
1008
  }
979
1009
  function mergePhaseMaps(phaseMap, fixturePhase) {
980
- const slotIds = new Set([...Object.keys(phaseMap), ...Object.keys(fixturePhase || {})]);
981
- const ordered = [...slotIds].sort((a, b) => {
982
- const na = Number(a);
983
- const nb = Number(b);
984
- if (!Number.isNaN(na) && !Number.isNaN(nb))
985
- return na - nb;
986
- return a.localeCompare(b);
987
- });
1010
+ const phaseKeys = Object.keys(phaseMap);
1011
+ const fixtureKeys = Object.keys(fixturePhase || {});
1012
+ const allKeys = [...new Set([...phaseKeys, ...fixtureKeys])];
1013
+ const allNumeric = allKeys.length > 0 && allKeys.every((k) => Number.isFinite(Number(k)) && String(Number(k)) === k);
1014
+ // Numeric stub ids (legacy "0","1") sort by number. Named stubs keep author
1015
+ // insertion order - localeCompare used to reorder password/submit/username
1016
+ // and advance the wrong todo index on fill.
1017
+ const ordered = allNumeric
1018
+ ? [...allKeys].sort((a, b) => Number(a) - Number(b))
1019
+ : [...phaseKeys, ...fixtureKeys.filter((k) => !phaseKeys.includes(k))];
988
1020
  const resolved = [];
989
1021
  for (const id of ordered) {
990
1022
  const merged = mergeSlot(phaseMap[id], fixturePhase?.[id]);
991
- if (merged)
992
- resolved.push(merged);
1023
+ if (merged) {
1024
+ resolved.push({
1025
+ ...merged,
1026
+ slotId: id,
1027
+ ...(merged.todo ? { todo: merged.todo } : {}),
1028
+ });
1029
+ }
993
1030
  }
994
1031
  return resolved;
995
1032
  }
@@ -1138,6 +1175,7 @@ export async function runStubPhase(block, phase, opts) {
1138
1175
  ...(bag.zoomOut !== undefined ? { zoomOut: bag.zoomOut } : {}),
1139
1176
  ...(bag.title !== undefined && bag.title !== "" ? { title: bag.title } : {}),
1140
1177
  ...(bag.todoPos !== undefined ? { todoPos: bag.todoPos } : {}),
1178
+ ...(bag.todoParallel === true ? { todoParallel: true } : {}),
1141
1179
  };
1142
1180
  }
1143
1181
  /**
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
4
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
5
5
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
6
6
  export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
7
- export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, DevicePreset, DeviceViewport, DeviceState, DeviceOrientation, } from "./highlights.js";
7
+ export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, WaygraphTodosOpts, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, DevicePreset, DeviceViewport, DeviceState, DeviceOrientation, } from "./highlights.js";
8
8
  export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
9
9
  export type { ActionPage } from "./types.js";
10
10
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
@@ -39,6 +39,8 @@ npx playwright install chromium
39
39
  | Coverage JSON + min gate (10%) | `npm run traverse:coverage` |
40
40
  | Shop demo stepper | `npm run demo:step` |
41
41
  | Shop demo autoplay | `npm run demo:autoplay` |
42
+ | **Device showcase** (mobile/tablet + portrait/landscape) | `npm run demo:device` |
43
+ | Device showcase + `.webm` | `npm run demo:device:video` |
42
44
  | **Cart bulk** demo (add-all → remove-all) | `npm run demo:bulk` |
43
45
  | Cart bulk stepper | `npm run demo:bulk:step` |
44
46
  | List flows | `npm run flows` |
@@ -25,6 +25,8 @@
25
25
  "prove:add-to-cart-menu": "node scripts/prove-add-to-cart-menu.mjs",
26
26
  "prove": "npm run prove:login-branch && npm run prove:graph && npm run prove:add-to-cart-menu && npm test",
27
27
  "record": "waygraph run --blocks \"loginFlow then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}})\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --video --no-step",
28
+ "demo:device": "waygraph demo --blocks deviceShowcaseFlow --auto-next --title \"Device showcase\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\"",
29
+ "demo:device:video": "waygraph demo --blocks deviceShowcaseFlow --auto-play-video --title \"Device showcase\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\" --video ./.tmp/device-showcase",
28
30
  "demo:bulk": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
29
31
  "demo:bulk:auto-next": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-next --title \"Cart bulk demo\"",
30
32
  "demo:bulk:video": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-play-video --title \"Cart bulk demo\"",
@@ -17,40 +17,48 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
17
17
  click: ".shopping_cart_link",
18
18
  verify: [Trait.url({ pathname: "/cart.html" })],
19
19
  stubBefore: (ctx: StubCtx) => {
20
- ctx.title("Open cart");
21
- ctx.todoId("saucedemo-cart");
22
- ctx.todos([
23
- { id: "cart-find", text: "Find the product" },
24
- { id: "cart-add", text: "Add to cart" },
25
- { id: "cart-open", text: "Open cart" },
26
- ]);
27
- ctx.todoIndex(2);
20
+ ctx.title("Open cart · landscape");
21
+ ctx.landscape();
22
+ ctx.todos(
23
+ "saucedemo",
24
+ [
25
+ { id: "login-user", text: "Enter username", done: true },
26
+ { id: "login-pass", text: "Enter password", done: true },
27
+ { id: "login-submit", text: "Tap Login", done: true },
28
+ { id: "cart-find", text: "Find the product", done: true },
29
+ { id: "cart-add", text: "Add to cart", done: true },
30
+ { id: "cart-open", text: "Open cart" },
31
+ ],
32
+ { title: "Device showcase", index: 5 },
33
+ );
28
34
  ctx.zoom(1.4);
29
35
  ctx.zoomOut(false);
30
36
  ctx.highlights({
31
37
  cart: {
32
38
  selector: ".shopping_cart_link",
33
39
  label: "Cart",
34
- detail: "Zoom cart chrome",
40
+ detail: "Open cart",
35
41
  focus: true,
36
42
  color: "#c9a6ff",
37
- zoom: 1.55,
38
43
  zoomOut: false,
39
44
  followMouse: false,
40
45
  weight: "bold",
46
+ gesture: "tap",
41
47
  },
42
48
  });
43
49
  },
44
50
  stubAfter: (ctx: StubCtx) => {
45
- ctx.banner("Your cart");
46
- ctx.zoomOut(false);
51
+ ctx.banner("Your cart · desktop");
52
+ ctx.clearDevice();
53
+ ctx.hideTodos();
54
+ ctx.zoomOut(true);
47
55
  ctx.ring("items", {
48
56
  selector: InventorySel.cartItem,
49
57
  label: "Cart items",
50
58
  detail: "Product landed in cart",
51
59
  focus: true,
52
60
  color: "#86efac",
53
- zoom: 1.4,
61
+ zoomOut: true,
54
62
  followMouse: false,
55
63
  duration: true,
56
64
  });
@@ -36,16 +36,21 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
36
36
  ],
37
37
  // Demo: zoom product -> add (stay zoomed), then badge after act.
38
38
  stubBefore: (ctx: StubCtx) => {
39
- ctx.title("Pick a product");
40
- // Back to desktop after mobile login (explicit clear - not omit/remain).
41
- ctx.clearDevice();
42
- ctx.todoId("saucedemo-cart");
43
- ctx.todos([
44
- { id: "cart-find", text: "Find the product" },
45
- { id: "cart-add", text: "Add to cart" },
46
- { id: "cart-open", text: "Open cart" },
47
- ]);
48
- ctx.todoIndex(0);
39
+ ctx.title("Pick a product · tablet");
40
+ ctx.device("tablet");
41
+ ctx.portrait();
42
+ ctx.todos(
43
+ "saucedemo",
44
+ [
45
+ { id: "login-user", text: "Enter username", done: true },
46
+ { id: "login-pass", text: "Enter password", done: true },
47
+ { id: "login-submit", text: "Tap Login", done: true },
48
+ { id: "cart-find", text: "Find the product" },
49
+ { id: "cart-add", text: "Add to cart" },
50
+ { id: "cart-open", text: "Open cart" },
51
+ ],
52
+ { title: "Device showcase", index: 3 },
53
+ );
49
54
  ctx.zoom(1.35);
50
55
  ctx.zoomOut(false);
51
56
  ctx.highlights({
@@ -71,13 +76,18 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
71
76
  },
72
77
  stubAfter: (ctx: StubCtx) => {
73
78
  ctx.banner("In the cart");
74
- ctx.todoId("saucedemo-cart");
75
- ctx.todos([
76
- { id: "cart-find", text: "Find the product" },
77
- { id: "cart-add", text: "Add to cart" },
78
- { id: "cart-open", text: "Open cart" },
79
- ]);
80
- ctx.todoIndex(1);
79
+ ctx.todos(
80
+ "saucedemo",
81
+ [
82
+ { id: "login-user", text: "Enter username", done: true },
83
+ { id: "login-pass", text: "Enter password", done: true },
84
+ { id: "login-submit", text: "Tap Login", done: true },
85
+ { id: "cart-find", text: "Find the product", done: true },
86
+ { id: "cart-add", text: "Add to cart" },
87
+ { id: "cart-open", text: "Open cart" },
88
+ ],
89
+ { title: "Device showcase", index: 4 },
90
+ );
81
91
  ctx.zoomOut(false);
82
92
  ctx.ring("badge", {
83
93
  selector: InventorySel.cartBadge,
@@ -85,7 +95,6 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
85
95
  detail: "Item count updated",
86
96
  focus: true,
87
97
  color: "#86efac",
88
- zoom: 1.6,
89
98
  zoomOut: false,
90
99
  duration: true,
91
100
  });
@@ -50,21 +50,27 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
50
50
  // Open lifecycle: banner title, todos, Screen Studio camera zoom,
51
51
  // sequential highlight queue (appear / dwell / fade), focus + color.
52
52
  stubBefore: (ctx: StubCtx) => {
53
- ctx.title("Signing in");
54
- // 0.13+: mobile viewport + touch theater; omit on later blocks = remain.
53
+ ctx.title("Signing in · mobile");
55
54
  ctx.device("mobile");
56
- ctx.todoId("saucedemo-login");
57
- ctx.todos([
58
- { id: "login-user", text: "Enter username" },
59
- { id: "login-pass", text: "Enter password" },
60
- { id: "login-submit", text: "Tap Login" },
61
- ]);
62
- ctx.todoIndex(0);
55
+ ctx.portrait();
56
+ ctx.todos(
57
+ "saucedemo",
58
+ [
59
+ { id: "login-user", text: "Enter username" },
60
+ { id: "login-pass", text: "Enter password" },
61
+ { id: "login-submit", text: "Tap Login" },
62
+ { id: "cart-find", text: "Find the product" },
63
+ { id: "cart-add", text: "Add to cart" },
64
+ { id: "cart-open", text: "Open cart" },
65
+ ],
66
+ { title: "Device showcase", index: 0 },
67
+ );
63
68
  ctx.zoom(1.35);
64
69
  ctx.highlights({
65
70
  username: {
66
71
  selector: "#user-name",
67
72
  label: "Username",
73
+ todo: "login-user",
68
74
  focus: true,
69
75
  color: "#c9a6ff",
70
76
  zoom: 1.4,
@@ -72,6 +78,7 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
72
78
  password: {
73
79
  selector: "#password",
74
80
  label: "Password",
81
+ todo: "login-pass",
75
82
  focus: true,
76
83
  color: "#c9a6ff",
77
84
  zoom: 1.4,
@@ -79,6 +86,7 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
79
86
  submit: {
80
87
  selector: "#login-button",
81
88
  label: "Login",
89
+ todo: "login-submit",
82
90
  focus: true,
83
91
  color: "#86efac",
84
92
  zoom: 1.55,
@@ -88,17 +96,17 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
88
96
  });
89
97
  },
90
98
  stubAfter: (ctx: StubCtx) => {
91
- // Fail branch lands on LoginPage with Epic sadface - do not ring the
92
- // inventory shelf. Success (LoggedIn) hands off to shopFlow.
93
99
  if (ctx.out && ctx.out.__state === "LoginPage") {
94
100
  ctx.title("Login blocked");
95
- ctx.todoId("saucedemo-login");
96
- ctx.todos([
97
- { id: "login-user", text: "Enter username", done: true },
98
- { id: "login-pass", text: "Enter password", done: true },
99
- { id: "login-submit", text: "Click Login", done: true },
100
- ]);
101
- ctx.todoStyle("checklist");
101
+ ctx.todos(
102
+ "saucedemo",
103
+ [
104
+ { id: "login-user", text: "Enter username", done: true },
105
+ { id: "login-pass", text: "Enter password", done: true },
106
+ { id: "login-submit", text: "Click Login", done: true },
107
+ ],
108
+ { title: "Sign in", style: "checklist" },
109
+ );
102
110
  ctx.ring("error", {
103
111
  selector: '[data-test="error"]',
104
112
  label: "Login error banner",
@@ -109,15 +117,20 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
109
117
  });
110
118
  return;
111
119
  }
112
- ctx.banner("Inventory");
113
- ctx.todoId("saucedemo-login");
114
- ctx.todos([
115
- { id: "login-user", text: "Enter username" },
116
- { id: "login-pass", text: "Enter password" },
117
- { id: "login-submit", text: "Click Login" },
118
- ]);
119
- ctx.todoIndex(2);
120
- // Landed on inventory - hand off to shopFlow for product -> cart zoom.
120
+ ctx.banner("Inventory · landscape");
121
+ ctx.landscape();
122
+ ctx.todos(
123
+ "saucedemo",
124
+ [
125
+ { id: "login-user", text: "Enter username", done: true },
126
+ { id: "login-pass", text: "Enter password", done: true },
127
+ { id: "login-submit", text: "Tap Login", done: true },
128
+ { id: "cart-find", text: "Find the product" },
129
+ { id: "cart-add", text: "Add to cart" },
130
+ { id: "cart-open", text: "Open cart" },
131
+ ],
132
+ { title: "Device showcase", index: 3 },
133
+ );
121
134
  ctx.ring("shelf", {
122
135
  selector: ".inventory_list",
123
136
  label: "Product shelf",
@@ -0,0 +1,28 @@
1
+ import { Engine, start, end, withTitle } from "waygraph";
2
+ import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
3
+ import { SubmitLoginForFlow } from "../blocks/saucedemo-web/methods/submit-login-for-flow.js";
4
+ import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.js";
5
+ import { NavCartBlock } from "../blocks/saucedemo-web/cart/nav-cart.block.js";
6
+
7
+ const engine = new Engine();
8
+
9
+ /**
10
+ * Short device/orientation showcase (0.13.3+):
11
+ * login mobile portrait -> land on inventory landscape ->
12
+ * add-to-cart tablet portrait -> open cart tablet landscape -> desktop
13
+ *
14
+ * Run:
15
+ * npm run demo:device
16
+ * npm run demo:device:video
17
+ */
18
+ export const deviceShowcaseFlow = withTitle(
19
+ engine.defineFlow([
20
+ start,
21
+ NavLoginBlock,
22
+ SubmitLoginForFlow,
23
+ AddToCartBlock,
24
+ NavCartBlock,
25
+ end,
26
+ ]),
27
+ "Device showcase",
28
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.13.3",
3
+ "version": "0.13.5",
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,4 +1,5 @@
1
1
  import { defineNavClickBlock, Trait } from "waygraph";
2
+ import type { StubCtx } from "waygraph";
2
3
  import type { CartPage } from "../../../states/checkout.states.js";
3
4
  import { InventorySel } from "../inventory/methods/inventory-items.js";
4
5
 
@@ -15,8 +16,10 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
15
16
  checkpoint: "CartPage",
16
17
  click: ".shopping_cart_link",
17
18
  verify: [Trait.url({ pathname: "/cart.html" })],
18
- stubBefore: (ctx) => {
19
- ctx.title("Open cart");
19
+ stubBefore: (ctx: StubCtx) => {
20
+ ctx.title("Open cart · landscape");
21
+ // Device showcase: rotate tablet to landscape before the cart tap.
22
+ ctx.landscape();
20
23
  ctx.todoId("saucedemo-cart");
21
24
  ctx.todos([
22
25
  { id: "cart-find", text: "Find the product" },
@@ -37,11 +40,14 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
37
40
  zoomOut: false,
38
41
  followMouse: false,
39
42
  weight: "bold",
43
+ gesture: "tap",
40
44
  },
41
45
  });
42
46
  },
43
- stubAfter: (ctx) => {
44
- ctx.banner("Your cart");
47
+ stubAfter: (ctx: StubCtx) => {
48
+ ctx.banner("Your cart · desktop");
49
+ // Showcase finale: back to desktop.
50
+ ctx.clearDevice();
45
51
  ctx.zoomOut(false);
46
52
  ctx.ring("items", {
47
53
  selector: InventorySel.cartItem,
@@ -1,4 +1,5 @@
1
1
  import { defineEffectBlock, checkpoint, type Checkpoint, type WaygraphInstanceOption } from "waygraph";
2
+ import type { StubCtx } from "waygraph";
2
3
  import type { ItemInCart } from "../../../../states/checkout.states.js";
3
4
  import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
4
5
  import { collectAddableItems, InventorySel } from "./inventory-items.js";
@@ -34,10 +35,11 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
34
35
  },
35
36
  ],
36
37
  // Demo: zoom product -> add (stay zoomed), then badge after act.
37
- stubBefore: (ctx) => {
38
- ctx.title("Pick a product");
39
- // Back to desktop after mobile login (explicit clear - not omit/remain).
40
- ctx.clearDevice();
38
+ stubBefore: (ctx: StubCtx) => {
39
+ ctx.title("Pick a product · tablet");
40
+ // Device showcase: tablet portrait (toast + lerp from mobile landscape).
41
+ ctx.device("tablet");
42
+ ctx.portrait();
41
43
  ctx.todoId("saucedemo-cart");
42
44
  ctx.todos([
43
45
  { id: "cart-find", text: "Find the product" },
@@ -68,7 +70,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
68
70
  },
69
71
  });
70
72
  },
71
- stubAfter: (ctx) => {
73
+ stubAfter: (ctx: StubCtx) => {
72
74
  ctx.banner("In the cart");
73
75
  ctx.todoId("saucedemo-cart");
74
76
  ctx.todos([
@@ -50,9 +50,10 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
50
50
  // Open lifecycle: banner title, todos, Screen Studio camera zoom,
51
51
  // sequential highlight queue (appear / dwell / fade), focus + color.
52
52
  stubBefore: (ctx: StubCtx) => {
53
- ctx.title("Signing in");
54
- // 0.13+: mobile viewport + touch theater; omit on later blocks = remain.
53
+ ctx.title("Signing in · mobile");
54
+ // 0.13.3: mobile portrait + touch; tap Login.
55
55
  ctx.device("mobile");
56
+ ctx.portrait();
56
57
  ctx.todoId("saucedemo-login");
57
58
  ctx.todos([
58
59
  { id: "login-user", text: "Enter username" },
@@ -109,7 +110,9 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
109
110
  });
110
111
  return;
111
112
  }
112
- ctx.banner("Inventory");
113
+ ctx.banner("Inventory · landscape");
114
+ // Rotate mobile to landscape on the product shelf (toast + lerp).
115
+ ctx.landscape();
113
116
  ctx.todoId("saucedemo-login");
114
117
  ctx.todos([
115
118
  { id: "login-user", text: "Enter username" },