waygraph 0.11.0 → 0.11.2

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
@@ -1343,12 +1343,18 @@ function extractVerifyHighlights(block, resultTag) {
1343
1343
  * __wgNext no-ops once already consumed - but looked live when it wasn't).
1344
1344
  * Disables the button and mem-key textareas, and swaps whichever gate
1345
1345
  * text was showing (manual button or "Auto-advancing...") to "Running...".
1346
- * When autoCollapsePanel is set (QA video / autoplay), tuck the stepper
1347
- * away so ring captions + the app fill the frame - Dan: stepper + auto panels.
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.)
1348
1354
  */
1349
- async function markStepRunning(page, opts) {
1355
+ async function markStepRunning(page, _opts) {
1350
1356
  await page
1351
- .evaluate(({ autoCollapsePanel }) => {
1357
+ .evaluate(() => {
1352
1358
  const runBtn = document.getElementById("wg-run");
1353
1359
  if (runBtn) {
1354
1360
  runBtn.disabled = true;
@@ -1359,18 +1365,16 @@ async function markStepRunning(page, opts) {
1359
1365
  document.querySelectorAll("#wg-panel textarea[data-key]").forEach((ta) => {
1360
1366
  ta.disabled = true;
1361
1367
  });
1362
- if (autoCollapsePanel) {
1363
- const panel = document.getElementById("wg-panel");
1364
- if (panel) {
1365
- panel.classList.add("wg-collapsed");
1366
- const toggle = panel.querySelector("[data-wg-toggle]");
1367
- if (toggle) toggle.textContent = "Show";
1368
- const titleEl = panel.querySelector(".wg-chrome-title");
1369
- const label = panel.dataset.wgStepLabel;
1370
- if (titleEl && label) titleEl.textContent = label;
1371
- }
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;
1372
1376
  }
1373
- }, { autoCollapsePanel: !!(opts && opts.autoCollapsePanel) })
1377
+ })
1374
1378
  .catch(() => {});
1375
1379
  }
1376
1380
 
@@ -1411,10 +1415,23 @@ async function captionForLocator(page, locator, stubs, fallback) {
1411
1415
  /**
1412
1416
  * Multi-step yap captions - not block lifecycle. Each slide waits for Next
1413
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.
1414
1423
  */
1415
- async function presentSlides(page, slides, gate, opts) {
1424
+ async function presentSlides(page, slides, _gate, opts) {
1416
1425
  const title = (opts && opts.title) || "waygraph demo";
1417
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;
1418
1435
  for (let i = 0; i < slides.length; i++) {
1419
1436
  const s = slides[i];
1420
1437
  const caption = formatHighlightCaption(s);
@@ -1430,113 +1447,148 @@ async function presentSlides(page, slides, gate, opts) {
1430
1447
  } else {
1431
1448
  await hideRing(page);
1432
1449
  }
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(() => {
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, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
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;
1526
1520
  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";
1521
+ autoNow = localStorage.getItem("wg-autoplay") === "1";
1534
1522
  } catch {
1535
1523
  /* ignore */
1536
1524
  }
1537
- })
1538
- .catch(() => {});
1539
- await gate();
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
+ }
1540
1592
  }
1541
1593
  await hideRing(page);
1542
1594
  }
@@ -1901,14 +1953,34 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1901
1953
  // already is.
1902
1954
  page.setViewportSize = async () => {};
1903
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;
1904
1965
  await page.exposeFunction("__wgNext", (edits) => {
1905
1966
  if (resolveNext) {
1906
1967
  const r = resolveNext;
1907
1968
  resolveNext = null;
1908
1969
  r(edits);
1970
+ } else {
1971
+ pendingNext = edits;
1909
1972
  }
1910
1973
  });
1911
- const waitForNext = () => new Promise((res) => (resolveNext = res));
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
+ });
1912
1984
  // WAYGRAPH_AUTOPLAY=1 only sets the STARTING checkbox state now - the
1913
1985
  // panel's own "Auto-advance" checkbox can flip it live, mid-run, and a
1914
1986
  // manual click always wins over an in-flight autoplay wait regardless of
@@ -2152,6 +2224,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2152
2224
  title,
2153
2225
  blockName: r.block.name,
2154
2226
  fast: pacing.fast,
2227
+ episodeNumber: r.episodeNumber,
2228
+ episodeTitle: r.episodeTitle,
2229
+ autoplayMs,
2155
2230
  });
2156
2231
  }
2157
2232
  let highlights;
@@ -2426,8 +2501,13 @@ async function main() {
2426
2501
  }
2427
2502
  }
2428
2503
  const step = process.env.WAYGRAPH_STEP === "1";
2429
- // Stepping through headless defeats the point - a human can't watch it.
2430
- const headed = process.env.WAYGRAPH_HEADED === "1" || step;
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;
2431
2511
  // Step mode defaults to a visible pace (each fill/click actually shows on
2432
2512
  // screen instead of snapping in) unless the human overrides it - an
2433
2513
  // explicit WAYGRAPH_SLOWMO=0 still means "off".
@@ -2815,13 +2895,23 @@ async function runTryDemo() {
2815
2895
  if (!destDir)
2816
2896
  return;
2817
2897
  process.env.WAYGRAPH_BASE_URL ??= "https://www.saucedemo.com";
2818
- // Default: headed stepper, manual Next. --video records that session (not headless rush).
2819
- process.env.WAYGRAPH_HEADED ??= "1";
2820
2898
  process.env.WAYGRAPH_STEP ??= "1";
2821
2899
  process.env.WAYGRAPH_AUTOPLAY ??= "0";
2822
- // Explicit --no-step --video = unattended headless .webm only.
2823
- if (process.env.WAYGRAPH_VIDEO && process.env.WAYGRAPH_STEP === "0") {
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) {
2824
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";
2825
2915
  }
2826
2916
  const videoTo = process.env.WAYGRAPH_VIDEO;
2827
2917
  console.log(videoTo
@@ -3107,6 +3197,10 @@ function parseRunFlags(argv) {
3107
3197
  else if (a === "--auto-play-video") {
3108
3198
  out.autoPlayVideo = true;
3109
3199
  }
3200
+ else if (a === "--auto-play-video-head") {
3201
+ out.autoPlayVideo = true;
3202
+ out.nonHeadless = true;
3203
+ }
3110
3204
  else if (a === "--fast") {
3111
3205
  out.fast = true;
3112
3206
  }
@@ -3215,8 +3309,16 @@ function applyRunFlags(flags, opts) {
3215
3309
  flags.autoplay = true;
3216
3310
  if (flags.video === undefined)
3217
3311
  flags.video = "";
3218
- if (flags.step === undefined)
3219
- flags.step = true;
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
+ }
3220
3322
  }
3221
3323
  if ((flags.fast || flags.fullStepper) && !opts?.allowDemoUi) {
3222
3324
  console.error("waygraph: --fast / --full are demo-only flags");
@@ -3420,7 +3522,8 @@ Primary (less is more):
3420
3522
  --auto-next Auto-advance steps (alias: --autoplay)
3421
3523
  --fast Faster transitions + shorter auto-next gates
3422
3524
  --full Classic wrap-all block chips (default: carousel)
3423
- --auto-play-video QA: --auto-next + --video (+ step)
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)
3424
3527
  --video-viewport WxH Recording size (default demo: 1920x1080)
3425
3528
  --title / --base-url
3426
3529
  waygraph run [--blocks <flow|file|spec>] Execute (no overlay unless --step)
package/dist/trait.d.ts CHANGED
@@ -33,7 +33,12 @@ export interface Trait {
33
33
  export declare function urlMatches(pattern: URLPatternInit): Trait;
34
34
  /** Passes when `selector`'s text content is exactly `expected`. */
35
35
  export declare function textEquals(selector: string, expected: string): Trait;
36
- /** Passes once `selector` is visible on the page, waiting for it to appear. */
36
+ /**
37
+ * Passes once at least one element matching `selector` is visible on the page,
38
+ * waiting for it to appear. Uses `.first()` so list selectors (e.g. one row per
39
+ * message/document) don't trip Playwright strict mode - the check means
40
+ * "something matching is visible", not "the selector is unambiguous".
41
+ */
37
42
  export declare function visible(selector: string): Trait;
38
43
  /**
39
44
  * Discoverable entry point for the built-in Trait factories - type `Trait.` in
package/dist/trait.js CHANGED
@@ -36,12 +36,17 @@ export function textEquals(selector, expected) {
36
36
  },
37
37
  };
38
38
  }
39
- /** Passes once `selector` is visible on the page, waiting for it to appear. */
39
+ /**
40
+ * Passes once at least one element matching `selector` is visible on the page,
41
+ * waiting for it to appear. Uses `.first()` so list selectors (e.g. one row per
42
+ * message/document) don't trip Playwright strict mode - the check means
43
+ * "something matching is visible", not "the selector is unambiguous".
44
+ */
40
45
  export function visible(selector) {
41
46
  return {
42
47
  name: `visible(${selector})`,
43
48
  async check(page) {
44
- await page.locator(selector).waitFor({ state: "visible" });
49
+ await page.locator(selector).first().waitFor({ state: "visible" });
45
50
  return true;
46
51
  },
47
52
  };
@@ -28,21 +28,23 @@ export const FinishOrderBlock = defineMethodBlock<CheckoutOverviewPage, OrderCom
28
28
  detail: "Thank you for your order!",
29
29
  },
30
30
  },
31
+ // Shown AFTER act (order already placed) - selectors must match the
32
+ // completion page, not #finish on the overview.
31
33
  slides: [
32
34
  {
33
35
  caption: "Checkout is a short pipeline",
34
- detail: "Cart → info → overview → done. Watch the last click place the order.",
36
+ detail: "Cart -> info -> overview -> done. You just placed the order.",
35
37
  tag: "YAP",
36
38
  },
37
39
  {
38
- caption: "Finish submits the overview",
39
- detail: "This click is the only mutating step on this screen.",
40
+ caption: "Completion page is the durable proof",
41
+ detail: "The thank-you header is what verify asserts on.",
40
42
  tag: "YAP",
41
- selector: "#finish",
43
+ selector: ".complete-header",
42
44
  },
43
45
  {
44
- caption: "Completion banner is the proof",
45
- detail: "The next ring calls out the thank-you header.",
46
+ caption: "Episode continues after this yap",
47
+ detail: "Next step rings the same banner, then you move on.",
46
48
  tag: "YAP",
47
49
  selector: ".complete-header",
48
50
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -28,21 +28,23 @@ export const FinishOrderBlock = defineMethodBlock<CheckoutOverviewPage, OrderCom
28
28
  detail: "Thank you for your order!",
29
29
  },
30
30
  },
31
+ // Shown AFTER act (order already placed) - selectors must match the
32
+ // completion page, not #finish on the overview.
31
33
  slides: [
32
34
  {
33
35
  caption: "Checkout is a short pipeline",
34
- detail: "Cart → info → overview → done. Watch the last click place the order.",
36
+ detail: "Cart -> info -> overview -> done. You just placed the order.",
35
37
  tag: "YAP",
36
38
  },
37
39
  {
38
- caption: "Finish submits the overview",
39
- detail: "This click is the only mutating step on this screen.",
40
+ caption: "Completion page is the durable proof",
41
+ detail: "The thank-you header is what verify asserts on.",
40
42
  tag: "YAP",
41
- selector: "#finish",
43
+ selector: ".complete-header",
42
44
  },
43
45
  {
44
- caption: "Completion banner is the proof",
45
- detail: "The next ring calls out the thank-you header.",
46
+ caption: "Episode continues after this yap",
47
+ detail: "Next step rings the same banner, then you move on.",
46
48
  tag: "YAP",
47
49
  selector: ".complete-header",
48
50
  },