waygraph 0.2.1 → 0.3.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 +211 -57
- package/dist/engine.d.ts +1 -0
- package/dist/engine.js +2 -0
- package/dist/types.d.ts +28 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -359,6 +359,7 @@ const RING_CSS =
|
|
|
359
359
|
"#wg-panel .wg-auto{margin-top:10px;font:600 13px system-ui,sans-serif;color:#c9a6ff;}" +
|
|
360
360
|
"#wg-panel h3{margin:0 0 8px;font-size:13px;color:#c9a6ff;font-weight:700;" +
|
|
361
361
|
"letter-spacing:.02em;text-transform:uppercase;}" +
|
|
362
|
+
"#wg-panel .wg-narration{margin:0 0 12px;font:italic 14px/1.4 system-ui,sans-serif;color:#f0e8ff;}" +
|
|
362
363
|
"#wg-progress{height:4px;background:#2a1650;border-radius:2px;margin:0 0 12px;overflow:hidden;}" +
|
|
363
364
|
"#wg-progress-bar{height:100%;background:#7C3AED;border-radius:2px;transition:width .3s ease;}" +
|
|
364
365
|
"#wg-modules{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px;}" +
|
|
@@ -366,6 +367,15 @@ const RING_CSS =
|
|
|
366
367
|
"#wg-modules .wg-mod-done{background:#2a1650;color:#9a7ad1;}" +
|
|
367
368
|
"#wg-modules .wg-mod-current{background:#7C3AED;color:#fff;}" +
|
|
368
369
|
"#wg-modules .wg-mod-upcoming{background:transparent;color:#5a4a80;border:1px solid #3a2a60;}" +
|
|
370
|
+
"#wg-banner{position:fixed;z-index:2147483647;top:14px;max-width:320px;" +
|
|
371
|
+
"background:rgba(20,10,40,.94);color:#fff;border-radius:12px;padding:10px 16px;" +
|
|
372
|
+
"font:14px/1.4 system-ui,sans-serif;box-shadow:0 8px 20px rgba(0,0,0,.3);" +
|
|
373
|
+
"border:1px solid rgba(124,58,237,.4);cursor:pointer;user-select:none;}" +
|
|
374
|
+
"#wg-banner[data-pos=left]{left:14px;right:auto;transform:none;}" +
|
|
375
|
+
"#wg-banner[data-pos=center]{left:50%;right:auto;transform:translateX(-50%);}" +
|
|
376
|
+
"#wg-banner[data-pos=right]{right:14px;left:auto;transform:none;}" +
|
|
377
|
+
"#wg-banner .wg-banner-tag{display:block;font-size:10px;font-weight:700;color:#c9a6ff;" +
|
|
378
|
+
"letter-spacing:.05em;text-transform:uppercase;margin-bottom:2px;}" +
|
|
369
379
|
"#wg-panel .wg-key{margin:8px 0;}" +
|
|
370
380
|
"#wg-panel label{display:block;font-size:12px;color:#d8c8ff;margin-bottom:3px;}" +
|
|
371
381
|
"#wg-panel textarea{width:100%;box-sizing:border-box;background:#0f0620;color:#fff;" +
|
|
@@ -381,21 +391,88 @@ const RING_CSS =
|
|
|
381
391
|
"background:transparent;border:1px solid #4b2a80;color:#9a7ad1;border-radius:6px;}" +
|
|
382
392
|
"#wg-panel .wg-toggle button.wg-active{background:#4b2a80;color:#fff;}";
|
|
383
393
|
|
|
384
|
-
|
|
394
|
+
// Purple dot favicon (matches the overlay's own theme color) - the tab-bar
|
|
395
|
+
// signal that "this Chromium window is a waygraph run," even at a glance
|
|
396
|
+
// across a taskbar/alt-tab, not just something visible inside the page.
|
|
397
|
+
const WAYGRAPH_FAVICON =
|
|
398
|
+
"data:image/svg+xml," +
|
|
399
|
+
encodeURIComponent(
|
|
400
|
+
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>" +
|
|
401
|
+
"<circle cx='16' cy='16' r='14' fill='#7C3AED'/></svg>",
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
async function installOverlay(page, title) {
|
|
385
405
|
await page.addStyleTag({ content: RING_CSS }).catch(() => {});
|
|
406
|
+
// Default top-left; override with WAYGRAPH_TITLE_POS=left|center|right.
|
|
407
|
+
// Click cycles left -> center -> right (persisted in localStorage so a
|
|
408
|
+
// navigation that rebuilds the banner keeps the human's last pick).
|
|
409
|
+
const envPos = (process.env.WAYGRAPH_TITLE_POS || "left").toLowerCase();
|
|
410
|
+
const bannerPos = envPos === "center" || envPos === "right" ? envPos : "left";
|
|
386
411
|
await page
|
|
387
|
-
.evaluate(
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
412
|
+
.evaluate(
|
|
413
|
+
({ title, favicon, bannerPos }) => {
|
|
414
|
+
if (!document.getElementById("wg-ring")) {
|
|
415
|
+
const ring = document.createElement("div");
|
|
416
|
+
ring.id = "wg-ring";
|
|
417
|
+
document.documentElement.appendChild(ring);
|
|
418
|
+
}
|
|
419
|
+
const POSITIONS = ["left", "center", "right"];
|
|
420
|
+
const applyPos = (el, pos) => {
|
|
421
|
+
el.dataset.pos = pos;
|
|
422
|
+
try {
|
|
423
|
+
localStorage.setItem("wg-banner-pos", pos);
|
|
424
|
+
} catch {
|
|
425
|
+
/* private mode / blocked storage - position still applies this page */
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
if (title && !document.getElementById("wg-banner")) {
|
|
429
|
+
const banner = document.createElement("div");
|
|
430
|
+
banner.id = "wg-banner";
|
|
431
|
+
let saved = null;
|
|
432
|
+
try {
|
|
433
|
+
saved = localStorage.getItem("wg-banner-pos");
|
|
434
|
+
} catch {
|
|
435
|
+
/* ignore */
|
|
436
|
+
}
|
|
437
|
+
const startPos =
|
|
438
|
+
saved && POSITIONS.includes(saved) ? saved : bannerPos;
|
|
439
|
+
applyPos(banner, startPos);
|
|
440
|
+
banner.title = "Click to move: top left / center / right";
|
|
441
|
+
banner.addEventListener("click", (e) => {
|
|
442
|
+
e.stopPropagation();
|
|
443
|
+
const i = POSITIONS.indexOf(banner.dataset.pos || "left");
|
|
444
|
+
applyPos(banner, POSITIONS[(i + 1) % POSITIONS.length]);
|
|
445
|
+
});
|
|
446
|
+
const tag = document.createElement("span");
|
|
447
|
+
tag.className = "wg-banner-tag";
|
|
448
|
+
tag.textContent = "waygraph demo";
|
|
449
|
+
const text = document.createElement("span");
|
|
450
|
+
text.textContent = title;
|
|
451
|
+
banner.appendChild(tag);
|
|
452
|
+
banner.appendChild(text);
|
|
453
|
+
document.documentElement.appendChild(banner);
|
|
454
|
+
}
|
|
455
|
+
// Tab title/favicon: a real navigation resets document.title and any
|
|
456
|
+
// <link rel="icon"> the new document brings, so re-check (not
|
|
457
|
+
// re-append) on every call instead of a one-time flag.
|
|
458
|
+
if (!document.title.startsWith("[waygraph] ")) {
|
|
459
|
+
document.title = "[waygraph] " + document.title;
|
|
460
|
+
}
|
|
461
|
+
let iconLink = document.querySelector("link[rel~='icon']");
|
|
462
|
+
if (!iconLink) {
|
|
463
|
+
iconLink = document.createElement("link");
|
|
464
|
+
iconLink.rel = "icon";
|
|
465
|
+
document.head.appendChild(iconLink);
|
|
466
|
+
}
|
|
467
|
+
if (iconLink.href !== favicon) iconLink.href = favicon;
|
|
468
|
+
},
|
|
469
|
+
{ title, favicon: WAYGRAPH_FAVICON, bannerPos },
|
|
470
|
+
)
|
|
394
471
|
.catch(() => {});
|
|
395
472
|
}
|
|
396
473
|
|
|
397
474
|
async function renderBeforeStep(page, info) {
|
|
398
|
-
await installOverlay(page);
|
|
475
|
+
await installOverlay(page, info.title);
|
|
399
476
|
// A ring left highlighting the PREVIOUS step's element (and its live
|
|
400
477
|
// resize/scroll tracker) shouldn't linger once a new step's own panel is
|
|
401
478
|
// up - only relevant when a Block's act() doesn't navigate away, since a
|
|
@@ -418,16 +495,23 @@ async function renderBeforeStep(page, info) {
|
|
|
418
495
|
const panel = document.createElement("div");
|
|
419
496
|
panel.id = "wg-panel";
|
|
420
497
|
const pct = Math.round((info.index / info.total) * 100);
|
|
498
|
+
const esc = (s) => String(s).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
421
499
|
const modulesHtml = info.allNames
|
|
422
500
|
.map((name, idx) => {
|
|
423
501
|
const cls = idx < info.index ? "wg-mod-done" : idx === info.index ? "wg-mod-current" : "wg-mod-upcoming";
|
|
424
|
-
|
|
502
|
+
const desc = info.allDescriptions && info.allDescriptions[idx];
|
|
503
|
+
const titleAttr = desc ? " title=\\"" + esc(desc) + "\\"" : "";
|
|
504
|
+
return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
|
|
425
505
|
})
|
|
426
506
|
.join("");
|
|
507
|
+
const narrationHtml = info.description
|
|
508
|
+
? "<div class=\\"wg-narration\\">" + esc(info.description) + "</div>"
|
|
509
|
+
: "";
|
|
427
510
|
let html =
|
|
428
511
|
"<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
|
|
429
512
|
"<div id=\\"wg-modules\\">" + modulesHtml + "</div>" +
|
|
430
|
-
"<h3>Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + "</h3>"
|
|
513
|
+
"<h3>Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + "</h3>" +
|
|
514
|
+
narrationHtml;
|
|
431
515
|
if (info.keys.length === 0) {
|
|
432
516
|
html += "<div class=\\"wg-key\\">(no MemKeys required)</div>";
|
|
433
517
|
}
|
|
@@ -458,46 +542,66 @@ async function renderBeforeStep(page, info) {
|
|
|
458
542
|
}
|
|
459
543
|
|
|
460
544
|
async function renderAfterStep(page, info) {
|
|
461
|
-
await installOverlay(page);
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
// window used to leave the ring frozen at its stale old position.
|
|
465
|
-
// Only the first recovered selector is drawn (one ring); best-effort - a
|
|
466
|
-
// selector that never resolves just leaves the ring hidden.
|
|
545
|
+
await installOverlay(page, info.title);
|
|
546
|
+
// Clear any tracker from a previous highlight before cycling through this
|
|
547
|
+
// step's own.
|
|
467
548
|
await page
|
|
468
|
-
.evaluate((
|
|
549
|
+
.evaluate(() => {
|
|
469
550
|
if (window.__wgRingTrack) {
|
|
470
551
|
window.removeEventListener("resize", window.__wgRingTrack);
|
|
471
552
|
window.removeEventListener("scroll", window.__wgRingTrack, true);
|
|
472
553
|
window.__wgRingTrack = null;
|
|
473
554
|
}
|
|
474
|
-
|
|
475
|
-
if (!ring) return;
|
|
476
|
-
const h = highlights[0];
|
|
477
|
-
if (!h) {
|
|
478
|
-
ring.style.opacity = "0";
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
const reposition = () => {
|
|
482
|
-
const el = document.querySelector(h.selector);
|
|
483
|
-
if (!el) {
|
|
484
|
-
ring.style.opacity = "0";
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
const box = el.getBoundingClientRect();
|
|
488
|
-
ring.style.left = box.x - 6 + "px";
|
|
489
|
-
ring.style.top = box.y - 6 + "px";
|
|
490
|
-
ring.style.width = box.width + 12 + "px";
|
|
491
|
-
ring.style.height = box.height + 12 + "px";
|
|
492
|
-
ring.setAttribute("data-label", h.label);
|
|
493
|
-
ring.style.opacity = "1";
|
|
494
|
-
};
|
|
495
|
-
reposition();
|
|
496
|
-
window.__wgRingTrack = reposition;
|
|
497
|
-
window.addEventListener("resize", reposition);
|
|
498
|
-
window.addEventListener("scroll", reposition, true);
|
|
499
|
-
}, info.highlights)
|
|
555
|
+
})
|
|
500
556
|
.catch(() => {});
|
|
557
|
+
const highlights = info.highlights || [];
|
|
558
|
+
// Cycle through EVERY declared/recovered highlight in order, each shown
|
|
559
|
+
// long enough to actually register - "it highlights something [...] then
|
|
560
|
+
// it highlights something [else] and next," not just the first one.
|
|
561
|
+
for (let i = 0; i < highlights.length; i++) {
|
|
562
|
+
const h = highlights[i];
|
|
563
|
+
try {
|
|
564
|
+
const box = await page.locator(h.selector).first().boundingBox();
|
|
565
|
+
if (box) {
|
|
566
|
+
await showRing(page, box, h.label);
|
|
567
|
+
await new Promise((res) => setTimeout(res, i === highlights.length - 1 ? 200 : 900));
|
|
568
|
+
}
|
|
569
|
+
} catch {
|
|
570
|
+
// best-effort - a selector that doesn't resolve just gets skipped
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (highlights.length > 0) {
|
|
574
|
+
// The LAST highlight is the one that stays lit while the human reads
|
|
575
|
+
// the after-step panel - give THAT one live resize/scroll tracking,
|
|
576
|
+
// same as before.
|
|
577
|
+
const last = highlights[highlights.length - 1];
|
|
578
|
+
await page
|
|
579
|
+
.evaluate((h) => {
|
|
580
|
+
const ring = document.getElementById("wg-ring");
|
|
581
|
+
if (!ring) return;
|
|
582
|
+
const reposition = () => {
|
|
583
|
+
const el = document.querySelector(h.selector);
|
|
584
|
+
if (!el) {
|
|
585
|
+
ring.style.opacity = "0";
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const box = el.getBoundingClientRect();
|
|
589
|
+
ring.style.left = box.x - 6 + "px";
|
|
590
|
+
ring.style.top = box.y - 6 + "px";
|
|
591
|
+
ring.style.width = box.width + 12 + "px";
|
|
592
|
+
ring.style.height = box.height + 12 + "px";
|
|
593
|
+
ring.setAttribute("data-label", h.label);
|
|
594
|
+
ring.style.opacity = "1";
|
|
595
|
+
};
|
|
596
|
+
reposition();
|
|
597
|
+
window.__wgRingTrack = reposition;
|
|
598
|
+
window.addEventListener("resize", reposition);
|
|
599
|
+
window.addEventListener("scroll", reposition, true);
|
|
600
|
+
}, last)
|
|
601
|
+
.catch(() => {});
|
|
602
|
+
} else {
|
|
603
|
+
await hideRing(page);
|
|
604
|
+
}
|
|
501
605
|
await page
|
|
502
606
|
.evaluate((info) => {
|
|
503
607
|
const old = document.getElementById("wg-panel");
|
|
@@ -509,10 +613,13 @@ async function renderAfterStep(page, info) {
|
|
|
509
613
|
? "End of chain - " + info.total + " / " + info.total + " blocks covered - " + info.blockName + " done"
|
|
510
614
|
: "Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + " done";
|
|
511
615
|
const buttonLabel = info.isLast ? "Finish" : "Next \\u25B6";
|
|
616
|
+
const escA = (s) => String(s).replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
512
617
|
const modulesHtml = info.allNames
|
|
513
618
|
.map((name, idx) => {
|
|
514
619
|
const cls = idx <= info.index ? "wg-mod-done" : "wg-mod-upcoming";
|
|
515
|
-
|
|
620
|
+
const desc = info.allDescriptions && info.allDescriptions[idx];
|
|
621
|
+
const titleAttr = desc ? " title=\\"" + escA(desc) + "\\"" : "";
|
|
622
|
+
return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
|
|
516
623
|
})
|
|
517
624
|
.join("");
|
|
518
625
|
// QA-friendly by default ("LoginSuccess" -> "Login Success") - raw
|
|
@@ -558,14 +665,37 @@ async function renderAfterStep(page, info) {
|
|
|
558
665
|
.catch(() => {});
|
|
559
666
|
}
|
|
560
667
|
|
|
668
|
+
/**
|
|
669
|
+
* A Block's own explicit instruction.highlights, if it declared any -
|
|
670
|
+
* arbitrary, author-controlled highlight points, independent of verify
|
|
671
|
+
* ("remember this ID" isn't a pass/fail check). Takes priority over
|
|
672
|
+
* anything recovered from verify traits below.
|
|
673
|
+
*/
|
|
674
|
+
function resolveDeclaredHighlights(block, resultTag) {
|
|
675
|
+
let highlights = block.instruction && block.instruction.highlights;
|
|
676
|
+
if (typeof highlights === "function") {
|
|
677
|
+
try {
|
|
678
|
+
highlights = highlights({ __state: resultTag });
|
|
679
|
+
} catch {
|
|
680
|
+
highlights = [];
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (!Array.isArray(highlights)) return [];
|
|
684
|
+
return highlights.filter((h) => h && typeof h.selector === "string" && typeof h.label === "string");
|
|
685
|
+
}
|
|
686
|
+
|
|
561
687
|
/**
|
|
562
688
|
* Recovers a DOM selector from a built-in Trait's own .name string -
|
|
563
689
|
* visible(sel) / text-equals(sel, "...") - since Trait.check itself is
|
|
564
690
|
* an opaque closure with no selector field of its own. Best-effort only: a
|
|
565
691
|
* hand-written bespoke Trait, or url-matches(...) (no DOM target), yields
|
|
566
692
|
* nothing to highlight, which is fine - the panel still shows the result.
|
|
693
|
+
* Only used as a FALLBACK when the Block declared no explicit
|
|
694
|
+
* instruction.highlights of its own - see resolveDeclaredHighlights.
|
|
567
695
|
*/
|
|
568
696
|
function extractVerifyHighlights(block, resultTag) {
|
|
697
|
+
const declared = resolveDeclaredHighlights(block, resultTag);
|
|
698
|
+
if (declared.length > 0) return declared;
|
|
569
699
|
let verify = block.instruction && block.instruction.verify;
|
|
570
700
|
if (typeof verify === "function") {
|
|
571
701
|
try {
|
|
@@ -747,14 +877,14 @@ function instrumentInteractionHighlighting(page, mem, slowMo) {
|
|
|
747
877
|
// helpers some Blocks use for their own pacing are NOT Playwright calls
|
|
748
878
|
// at all and can't be touched this way - a real, disclosed limit, not
|
|
749
879
|
// silently ignored.
|
|
750
|
-
//
|
|
751
|
-
// flakiness around slow-but-legitimate hydration
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
//
|
|
757
|
-
const WAIT_CAP_MS =
|
|
880
|
+
// 3000ms was the first, deliberately conservative pass (this codebase
|
|
881
|
+
// has documented real flakiness around slow-but-legitimate hydration
|
|
882
|
+
// waits, so a too-aggressive cap risks trading "one annoying dead wait"
|
|
883
|
+
// for "logins that sometimes fail outright"). Verified empirically
|
|
884
|
+
// (3 consecutive real login runs, no failures) that those legitimate
|
|
885
|
+
// waits actually resolve in well under a second in practice - the cap
|
|
886
|
+
// was never close to touching them - so it's safe to tighten further.
|
|
887
|
+
const WAIT_CAP_MS = 1500;
|
|
758
888
|
if (!proto.__wgWaitForPatched) {
|
|
759
889
|
proto.__wgWaitForPatched = true;
|
|
760
890
|
const originalWaitFor = proto.waitFor;
|
|
@@ -773,7 +903,7 @@ function instrumentInteractionHighlighting(page, mem, slowMo) {
|
|
|
773
903
|
}
|
|
774
904
|
}
|
|
775
905
|
|
|
776
|
-
async function runStepMode(engine, start, end, context, page, mem, resolved, slowMo) {
|
|
906
|
+
async function runStepMode(engine, start, end, context, page, mem, resolved, slowMo, title) {
|
|
777
907
|
instrumentInteractionHighlighting(page, mem, slowMo);
|
|
778
908
|
// Some real Blocks (e.g. zsign-all's login.block.ts) call
|
|
779
909
|
// page.setViewportSize({ width: 1280, height: 720 }) inside their own
|
|
@@ -803,6 +933,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
803
933
|
const autoplayMs = process.env.WAYGRAPH_AUTOPLAY_MS ? Number(process.env.WAYGRAPH_AUTOPLAY_MS) : 1800;
|
|
804
934
|
const gate = () => (autoplay ? new Promise((res) => setTimeout(() => res({}), autoplayMs)) : waitForNext());
|
|
805
935
|
const allNames = resolved.map((r) => r.block.name);
|
|
936
|
+
const allDescriptions = resolved.map((r) => r.block.description || "");
|
|
806
937
|
|
|
807
938
|
let result;
|
|
808
939
|
for (let i = 0; i < resolved.length; i++) {
|
|
@@ -817,7 +948,17 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
817
948
|
}
|
|
818
949
|
return { name: k.name, value };
|
|
819
950
|
});
|
|
820
|
-
await renderBeforeStep(page, {
|
|
951
|
+
await renderBeforeStep(page, {
|
|
952
|
+
index: i,
|
|
953
|
+
total: resolved.length,
|
|
954
|
+
blockName: r.block.name,
|
|
955
|
+
description: r.block.description || "",
|
|
956
|
+
keys,
|
|
957
|
+
allNames,
|
|
958
|
+
allDescriptions,
|
|
959
|
+
autoplay,
|
|
960
|
+
title,
|
|
961
|
+
});
|
|
821
962
|
const edits = await gate();
|
|
822
963
|
for (const k of requires) {
|
|
823
964
|
if (edits[k.name] !== undefined) {
|
|
@@ -845,7 +986,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
845
986
|
highlights,
|
|
846
987
|
isLast: i === resolved.length - 1,
|
|
847
988
|
allNames,
|
|
989
|
+
allDescriptions,
|
|
848
990
|
autoplay,
|
|
991
|
+
title,
|
|
849
992
|
});
|
|
850
993
|
await gate();
|
|
851
994
|
}
|
|
@@ -901,6 +1044,7 @@ async function main() {
|
|
|
901
1044
|
? 350
|
|
902
1045
|
: undefined;
|
|
903
1046
|
const baseURL = process.env.WAYGRAPH_BASE_URL;
|
|
1047
|
+
const title = process.env.WAYGRAPH_TITLE;
|
|
904
1048
|
const engine = new Engine({ headless: !headed, slowMo });
|
|
905
1049
|
let result;
|
|
906
1050
|
if (step || baseURL) {
|
|
@@ -926,7 +1070,7 @@ async function main() {
|
|
|
926
1070
|
if (baseURL) {
|
|
927
1071
|
await page.goto(baseURL).catch(() => {});
|
|
928
1072
|
}
|
|
929
|
-
result = await runStepMode(engine, start, end, context, page, mem, resolved, slowMo);
|
|
1073
|
+
result = await runStepMode(engine, start, end, context, page, mem, resolved, slowMo, title);
|
|
930
1074
|
} else {
|
|
931
1075
|
const blocks = resolved.map((r) => r.block);
|
|
932
1076
|
const chained = blocks.reduce((a, b) => connect(a, b));
|
|
@@ -1061,8 +1205,18 @@ Usage:
|
|
|
1061
1205
|
just confirmed, then "Next" before moving on.
|
|
1062
1206
|
Implies headed - stepping through headless defeats
|
|
1063
1207
|
the point.
|
|
1208
|
+
WAYGRAPH_AUTOPLAY=1 step mode only - hands-off, advances on a
|
|
1209
|
+
timer instead of waiting for clicks
|
|
1210
|
+
WAYGRAPH_AUTOPLAY_MS=ms delay between auto-advances (default 1800)
|
|
1211
|
+
WAYGRAPH_TITLE="..." step mode only - a persistent top banner
|
|
1212
|
+
naming what this whole run is about (default
|
|
1213
|
+
top-left; click the banner to cycle left /
|
|
1214
|
+
center / right)
|
|
1215
|
+
WAYGRAPH_TITLE_POS=left|center|right
|
|
1216
|
+
initial banner position (default left); click
|
|
1217
|
+
still cycles and remembers via localStorage
|
|
1064
1218
|
|
|
1065
|
-
"project" defaults to the current directory.
|
|
1219
|
+
"project" defaults to the current directory.
|
|
1066
1220
|
`);
|
|
1067
1221
|
process.exit(0);
|
|
1068
1222
|
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -299,6 +299,7 @@ export declare function branch<In extends Checkpoint<string>, Out extends Checkp
|
|
|
299
299
|
*/
|
|
300
300
|
export declare function defineBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(base: {
|
|
301
301
|
name: string;
|
|
302
|
+
description?: Block<In, Out>["description"];
|
|
302
303
|
instruction: Instruction<In, Out, any>;
|
|
303
304
|
next?: Block<In, Out>["next"];
|
|
304
305
|
requires?: Block<In, Out>["requires"];
|
package/dist/engine.js
CHANGED
|
@@ -284,6 +284,7 @@ function stripMethods(block) {
|
|
|
284
284
|
return {
|
|
285
285
|
name: block.name,
|
|
286
286
|
instruction: block.instruction,
|
|
287
|
+
...(block.description ? { description: block.description } : {}),
|
|
287
288
|
...(block.next ? { next: block.next } : {}),
|
|
288
289
|
...(block.requires ? { requires: block.requires } : {}),
|
|
289
290
|
...(block.routes ? { routes: block.routes } : {}),
|
|
@@ -302,6 +303,7 @@ export function defineBlock(base) {
|
|
|
302
303
|
const plain = {
|
|
303
304
|
name: base.name,
|
|
304
305
|
instruction: base.instruction,
|
|
306
|
+
...(base.description ? { description: base.description } : {}),
|
|
305
307
|
...(base.next ? { next: base.next } : {}),
|
|
306
308
|
...(base.requires ? { requires: base.requires } : {}),
|
|
307
309
|
...(base.routes ? { routes: base.routes } : {}),
|
package/dist/types.d.ts
CHANGED
|
@@ -51,6 +51,24 @@ export interface Instruction<In extends Checkpoint<string>, Out extends Checkpoi
|
|
|
51
51
|
* {@link Block.modVerify} for changing this from outside the Block's own file.
|
|
52
52
|
*/
|
|
53
53
|
verify?: Trait[] | ((out: Out) => Trait[]);
|
|
54
|
+
/**
|
|
55
|
+
* Optional. Arbitrary highlight points for step-mode tooling to show, in
|
|
56
|
+
* order, after this Block resolves - independent of `verify`, for
|
|
57
|
+
* calling out something worth a human's attention regardless of whether
|
|
58
|
+
* it's also being asserted ("remember this signature ID" isn't a
|
|
59
|
+
* pass/fail check). A function form picks different highlights per
|
|
60
|
+
* resolved `Out`, same as `verify`. Purely additive UI narration - never
|
|
61
|
+
* consulted by `runGraph`/`connect`/anything that decides behavior.
|
|
62
|
+
* @example highlights: (out) => [{ selector: "#signature-id", label: "remember this ID" }]
|
|
63
|
+
*/
|
|
64
|
+
highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
|
|
65
|
+
}
|
|
66
|
+
/** One arbitrary highlight point - see {@link Instruction.highlights}. */
|
|
67
|
+
export interface WaygraphHighlight {
|
|
68
|
+
/** CSS selector for the element to highlight. */
|
|
69
|
+
selector: string;
|
|
70
|
+
/** Caption shown next to the highlight ring. */
|
|
71
|
+
label: string;
|
|
54
72
|
}
|
|
55
73
|
/**
|
|
56
74
|
* A Block wraps one instruction. Its Observed type is an internal detail, erased here.
|
|
@@ -71,6 +89,16 @@ export interface Instruction<In extends Checkpoint<string>, Out extends Checkpoi
|
|
|
71
89
|
export interface Block<In extends Checkpoint<string>, Out extends Checkpoint<string>> {
|
|
72
90
|
/** A short, stable label - shows up in every error this Block can produce ("trait X failed after Y"). */
|
|
73
91
|
name: string;
|
|
92
|
+
/**
|
|
93
|
+
* A human-readable sentence describing what this Block actually tests or
|
|
94
|
+
* does - "the outsider case: a user outside a request's audience is
|
|
95
|
+
* denied its detail page," not a restatement of the code. Purely
|
|
96
|
+
* additive, optional narration for a human watching a run (step-mode
|
|
97
|
+
* tooling shows it before the Block runs) - never read by
|
|
98
|
+
* `runGraph`/`connect`/anything that decides behavior. A Block with none
|
|
99
|
+
* just shows its `name` instead.
|
|
100
|
+
*/
|
|
101
|
+
description?: string;
|
|
74
102
|
/** The act/observe/resolve/verify pipeline itself. See {@link Instruction}. */
|
|
75
103
|
instruction: Instruction<In, Out, any>;
|
|
76
104
|
/**
|