waygraph 0.10.9 → 0.11.0

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
@@ -200,6 +200,12 @@ const CHAIN_RUNNER_SCRIPT = `
200
200
  import { readdirSync, mkdirSync } from "node:fs";
201
201
  import { join } from "node:path";
202
202
  import { tmpdir } from "node:os";
203
+ import {
204
+ resolveHighlightSlots,
205
+ resolveSlides,
206
+ formatHighlightCaption,
207
+ hasAuthoredStubAfter,
208
+ } from "waygraph";
203
209
 
204
210
  function walkDir(dir, pattern) {
205
211
  const results = [];
@@ -395,8 +401,8 @@ const RING_CSS =
395
401
  // pushed back onto screen, same width, never shrunk. See
396
402
  // window.__wgPositionRing in installOverlay.
397
403
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
398
- "white-space:nowrap;padding:4px 9px;border-radius:7px;background:#7C3AED;color:#fff;" +
399
- "font:600 12px/1.2 system-ui,sans-serif;transition:opacity .3s ease;}" +
404
+ "max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
405
+ "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease;}" +
400
406
  // Mouse cursor icon that travels to a target before it's acted on, plus a
401
407
  // quick expanding ripple at the moment of a click - same idea as
402
408
  // help-center-clip-engine's #clip-cursor/#clip-ring (video-pipeline). The
@@ -1379,6 +1385,162 @@ async function showRing(page, box, label) {
1379
1385
  .catch(() => {});
1380
1386
  }
1381
1387
 
1388
+ /** Match a locator to a stubBefore slot by overlapping bounding boxes. */
1389
+ async function captionForLocator(page, locator, stubs, fallback) {
1390
+ if (!stubs || stubs.length === 0) return fallback;
1391
+ const box = await locator.boundingBox().catch(() => null);
1392
+ if (!box) return fallback;
1393
+ for (const s of stubs) {
1394
+ try {
1395
+ const b = await page.locator(s.selector).first().boundingBox();
1396
+ if (
1397
+ b &&
1398
+ Math.abs(b.x - box.x) < 4 &&
1399
+ Math.abs(b.y - box.y) < 4 &&
1400
+ Math.abs(b.width - box.width) < 8
1401
+ ) {
1402
+ return formatHighlightCaption(s);
1403
+ }
1404
+ } catch {
1405
+ /* skip */
1406
+ }
1407
+ }
1408
+ return fallback;
1409
+ }
1410
+
1411
+ /**
1412
+ * Multi-step yap captions - not block lifecycle. Each slide waits for Next
1413
+ * (or auto-next), optional ring via slide.selector.
1414
+ */
1415
+ async function presentSlides(page, slides, gate, opts) {
1416
+ const title = (opts && opts.title) || "waygraph demo";
1417
+ const blockName = (opts && opts.blockName) || "";
1418
+ for (let i = 0; i < slides.length; i++) {
1419
+ const s = slides[i];
1420
+ const caption = formatHighlightCaption(s);
1421
+ const isLast = i === slides.length - 1;
1422
+ await installOverlay(page, title);
1423
+ if (s.selector) {
1424
+ try {
1425
+ const box = await page.locator(s.selector).first().boundingBox();
1426
+ if (box) await showRing(page, box, caption);
1427
+ } catch {
1428
+ await hideRing(page);
1429
+ }
1430
+ } else {
1431
+ await hideRing(page);
1432
+ }
1433
+ await page
1434
+ .evaluate(
1435
+ (info) => {
1436
+ let panel = document.getElementById("wg-panel");
1437
+ const isNew = !panel;
1438
+ if (!panel) {
1439
+ panel = document.createElement("div");
1440
+ panel.id = "wg-panel";
1441
+ }
1442
+ panel.classList.remove("wg-collapsed", "wg-error", "wg-expected");
1443
+ const esc = (t) =>
1444
+ String(t).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
1445
+ panel.innerHTML =
1446
+ "<div class=\\"wg-chrome\\"><span class=\\"wg-chrome-title\\">waygraph demo</span>" +
1447
+ "<button type=\\"button\\" class=\\"wg-hide-btn\\" data-wg-toggle=\\"1\\">Hide</button></div>" +
1448
+ "<div class=\\"wg-body\\">" +
1449
+ "<h3>Slide " +
1450
+ (info.index + 1) +
1451
+ " / " +
1452
+ info.total +
1453
+ (info.blockName ? " · " + esc(info.blockName) : "") +
1454
+ "</h3>" +
1455
+ (info.tag
1456
+ ? "<div class=\\"wg-episode\\" style=\\"border:none;padding:0;margin:0 0 6px;font-size:11px;color:#c9a6ff\\">" +
1457
+ esc(info.tag) +
1458
+ "</div>"
1459
+ : "") +
1460
+ "<div class=\\"wg-narration\\">" +
1461
+ esc(info.caption) +
1462
+ "</div>" +
1463
+ (info.detail
1464
+ ? "<div class=\\"wg-result-pretty\\" style=\\"margin:0 0 12px;color:#f0e8ff\\">" +
1465
+ esc(info.detail) +
1466
+ "</div>"
1467
+ : "") +
1468
+ "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
1469
+ (info.autoNow ? " checked" : "") +
1470
+ "> Auto-advance</label></div>" +
1471
+ "<div id=\\"wg-gate-manual\\"" +
1472
+ (info.autoNow ? " style=\\"display:none\\"" : "") +
1473
+ "><button id=\\"wg-run\\">" +
1474
+ (info.isLast ? "Continue \\u25B6" : "Next slide \\u25B6") +
1475
+ "</button></div>" +
1476
+ "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" +
1477
+ (info.autoNow ? "" : " style=\\"display:none\\"") +
1478
+ ">Auto-advancing...</div>" +
1479
+ "</div>";
1480
+ if (isNew) {
1481
+ document.documentElement.appendChild(panel);
1482
+ requestAnimationFrame(() => panel.classList.add("wg-in"));
1483
+ }
1484
+ if (window.__wgWirePanelChrome) {
1485
+ window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1486
+ stepLabel:
1487
+ "Slide " +
1488
+ (info.index + 1) +
1489
+ " / " +
1490
+ info.total +
1491
+ (info.blockName ? " · " + info.blockName : ""),
1492
+ forceCollapsed: false,
1493
+ });
1494
+ }
1495
+ const cb = document.getElementById("wg-autoplay-cb");
1496
+ if (cb) {
1497
+ cb.addEventListener("change", () => {
1498
+ try {
1499
+ localStorage.setItem("wg-autoplay", cb.checked ? "1" : "0");
1500
+ } catch {
1501
+ /* ignore */
1502
+ }
1503
+ const manual = document.getElementById("wg-gate-manual");
1504
+ const auto = document.getElementById("wg-gate-auto");
1505
+ if (manual) manual.style.display = cb.checked ? "none" : "";
1506
+ if (auto) auto.style.display = cb.checked ? "" : "none";
1507
+ });
1508
+ }
1509
+ const runBtn = document.getElementById("wg-run");
1510
+ if (runBtn) runBtn.addEventListener("click", () => window.__wgNext({}));
1511
+ },
1512
+ {
1513
+ index: i,
1514
+ total: slides.length,
1515
+ caption: s.caption,
1516
+ detail: s.detail || "",
1517
+ tag: s.tag || "",
1518
+ blockName,
1519
+ isLast,
1520
+ autoNow: false,
1521
+ },
1522
+ )
1523
+ .catch(() => {});
1524
+ await page
1525
+ .evaluate(() => {
1526
+ try {
1527
+ const on = localStorage.getItem("wg-autoplay") === "1";
1528
+ const cb = document.getElementById("wg-autoplay-cb");
1529
+ if (cb) cb.checked = on;
1530
+ const manual = document.getElementById("wg-gate-manual");
1531
+ const auto = document.getElementById("wg-gate-auto");
1532
+ if (manual) manual.style.display = on ? "none" : "";
1533
+ if (auto) auto.style.display = on ? "" : "none";
1534
+ } catch {
1535
+ /* ignore */
1536
+ }
1537
+ })
1538
+ .catch(() => {});
1539
+ await gate();
1540
+ }
1541
+ await hideRing(page);
1542
+ }
1543
+
1382
1544
  async function hideRing(page) {
1383
1545
  await page
1384
1546
  .evaluate(() => {
@@ -1455,7 +1617,7 @@ async function wasJustNarrated(page) {
1455
1617
  .catch(() => false);
1456
1618
  }
1457
1619
 
1458
- function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
1620
+ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef) {
1459
1621
  // Playwright's own slowMo ALREADY pauses after every single low-level
1460
1622
  // action it dispatches - and pressSequentially() fires one such action
1461
1623
  // PER CHARACTER. Also giving pressSequentially its own fixed delay
@@ -1469,6 +1631,7 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
1469
1631
  // per-block (WAYGRAPH_FAST_BLOCKS), and the Locator.fill/click patches
1470
1632
  // below are installed once on the shared prototype but invoked once per
1471
1633
  // real interaction, long after this closure was created.
1634
+ // stubBeforeRef.current = resolved stubBefore slots for the active block.
1472
1635
  const typeDelay = () => (pacing.fast ? 0 : slowMo ? 0 : 30);
1473
1636
  // Click is a single action, not per-character, so it doesn't compound
1474
1637
  // the same way - but slowMo still adds its own pause around the actual
@@ -1505,9 +1668,11 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
1505
1668
  // Only trust the "last mem.get()" as THIS fill's source if it
1506
1669
  // happened recently - a stale read from several actions ago is
1507
1670
  // more likely unrelated than actually describing this field.
1508
- const label = memTrack.lastKeyName && Date.now() - memTrack.at < 3000
1671
+ const fallback = memTrack.lastKeyName && Date.now() - memTrack.at < 3000
1509
1672
  ? "from mem: " + memTrack.lastKeyName
1510
1673
  : "writing from mem";
1674
+ const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
1675
+ const label = await captionForLocator(page, this, stubs, fallback);
1511
1676
  await moveCursorTo(page, box, cursorMs(500));
1512
1677
  await showRing(page, box, label);
1513
1678
  await new Promise((res) => setTimeout(res, pacing.fast ? 0 : 200));
@@ -1560,10 +1725,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
1560
1725
  const box = await this.boundingBox();
1561
1726
  const narrated = box ? await wasJustNarrated(page) : false;
1562
1727
  if (box && !narrated) {
1563
- let label = "click";
1728
+ let fallback = "click";
1564
1729
  try {
1565
1730
  const text = (await this.textContent())?.trim();
1566
- if (text && text.length > 0 && text.length <= 30) label = text;
1731
+ if (text && text.length > 0 && text.length <= 30) fallback = text;
1567
1732
  } catch {
1568
1733
  // element has no simple text (an icon button, say) - generic label is fine
1569
1734
  }
@@ -1571,10 +1736,12 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
1571
1736
  // (set on page by runStepMode just before act).
1572
1737
  try {
1573
1738
  const navLabel = await page.evaluate(() => window.__wgPendingNavClickLabel || null);
1574
- if (navLabel) label = String(navLabel);
1739
+ if (navLabel) fallback = String(navLabel);
1575
1740
  } catch {
1576
1741
  /* ignore */
1577
1742
  }
1743
+ const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
1744
+ const label = await captionForLocator(page, this, stubs, fallback);
1578
1745
  clickPoint = await moveCursorTo(page, box, cursorMs(600));
1579
1746
  await showRing(page, box, label);
1580
1747
  // "pop for a few seconds" - Dan's own phrase, matching the
@@ -1703,7 +1870,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1703
1870
  const demoFast = process.env.WAYGRAPH_DEMO_FAST === "1";
1704
1871
  const stepperMode = process.env.WAYGRAPH_STEPPER === "full" ? "full" : "carousel";
1705
1872
  const pacing = { fast: demoFast };
1706
- instrumentInteractionHighlighting(page, mem, slowMo, pacing);
1873
+ const stubBeforeRef = { current: [] };
1874
+ instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef);
1707
1875
  // Force the panel checkbox from this process's flags/env at run start.
1708
1876
  // installOverlay only seeds localStorage when the key is null (so mid-run
1709
1877
  // checkbox clicks survive navigations). Without this, a prior --autoplay
@@ -1852,6 +2020,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1852
2020
  await resetPageState(context, page, baseURL);
1853
2021
  }
1854
2022
  pacing.fast = demoFast || fastBlockNames.has(r.block.name);
2023
+ const fixtures = r.highlightFixtures;
2024
+ stubBeforeRef.current = resolveHighlightSlots(r.block, "stubBefore", { fixtures });
1855
2025
  const isNavBlock = r.block.__waygraphKind === "nav";
1856
2026
  const autoNow = await currentAutoplay();
1857
2027
  // Auto-next: tuck stepper on NavBlocks so the page transition fills the frame.
@@ -1975,7 +2145,26 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1975
2145
  })
1976
2146
  .catch(() => {});
1977
2147
  result = stepOutcome.result;
1978
- const highlights = extractVerifyHighlights(r.block, result.__state);
2148
+ const fixturesAfter = r.highlightFixtures;
2149
+ const slides = resolveSlides(r.block, { out: result, fixtures: fixturesAfter });
2150
+ if (slides.length > 0) {
2151
+ await presentSlides(page, slides, gate, {
2152
+ title,
2153
+ blockName: r.block.name,
2154
+ fast: pacing.fast,
2155
+ });
2156
+ }
2157
+ let highlights;
2158
+ if (hasAuthoredStubAfter(r.block, result, fixturesAfter)) {
2159
+ highlights = resolveHighlightSlots(r.block, "stubAfter", { out: result, fixtures: fixturesAfter }).map(
2160
+ (h) => ({
2161
+ selector: h.selector,
2162
+ label: formatHighlightCaption(h),
2163
+ }),
2164
+ );
2165
+ } else {
2166
+ highlights = extractVerifyHighlights(r.block, result.__state);
2167
+ }
1979
2168
  await renderAfterStep(page, {
1980
2169
  index: i,
1981
2170
  total: resolved.length,
@@ -2126,6 +2315,7 @@ async function main() {
2126
2315
  block: bi.block,
2127
2316
  exportName: bi.name,
2128
2317
  seedMem: idx === 0 ? seedMem : undefined,
2318
+ highlightFixtures: flow.highlightFixtures,
2129
2319
  expectedFailureReason:
2130
2320
  flow.expectedFailureReason && idx === blockInfos.length - 1
2131
2321
  ? flow.expectedFailureReason
@@ -2187,6 +2377,7 @@ async function main() {
2187
2377
  episodeNumber: episodeCounter,
2188
2378
  episodeTitle: flow.title || seg.ref,
2189
2379
  expectedFailureReason: flow.expectedFailureReason,
2380
+ highlightFixtures: flow.highlightFixtures,
2190
2381
  seedMem: () => seedMemForFlow(mem, flow.blocks(), seg.json, seg.ref),
2191
2382
  });
2192
2383
  } else {
@@ -2195,6 +2386,7 @@ async function main() {
2195
2386
  flowMeta.push({
2196
2387
  episodeNumber: undefined,
2197
2388
  episodeTitle: undefined,
2389
+ highlightFixtures: undefined,
2198
2390
  seedMem: () => seedMemForBlock(mem, r, seg.json),
2199
2391
  });
2200
2392
  }
@@ -2216,6 +2408,7 @@ async function main() {
2216
2408
  resetSession: bi.resetSessionBefore === true,
2217
2409
  episodeNumber: meta.episodeNumber,
2218
2410
  episodeTitle: meta.episodeTitle,
2411
+ highlightFixtures: meta.highlightFixtures,
2219
2412
  expectedFailureReason:
2220
2413
  meta.expectedFailureReason && remainingInFlow === 1 ? meta.expectedFailureReason : undefined,
2221
2414
  seedMem: isFirstOfSegment ? meta.seedMem : undefined,
package/dist/engine.d.ts CHANGED
@@ -180,6 +180,12 @@ export interface Flow<Out extends Checkpoint<string>> {
180
180
  * alarming one indistinguishable from a real break.
181
181
  */
182
182
  readonly expectedFailureReason?: string;
183
+ /**
184
+ * Set once this Flow has been wrapped in {@link withHighlightFixtures} -
185
+ * demo narration overrides (unlimited purposes on `.flow.ts`) keyed by
186
+ * block.name. `run()` never reads this; step/`demo` merges onto block stubs.
187
+ */
188
+ readonly highlightFixtures?: import("./highlights.js").HighlightFixtureMap;
183
189
  /**
184
190
  * This Flow's constituent Blocks, in order between `start`/`end`, as plain
185
191
  * data - for introspection/visualization tools, without running anything.
@@ -266,6 +272,14 @@ export declare function withTitle<Out extends Checkpoint<string>>(flow: Flow<Out
266
272
  * );
267
273
  */
268
274
  export declare function withExpectedFailure<Out extends Checkpoint<string>>(flow: Flow<Out>, reason: string): Flow<Out>;
275
+ /**
276
+ * Attaches demo highlight fixtures to a Flow - unlimited purposes on `.flow.ts`
277
+ * (AC copy, BUG/GATE tags, detail) merged onto each block's stubBefore/stubAfter
278
+ * slots. Non-destructive, same pattern as {@link withTitle}.
279
+ * Demo/`chain --step` reads `flow.highlightFixtures`; `run()` ignores it.
280
+ * @example withHighlightFixtures(issueFlow, { "submit-login": { stubBefore: { email: { label: "AC-1" } } } })
281
+ */
282
+ export declare function withHighlightFixtures<Out extends Checkpoint<string>>(flow: Flow<Out>, fixtures: import("./highlights.js").HighlightFixtureMap): Flow<Out>;
269
283
  /**
270
284
  * Runs several complete, independent Flows one after another, reusing the
271
285
  * same page/context/mem in order - each remains its own validated
@@ -493,6 +507,9 @@ export type NavBlockOptions<Out extends Checkpoint<string>> = {
493
507
  checkpoint: Out["__state"];
494
508
  requires?: readonly MemKey<any>[];
495
509
  verify?: Trait[] | ((out: Out) => Trait[]);
510
+ stubBefore?: import("./highlights.js").HighlightStubPhase | ((out: Out) => import("./highlights.js").HighlightStubPhase);
511
+ stubAfter?: import("./highlights.js").HighlightStubPhase | ((out: Out) => import("./highlights.js").HighlightStubPhase);
512
+ /** @deprecated Prefer stubAfter. */
496
513
  highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
497
514
  /**
498
515
  * Live per-instance menu rows for `waygraph auto` (see {@link Block.instanceOptions}).
@@ -553,6 +570,9 @@ export type NavClickBlockOptions<Out extends Checkpoint<string>> = {
553
570
  click: string | ((mem: MemPage) => string);
554
571
  requires?: readonly MemKey<any>[];
555
572
  verify?: Trait[] | ((out: Out) => Trait[]);
573
+ stubBefore?: import("./highlights.js").HighlightStubPhase | ((out: Out) => import("./highlights.js").HighlightStubPhase);
574
+ stubAfter?: import("./highlights.js").HighlightStubPhase | ((out: Out) => import("./highlights.js").HighlightStubPhase);
575
+ /** @deprecated Prefer stubAfter. */
556
576
  highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
557
577
  instanceOptions?: Block<Checkpoint<string>, Out>["instanceOptions"];
558
578
  };
@@ -581,6 +601,9 @@ export type PageBlockOptions<Out extends Checkpoint<string>> = {
581
601
  /** Checkpoint tag for this screen (hub). */
582
602
  checkpoint: Out["__state"];
583
603
  verify?: Trait[] | ((out: Out) => Trait[]);
604
+ stubBefore?: import("./highlights.js").HighlightStubPhase | ((out: Out) => import("./highlights.js").HighlightStubPhase);
605
+ stubAfter?: import("./highlights.js").HighlightStubPhase | ((out: Out) => import("./highlights.js").HighlightStubPhase);
606
+ /** @deprecated Prefer stubAfter. */
584
607
  highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
585
608
  requires?: readonly MemKey<any>[];
586
609
  /**
package/dist/engine.js CHANGED
@@ -377,6 +377,21 @@ export function withExpectedFailure(flow, reason) {
377
377
  modBlockVerify: (block, nameOrIndex, newCheck) => withExpectedFailure(flow.modBlockVerify(block, nameOrIndex, newCheck), reason),
378
378
  };
379
379
  }
380
+ /**
381
+ * Attaches demo highlight fixtures to a Flow - unlimited purposes on `.flow.ts`
382
+ * (AC copy, BUG/GATE tags, detail) merged onto each block's stubBefore/stubAfter
383
+ * slots. Non-destructive, same pattern as {@link withTitle}.
384
+ * Demo/`chain --step` reads `flow.highlightFixtures`; `run()` ignores it.
385
+ * @example withHighlightFixtures(issueFlow, { "submit-login": { stubBefore: { email: { label: "AC-1" } } } })
386
+ */
387
+ export function withHighlightFixtures(flow, fixtures) {
388
+ return {
389
+ ...flow,
390
+ highlightFixtures: fixtures,
391
+ withBlockVerify: (block, verify) => withHighlightFixtures(flow.withBlockVerify(block, verify), fixtures),
392
+ modBlockVerify: (block, nameOrIndex, newCheck) => withHighlightFixtures(flow.modBlockVerify(block, nameOrIndex, newCheck), fixtures),
393
+ };
394
+ }
380
395
  function locateInChain(flows, block) {
381
396
  if (typeof block === "number") {
382
397
  let remaining = block;
@@ -659,6 +674,8 @@ export function defineNavBlock(options) {
659
674
  },
660
675
  resolve: () => checkpoint(options.checkpoint),
661
676
  ...(options.verify ? { verify: options.verify } : {}),
677
+ ...(options.stubBefore ? { stubBefore: options.stubBefore } : {}),
678
+ ...(options.stubAfter ? { stubAfter: options.stubAfter } : {}),
662
679
  ...(options.highlights ? { highlights: options.highlights } : {}),
663
680
  },
664
681
  });
@@ -692,6 +709,8 @@ export function defineNavClickBlock(options) {
692
709
  ...(options.description ? { description: options.description } : {}),
693
710
  ...(options.requires ? { requires: options.requires } : {}),
694
711
  ...(options.verify ? { verify: options.verify } : {}),
712
+ ...(options.stubBefore ? { stubBefore: options.stubBefore } : {}),
713
+ ...(options.stubAfter ? { stubAfter: options.stubAfter } : {}),
695
714
  ...(options.highlights ? { highlights: options.highlights } : {}),
696
715
  ...(options.instanceOptions ? { instanceOptions: options.instanceOptions } : {}),
697
716
  });
@@ -738,6 +757,8 @@ export function definePageBlock(options) {
738
757
  },
739
758
  resolve: () => checkpoint(options.checkpoint),
740
759
  ...(options.verify ? { verify: options.verify } : {}),
760
+ ...(options.stubBefore ? { stubBefore: options.stubBefore } : {}),
761
+ ...(options.stubAfter ? { stubAfter: options.stubAfter } : {}),
741
762
  ...(options.highlights ? { highlights: options.highlights } : {}),
742
763
  },
743
764
  });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Block stubBefore / stubAfter + flow highlightFixtures + demo slides.
3
+ * Demo narration only - never affects runGraph pass/fail.
4
+ *
5
+ * Lifecycle (stubs) ≠ yap slides: stubs are fill/after rings tied to the block;
6
+ * slides are multi-step captions with Next between them (long-process explain).
7
+ */
8
+ import type { Block, Checkpoint } from "./types.js";
9
+ /** One named highlight slot on a block (selector + caption). */
10
+ export interface WaygraphHighlightStub {
11
+ selector: string;
12
+ /** Primary caption line. */
13
+ label: string;
14
+ /** Extra caption line (ring / slide body). */
15
+ detail?: string;
16
+ /** Short badge, e.g. AC / BUG / GATE / YAP. */
17
+ tag?: string;
18
+ }
19
+ /** Flow fixture patch: label required; selector optional (inherits from stub). */
20
+ export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag">;
21
+ export type HighlightStubPhase = Record<string, WaygraphHighlightStub>;
22
+ export type HighlightStubPhaseOrFn<Out extends Checkpoint<string>> = HighlightStubPhase | ((out: Out) => HighlightStubPhase);
23
+ /**
24
+ * One yap/slide in a multi-step demo caption sequence.
25
+ * Not block lifecycle - demo pauses (Next / auto-next) between slides.
26
+ */
27
+ export interface WaygraphSlide {
28
+ /** Primary caption (required). */
29
+ caption: string;
30
+ detail?: string;
31
+ tag?: string;
32
+ /** Optional ring while this slide is showing. */
33
+ selector?: string;
34
+ }
35
+ export type WaygraphSlidesOrFn<Out extends Checkpoint<string>> = readonly WaygraphSlide[] | ((out: Out) => readonly WaygraphSlide[]);
36
+ /** Per-block fixture map attached to a Flow via {@link withHighlightFixtures}. */
37
+ export type HighlightFixtureMap = Record<string, {
38
+ stubBefore?: Record<string, WaygraphHighlightFixture>;
39
+ stubAfter?: Record<string, WaygraphHighlightFixture>;
40
+ /** Override / supply multi-step yap slides for this block (demo only). */
41
+ slides?: readonly WaygraphSlide[];
42
+ }>;
43
+ /** Legacy array highlight (shimmed to stubAfter keys "0", "1", ...). */
44
+ export interface WaygraphHighlight {
45
+ selector: string;
46
+ label: string;
47
+ }
48
+ export type ResolvedHighlight = WaygraphHighlightStub;
49
+ /**
50
+ * Resolve ordered highlight slots for a phase, merging flow fixtures over block stubs.
51
+ * Shim: empty stubAfter + legacy `instruction.highlights` -> auto-keyed stubAfter.
52
+ */
53
+ export declare function resolveHighlightSlots(block: Block<any, any>, phase: "stubBefore" | "stubAfter", opts?: {
54
+ out?: Checkpoint<string>;
55
+ fixtures?: HighlightFixtureMap;
56
+ }): ResolvedHighlight[];
57
+ /**
58
+ * Resolve multi-step yap slides: flow fixture `slides` wins when present,
59
+ * else `instruction.slides` (function form allowed).
60
+ */
61
+ export declare function resolveSlides(block: Block<any, any>, opts?: {
62
+ out?: Checkpoint<string>;
63
+ fixtures?: HighlightFixtureMap;
64
+ }): WaygraphSlide[];
65
+ /** Caption for ring / slide UI (label|caption + optional detail/tag). */
66
+ export declare function formatHighlightCaption(h: {
67
+ label?: string;
68
+ caption?: string;
69
+ detail?: string;
70
+ tag?: string;
71
+ }): string;
72
+ /**
73
+ * True when the block authored a non-empty stubAfter (after resolve + shim),
74
+ * including legacy highlights. When true, demo should not use verify-trait fallback.
75
+ */
76
+ export declare function hasAuthoredStubAfter(block: Block<any, any>, out?: Checkpoint<string>, fixtures?: HighlightFixtureMap): boolean;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Block stubBefore / stubAfter + flow highlightFixtures + demo slides.
3
+ * Demo narration only - never affects runGraph pass/fail.
4
+ *
5
+ * Lifecycle (stubs) ≠ yap slides: stubs are fill/after rings tied to the block;
6
+ * slides are multi-step captions with Next between them (long-process explain).
7
+ */
8
+ function isStubPhase(v) {
9
+ return !!v && typeof v === "object" && !Array.isArray(v);
10
+ }
11
+ function mergeSlot(base, patch) {
12
+ if (!base && !patch)
13
+ return null;
14
+ if (!base && patch) {
15
+ if (!patch.selector)
16
+ return null;
17
+ return {
18
+ selector: patch.selector,
19
+ label: patch.label,
20
+ ...(patch.detail ? { detail: patch.detail } : {}),
21
+ ...(patch.tag ? { tag: patch.tag } : {}),
22
+ };
23
+ }
24
+ if (base && !patch)
25
+ return { ...base };
26
+ return {
27
+ selector: patch.selector ?? base.selector,
28
+ label: patch.label,
29
+ ...(patch.detail !== undefined
30
+ ? { detail: patch.detail }
31
+ : base.detail
32
+ ? { detail: base.detail }
33
+ : {}),
34
+ ...(patch.tag !== undefined ? { tag: patch.tag } : base.tag ? { tag: base.tag } : {}),
35
+ };
36
+ }
37
+ function phaseFromLegacyHighlights(highlights) {
38
+ const out = {};
39
+ if (!highlights)
40
+ return out;
41
+ highlights.forEach((h, i) => {
42
+ if (h && typeof h.selector === "string" && typeof h.label === "string") {
43
+ out[String(i)] = { selector: h.selector, label: h.label };
44
+ }
45
+ });
46
+ return out;
47
+ }
48
+ /**
49
+ * Resolve ordered highlight slots for a phase, merging flow fixtures over block stubs.
50
+ * Shim: empty stubAfter + legacy `instruction.highlights` -> auto-keyed stubAfter.
51
+ */
52
+ export function resolveHighlightSlots(block, phase, opts) {
53
+ const instr = block.instruction;
54
+ let raw = phase === "stubBefore" ? instr.stubBefore : instr.stubAfter;
55
+ let phaseMap = {};
56
+ if (typeof raw === "function") {
57
+ try {
58
+ phaseMap = raw(opts?.out ?? { __state: "" }) || {};
59
+ }
60
+ catch {
61
+ phaseMap = {};
62
+ }
63
+ }
64
+ else if (isStubPhase(raw)) {
65
+ phaseMap = { ...raw };
66
+ }
67
+ if (phase === "stubAfter" && Object.keys(phaseMap).length === 0 && instr.highlights) {
68
+ let legacy = instr.highlights;
69
+ if (typeof legacy === "function") {
70
+ try {
71
+ legacy = legacy(opts?.out ?? { __state: "" });
72
+ }
73
+ catch {
74
+ legacy = [];
75
+ }
76
+ }
77
+ phaseMap = phaseFromLegacyHighlights(Array.isArray(legacy) ? legacy : []);
78
+ }
79
+ const fixturePhase = opts?.fixtures?.[block.name]?.[phase];
80
+ const slotIds = new Set([...Object.keys(phaseMap), ...Object.keys(fixturePhase || {})]);
81
+ const ordered = [...slotIds].sort((a, b) => {
82
+ const na = Number(a);
83
+ const nb = Number(b);
84
+ if (!Number.isNaN(na) && !Number.isNaN(nb))
85
+ return na - nb;
86
+ return a.localeCompare(b);
87
+ });
88
+ const resolved = [];
89
+ for (const id of ordered) {
90
+ const merged = mergeSlot(phaseMap[id], fixturePhase?.[id]);
91
+ if (merged)
92
+ resolved.push(merged);
93
+ }
94
+ return resolved;
95
+ }
96
+ /**
97
+ * Resolve multi-step yap slides: flow fixture `slides` wins when present,
98
+ * else `instruction.slides` (function form allowed).
99
+ */
100
+ export function resolveSlides(block, opts) {
101
+ const fromFixture = opts?.fixtures?.[block.name]?.slides;
102
+ if (fromFixture && fromFixture.length > 0) {
103
+ return [...fromFixture];
104
+ }
105
+ const instr = block.instruction;
106
+ let raw = instr.slides;
107
+ if (typeof raw === "function") {
108
+ try {
109
+ raw = raw(opts?.out ?? { __state: "" });
110
+ }
111
+ catch {
112
+ raw = [];
113
+ }
114
+ }
115
+ if (!Array.isArray(raw))
116
+ return [];
117
+ return raw.filter((s) => s && typeof s.caption === "string" && s.caption.length > 0);
118
+ }
119
+ /** Caption for ring / slide UI (label|caption + optional detail/tag). */
120
+ export function formatHighlightCaption(h) {
121
+ const primary = h.caption ?? h.label ?? "";
122
+ const tag = h.tag ? `[${h.tag}] ` : "";
123
+ if (h.detail)
124
+ return `${tag}${primary} - ${h.detail}`;
125
+ return `${tag}${primary}`;
126
+ }
127
+ /**
128
+ * True when the block authored a non-empty stubAfter (after resolve + shim),
129
+ * including legacy highlights. When true, demo should not use verify-trait fallback.
130
+ */
131
+ export function hasAuthoredStubAfter(block, out, fixtures) {
132
+ return resolveHighlightSlots(block, "stubAfter", {
133
+ ...(out !== undefined ? { out } : {}),
134
+ ...(fixtures !== undefined ? { fixtures } : {}),
135
+ }).length > 0;
136
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,9 @@ export type { Checkpoint, Instruction, Block, DefinedBlock, Start, WaygraphInsta
2
2
  export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
4
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
5
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, chainFlow, } from "./engine.js";
5
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
6
+ export { resolveHighlightSlots, resolveSlides, formatHighlightCaption, hasAuthoredStubAfter, } from "./highlights.js";
7
+ export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, HighlightStubPhase, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, } from "./highlights.js";
6
8
  export type { Flow, EngineConfig, ComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
7
9
  export type { ActionPage } from "./types.js";
8
10
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
3
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
4
- export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, chainFlow, } from "./engine.js";
4
+ export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
5
+ export { resolveHighlightSlots, resolveSlides, formatHighlightCaption, hasAuthoredStubAfter, } from "./highlights.js";
5
6
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
package/dist/types.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { Page } from "@playwright/test";
2
2
  import type { MemPage, MemKey } from "./mem-page.js";
3
3
  import type { Trait } from "./trait.js";
4
+ import type { HighlightStubPhaseOrFn, WaygraphHighlight, WaygraphSlidesOrFn } from "./highlights.js";
5
+ export type { WaygraphHighlight, WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, HighlightStubPhase, HighlightStubPhaseOrFn, HighlightFixtureMap, ResolvedHighlight, } from "./highlights.js";
4
6
  /**
5
7
  * A Checkpoint is identified solely by its string tag - a phantom marker with no
6
8
  * embedded data. Two Checkpoints with the same tag are the same state everywhere.
@@ -88,24 +90,33 @@ export interface Instruction<In extends Checkpoint<string>, Out extends Checkpoi
88
90
  */
89
91
  verify?: Trait[] | ((out: Out) => Trait[]);
90
92
  /**
91
- * Optional. Arbitrary highlight points for step-mode tooling to show, in
92
- * order, after this Block resolves - independent of `verify`, for
93
- * calling out something worth a human's attention regardless of whether
94
- * it's also being asserted ("remember this signature ID" isn't a
95
- * pass/fail check). A function form picks different highlights per
96
- * resolved `Out`, same as `verify`. Purely additive UI narration - never
97
- * consulted by `runGraph`/`connect`/anything that decides behavior.
98
- * @example highlights: (out) => [{ selector: "#signature-id", label: "remember this ID" }]
93
+ * Catch-all demo slots shown **before / during** `act` (fill/click rings).
94
+ * Named map; `{}` is valid. Flow {@link HighlightFixtureMap} overrides labels.
95
+ * @example stubBefore: { email: { selector: "#email", label: "Email" } }
96
+ */
97
+ stubBefore?: HighlightStubPhaseOrFn<Out>;
98
+ /**
99
+ * Catch-all demo slots shown **after** resolve (step panel rings).
100
+ * Named map; `{}` is valid. Prefer this over legacy {@link Instruction.highlights}.
101
+ * @example stubAfter: { badge: { selector: ".cart", label: "Cart updated" } }
102
+ */
103
+ stubAfter?: HighlightStubPhaseOrFn<Out>;
104
+ /**
105
+ * Multi-step demo captions ("yap" slides) with Next between each -
106
+ * not block lifecycle. Explains a long process; optional `selector` rings
107
+ * while that slide is up. Flow fixtures may override via `slides`.
108
+ * @example slides: [
109
+ * { caption: "Checkout is three screens", tag: "YAP" },
110
+ * { caption: "Overview confirms totals", selector: ".summary_info" },
111
+ * ]
112
+ */
113
+ slides?: WaygraphSlidesOrFn<Out>;
114
+ /**
115
+ * @deprecated Prefer {@link Instruction.stubAfter}. Shimmed to stubAfter keys
116
+ * `"0"`, `"1"`, … when stubAfter is empty.
99
117
  */
100
118
  highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
101
119
  }
102
- /** One arbitrary highlight point - see {@link Instruction.highlights}. */
103
- export interface WaygraphHighlight {
104
- /** CSS selector for the element to highlight. */
105
- selector: string;
106
- /** Caption shown next to the highlight ring. */
107
- label: string;
108
- }
109
120
  /**
110
121
  * One selectable option for a Block whose real choices depend on the live
111
122
  * page - "which of these items to add to cart" is only knowable by looking
@@ -18,6 +18,34 @@ export const FinishOrderBlock = defineMethodBlock<CheckoutOverviewPage, OrderCom
18
18
  },
19
19
  resolve: () => checkpoint("OrderComplete"),
20
20
  verify: [Trait.text(".complete-header", "Thank you for your order!")],
21
- highlights: () => [{ selector: ".complete-header", label: "remember this: order confirmed" }],
21
+ stubBefore: {
22
+ finish: { selector: "#finish", label: "Finish" },
23
+ },
24
+ stubAfter: {
25
+ thanks: {
26
+ selector: ".complete-header",
27
+ label: "Order confirmed",
28
+ detail: "Thank you for your order!",
29
+ },
30
+ },
31
+ slides: [
32
+ {
33
+ caption: "Checkout is a short pipeline",
34
+ detail: "Cart → info → overview → done. Watch the last click place the order.",
35
+ tag: "YAP",
36
+ },
37
+ {
38
+ caption: "Finish submits the overview",
39
+ detail: "This click is the only mutating step on this screen.",
40
+ tag: "YAP",
41
+ selector: "#finish",
42
+ },
43
+ {
44
+ caption: "Completion banner is the proof",
45
+ detail: "The next ring calls out the thank-you header.",
46
+ tag: "YAP",
47
+ selector: ".complete-header",
48
+ },
49
+ ],
22
50
  },
23
51
  });
@@ -30,7 +30,14 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
30
30
  },
31
31
  },
32
32
  ],
33
- highlights: () => [{ selector: InventorySel.cartBadge, label: "remember this: cart now has the item" }],
33
+ stubBefore: {},
34
+ stubAfter: {
35
+ badge: {
36
+ selector: InventorySel.cartBadge,
37
+ label: "Cart badge",
38
+ detail: "Item is in the cart",
39
+ },
40
+ },
34
41
  },
35
42
  async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
36
43
  const items = await collectAddableItems(page);
@@ -38,5 +38,11 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
38
38
  out.__state === "LoggedIn"
39
39
  ? [Trait.url({ pathname: "/inventory.html" })]
40
40
  : [Trait.visible('[data-test="error"]')],
41
+ stubBefore: {
42
+ username: { selector: "#user-name", label: "Username" },
43
+ password: { selector: "#password", label: "Password" },
44
+ submit: { selector: "#login-button", label: "Login" },
45
+ },
46
+ stubAfter: {},
41
47
  },
42
48
  });
@@ -1,4 +1,4 @@
1
- import { Engine, start, end, withTitle } from "waygraph";
1
+ import { Engine, start, end, withTitle, withHighlightFixtures } from "waygraph";
2
2
  import type {
3
3
  LoginPage,
4
4
  LoggedIn,
@@ -21,25 +21,64 @@ const engine = new Engine({ headless: false, slowMo: 250 });
21
21
  // Atomic nav/action per real URL - the shape `waygraph auto` discovers.
22
22
  // Explicit type args: AddToCartBlock's In is Checkpoint<string> (wildcard),
23
23
  // which trips defineFlow overload inference across a long array.
24
- export const checkoutFlow = withTitle(
25
- engine.defineFlow<
26
- LoginPage,
27
- LoggedIn,
28
- ItemInCart,
29
- CartPage,
30
- CheckoutInfoPage,
31
- CheckoutOverviewPage,
32
- OrderComplete
33
- >([
34
- start,
35
- NavLoginBlock,
36
- SubmitLoginForFlow,
37
- AddToCartBlock,
38
- NavCartBlock,
39
- NavCheckoutInfoBlock,
40
- SubmitCheckoutInfoBlock,
41
- FinishOrderBlock,
42
- end,
43
- ]),
44
- "Owner: Full Checkout",
24
+ //
25
+ // Demo narration showcase (waygraph 0.11+):
26
+ // - Block stubs: submit-login stubBefore, add-to-cart / finish-order stubAfter
27
+ // - Flow fixtures: ticket-style captions (unlimited purposes on .flow.ts)
28
+ // - Slides on finish-order: multi-step yap with Next between slides
29
+ export const checkoutFlow = withHighlightFixtures(
30
+ withTitle(
31
+ engine.defineFlow<
32
+ LoginPage,
33
+ LoggedIn,
34
+ ItemInCart,
35
+ CartPage,
36
+ CheckoutInfoPage,
37
+ CheckoutOverviewPage,
38
+ OrderComplete
39
+ >([
40
+ start,
41
+ NavLoginBlock,
42
+ SubmitLoginForFlow,
43
+ AddToCartBlock,
44
+ NavCartBlock,
45
+ NavCheckoutInfoBlock,
46
+ SubmitCheckoutInfoBlock,
47
+ FinishOrderBlock,
48
+ end,
49
+ ]),
50
+ "Owner: Full Checkout",
51
+ ),
52
+ {
53
+ "submit-login": {
54
+ stubBefore: {
55
+ username: {
56
+ label: "Demo user",
57
+ detail: "standard_user from Mem — library block stays neutral.",
58
+ tag: "AC",
59
+ },
60
+ password: { label: "Password", detail: "Filled from Mem credentials." },
61
+ submit: { label: "Sign in", tag: "GATE" },
62
+ },
63
+ },
64
+ "add-to-cart": {
65
+ stubAfter: {
66
+ badge: {
67
+ label: "Cart now has the item",
68
+ detail: "Badge updates after Add to cart.",
69
+ tag: "AC",
70
+ },
71
+ },
72
+ },
73
+ "finish-order": {
74
+ stubAfter: {
75
+ thanks: {
76
+ label: "Order placed",
77
+ detail: "Completion header is the durable proof.",
78
+ tag: "GATE",
79
+ },
80
+ },
81
+ // Optional: fixture can replace block slides; leave unset to use block slides.
82
+ },
83
+ },
45
84
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.10.9",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  "baseUrl": "https://www.saucedemo.com"
24
24
  },
25
25
  "dependencies": {
26
- "waygraph": "^0.10.9"
26
+ "waygraph": "^0.11.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@playwright/test": "^1.55.0",
@@ -18,6 +18,34 @@ export const FinishOrderBlock = defineMethodBlock<CheckoutOverviewPage, OrderCom
18
18
  },
19
19
  resolve: () => checkpoint("OrderComplete"),
20
20
  verify: [Trait.text(".complete-header", "Thank you for your order!")],
21
- highlights: () => [{ selector: ".complete-header", label: "remember this: order confirmed" }],
21
+ stubBefore: {
22
+ finish: { selector: "#finish", label: "Finish" },
23
+ },
24
+ stubAfter: {
25
+ thanks: {
26
+ selector: ".complete-header",
27
+ label: "Order confirmed",
28
+ detail: "Thank you for your order!",
29
+ },
30
+ },
31
+ slides: [
32
+ {
33
+ caption: "Checkout is a short pipeline",
34
+ detail: "Cart → info → overview → done. Watch the last click place the order.",
35
+ tag: "YAP",
36
+ },
37
+ {
38
+ caption: "Finish submits the overview",
39
+ detail: "This click is the only mutating step on this screen.",
40
+ tag: "YAP",
41
+ selector: "#finish",
42
+ },
43
+ {
44
+ caption: "Completion banner is the proof",
45
+ detail: "The next ring calls out the thank-you header.",
46
+ tag: "YAP",
47
+ selector: ".complete-header",
48
+ },
49
+ ],
22
50
  },
23
51
  });
@@ -30,7 +30,14 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
30
30
  },
31
31
  },
32
32
  ],
33
- highlights: () => [{ selector: InventorySel.cartBadge, label: "remember this: cart now has the item" }],
33
+ stubBefore: {},
34
+ stubAfter: {
35
+ badge: {
36
+ selector: InventorySel.cartBadge,
37
+ label: "Cart badge",
38
+ detail: "Item is in the cart",
39
+ },
40
+ },
34
41
  },
35
42
  async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
36
43
  const items = await collectAddableItems(page);
@@ -38,5 +38,11 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
38
38
  out.__state === "LoggedIn"
39
39
  ? [Trait.url({ pathname: "/inventory.html" })]
40
40
  : [Trait.visible('[data-test="error"]')],
41
+ stubBefore: {
42
+ username: { selector: "#user-name", label: "Username" },
43
+ password: { selector: "#password", label: "Password" },
44
+ submit: { selector: "#login-button", label: "Login" },
45
+ },
46
+ stubAfter: {},
41
47
  },
42
48
  });
@@ -1,4 +1,4 @@
1
- import { Engine, start, end, withTitle } from "waygraph";
1
+ import { Engine, start, end, withTitle, withHighlightFixtures } from "waygraph";
2
2
  import type {
3
3
  LoginPage,
4
4
  LoggedIn,
@@ -21,25 +21,64 @@ const engine = new Engine({ headless: false, slowMo: 250 });
21
21
  // Atomic nav/action per real URL - the shape `waygraph auto` discovers.
22
22
  // Explicit type args: AddToCartBlock's In is Checkpoint<string> (wildcard),
23
23
  // which trips defineFlow overload inference across a long array.
24
- export const checkoutFlow = withTitle(
25
- engine.defineFlow<
26
- LoginPage,
27
- LoggedIn,
28
- ItemInCart,
29
- CartPage,
30
- CheckoutInfoPage,
31
- CheckoutOverviewPage,
32
- OrderComplete
33
- >([
34
- start,
35
- NavLoginBlock,
36
- SubmitLoginForFlow,
37
- AddToCartBlock,
38
- NavCartBlock,
39
- NavCheckoutInfoBlock,
40
- SubmitCheckoutInfoBlock,
41
- FinishOrderBlock,
42
- end,
43
- ]),
44
- "Owner: Full Checkout",
24
+ //
25
+ // Demo narration showcase (waygraph 0.11+):
26
+ // - Block stubs: submit-login stubBefore, add-to-cart / finish-order stubAfter
27
+ // - Flow fixtures: ticket-style captions (unlimited purposes on .flow.ts)
28
+ // - Slides on finish-order: multi-step yap with Next between slides
29
+ export const checkoutFlow = withHighlightFixtures(
30
+ withTitle(
31
+ engine.defineFlow<
32
+ LoginPage,
33
+ LoggedIn,
34
+ ItemInCart,
35
+ CartPage,
36
+ CheckoutInfoPage,
37
+ CheckoutOverviewPage,
38
+ OrderComplete
39
+ >([
40
+ start,
41
+ NavLoginBlock,
42
+ SubmitLoginForFlow,
43
+ AddToCartBlock,
44
+ NavCartBlock,
45
+ NavCheckoutInfoBlock,
46
+ SubmitCheckoutInfoBlock,
47
+ FinishOrderBlock,
48
+ end,
49
+ ]),
50
+ "Owner: Full Checkout",
51
+ ),
52
+ {
53
+ "submit-login": {
54
+ stubBefore: {
55
+ username: {
56
+ label: "Demo user",
57
+ detail: "standard_user from Mem — library block stays neutral.",
58
+ tag: "AC",
59
+ },
60
+ password: { label: "Password", detail: "Filled from Mem credentials." },
61
+ submit: { label: "Sign in", tag: "GATE" },
62
+ },
63
+ },
64
+ "add-to-cart": {
65
+ stubAfter: {
66
+ badge: {
67
+ label: "Cart now has the item",
68
+ detail: "Badge updates after Add to cart.",
69
+ tag: "AC",
70
+ },
71
+ },
72
+ },
73
+ "finish-order": {
74
+ stubAfter: {
75
+ thanks: {
76
+ label: "Order placed",
77
+ detail: "Completion header is the durable proof.",
78
+ tag: "GATE",
79
+ },
80
+ },
81
+ // Optional: fixture can replace block slides; leave unset to use block slides.
82
+ },
83
+ },
45
84
  );
@@ -18,7 +18,7 @@
18
18
  "baseUrl": "https://example.com"
19
19
  },
20
20
  "dependencies": {
21
- "waygraph": "^0.10.9"
21
+ "waygraph": "^0.11.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@playwright/test": "^1.55.0",