waygraph 0.12.46 → 0.12.48

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
@@ -1724,6 +1724,23 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1724
1724
  // rings stay visible ~2s before the error panel (PIA stubOnError RFC).
1725
1725
  const defaultHoldMs = opts && opts.defaultHoldMs != null ? opts.defaultHoldMs : null;
1726
1726
  const pace = opts && opts.pace !== undefined ? opts.pace : undefined;
1727
+ let dock = opts && opts.todoDock;
1728
+ const advanceTodos = !(opts && opts.advanceTodos === false);
1729
+ // Push stubAfter/before dock before the first ring so Method after-panels
1730
+ // show the updated checklist immediately (not only after the ring cycle).
1731
+ if (dock) {
1732
+ await page
1733
+ .evaluate((d) => {
1734
+ if (window.__wgSyncTodos) window.__wgSyncTodos({ sync: "set", dock: d });
1735
+ }, dock)
1736
+ .catch(() => {});
1737
+ } else if (opts && opts.todoSync === "clear") {
1738
+ await page
1739
+ .evaluate(() => {
1740
+ if (window.__wgSyncTodos) window.__wgSyncTodos({ sync: "clear" });
1741
+ })
1742
+ .catch(() => {});
1743
+ }
1727
1744
  for (let i = 0; i < list.length; i++) {
1728
1745
  const h = list[i];
1729
1746
  try {
@@ -1735,7 +1752,18 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1735
1752
  size: h.size,
1736
1753
  weight: h.weight,
1737
1754
  });
1738
- if ((opts && opts.todos && opts.todos.length) || (h.todos && h.todos.length)) {
1755
+ // Do NOT advanceTodoDock by ring index after stubAfter already set
1756
+ // progress - that reset current back to 0 and the next block looked
1757
+ // blank. Mid-act Method advance still uses syncTodoDockAdvance.
1758
+ if (dock && advanceTodos) {
1759
+ dock = advanceTodoDock(dock, i);
1760
+ if (opts && opts.todoDockRef) opts.todoDockRef.current = dock;
1761
+ await page
1762
+ .evaluate((d) => {
1763
+ if (window.__wgSyncTodos) window.__wgSyncTodos({ sync: "set", dock: d });
1764
+ }, dock)
1765
+ .catch(() => {});
1766
+ } else if (!dock && ((opts && opts.todos && opts.todos.length) || (h.todos && h.todos.length))) {
1739
1767
  const rows =
1740
1768
  opts && opts.todos && opts.todos.length
1741
1769
  ? opts.todos
@@ -1743,7 +1771,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1743
1771
  await page
1744
1772
  .evaluate((todos) => {
1745
1773
  if (window.__wgSyncTodos) {
1746
- window.__wgSyncTodos(todos);
1774
+ window.__wgSyncTodos({ sync: "set", list: todos });
1747
1775
  return;
1748
1776
  }
1749
1777
  // Fallback for pages that never got installOverlay sync helper.
@@ -1836,7 +1864,17 @@ async function renderAfterStep(page, info) {
1836
1864
  await cycleHighlightRings(page, highlights, gatesFast, {
1837
1865
  pace: info.pace,
1838
1866
  stepLabel: miniStepLabel(info),
1867
+ todoDock: info.todoDock,
1868
+ todos: info.todos,
1869
+ todoSync: info.todoSync,
1870
+ todoDockRef: info.todoDockRef,
1871
+ // stubAfter already authored progress - do not re-index by ring 0..n
1872
+ advanceTodos: false,
1839
1873
  });
1874
+ // After-ring advances may have moved the dock - surface latest for caller.
1875
+ if (info.todoDockRef && info.todoDockRef.current) {
1876
+ info.todoDock = info.todoDockRef.current;
1877
+ }
1840
1878
  const afterPayload = { ...info, stepLabel: miniStepLabel(info) };
1841
1879
  await page
1842
1880
  .evaluate((info) => {
@@ -2246,6 +2284,46 @@ async function matchStubForLocator(page, locator, stubs) {
2246
2284
  return null;
2247
2285
  }
2248
2286
 
2287
+ /** 0-based stub index for Method fill/click todo advance; -1 if none. */
2288
+ async function matchStubIndexForLocator(page, locator, stubs) {
2289
+ if (!stubs || stubs.length === 0) return -1;
2290
+ const box = await locator.boundingBox().catch(() => null);
2291
+ if (!box) return -1;
2292
+ for (let i = 0; i < stubs.length; i++) {
2293
+ const s = stubs[i];
2294
+ try {
2295
+ const b = await page.locator(s.selector).first().boundingBox();
2296
+ if (
2297
+ b &&
2298
+ Math.abs(b.x - box.x) < 4 &&
2299
+ Math.abs(b.y - box.y) < 4 &&
2300
+ Math.abs(b.width - box.width) < 8
2301
+ ) {
2302
+ return i;
2303
+ }
2304
+ } catch {
2305
+ /* skip */
2306
+ }
2307
+ }
2308
+ return -1;
2309
+ }
2310
+
2311
+ /** Advance sequential todo dock to stubIndex and push to the page (Method act). */
2312
+ async function syncTodoDockAdvance(page, todoDockRef, stubIndex) {
2313
+ if (!todoDockRef || stubIndex < 0) return;
2314
+ const prev = todoDockRef.current;
2315
+ if (!prev) return;
2316
+ const advanced = advanceTodoDock(prev, stubIndex);
2317
+ todoDockRef.current = advanced;
2318
+ await page
2319
+ .evaluate((dock) => {
2320
+ if (window.__wgSyncTodos) {
2321
+ window.__wgSyncTodos({ sync: "set", dock });
2322
+ }
2323
+ }, advanced)
2324
+ .catch(() => {});
2325
+ }
2326
+
2249
2327
  async function captionForLocator(page, locator, stubs, fallback) {
2250
2328
  const matched = await matchStubForLocator(page, locator, stubs);
2251
2329
  if (matched) {
@@ -2619,7 +2697,7 @@ async function wasJustNarrated(page) {
2619
2697
  .catch(() => false);
2620
2698
  }
2621
2699
 
2622
- function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef) {
2700
+ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef, todoDockRef) {
2623
2701
  // Playwright's own slowMo ALREADY pauses after every single low-level
2624
2702
  // action it dispatches - and pressSequentially() fires one such action
2625
2703
  // PER CHARACTER. Also giving pressSequentially its own fixed delay
@@ -2675,6 +2753,8 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2675
2753
  : "writing from mem";
2676
2754
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
2677
2755
  const cap = await captionForLocator(page, this, stubs, fallback);
2756
+ const stubIdx = await matchStubIndexForLocator(page, this, stubs);
2757
+ await syncTodoDockAdvance(page, todoDockRef, stubIdx);
2678
2758
  await moveCursorTo(page, box, cursorMs(500));
2679
2759
  await showRing(page, box, cap.label, cap.tone, {
2680
2760
  size: cap.size,
@@ -2749,6 +2829,8 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2749
2829
  }
2750
2830
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
2751
2831
  const cap = await captionForLocator(page, this, stubs, fallback);
2832
+ const stubIdx = await matchStubIndexForLocator(page, this, stubs);
2833
+ await syncTodoDockAdvance(page, todoDockRef, stubIdx);
2752
2834
  clickTone = cap.tone;
2753
2835
  clickPoint = await moveCursorTo(page, box, cursorMs(600));
2754
2836
  await showRing(page, box, cap.label, cap.tone, {
@@ -2887,7 +2969,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2887
2969
  const stepperMode = process.env.WAYGRAPH_STEPPER === "full" ? "full" : "carousel";
2888
2970
  const pacing = { gatesFast: demoFast, gatesSlow: false, skipTheater: false, demoPace: "normal" };
2889
2971
  const stubBeforeRef = { current: [] };
2890
- instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef);
2972
+ /** Live todo dock for Method fill/click advance (same carry as lastTodoDock). */
2973
+ const todoDockRef = { current: undefined };
2974
+ instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef, todoDockRef);
2891
2975
  // Force the panel checkbox from this process's flags/env at run start.
2892
2976
  // installOverlay only seeds localStorage when the key is null (so mid-run
2893
2977
  // checkbox clicks survive navigations). Without this, a prior --autoplay
@@ -3108,6 +3192,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3108
3192
  todoPos: stubBeforePhase.todoPos,
3109
3193
  });
3110
3194
  lastTodoDock = appliedBefore.dock;
3195
+ todoDockRef.current = lastTodoDock;
3111
3196
  const stubBeforeTodos =
3112
3197
  (lastTodoDock && lastTodoDock.groups[0] && lastTodoDock.groups[0].items) ||
3113
3198
  stubBeforePhase.todos ||
@@ -3265,6 +3350,10 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3265
3350
  })
3266
3351
  .catch(() => {});
3267
3352
  result = stepOutcome.result;
3353
+ // Mid-act Method fill/click advances live on todoDockRef - fold that
3354
+ // back into lastTodoDock before stubAfter / next-block keep, or the
3355
+ // next step reverts to the pre-act (blank / index-0) checklist.
3356
+ if (todoDockRef.current) lastTodoDock = todoDockRef.current;
3268
3357
  // withExpectedFailure last block that SUCCEEDS on the intentional fail
3269
3358
  // branch (e.g. submit-login -> LoginPage + error banner). Still show
3270
3359
  // stubOnError rings + amber "expected outcome" panel - branching no
@@ -3341,6 +3430,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3341
3430
  todoPos: afterPhase.todoPos,
3342
3431
  });
3343
3432
  lastTodoDock = appliedAfter.dock;
3433
+ todoDockRef.current = lastTodoDock;
3344
3434
  stubAfterTodos =
3345
3435
  (lastTodoDock && lastTodoDock.groups[0] && lastTodoDock.groups[0].items) ||
3346
3436
  afterPhase.todos ||
@@ -3392,6 +3482,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3392
3482
  todoDock: lastTodoDock,
3393
3483
  todoSync: appliedAfter.sync,
3394
3484
  todoId: lastTodoDock && lastTodoDock.id,
3485
+ todoDockRef,
3395
3486
  forceCollapsed:
3396
3487
  !!process.env.WAYGRAPH_VIDEO ||
3397
3488
  process.env.WAYGRAPH_MINI === "1" ||
@@ -3399,6 +3490,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3399
3490
  ? true
3400
3491
  : undefined,
3401
3492
  });
3493
+ // Ring cycle / stubAfter may have advanced the dock - persist for next block.
3494
+ if (todoDockRef.current) lastTodoDock = todoDockRef.current;
3402
3495
  await gate();
3403
3496
  }
3404
3497
  await teardownOverlay(page);
@@ -17,7 +17,7 @@
17
17
  }
18
18
  },
19
19
  "../..": {
20
- "version": "0.12.46",
20
+ "version": "0.12.47",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "tsx": "^4.0.0"
@@ -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,7 +16,7 @@ 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
+ stubBefore: (ctx: StubCtx) => {
19
20
  ctx.title("Open cart");
20
21
  ctx.todoId("saucedemo-cart");
21
22
  ctx.todos([
@@ -40,7 +41,7 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
40
41
  },
41
42
  });
42
43
  },
43
- stubAfter: (ctx) => {
44
+ stubAfter: (ctx: StubCtx) => {
44
45
  ctx.banner("Your cart");
45
46
  ctx.zoomOut(false);
46
47
  ctx.ring("items", {
@@ -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,7 +35,7 @@ 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
+ stubBefore: (ctx: StubCtx) => {
38
39
  ctx.title("Pick a product");
39
40
  ctx.todoId("saucedemo-cart");
40
41
  ctx.todos([
@@ -66,7 +67,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
66
67
  },
67
68
  });
68
69
  },
69
- stubAfter: (ctx) => {
70
+ stubAfter: (ctx: StubCtx) => {
70
71
  ctx.banner("In the cart");
71
72
  ctx.todoId("saucedemo-cart");
72
73
  ctx.todos([
@@ -1,4 +1,4 @@
1
- import { defineMethodBlock, checkpoint, Trait } from "waygraph";
1
+ import { defineMethodBlock, checkpoint, Trait, type StubCtx } from "waygraph";
2
2
  import type { LoginPage, LoginSubmitOutcome } from "../../../states/checkout.states.js";
3
3
  import { LoginCreds } from "../../../states/checkout.mem-keys.js";
4
4
 
@@ -9,8 +9,10 @@ type LoginObserve = "success" | "failure";
9
9
  * Helper: defineMethodBlock
10
10
  * Route: saucedemo-web/methods/ (page URL /)
11
11
  *
12
- * Fills credentials and submits - never page.goto (nav-login owns that).
12
+ * Fills credentials and submits - never page.goto (nav-login Page hub owns that).
13
13
  * Branches: good creds -> LoggedIn; bad/locked -> LoginPage with error banner.
14
+ * stubBefore/stubAfter drive the floating todo dock (Method fill/click advances
15
+ * todoIndex mid-act; stubAfter marks complete / fail).
14
16
  */
15
17
  export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOutcome>({
16
18
  name: "submit-login",
@@ -47,7 +49,7 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
47
49
  : [Trait.visible('[data-test="error"]')],
48
50
  // Open lifecycle: banner title, todos, Screen Studio camera zoom,
49
51
  // sequential highlight queue (appear / dwell / fade), focus + color.
50
- stubBefore: (ctx) => {
52
+ stubBefore: (ctx: StubCtx) => {
51
53
  ctx.title("Signing in");
52
54
  ctx.todoId("saucedemo-login");
53
55
  ctx.todos([
@@ -82,18 +84,18 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
82
84
  },
83
85
  });
84
86
  },
85
- stubAfter: (ctx) => {
87
+ stubAfter: (ctx: StubCtx) => {
86
88
  // Fail branch lands on LoginPage with Epic sadface - do not ring the
87
89
  // inventory shelf. Success (LoggedIn) hands off to shopFlow.
88
90
  if (ctx.out && ctx.out.__state === "LoginPage") {
89
91
  ctx.title("Login blocked");
90
92
  ctx.todoId("saucedemo-login");
91
93
  ctx.todos([
92
- { id: "login-user", text: "Enter username" },
93
- { id: "login-pass", text: "Enter password" },
94
- { id: "login-submit", text: "Click Login" },
94
+ { id: "login-user", text: "Enter username", done: true },
95
+ { id: "login-pass", text: "Enter password", done: true },
96
+ { id: "login-submit", text: "Click Login", done: true },
95
97
  ]);
96
- ctx.todoIndex(2);
98
+ ctx.todoStyle("checklist");
97
99
  ctx.ring("error", {
98
100
  selector: '[data-test="error"]',
99
101
  label: "Login error banner",
@@ -123,7 +125,7 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
123
125
  duration: true,
124
126
  });
125
127
  },
126
- stubOnError: (ctx) => {
128
+ stubOnError: (ctx: StubCtx) => {
127
129
  ctx.title("Login failed");
128
130
  ctx.ring("error", {
129
131
  selector: '[data-test="error"]',
@@ -1,17 +1,23 @@
1
- import { defineNavBlock, Trait } from "waygraph";
1
+ import { definePageBlock, Trait } from "waygraph";
2
2
  import type { LoginPage } from "../../states/checkout.states.js";
3
+ import { SubmitLoginActionBlock } from "./methods/submit-login.method.block.js";
3
4
 
4
5
  /**
5
- * Kind: Nav
6
- * Helper: defineNavBlock
6
+ * Kind: Page
7
+ * Helper: definePageBlock
7
8
  * Route: saucedemo-web/ (= /)
8
9
  *
9
- * page.goto to the login form lives here only. submit-login Action never navigates.
10
+ * Login screen hub. Deep-link via url; method submit-login hangs off this
11
+ * page (stubBefore/stubAfter todos live on the Method). Name stays
12
+ * `nav-login` so existing flows / chain specs keep working.
10
13
  */
11
- export const NavLoginBlock = defineNavBlock<LoginPage>({
14
+ export const NavLoginBlock = definePageBlock<LoginPage>({
12
15
  name: "nav-login",
13
16
  description: "Goes to the Swag Labs login page.",
14
17
  checkpoint: "LoginPage",
15
18
  url: "/",
16
19
  verify: [Trait.visible("#login-button")],
20
+ methods: {
21
+ submitLogin: () => SubmitLoginActionBlock,
22
+ },
17
23
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.46",
3
+ "version": "0.12.48",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -17,7 +17,12 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
17
17
  verify: [Trait.url({ pathname: "/cart.html" })],
18
18
  stubBefore: (ctx) => {
19
19
  ctx.title("Open cart");
20
- ctx.todos(["Find the product", "Add to cart", "Open cart"]);
20
+ ctx.todoId("saucedemo-cart");
21
+ ctx.todos([
22
+ { id: "cart-find", text: "Find the product" },
23
+ { id: "cart-add", text: "Add to cart" },
24
+ { id: "cart-open", text: "Open cart" },
25
+ ]);
21
26
  ctx.todoIndex(2);
22
27
  ctx.zoom(1.4);
23
28
  ctx.zoomOut(false);
@@ -36,7 +36,12 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
36
36
  // Demo: zoom product -> add (stay zoomed), then badge after act.
37
37
  stubBefore: (ctx) => {
38
38
  ctx.title("Pick a product");
39
- ctx.todos(["Find the product", "Add to cart", "Open cart"]);
39
+ ctx.todoId("saucedemo-cart");
40
+ ctx.todos([
41
+ { id: "cart-find", text: "Find the product" },
42
+ { id: "cart-add", text: "Add to cart" },
43
+ { id: "cart-open", text: "Open cart" },
44
+ ]);
40
45
  ctx.todoIndex(0);
41
46
  ctx.zoom(1.35);
42
47
  ctx.zoomOut(false);
@@ -63,7 +68,12 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
63
68
  },
64
69
  stubAfter: (ctx) => {
65
70
  ctx.banner("In the cart");
66
- ctx.todos(["Find the product", "Add to cart", "Open cart"]);
71
+ ctx.todoId("saucedemo-cart");
72
+ ctx.todos([
73
+ { id: "cart-find", text: "Find the product" },
74
+ { id: "cart-add", text: "Add to cart" },
75
+ { id: "cart-open", text: "Open cart" },
76
+ ]);
67
77
  ctx.todoIndex(1);
68
78
  ctx.zoomOut(false);
69
79
  ctx.ring("badge", {
@@ -1,4 +1,4 @@
1
- import { defineMethodBlock, checkpoint, Trait } from "waygraph";
1
+ import { defineMethodBlock, checkpoint, Trait, type StubCtx } from "waygraph";
2
2
  import type { LoginPage, LoginSubmitOutcome } from "../../../states/checkout.states.js";
3
3
  import { LoginCreds } from "../../../states/checkout.mem-keys.js";
4
4
 
@@ -9,8 +9,10 @@ type LoginObserve = "success" | "failure";
9
9
  * Helper: defineMethodBlock
10
10
  * Route: saucedemo-web/methods/ (page URL /)
11
11
  *
12
- * Fills credentials and submits - never page.goto (nav-login owns that).
12
+ * Fills credentials and submits - never page.goto (nav-login Page hub owns that).
13
13
  * Branches: good creds -> LoggedIn; bad/locked -> LoginPage with error banner.
14
+ * stubBefore/stubAfter drive the floating todo dock (Method fill/click advances
15
+ * todoIndex mid-act; stubAfter marks complete / fail).
14
16
  */
15
17
  export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOutcome>({
16
18
  name: "submit-login",
@@ -47,9 +49,14 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
47
49
  : [Trait.visible('[data-test="error"]')],
48
50
  // Open lifecycle: banner title, todos, Screen Studio camera zoom,
49
51
  // sequential highlight queue (appear / dwell / fade), focus + color.
50
- stubBefore: (ctx) => {
52
+ stubBefore: (ctx: StubCtx) => {
51
53
  ctx.title("Signing in");
52
- ctx.todos(["Enter username", "Enter password", "Click Login"]);
54
+ ctx.todoId("saucedemo-login");
55
+ ctx.todos([
56
+ { id: "login-user", text: "Enter username" },
57
+ { id: "login-pass", text: "Enter password" },
58
+ { id: "login-submit", text: "Click Login" },
59
+ ]);
53
60
  ctx.todoIndex(0);
54
61
  ctx.zoom(1.35);
55
62
  ctx.highlights({
@@ -77,13 +84,18 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
77
84
  },
78
85
  });
79
86
  },
80
- stubAfter: (ctx) => {
87
+ stubAfter: (ctx: StubCtx) => {
81
88
  // Fail branch lands on LoginPage with Epic sadface - do not ring the
82
89
  // inventory shelf. Success (LoggedIn) hands off to shopFlow.
83
90
  if (ctx.out && ctx.out.__state === "LoginPage") {
84
91
  ctx.title("Login blocked");
85
- ctx.todos(["Enter username", "Enter password", "Click Login"]);
86
- ctx.todoIndex(2);
92
+ ctx.todoId("saucedemo-login");
93
+ ctx.todos([
94
+ { id: "login-user", text: "Enter username", done: true },
95
+ { id: "login-pass", text: "Enter password", done: true },
96
+ { id: "login-submit", text: "Click Login", done: true },
97
+ ]);
98
+ ctx.todoStyle("checklist");
87
99
  ctx.ring("error", {
88
100
  selector: '[data-test="error"]',
89
101
  label: "Login error banner",
@@ -95,7 +107,12 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
95
107
  return;
96
108
  }
97
109
  ctx.banner("Inventory");
98
- ctx.todos(["Enter username", "Enter password", "Click Login"]);
110
+ ctx.todoId("saucedemo-login");
111
+ ctx.todos([
112
+ { id: "login-user", text: "Enter username" },
113
+ { id: "login-pass", text: "Enter password" },
114
+ { id: "login-submit", text: "Click Login" },
115
+ ]);
99
116
  ctx.todoIndex(2);
100
117
  // Landed on inventory - hand off to shopFlow for product -> cart zoom.
101
118
  ctx.ring("shelf", {
@@ -108,7 +125,7 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
108
125
  duration: true,
109
126
  });
110
127
  },
111
- stubOnError: (ctx) => {
128
+ stubOnError: (ctx: StubCtx) => {
112
129
  ctx.title("Login failed");
113
130
  ctx.ring("error", {
114
131
  selector: '[data-test="error"]',
@@ -1,17 +1,23 @@
1
- import { defineNavBlock, Trait } from "waygraph";
1
+ import { definePageBlock, Trait } from "waygraph";
2
2
  import type { LoginPage } from "../../states/checkout.states.js";
3
+ import { SubmitLoginActionBlock } from "./methods/submit-login.method.block.js";
3
4
 
4
5
  /**
5
- * Kind: Nav
6
- * Helper: defineNavBlock
6
+ * Kind: Page
7
+ * Helper: definePageBlock
7
8
  * Route: saucedemo-web/ (= /)
8
9
  *
9
- * page.goto to the login form lives here only. submit-login Action never navigates.
10
+ * Login screen hub. Deep-link via url; method submit-login hangs off this
11
+ * page (stubBefore/stubAfter todos live on the Method). Name stays
12
+ * `nav-login` so existing flows / chain specs keep working.
10
13
  */
11
- export const NavLoginBlock = defineNavBlock<LoginPage>({
14
+ export const NavLoginBlock = definePageBlock<LoginPage>({
12
15
  name: "nav-login",
13
16
  description: "Goes to the Swag Labs login page.",
14
17
  checkpoint: "LoginPage",
15
18
  url: "/",
16
19
  verify: [Trait.visible("#login-button")],
20
+ methods: {
21
+ submitLogin: () => SubmitLoginActionBlock,
22
+ },
17
23
  });