waygraph 0.10.9 → 0.11.1
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 +330 -34
- 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/trait.d.ts +6 -1
- package/dist/trait.js +7 -2
- package/dist/types.d.ts +26 -15
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +31 -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 +31 -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
|
|
@@ -1337,12 +1343,18 @@ function extractVerifyHighlights(block, resultTag) {
|
|
|
1337
1343
|
* __wgNext no-ops once already consumed - but looked live when it wasn't).
|
|
1338
1344
|
* Disables the button and mem-key textareas, and swaps whichever gate
|
|
1339
1345
|
* text was showing (manual button or "Auto-advancing...") to "Running...".
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1346
|
+
* ALWAYS collapses the panel while act() runs - leaving it expanded was a
|
|
1347
|
+
* real bug: #wg-panel sits at z-index max with pointer-events:auto, so
|
|
1348
|
+
* Playwright's login/add-to-cart clicks hit the stepper instead of the app
|
|
1349
|
+
* ("subtree intercepts pointer events"). Empty/partial fills + a lucky
|
|
1350
|
+
* edge click on Login then produced Sauce Demo's Epic sadface, verify
|
|
1351
|
+
* still passed on the error banner, and the next block blew up. Collapse
|
|
1352
|
+
* also matches Dan's "step is running / can't interrupt" ask. (_opts kept
|
|
1353
|
+
* for call-site compat; autoCollapsePanel is now unconditional.)
|
|
1342
1354
|
*/
|
|
1343
|
-
async function markStepRunning(page,
|
|
1355
|
+
async function markStepRunning(page, _opts) {
|
|
1344
1356
|
await page
|
|
1345
|
-
.evaluate((
|
|
1357
|
+
.evaluate(() => {
|
|
1346
1358
|
const runBtn = document.getElementById("wg-run");
|
|
1347
1359
|
if (runBtn) {
|
|
1348
1360
|
runBtn.disabled = true;
|
|
@@ -1353,18 +1365,16 @@ async function markStepRunning(page, opts) {
|
|
|
1353
1365
|
document.querySelectorAll("#wg-panel textarea[data-key]").forEach((ta) => {
|
|
1354
1366
|
ta.disabled = true;
|
|
1355
1367
|
});
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
if (titleEl && label) titleEl.textContent = label;
|
|
1365
|
-
}
|
|
1368
|
+
const panel = document.getElementById("wg-panel");
|
|
1369
|
+
if (panel) {
|
|
1370
|
+
panel.classList.add("wg-collapsed");
|
|
1371
|
+
const toggle = panel.querySelector("[data-wg-toggle]");
|
|
1372
|
+
if (toggle) toggle.textContent = "Show";
|
|
1373
|
+
const titleEl = panel.querySelector(".wg-chrome-title");
|
|
1374
|
+
const label = panel.dataset.wgStepLabel;
|
|
1375
|
+
if (titleEl && label) titleEl.textContent = label;
|
|
1366
1376
|
}
|
|
1367
|
-
}
|
|
1377
|
+
})
|
|
1368
1378
|
.catch(() => {});
|
|
1369
1379
|
}
|
|
1370
1380
|
|
|
@@ -1379,6 +1389,210 @@ async function showRing(page, box, label) {
|
|
|
1379
1389
|
.catch(() => {});
|
|
1380
1390
|
}
|
|
1381
1391
|
|
|
1392
|
+
/** Match a locator to a stubBefore slot by overlapping bounding boxes. */
|
|
1393
|
+
async function captionForLocator(page, locator, stubs, fallback) {
|
|
1394
|
+
if (!stubs || stubs.length === 0) return fallback;
|
|
1395
|
+
const box = await locator.boundingBox().catch(() => null);
|
|
1396
|
+
if (!box) return fallback;
|
|
1397
|
+
for (const s of stubs) {
|
|
1398
|
+
try {
|
|
1399
|
+
const b = await page.locator(s.selector).first().boundingBox();
|
|
1400
|
+
if (
|
|
1401
|
+
b &&
|
|
1402
|
+
Math.abs(b.x - box.x) < 4 &&
|
|
1403
|
+
Math.abs(b.y - box.y) < 4 &&
|
|
1404
|
+
Math.abs(b.width - box.width) < 8
|
|
1405
|
+
) {
|
|
1406
|
+
return formatHighlightCaption(s);
|
|
1407
|
+
}
|
|
1408
|
+
} catch {
|
|
1409
|
+
/* skip */
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return fallback;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Multi-step yap captions - not block lifecycle. Each slide waits for Next
|
|
1417
|
+
* (or auto-next), optional ring via slide.selector.
|
|
1418
|
+
*
|
|
1419
|
+
* Advance wait does NOT use exposeFunction/gate(): after a real navigation
|
|
1420
|
+
* (e.g. finish-order -> checkout-complete) the panel can render while
|
|
1421
|
+
* __wgNext is momentarily wedged, which left "Next slide" looking live but
|
|
1422
|
+
* doing nothing. In-page data-wg-acked + autoplay poll is the source of truth.
|
|
1423
|
+
*/
|
|
1424
|
+
async function presentSlides(page, slides, _gate, opts) {
|
|
1425
|
+
const title = (opts && opts.title) || "waygraph demo";
|
|
1426
|
+
const blockName = (opts && opts.blockName) || "";
|
|
1427
|
+
const episodeNumber = opts && opts.episodeNumber;
|
|
1428
|
+
const episodeTitle = (opts && opts.episodeTitle) || "";
|
|
1429
|
+
const autoplayMs =
|
|
1430
|
+
opts && opts.autoplayMs
|
|
1431
|
+
? Number(opts.autoplayMs)
|
|
1432
|
+
: process.env.WAYGRAPH_AUTOPLAY_MS
|
|
1433
|
+
? Number(process.env.WAYGRAPH_AUTOPLAY_MS)
|
|
1434
|
+
: 1800;
|
|
1435
|
+
for (let i = 0; i < slides.length; i++) {
|
|
1436
|
+
const s = slides[i];
|
|
1437
|
+
const caption = formatHighlightCaption(s);
|
|
1438
|
+
const isLast = i === slides.length - 1;
|
|
1439
|
+
await installOverlay(page, title);
|
|
1440
|
+
if (s.selector) {
|
|
1441
|
+
try {
|
|
1442
|
+
const box = await page.locator(s.selector).first().boundingBox();
|
|
1443
|
+
if (box) await showRing(page, box, caption);
|
|
1444
|
+
} catch {
|
|
1445
|
+
await hideRing(page);
|
|
1446
|
+
}
|
|
1447
|
+
} else {
|
|
1448
|
+
await hideRing(page);
|
|
1449
|
+
}
|
|
1450
|
+
await page.evaluate(
|
|
1451
|
+
(info) => {
|
|
1452
|
+
let panel = document.getElementById("wg-panel");
|
|
1453
|
+
const isNew = !panel;
|
|
1454
|
+
if (!panel) {
|
|
1455
|
+
panel = document.createElement("div");
|
|
1456
|
+
panel.id = "wg-panel";
|
|
1457
|
+
}
|
|
1458
|
+
panel.classList.remove("wg-collapsed", "wg-error", "wg-expected");
|
|
1459
|
+
const esc = (t) =>
|
|
1460
|
+
String(t).replace(/&/g, "&").replace(/</g, "<").replace(/"/g, """);
|
|
1461
|
+
const episodeLine =
|
|
1462
|
+
info.episodeNumber !== undefined && info.episodeNumber !== null
|
|
1463
|
+
? "<div class=\\"wg-episode\\">Episode " +
|
|
1464
|
+
info.episodeNumber +
|
|
1465
|
+
(info.episodeTitle ? ": " + esc(info.episodeTitle) : "") +
|
|
1466
|
+
" · yap slides</div>"
|
|
1467
|
+
: "";
|
|
1468
|
+
panel.innerHTML =
|
|
1469
|
+
"<div class=\\"wg-chrome\\"><span class=\\"wg-chrome-title\\">waygraph demo</span>" +
|
|
1470
|
+
"<button type=\\"button\\" class=\\"wg-hide-btn\\" data-wg-toggle=\\"1\\">Hide</button></div>" +
|
|
1471
|
+
"<div class=\\"wg-body\\">" +
|
|
1472
|
+
episodeLine +
|
|
1473
|
+
"<h3>Slide " +
|
|
1474
|
+
(info.index + 1) +
|
|
1475
|
+
" / " +
|
|
1476
|
+
info.total +
|
|
1477
|
+
(info.blockName ? " · " + esc(info.blockName) : "") +
|
|
1478
|
+
"</h3>" +
|
|
1479
|
+
(info.tag
|
|
1480
|
+
? "<div style=\\"border:none;padding:0;margin:0 0 6px;font-size:11px;color:#c9a6ff\\">" +
|
|
1481
|
+
esc(info.tag) +
|
|
1482
|
+
"</div>"
|
|
1483
|
+
: "") +
|
|
1484
|
+
"<div class=\\"wg-narration\\">" +
|
|
1485
|
+
esc(info.caption) +
|
|
1486
|
+
"</div>" +
|
|
1487
|
+
(info.detail
|
|
1488
|
+
? "<div class=\\"wg-result-pretty\\" style=\\"margin:0 0 12px;color:#f0e8ff\\">" +
|
|
1489
|
+
esc(info.detail) +
|
|
1490
|
+
"</div>"
|
|
1491
|
+
: "") +
|
|
1492
|
+
"<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
|
|
1493
|
+
(info.autoNow ? " checked" : "") +
|
|
1494
|
+
"> Auto-advance</label></div>" +
|
|
1495
|
+
"<div id=\\"wg-gate-manual\\"" +
|
|
1496
|
+
(info.autoNow ? " style=\\"display:none\\"" : "") +
|
|
1497
|
+
"><button type=\\"button\\" id=\\"wg-run\\">" +
|
|
1498
|
+
(info.isLast ? "Continue \\u25B6" : "Next slide \\u25B6") +
|
|
1499
|
+
"</button></div>" +
|
|
1500
|
+
"<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" +
|
|
1501
|
+
(info.autoNow ? "" : " style=\\"display:none\\"") +
|
|
1502
|
+
">Auto-advancing...</div>" +
|
|
1503
|
+
"</div>";
|
|
1504
|
+
if (isNew) {
|
|
1505
|
+
document.documentElement.appendChild(panel);
|
|
1506
|
+
requestAnimationFrame(() => panel.classList.add("wg-in"));
|
|
1507
|
+
}
|
|
1508
|
+
if (window.__wgWirePanelChrome) {
|
|
1509
|
+
window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
|
|
1510
|
+
stepLabel:
|
|
1511
|
+
"Slide " +
|
|
1512
|
+
(info.index + 1) +
|
|
1513
|
+
" / " +
|
|
1514
|
+
info.total +
|
|
1515
|
+
(info.blockName ? " · " + info.blockName : ""),
|
|
1516
|
+
forceCollapsed: false,
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
let autoNow = false;
|
|
1520
|
+
try {
|
|
1521
|
+
autoNow = localStorage.getItem("wg-autoplay") === "1";
|
|
1522
|
+
} catch {
|
|
1523
|
+
/* ignore */
|
|
1524
|
+
}
|
|
1525
|
+
const cb = document.getElementById("wg-autoplay-cb");
|
|
1526
|
+
if (cb) {
|
|
1527
|
+
cb.checked = autoNow;
|
|
1528
|
+
cb.addEventListener("change", () => {
|
|
1529
|
+
try {
|
|
1530
|
+
localStorage.setItem("wg-autoplay", cb.checked ? "1" : "0");
|
|
1531
|
+
} catch {
|
|
1532
|
+
/* ignore */
|
|
1533
|
+
}
|
|
1534
|
+
const manual = document.getElementById("wg-gate-manual");
|
|
1535
|
+
const auto = document.getElementById("wg-gate-auto");
|
|
1536
|
+
if (manual) manual.style.display = cb.checked ? "none" : "";
|
|
1537
|
+
if (auto) auto.style.display = cb.checked ? "" : "none";
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
const manual = document.getElementById("wg-gate-manual");
|
|
1541
|
+
const auto = document.getElementById("wg-gate-auto");
|
|
1542
|
+
if (manual) manual.style.display = autoNow ? "none" : "";
|
|
1543
|
+
if (auto) auto.style.display = autoNow ? "" : "none";
|
|
1544
|
+
const runBtn = document.getElementById("wg-run");
|
|
1545
|
+
if (runBtn) {
|
|
1546
|
+
runBtn.removeAttribute("data-wg-acked");
|
|
1547
|
+
runBtn.addEventListener(
|
|
1548
|
+
"click",
|
|
1549
|
+
(e) => {
|
|
1550
|
+
e.preventDefault();
|
|
1551
|
+
e.stopPropagation();
|
|
1552
|
+
runBtn.setAttribute("data-wg-acked", "1");
|
|
1553
|
+
},
|
|
1554
|
+
{ once: true },
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
index: i,
|
|
1560
|
+
total: slides.length,
|
|
1561
|
+
caption: s.caption,
|
|
1562
|
+
detail: s.detail || "",
|
|
1563
|
+
tag: s.tag || "",
|
|
1564
|
+
blockName,
|
|
1565
|
+
isLast,
|
|
1566
|
+
autoNow: false,
|
|
1567
|
+
episodeNumber: episodeNumber !== undefined ? episodeNumber : null,
|
|
1568
|
+
episodeTitle,
|
|
1569
|
+
},
|
|
1570
|
+
);
|
|
1571
|
+
const started = Date.now();
|
|
1572
|
+
for (;;) {
|
|
1573
|
+
const acked = await page
|
|
1574
|
+
.evaluate(() => {
|
|
1575
|
+
const btn = document.getElementById("wg-run");
|
|
1576
|
+
return !!(btn && btn.getAttribute("data-wg-acked") === "1");
|
|
1577
|
+
})
|
|
1578
|
+
.catch(() => false);
|
|
1579
|
+
if (acked) break;
|
|
1580
|
+
const auto = await page
|
|
1581
|
+
.evaluate(() => {
|
|
1582
|
+
try {
|
|
1583
|
+
return localStorage.getItem("wg-autoplay") === "1";
|
|
1584
|
+
} catch {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
})
|
|
1588
|
+
.catch(() => false);
|
|
1589
|
+
if (auto && Date.now() - started >= autoplayMs) break;
|
|
1590
|
+
await new Promise((res) => setTimeout(res, 100));
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
await hideRing(page);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1382
1596
|
async function hideRing(page) {
|
|
1383
1597
|
await page
|
|
1384
1598
|
.evaluate(() => {
|
|
@@ -1455,7 +1669,7 @@ async function wasJustNarrated(page) {
|
|
|
1455
1669
|
.catch(() => false);
|
|
1456
1670
|
}
|
|
1457
1671
|
|
|
1458
|
-
function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
|
|
1672
|
+
function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef) {
|
|
1459
1673
|
// Playwright's own slowMo ALREADY pauses after every single low-level
|
|
1460
1674
|
// action it dispatches - and pressSequentially() fires one such action
|
|
1461
1675
|
// PER CHARACTER. Also giving pressSequentially its own fixed delay
|
|
@@ -1469,6 +1683,7 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
|
|
|
1469
1683
|
// per-block (WAYGRAPH_FAST_BLOCKS), and the Locator.fill/click patches
|
|
1470
1684
|
// below are installed once on the shared prototype but invoked once per
|
|
1471
1685
|
// real interaction, long after this closure was created.
|
|
1686
|
+
// stubBeforeRef.current = resolved stubBefore slots for the active block.
|
|
1472
1687
|
const typeDelay = () => (pacing.fast ? 0 : slowMo ? 0 : 30);
|
|
1473
1688
|
// Click is a single action, not per-character, so it doesn't compound
|
|
1474
1689
|
// the same way - but slowMo still adds its own pause around the actual
|
|
@@ -1505,9 +1720,11 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
|
|
|
1505
1720
|
// Only trust the "last mem.get()" as THIS fill's source if it
|
|
1506
1721
|
// happened recently - a stale read from several actions ago is
|
|
1507
1722
|
// more likely unrelated than actually describing this field.
|
|
1508
|
-
const
|
|
1723
|
+
const fallback = memTrack.lastKeyName && Date.now() - memTrack.at < 3000
|
|
1509
1724
|
? "from mem: " + memTrack.lastKeyName
|
|
1510
1725
|
: "writing from mem";
|
|
1726
|
+
const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
|
|
1727
|
+
const label = await captionForLocator(page, this, stubs, fallback);
|
|
1511
1728
|
await moveCursorTo(page, box, cursorMs(500));
|
|
1512
1729
|
await showRing(page, box, label);
|
|
1513
1730
|
await new Promise((res) => setTimeout(res, pacing.fast ? 0 : 200));
|
|
@@ -1560,10 +1777,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
|
|
|
1560
1777
|
const box = await this.boundingBox();
|
|
1561
1778
|
const narrated = box ? await wasJustNarrated(page) : false;
|
|
1562
1779
|
if (box && !narrated) {
|
|
1563
|
-
let
|
|
1780
|
+
let fallback = "click";
|
|
1564
1781
|
try {
|
|
1565
1782
|
const text = (await this.textContent())?.trim();
|
|
1566
|
-
if (text && text.length > 0 && text.length <= 30)
|
|
1783
|
+
if (text && text.length > 0 && text.length <= 30) fallback = text;
|
|
1567
1784
|
} catch {
|
|
1568
1785
|
// element has no simple text (an icon button, say) - generic label is fine
|
|
1569
1786
|
}
|
|
@@ -1571,10 +1788,12 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
|
|
|
1571
1788
|
// (set on page by runStepMode just before act).
|
|
1572
1789
|
try {
|
|
1573
1790
|
const navLabel = await page.evaluate(() => window.__wgPendingNavClickLabel || null);
|
|
1574
|
-
if (navLabel)
|
|
1791
|
+
if (navLabel) fallback = String(navLabel);
|
|
1575
1792
|
} catch {
|
|
1576
1793
|
/* ignore */
|
|
1577
1794
|
}
|
|
1795
|
+
const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
|
|
1796
|
+
const label = await captionForLocator(page, this, stubs, fallback);
|
|
1578
1797
|
clickPoint = await moveCursorTo(page, box, cursorMs(600));
|
|
1579
1798
|
await showRing(page, box, label);
|
|
1580
1799
|
// "pop for a few seconds" - Dan's own phrase, matching the
|
|
@@ -1703,7 +1922,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
1703
1922
|
const demoFast = process.env.WAYGRAPH_DEMO_FAST === "1";
|
|
1704
1923
|
const stepperMode = process.env.WAYGRAPH_STEPPER === "full" ? "full" : "carousel";
|
|
1705
1924
|
const pacing = { fast: demoFast };
|
|
1706
|
-
|
|
1925
|
+
const stubBeforeRef = { current: [] };
|
|
1926
|
+
instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef);
|
|
1707
1927
|
// Force the panel checkbox from this process's flags/env at run start.
|
|
1708
1928
|
// installOverlay only seeds localStorage when the key is null (so mid-run
|
|
1709
1929
|
// checkbox clicks survive navigations). Without this, a prior --autoplay
|
|
@@ -1733,14 +1953,34 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
1733
1953
|
// already is.
|
|
1734
1954
|
page.setViewportSize = async () => {};
|
|
1735
1955
|
let resolveNext = null;
|
|
1956
|
+
// Render-to-gate race: a slide/step panel's Next button is wired to
|
|
1957
|
+
// window.__wgNext BEFORE gate() calls waitForNext() (there is at least
|
|
1958
|
+
// one more await - the autoplay-sync evaluate - in between on the
|
|
1959
|
+
// presentSlides path). A click landing in that small window used to be
|
|
1960
|
+
// dropped silently (resolveNext was still null) - the panel then looked
|
|
1961
|
+
// "stuck" forever since gate() started listening AFTER the click already
|
|
1962
|
+
// fired and no second click ever came. Queue it instead: the very next
|
|
1963
|
+
// waitForNext() call picks up a pending click immediately.
|
|
1964
|
+
let pendingNext = null;
|
|
1736
1965
|
await page.exposeFunction("__wgNext", (edits) => {
|
|
1737
1966
|
if (resolveNext) {
|
|
1738
1967
|
const r = resolveNext;
|
|
1739
1968
|
resolveNext = null;
|
|
1740
1969
|
r(edits);
|
|
1970
|
+
} else {
|
|
1971
|
+
pendingNext = edits;
|
|
1741
1972
|
}
|
|
1742
1973
|
});
|
|
1743
|
-
const waitForNext = () =>
|
|
1974
|
+
const waitForNext = () =>
|
|
1975
|
+
new Promise((res) => {
|
|
1976
|
+
if (pendingNext !== null) {
|
|
1977
|
+
const edits = pendingNext;
|
|
1978
|
+
pendingNext = null;
|
|
1979
|
+
res(edits);
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
resolveNext = res;
|
|
1983
|
+
});
|
|
1744
1984
|
// WAYGRAPH_AUTOPLAY=1 only sets the STARTING checkbox state now - the
|
|
1745
1985
|
// panel's own "Auto-advance" checkbox can flip it live, mid-run, and a
|
|
1746
1986
|
// manual click always wins over an in-flight autoplay wait regardless of
|
|
@@ -1852,6 +2092,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
1852
2092
|
await resetPageState(context, page, baseURL);
|
|
1853
2093
|
}
|
|
1854
2094
|
pacing.fast = demoFast || fastBlockNames.has(r.block.name);
|
|
2095
|
+
const fixtures = r.highlightFixtures;
|
|
2096
|
+
stubBeforeRef.current = resolveHighlightSlots(r.block, "stubBefore", { fixtures });
|
|
1855
2097
|
const isNavBlock = r.block.__waygraphKind === "nav";
|
|
1856
2098
|
const autoNow = await currentAutoplay();
|
|
1857
2099
|
// Auto-next: tuck stepper on NavBlocks so the page transition fills the frame.
|
|
@@ -1975,7 +2217,29 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
1975
2217
|
})
|
|
1976
2218
|
.catch(() => {});
|
|
1977
2219
|
result = stepOutcome.result;
|
|
1978
|
-
const
|
|
2220
|
+
const fixturesAfter = r.highlightFixtures;
|
|
2221
|
+
const slides = resolveSlides(r.block, { out: result, fixtures: fixturesAfter });
|
|
2222
|
+
if (slides.length > 0) {
|
|
2223
|
+
await presentSlides(page, slides, gate, {
|
|
2224
|
+
title,
|
|
2225
|
+
blockName: r.block.name,
|
|
2226
|
+
fast: pacing.fast,
|
|
2227
|
+
episodeNumber: r.episodeNumber,
|
|
2228
|
+
episodeTitle: r.episodeTitle,
|
|
2229
|
+
autoplayMs,
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
let highlights;
|
|
2233
|
+
if (hasAuthoredStubAfter(r.block, result, fixturesAfter)) {
|
|
2234
|
+
highlights = resolveHighlightSlots(r.block, "stubAfter", { out: result, fixtures: fixturesAfter }).map(
|
|
2235
|
+
(h) => ({
|
|
2236
|
+
selector: h.selector,
|
|
2237
|
+
label: formatHighlightCaption(h),
|
|
2238
|
+
}),
|
|
2239
|
+
);
|
|
2240
|
+
} else {
|
|
2241
|
+
highlights = extractVerifyHighlights(r.block, result.__state);
|
|
2242
|
+
}
|
|
1979
2243
|
await renderAfterStep(page, {
|
|
1980
2244
|
index: i,
|
|
1981
2245
|
total: resolved.length,
|
|
@@ -2126,6 +2390,7 @@ async function main() {
|
|
|
2126
2390
|
block: bi.block,
|
|
2127
2391
|
exportName: bi.name,
|
|
2128
2392
|
seedMem: idx === 0 ? seedMem : undefined,
|
|
2393
|
+
highlightFixtures: flow.highlightFixtures,
|
|
2129
2394
|
expectedFailureReason:
|
|
2130
2395
|
flow.expectedFailureReason && idx === blockInfos.length - 1
|
|
2131
2396
|
? flow.expectedFailureReason
|
|
@@ -2187,6 +2452,7 @@ async function main() {
|
|
|
2187
2452
|
episodeNumber: episodeCounter,
|
|
2188
2453
|
episodeTitle: flow.title || seg.ref,
|
|
2189
2454
|
expectedFailureReason: flow.expectedFailureReason,
|
|
2455
|
+
highlightFixtures: flow.highlightFixtures,
|
|
2190
2456
|
seedMem: () => seedMemForFlow(mem, flow.blocks(), seg.json, seg.ref),
|
|
2191
2457
|
});
|
|
2192
2458
|
} else {
|
|
@@ -2195,6 +2461,7 @@ async function main() {
|
|
|
2195
2461
|
flowMeta.push({
|
|
2196
2462
|
episodeNumber: undefined,
|
|
2197
2463
|
episodeTitle: undefined,
|
|
2464
|
+
highlightFixtures: undefined,
|
|
2198
2465
|
seedMem: () => seedMemForBlock(mem, r, seg.json),
|
|
2199
2466
|
});
|
|
2200
2467
|
}
|
|
@@ -2216,6 +2483,7 @@ async function main() {
|
|
|
2216
2483
|
resetSession: bi.resetSessionBefore === true,
|
|
2217
2484
|
episodeNumber: meta.episodeNumber,
|
|
2218
2485
|
episodeTitle: meta.episodeTitle,
|
|
2486
|
+
highlightFixtures: meta.highlightFixtures,
|
|
2219
2487
|
expectedFailureReason:
|
|
2220
2488
|
meta.expectedFailureReason && remainingInFlow === 1 ? meta.expectedFailureReason : undefined,
|
|
2221
2489
|
seedMem: isFirstOfSegment ? meta.seedMem : undefined,
|
|
@@ -2233,8 +2501,13 @@ async function main() {
|
|
|
2233
2501
|
}
|
|
2234
2502
|
}
|
|
2235
2503
|
const step = process.env.WAYGRAPH_STEP === "1";
|
|
2236
|
-
// Stepping through headless defeats the point
|
|
2237
|
-
|
|
2504
|
+
// Stepping through headless defeats the point for a MANUAL demo - a
|
|
2505
|
+
// human can't watch it. But an explicit WAYGRAPH_HEADED=0 always wins
|
|
2506
|
+
// over that default: --auto-play-video sets STEP=1 (it still needs the
|
|
2507
|
+
// overlay/narration machinery) AND HEADED=0 (unattended + recorded,
|
|
2508
|
+
// nobody has to watch a live window for it to be right).
|
|
2509
|
+
const headed =
|
|
2510
|
+
process.env.WAYGRAPH_HEADED === "0" ? false : process.env.WAYGRAPH_HEADED === "1" || step;
|
|
2238
2511
|
// Step mode defaults to a visible pace (each fill/click actually shows on
|
|
2239
2512
|
// screen instead of snapping in) unless the human overrides it - an
|
|
2240
2513
|
// explicit WAYGRAPH_SLOWMO=0 still means "off".
|
|
@@ -2622,13 +2895,23 @@ async function runTryDemo() {
|
|
|
2622
2895
|
if (!destDir)
|
|
2623
2896
|
return;
|
|
2624
2897
|
process.env.WAYGRAPH_BASE_URL ??= "https://www.saucedemo.com";
|
|
2625
|
-
// Default: headed stepper, manual Next. --video records that session (not headless rush).
|
|
2626
|
-
process.env.WAYGRAPH_HEADED ??= "1";
|
|
2627
2898
|
process.env.WAYGRAPH_STEP ??= "1";
|
|
2628
2899
|
process.env.WAYGRAPH_AUTOPLAY ??= "0";
|
|
2629
|
-
// Explicit --no-step --video = unattended headless .webm only
|
|
2630
|
-
|
|
2900
|
+
// Explicit --no-step --video = unattended headless .webm only (the
|
|
2901
|
+
// documented cloud/CI recipe above) - nobody needs to watch a live
|
|
2902
|
+
// window for Playwright's own recordVideo to capture the right thing.
|
|
2903
|
+
// This must be decided BEFORE the headed default below, or the
|
|
2904
|
+
// unconditional "??=" there always wins and the documented recipe never
|
|
2905
|
+
// actually runs headless.
|
|
2906
|
+
const unattendedVideo = Boolean(process.env.WAYGRAPH_VIDEO) && process.env.WAYGRAPH_STEP === "0";
|
|
2907
|
+
if (unattendedVideo) {
|
|
2631
2908
|
process.env.WAYGRAPH_AUTOPLAY ??= "1";
|
|
2909
|
+
process.env.WAYGRAPH_HEADED ??= "0";
|
|
2910
|
+
}
|
|
2911
|
+
else {
|
|
2912
|
+
// Default: headed stepper, manual Next. --video records that session
|
|
2913
|
+
// too (not a headless rush) as long as --step is still on.
|
|
2914
|
+
process.env.WAYGRAPH_HEADED ??= "1";
|
|
2632
2915
|
}
|
|
2633
2916
|
const videoTo = process.env.WAYGRAPH_VIDEO;
|
|
2634
2917
|
console.log(videoTo
|
|
@@ -2914,6 +3197,10 @@ function parseRunFlags(argv) {
|
|
|
2914
3197
|
else if (a === "--auto-play-video") {
|
|
2915
3198
|
out.autoPlayVideo = true;
|
|
2916
3199
|
}
|
|
3200
|
+
else if (a === "--auto-play-video-head") {
|
|
3201
|
+
out.autoPlayVideo = true;
|
|
3202
|
+
out.nonHeadless = true;
|
|
3203
|
+
}
|
|
2917
3204
|
else if (a === "--fast") {
|
|
2918
3205
|
out.fast = true;
|
|
2919
3206
|
}
|
|
@@ -3022,8 +3309,16 @@ function applyRunFlags(flags, opts) {
|
|
|
3022
3309
|
flags.autoplay = true;
|
|
3023
3310
|
if (flags.video === undefined)
|
|
3024
3311
|
flags.video = "";
|
|
3025
|
-
|
|
3026
|
-
|
|
3312
|
+
// Overlay narration (captions/rings/slides) needs step mode to run, but
|
|
3313
|
+
// --auto-play-video is unattended-and-recorded by design - no human
|
|
3314
|
+
// has to watch a live window for Playwright's own recordVideo to
|
|
3315
|
+
// capture the right thing. Headless by default; --non-headless (or the
|
|
3316
|
+
// --auto-play-video-head alias, parsed above) opts back into a
|
|
3317
|
+
// visible browser for someone who wants to watch it live too.
|
|
3318
|
+
process.env.WAYGRAPH_STEP = "1";
|
|
3319
|
+
if (!flags.nonHeadless && process.env.WAYGRAPH_HEADED === undefined) {
|
|
3320
|
+
process.env.WAYGRAPH_HEADED = "0";
|
|
3321
|
+
}
|
|
3027
3322
|
}
|
|
3028
3323
|
if ((flags.fast || flags.fullStepper) && !opts?.allowDemoUi) {
|
|
3029
3324
|
console.error("waygraph: --fast / --full are demo-only flags");
|
|
@@ -3227,7 +3522,8 @@ Primary (less is more):
|
|
|
3227
3522
|
--auto-next Auto-advance steps (alias: --autoplay)
|
|
3228
3523
|
--fast Faster transitions + shorter auto-next gates
|
|
3229
3524
|
--full Classic wrap-all block chips (default: carousel)
|
|
3230
|
-
--auto-play-video
|
|
3525
|
+
--auto-play-video Unattended + recorded: --auto-next + --video (+ step); headless by default
|
|
3526
|
+
--auto-play-video-head Same, but keep the browser visible (--non-headless)
|
|
3231
3527
|
--video-viewport WxH Recording size (default demo: 1920x1080)
|
|
3232
3528
|
--title / --base-url
|
|
3233
3529
|
waygraph run [--blocks <flow|file|spec>] Execute (no overlay unless --step)
|
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
|
});
|