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 +202 -9
- package/dist/engine.d.ts +23 -0
- package/dist/engine.js +21 -0
- package/dist/highlights.d.ts +76 -0
- package/dist/highlights.js +136 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/types.d.ts +26 -15
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +29 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +8 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +6 -0
- package/examples/saucedemo/src/flows/checkout.flow.ts +61 -22
- package/package.json +1 -1
- package/templates/quickstart/package.json +1 -1
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +29 -1
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +8 -1
- package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +6 -0
- package/templates/quickstart/src/flows/checkout.flow.ts +61 -22
- package/templates/scaffold/package.json +1 -1
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:
|
|
399
|
-
"font:600 12px/1.
|
|
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, "&").replace(/</g, "<").replace(/"/g, """);
|
|
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
|
|
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
|
|
1728
|
+
let fallback = "click";
|
|
1564
1729
|
try {
|
|
1565
1730
|
const text = (await this.textContent())?.trim();
|
|
1566
|
-
if (text && text.length > 0 && text.length <= 30)
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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
|
-
|
|
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
|
});
|
package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts
CHANGED
|
@@ -30,7 +30,14 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
],
|
|
33
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
});
|
package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts
CHANGED
|
@@ -30,7 +30,14 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
],
|
|
33
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
);
|