waygraph 0.12.42 → 0.12.43
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/README.md +20 -1
- package/dist/auto-explore-run.js +49 -0
- package/dist/cli.js +332 -56
- package/dist/engine.d.ts +8 -8
- package/dist/engine.js +8 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/overlay-beacon.d.ts +44 -0
- package/dist/overlay-beacon.js +85 -0
- package/dist/step-overlay.d.ts +15 -0
- package/dist/step-overlay.js +11 -0
- package/examples/saucedemo/package-lock.json +1 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +16 -0
- package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +11 -7
- package/package.json +1 -1
- package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +16 -0
- package/templates/quickstart/src/flows/viewer-blocked.flow.ts +11 -7
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A typed graph of reusable Blocks for driving a browser through E2E flows, in place of a
|
|
4
4
|
flat pile of ad hoc helper functions.
|
|
5
5
|
|
|
6
|
-
**Current:** `0.
|
|
6
|
+
**Current:** `0.12.43` — overlay beacons (blind-agent gate), expected-failure panel on branched LoginPage, `--mini`, Hide stickiness.
|
|
7
7
|
|
|
8
8
|
This repo is the `waygraph` npm package itself. The full live **Sauce Demo** example
|
|
9
9
|
(Page inventory hub, Effect Add/Remove, MemNav Open details, `waygraph auto`) ships
|
|
@@ -429,6 +429,25 @@ npm run test # playwright test
|
|
|
429
429
|
npm run build # tsc -p tsconfig.build.json, emits dist/
|
|
430
430
|
```
|
|
431
431
|
|
|
432
|
+
### Blind-agent overlay gate (anti-blank)
|
|
433
|
+
|
|
434
|
+
Every live modal (`#wg-panel`, `#wg-banner`, `#wg-auto-panel`) stamps
|
|
435
|
+
`data-wg-ui` / `data-wg-modal` / `data-wg-ready="1"`. Do **not** claim the
|
|
436
|
+
stepper works unless one of these passes:
|
|
437
|
+
|
|
438
|
+
```bash
|
|
439
|
+
# Playwright helper (exported from waygraph)
|
|
440
|
+
import { assertWgOverlayReady } from "waygraph";
|
|
441
|
+
await assertWgOverlayReady(page); // throws on blank / opacity-0 / missing stamp
|
|
442
|
+
|
|
443
|
+
# Live demo prove (exit 0 = painted, exit 2 = blank)
|
|
444
|
+
WAYGRAPH_PROVE_EXIT=1 WAYGRAPH_PROVE_SHOT=/tmp/wg.png \
|
|
445
|
+
waygraph demo --blocks loginFlow --data '...'
|
|
446
|
+
# stderr: WAYGRAPH_PROVE {"ok":true,"beacons":[...],"readyAttr":"1"}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Tests: `tests/cli/overlay-beacon.spec.ts`. Helpers: `src/overlay-beacon.ts`.
|
|
450
|
+
|
|
432
451
|
## Layout
|
|
433
452
|
|
|
434
453
|
```
|
package/dist/auto-explore-run.js
CHANGED
|
@@ -177,6 +177,19 @@ async function setAutoPanelRunning(page, label) {
|
|
|
177
177
|
if (typeof window.__wgWirePanelChrome === "function") {
|
|
178
178
|
window.__wgWirePanelChrome(panel, "wg-auto-panel-hidden", "waygraph auto");
|
|
179
179
|
}
|
|
180
|
+
if (typeof window.__wgStampModal === "function") {
|
|
181
|
+
window.__wgStampModal(panel, "auto-panel", {
|
|
182
|
+
phase: "running",
|
|
183
|
+
block: label,
|
|
184
|
+
ready: true,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
panel.setAttribute("data-wg-ui", "1");
|
|
189
|
+
panel.setAttribute("data-wg-modal", "auto-panel");
|
|
190
|
+
panel.setAttribute("data-wg-phase", "running");
|
|
191
|
+
panel.setAttribute("data-wg-ready", "1");
|
|
192
|
+
}
|
|
180
193
|
}, label)
|
|
181
194
|
.catch(() => { });
|
|
182
195
|
}
|
|
@@ -399,6 +412,7 @@ async function headfulPick(page, menu, library, mem, waitPick) {
|
|
|
399
412
|
}
|
|
400
413
|
const apply = (hidden) => {
|
|
401
414
|
el.classList.toggle("wg-collapsed", hidden);
|
|
415
|
+
el.setAttribute("data-wg-collapsed", hidden ? "1" : "0");
|
|
402
416
|
const t = el.querySelector("[data-wg-toggle]");
|
|
403
417
|
if (t) t.textContent = hidden ? "Show" : "Hide";
|
|
404
418
|
try { localStorage.setItem(storageKey, hidden ? "1" : "0"); } catch {}
|
|
@@ -416,6 +430,41 @@ async function headfulPick(page, menu, library, mem, waitPick) {
|
|
|
416
430
|
}
|
|
417
431
|
};
|
|
418
432
|
window.__wgWirePanelChrome(root, "wg-auto-panel-hidden", "waygraph auto");
|
|
433
|
+
if (typeof window.__wgStampModal === "function") {
|
|
434
|
+
window.__wgStampModal(root, "auto-panel", { phase: "pick", ready: true });
|
|
435
|
+
} else {
|
|
436
|
+
root.setAttribute("data-wg-ui", "1");
|
|
437
|
+
root.setAttribute("data-wg-modal", "auto-panel");
|
|
438
|
+
root.setAttribute("data-wg-phase", "pick");
|
|
439
|
+
root.setAttribute(
|
|
440
|
+
"data-wg-collapsed",
|
|
441
|
+
root.classList.contains("wg-collapsed") ? "1" : "0",
|
|
442
|
+
);
|
|
443
|
+
root.setAttribute("data-wg-ready", "1");
|
|
444
|
+
window.__wgOverlayBeacon =
|
|
445
|
+
window.__wgOverlayBeacon ||
|
|
446
|
+
(() =>
|
|
447
|
+
Array.from(document.querySelectorAll('[data-wg-ui="1"]')).map((el) => {
|
|
448
|
+
const r = el.getBoundingClientRect();
|
|
449
|
+
const st = getComputedStyle(el);
|
|
450
|
+
return {
|
|
451
|
+
id: el.id || null,
|
|
452
|
+
modal: el.getAttribute("data-wg-modal"),
|
|
453
|
+
ready: el.getAttribute("data-wg-ready") === "1",
|
|
454
|
+
phase: el.getAttribute("data-wg-phase"),
|
|
455
|
+
step: el.getAttribute("data-wg-step"),
|
|
456
|
+
block: el.getAttribute("data-wg-block"),
|
|
457
|
+
collapsed:
|
|
458
|
+
el.getAttribute("data-wg-collapsed") === "1" ||
|
|
459
|
+
el.classList.contains("wg-collapsed"),
|
|
460
|
+
opacity: st.opacity,
|
|
461
|
+
textLen: (el.innerText || "").trim().length,
|
|
462
|
+
w: Math.round(r.width),
|
|
463
|
+
h: Math.round(r.height),
|
|
464
|
+
visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
|
|
465
|
+
};
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
419
468
|
root.querySelectorAll("button[data-idx]").forEach((btn) => {
|
|
420
469
|
btn.addEventListener("click", () => {
|
|
421
470
|
const idx = Number(btn.getAttribute("data-idx"));
|
package/dist/cli.js
CHANGED
|
@@ -858,6 +858,47 @@ async function installOverlay(page, title) {
|
|
|
858
858
|
document.head.appendChild(iconLink);
|
|
859
859
|
}
|
|
860
860
|
if (iconLink.href !== favicon) iconLink.href = favicon;
|
|
861
|
+
// Blind-agent overlay beacons: every modal root gets data-wg-ui /
|
|
862
|
+
// data-wg-modal / data-wg-ready so tests can fail on a blank panel.
|
|
863
|
+
window.__wgStampModal = (el, kind, meta) => {
|
|
864
|
+
if (!el) return;
|
|
865
|
+
meta = meta || {};
|
|
866
|
+
el.setAttribute("data-wg-ui", "1");
|
|
867
|
+
el.setAttribute("data-wg-modal", kind || "panel");
|
|
868
|
+
if (meta.phase != null) el.setAttribute("data-wg-phase", String(meta.phase));
|
|
869
|
+
if (meta.step != null) el.setAttribute("data-wg-step", String(meta.step));
|
|
870
|
+
if (meta.total != null) el.setAttribute("data-wg-total", String(meta.total));
|
|
871
|
+
if (meta.block != null) el.setAttribute("data-wg-block", String(meta.block));
|
|
872
|
+
el.setAttribute(
|
|
873
|
+
"data-wg-collapsed",
|
|
874
|
+
el.classList.contains("wg-collapsed") ? "1" : "0",
|
|
875
|
+
);
|
|
876
|
+
el.setAttribute("data-wg-ready", meta.ready === false ? "0" : "1");
|
|
877
|
+
};
|
|
878
|
+
window.__wgOverlayBeacon = () => {
|
|
879
|
+
return Array.from(document.querySelectorAll("[data-wg-ui=\\"1\\"]")).map((el) => {
|
|
880
|
+
const r = el.getBoundingClientRect();
|
|
881
|
+
const st = getComputedStyle(el);
|
|
882
|
+
return {
|
|
883
|
+
id: el.id || null,
|
|
884
|
+
modal: el.getAttribute("data-wg-modal"),
|
|
885
|
+
ready: el.getAttribute("data-wg-ready") === "1",
|
|
886
|
+
phase: el.getAttribute("data-wg-phase"),
|
|
887
|
+
step: el.getAttribute("data-wg-step"),
|
|
888
|
+
block: el.getAttribute("data-wg-block"),
|
|
889
|
+
collapsed:
|
|
890
|
+
el.getAttribute("data-wg-collapsed") === "1" ||
|
|
891
|
+
el.classList.contains("wg-collapsed"),
|
|
892
|
+
opacity: st.opacity,
|
|
893
|
+
textLen: (el.innerText || "").trim().length,
|
|
894
|
+
w: Math.round(r.width),
|
|
895
|
+
h: Math.round(r.height),
|
|
896
|
+
visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
|
|
897
|
+
};
|
|
898
|
+
});
|
|
899
|
+
};
|
|
900
|
+
const existingBanner = document.getElementById("wg-banner");
|
|
901
|
+
if (existingBanner) window.__wgStampModal(existingBanner, "banner", { ready: true });
|
|
861
902
|
// Hide/Show for #wg-panel - call after every panel.innerHTML refresh.
|
|
862
903
|
// Collapsed = "N / M · block" + Next (when not auto). Never pass
|
|
863
904
|
// forceCollapsed:false - omit so Hide/localStorage wins.
|
|
@@ -937,6 +978,7 @@ async function installOverlay(page, title) {
|
|
|
937
978
|
};
|
|
938
979
|
const apply = (hidden, persist) => {
|
|
939
980
|
panel.classList.toggle("wg-collapsed", hidden);
|
|
981
|
+
panel.setAttribute("data-wg-collapsed", hidden ? "1" : "0");
|
|
940
982
|
const t = panel.querySelector("[data-wg-toggle]");
|
|
941
983
|
const titleEl = panel.querySelector(".wg-chrome-title");
|
|
942
984
|
const label = panel.dataset.wgStepLabel || stepLabel;
|
|
@@ -1027,6 +1069,8 @@ async function renderBeforeStep(page, info) {
|
|
|
1027
1069
|
if (window.__wgHideCursor) window.__wgHideCursor();
|
|
1028
1070
|
})
|
|
1029
1071
|
.catch(() => {});
|
|
1072
|
+
// miniStepLabel / todosHtmlFromInfo are Node-only — browser evaluate has no closure.
|
|
1073
|
+
const payload = { ...info, stepLabel: miniStepLabel(info) };
|
|
1030
1074
|
await page
|
|
1031
1075
|
.evaluate((info) => {
|
|
1032
1076
|
// Reused in place, not removed + recreated, every step - the old
|
|
@@ -1093,6 +1137,33 @@ async function renderBeforeStep(page, info) {
|
|
|
1093
1137
|
esc(info.paceLabel || "") +
|
|
1094
1138
|
"</span></div>"
|
|
1095
1139
|
: "";
|
|
1140
|
+
const todosHtml = (() => {
|
|
1141
|
+
const list = info.todos || [];
|
|
1142
|
+
if (!list.length) return "";
|
|
1143
|
+
return (
|
|
1144
|
+
'<ul id=\\"wg-todos\\">' +
|
|
1145
|
+
list
|
|
1146
|
+
.map((t) => {
|
|
1147
|
+
const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
|
|
1148
|
+
const mark = t.done
|
|
1149
|
+
? String.fromCharCode(0x2713)
|
|
1150
|
+
: t.current
|
|
1151
|
+
? String.fromCharCode(0x2192)
|
|
1152
|
+
: String.fromCharCode(0x25cb);
|
|
1153
|
+
return (
|
|
1154
|
+
'<li class=\\"' +
|
|
1155
|
+
cls +
|
|
1156
|
+
'\\"><span class=\\"wg-todo-mark\\">' +
|
|
1157
|
+
mark +
|
|
1158
|
+
'</span><span>' +
|
|
1159
|
+
esc(t.text || "") +
|
|
1160
|
+
"</span></li>"
|
|
1161
|
+
);
|
|
1162
|
+
})
|
|
1163
|
+
.join("") +
|
|
1164
|
+
"</ul>"
|
|
1165
|
+
);
|
|
1166
|
+
})();
|
|
1096
1167
|
let html =
|
|
1097
1168
|
"<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
|
|
1098
1169
|
episodesHtml +
|
|
@@ -1100,7 +1171,7 @@ async function renderBeforeStep(page, info) {
|
|
|
1100
1171
|
"<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
|
|
1101
1172
|
"<h3>Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + "</h3>" +
|
|
1102
1173
|
narrationHtml +
|
|
1103
|
-
|
|
1174
|
+
todosHtml;
|
|
1104
1175
|
if (info.keys.length === 0) {
|
|
1105
1176
|
html += "<div class=\\"wg-key\\">(no MemKeys required)</div>";
|
|
1106
1177
|
}
|
|
@@ -1130,13 +1201,24 @@ async function renderBeforeStep(page, info) {
|
|
|
1130
1201
|
if (isNewPanel) {
|
|
1131
1202
|
document.documentElement.appendChild(panel);
|
|
1132
1203
|
requestAnimationFrame(() => panel.classList.add("wg-in"));
|
|
1204
|
+
} else {
|
|
1205
|
+
panel.classList.add("wg-in");
|
|
1133
1206
|
}
|
|
1134
1207
|
if (window.__wgWirePanelChrome) {
|
|
1135
1208
|
window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
|
|
1136
|
-
stepLabel:
|
|
1209
|
+
stepLabel: info.stepLabel || "",
|
|
1137
1210
|
forceCollapsed: info.forceCollapsed === true ? true : undefined,
|
|
1138
1211
|
});
|
|
1139
1212
|
}
|
|
1213
|
+
if (window.__wgStampModal) {
|
|
1214
|
+
window.__wgStampModal(panel, "panel", {
|
|
1215
|
+
phase: "before",
|
|
1216
|
+
step: info.index + 1,
|
|
1217
|
+
total: info.total,
|
|
1218
|
+
block: info.blockName || "",
|
|
1219
|
+
ready: true,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1140
1222
|
// Live, human-readable preview of what a MemKey's raw JSON will
|
|
1141
1223
|
// actually write - object fields become "Field: value" lines
|
|
1142
1224
|
// (camelCase split the same way state tags already are); a
|
|
@@ -1206,8 +1288,40 @@ async function renderBeforeStep(page, info) {
|
|
|
1206
1288
|
if (auto) auto.style.display = cb.checked ? "" : "none";
|
|
1207
1289
|
});
|
|
1208
1290
|
}
|
|
1209
|
-
},
|
|
1291
|
+
}, payload)
|
|
1210
1292
|
.catch(() => {});
|
|
1293
|
+
if (process.env.WAYGRAPH_PROVE_SHOT || process.env.WAYGRAPH_PROVE_EXIT === "1") {
|
|
1294
|
+
// Default: prove once on first painted panel. PROVE_EVERY=1 re-checks
|
|
1295
|
+
// every step (noisy; useful when hunting a mid-chain blank).
|
|
1296
|
+
const proveEvery = process.env.WAYGRAPH_PROVE_EVERY === "1";
|
|
1297
|
+
const already = globalThis.__wgProveDone;
|
|
1298
|
+
if (!already || proveEvery || process.env.WAYGRAPH_PROVE_EXIT === "1") {
|
|
1299
|
+
await page.waitForTimeout(500).catch(() => {});
|
|
1300
|
+
if (process.env.WAYGRAPH_PROVE_SHOT) {
|
|
1301
|
+
await page.screenshot({ path: process.env.WAYGRAPH_PROVE_SHOT }).catch(() => {});
|
|
1302
|
+
}
|
|
1303
|
+
const ok = await page
|
|
1304
|
+
.evaluate(() => {
|
|
1305
|
+
const beacons =
|
|
1306
|
+
typeof window.__wgOverlayBeacon === "function" ? window.__wgOverlayBeacon() : [];
|
|
1307
|
+
const panel = beacons.find((b) => b.modal === "panel" && b.ready && b.visible);
|
|
1308
|
+
const textOk = panel && panel.textLen >= 12;
|
|
1309
|
+
const opacityOk = panel && Number(panel.opacity) > 0.5;
|
|
1310
|
+
return {
|
|
1311
|
+
ok: !!(panel && textOk && opacityOk),
|
|
1312
|
+
beacons,
|
|
1313
|
+
hasRun: !!document.getElementById("wg-run"),
|
|
1314
|
+
readyAttr: document.getElementById("wg-panel")?.getAttribute("data-wg-ready") || null,
|
|
1315
|
+
};
|
|
1316
|
+
})
|
|
1317
|
+
.catch((e) => ({ ok: false, reason: String(e && e.message ? e.message : e) }));
|
|
1318
|
+
console.error("WAYGRAPH_PROVE " + JSON.stringify(ok));
|
|
1319
|
+
globalThis.__wgProveDone = true;
|
|
1320
|
+
if (process.env.WAYGRAPH_PROVE_EXIT === "1") {
|
|
1321
|
+
process.exit(ok && ok.ok ? 0 : 2);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1211
1325
|
}
|
|
1212
1326
|
|
|
1213
1327
|
async function ensureSelectorInView(page, selector) {
|
|
@@ -1335,6 +1449,85 @@ function miniStepLabel(info) {
|
|
|
1335
1449
|
);
|
|
1336
1450
|
}
|
|
1337
1451
|
|
|
1452
|
+
/**
|
|
1453
|
+
* stubOnError rings + amber "expected outcome" / red error panel, then wait
|
|
1454
|
+
* for Stop/Retry. Used when a step throws OR when withExpectedFailure's last
|
|
1455
|
+
* block succeeds on the intentional fail branch (e.g. LoginPage + error banner).
|
|
1456
|
+
*/
|
|
1457
|
+
async function presentFailPanel(page, opts) {
|
|
1458
|
+
const {
|
|
1459
|
+
block,
|
|
1460
|
+
fixtures,
|
|
1461
|
+
error,
|
|
1462
|
+
index,
|
|
1463
|
+
total,
|
|
1464
|
+
blockName,
|
|
1465
|
+
message,
|
|
1466
|
+
allNames,
|
|
1467
|
+
allDescriptions,
|
|
1468
|
+
moduleIndex,
|
|
1469
|
+
allEpisodes,
|
|
1470
|
+
title,
|
|
1471
|
+
episodeNumber,
|
|
1472
|
+
episodeTitle,
|
|
1473
|
+
expectedFailureReason,
|
|
1474
|
+
stepperMode,
|
|
1475
|
+
gatesFast,
|
|
1476
|
+
demoPace,
|
|
1477
|
+
gate,
|
|
1478
|
+
} = opts;
|
|
1479
|
+
if (hasAuthoredStubOnError(block, fixtures)) {
|
|
1480
|
+
await page
|
|
1481
|
+
.evaluate(() => {
|
|
1482
|
+
if (window.__wgRingTrack) {
|
|
1483
|
+
window.removeEventListener("resize", window.__wgRingTrack);
|
|
1484
|
+
window.removeEventListener("scroll", window.__wgRingTrack, true);
|
|
1485
|
+
window.__wgRingTrack = null;
|
|
1486
|
+
}
|
|
1487
|
+
})
|
|
1488
|
+
.catch(() => {});
|
|
1489
|
+
const errPhase = await runStubPhase(block, "stubOnError", {
|
|
1490
|
+
fixtures,
|
|
1491
|
+
error,
|
|
1492
|
+
out: opts.out,
|
|
1493
|
+
});
|
|
1494
|
+
const errHighlights = errPhase.highlights.map((h) => {
|
|
1495
|
+
const styled = applyHighlightStyleDefaults(h, opts.highlightStyle);
|
|
1496
|
+
return {
|
|
1497
|
+
selector: styled.selector,
|
|
1498
|
+
label: formatHighlightCaption(styled),
|
|
1499
|
+
duration: styled.duration,
|
|
1500
|
+
fastMode: styled.fastMode,
|
|
1501
|
+
tone: styled.tone,
|
|
1502
|
+
size: styled.size,
|
|
1503
|
+
weight: styled.weight,
|
|
1504
|
+
zoom: styled.zoom,
|
|
1505
|
+
};
|
|
1506
|
+
});
|
|
1507
|
+
await cycleHighlightRings(page, errHighlights, !!gatesFast, {
|
|
1508
|
+
defaultHoldMs: 2000,
|
|
1509
|
+
pace: demoPace,
|
|
1510
|
+
todos: errPhase.todos,
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
await renderStepError(page, {
|
|
1514
|
+
index,
|
|
1515
|
+
total,
|
|
1516
|
+
blockName,
|
|
1517
|
+
message,
|
|
1518
|
+
allNames,
|
|
1519
|
+
allDescriptions,
|
|
1520
|
+
moduleIndex,
|
|
1521
|
+
allEpisodes,
|
|
1522
|
+
title,
|
|
1523
|
+
episodeNumber,
|
|
1524
|
+
episodeTitle,
|
|
1525
|
+
expectedFailureReason,
|
|
1526
|
+
stepperMode,
|
|
1527
|
+
});
|
|
1528
|
+
return gate();
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1338
1531
|
async function cycleHighlightRings(page, highlights, gatesFast, opts) {
|
|
1339
1532
|
const list = highlights || [];
|
|
1340
1533
|
// defaultHoldMs: when set (fail path), use instead of legacy 900/200 so BUG
|
|
@@ -1447,7 +1640,9 @@ async function renderAfterStep(page, info) {
|
|
|
1447
1640
|
// 900ms / 200ms holds when set.
|
|
1448
1641
|
await cycleHighlightRings(page, highlights, gatesFast, {
|
|
1449
1642
|
pace: info.pace,
|
|
1643
|
+
stepLabel: miniStepLabel(info),
|
|
1450
1644
|
});
|
|
1645
|
+
const afterPayload = { ...info, stepLabel: miniStepLabel(info) };
|
|
1451
1646
|
await page
|
|
1452
1647
|
.evaluate((info) => {
|
|
1453
1648
|
// Reused in place - see renderBeforeStep's own comment on this.
|
|
@@ -1525,25 +1720,63 @@ async function renderAfterStep(page, info) {
|
|
|
1525
1720
|
escA(info.paceLabel || "") +
|
|
1526
1721
|
"</span></div>"
|
|
1527
1722
|
: "";
|
|
1723
|
+
const todosHtml = (() => {
|
|
1724
|
+
const list = info.todos || [];
|
|
1725
|
+
if (!list.length) return "";
|
|
1726
|
+
return (
|
|
1727
|
+
'<ul id=\\"wg-todos\\">' +
|
|
1728
|
+
list
|
|
1729
|
+
.map((t) => {
|
|
1730
|
+
const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
|
|
1731
|
+
const mark = t.done
|
|
1732
|
+
? String.fromCharCode(0x2713)
|
|
1733
|
+
: t.current
|
|
1734
|
+
? String.fromCharCode(0x2192)
|
|
1735
|
+
: String.fromCharCode(0x25cb);
|
|
1736
|
+
return (
|
|
1737
|
+
'<li class=\\"' +
|
|
1738
|
+
cls +
|
|
1739
|
+
'\\"><span class=\\"wg-todo-mark\\">' +
|
|
1740
|
+
mark +
|
|
1741
|
+
'</span><span>' +
|
|
1742
|
+
escA(t.text || "") +
|
|
1743
|
+
"</span></li>"
|
|
1744
|
+
);
|
|
1745
|
+
})
|
|
1746
|
+
.join("") +
|
|
1747
|
+
"</ul>"
|
|
1748
|
+
);
|
|
1749
|
+
})();
|
|
1528
1750
|
panel.innerHTML =
|
|
1529
1751
|
"<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
|
|
1530
1752
|
episodesHtml +
|
|
1531
1753
|
paceHtml +
|
|
1532
1754
|
"<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
|
|
1533
1755
|
"<h3>" + heading + "</h3>" +
|
|
1534
|
-
|
|
1756
|
+
todosHtml +
|
|
1535
1757
|
resultHtml +
|
|
1536
1758
|
gateHtml;
|
|
1537
1759
|
if (isNewPanel) {
|
|
1538
1760
|
document.documentElement.appendChild(panel);
|
|
1539
1761
|
requestAnimationFrame(() => panel.classList.add("wg-in"));
|
|
1762
|
+
} else {
|
|
1763
|
+
panel.classList.add("wg-in");
|
|
1540
1764
|
}
|
|
1541
1765
|
if (window.__wgWirePanelChrome) {
|
|
1542
1766
|
window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
|
|
1543
|
-
stepLabel:
|
|
1767
|
+
stepLabel: info.stepLabel || "",
|
|
1544
1768
|
forceCollapsed: info.forceCollapsed === true ? true : undefined,
|
|
1545
1769
|
});
|
|
1546
1770
|
}
|
|
1771
|
+
if (window.__wgStampModal) {
|
|
1772
|
+
window.__wgStampModal(panel, "panel", {
|
|
1773
|
+
phase: "after",
|
|
1774
|
+
step: info.index + 1,
|
|
1775
|
+
total: info.total,
|
|
1776
|
+
block: info.blockName || "",
|
|
1777
|
+
ready: true,
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1547
1780
|
panel.querySelectorAll(".wg-toggle-btn").forEach((btn) => {
|
|
1548
1781
|
btn.addEventListener("click", () => {
|
|
1549
1782
|
const wantPretty = btn.getAttribute("data-mode") === "pretty";
|
|
@@ -1570,7 +1803,7 @@ async function renderAfterStep(page, info) {
|
|
|
1570
1803
|
if (auto) auto.style.display = cb.checked ? "" : "none";
|
|
1571
1804
|
});
|
|
1572
1805
|
}
|
|
1573
|
-
},
|
|
1806
|
+
}, afterPayload)
|
|
1574
1807
|
.catch(() => {});
|
|
1575
1808
|
}
|
|
1576
1809
|
|
|
@@ -1582,6 +1815,7 @@ async function renderAfterStep(page, info) {
|
|
|
1582
1815
|
*/
|
|
1583
1816
|
async function renderStepError(page, info) {
|
|
1584
1817
|
await installOverlay(page, info.title);
|
|
1818
|
+
const errorPayload = { ...info, stepLabel: miniStepLabel(info) };
|
|
1585
1819
|
await page
|
|
1586
1820
|
.evaluate((info) => {
|
|
1587
1821
|
// Reused in place - see renderBeforeStep's own comment on this.
|
|
@@ -1639,18 +1873,29 @@ async function renderStepError(page, info) {
|
|
|
1639
1873
|
if (isNewPanel) {
|
|
1640
1874
|
document.documentElement.appendChild(panel);
|
|
1641
1875
|
requestAnimationFrame(() => panel.classList.add("wg-in"));
|
|
1876
|
+
} else {
|
|
1877
|
+
panel.classList.add("wg-in");
|
|
1642
1878
|
}
|
|
1643
1879
|
if (window.__wgWirePanelChrome) {
|
|
1644
1880
|
window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
|
|
1645
|
-
stepLabel:
|
|
1881
|
+
stepLabel: info.stepLabel || "",
|
|
1646
1882
|
forceCollapsed: info.forceCollapsed === true ? true : undefined,
|
|
1647
1883
|
});
|
|
1648
1884
|
}
|
|
1885
|
+
if (window.__wgStampModal) {
|
|
1886
|
+
window.__wgStampModal(panel, "panel", {
|
|
1887
|
+
phase: "error",
|
|
1888
|
+
step: info.index + 1,
|
|
1889
|
+
total: info.total,
|
|
1890
|
+
block: info.blockName || "",
|
|
1891
|
+
ready: true,
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1649
1894
|
const runBtn = document.getElementById("wg-run");
|
|
1650
1895
|
if (runBtn) runBtn.addEventListener("click", () => window.__wgNext({}));
|
|
1651
1896
|
const retryBtn = document.getElementById("wg-error-retry");
|
|
1652
1897
|
if (retryBtn) retryBtn.addEventListener("click", () => window.__wgNext({ __wgRetry: "1" }));
|
|
1653
|
-
},
|
|
1898
|
+
}, errorPayload)
|
|
1654
1899
|
.catch(() => {});
|
|
1655
1900
|
}
|
|
1656
1901
|
|
|
@@ -1761,6 +2006,14 @@ async function markStepRunning(page, _opts) {
|
|
|
1761
2006
|
const panel = document.getElementById("wg-panel");
|
|
1762
2007
|
if (panel) {
|
|
1763
2008
|
panel.classList.add("wg-collapsed");
|
|
2009
|
+
panel.setAttribute("data-wg-collapsed", "1");
|
|
2010
|
+
panel.setAttribute("data-wg-phase", "running");
|
|
2011
|
+
if (typeof window.__wgStampModal === "function") {
|
|
2012
|
+
window.__wgStampModal(panel, "panel", {
|
|
2013
|
+
phase: "running",
|
|
2014
|
+
ready: true,
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
1764
2017
|
const toggle = panel.querySelector("[data-wg-toggle]");
|
|
1765
2018
|
if (toggle) toggle.textContent = "Show";
|
|
1766
2019
|
const titleEl = panel.querySelector(".wg-chrome-title");
|
|
@@ -1978,6 +2231,15 @@ async function presentSlides(page, slides, _gate, opts) {
|
|
|
1978
2231
|
forceCollapsed: info.forceCollapsed === true ? true : undefined,
|
|
1979
2232
|
});
|
|
1980
2233
|
}
|
|
2234
|
+
if (window.__wgStampModal) {
|
|
2235
|
+
window.__wgStampModal(panel, "panel", {
|
|
2236
|
+
phase: "slide",
|
|
2237
|
+
step: info.index + 1,
|
|
2238
|
+
total: info.total,
|
|
2239
|
+
block: info.blockName || "",
|
|
2240
|
+
ready: true,
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
1981
2243
|
let autoNow = false;
|
|
1982
2244
|
try {
|
|
1983
2245
|
autoNow = localStorage.getItem("wg-autoplay") === "1";
|
|
@@ -2451,7 +2713,10 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2451
2713
|
// installOverlay only seeds localStorage when the key is null (so mid-run
|
|
2452
2714
|
// checkbox clicks survive navigations). Without this, a prior --autoplay
|
|
2453
2715
|
// session sticks forever and agents cannot flip back with --no-autoplay.
|
|
2454
|
-
|
|
2716
|
+
// Must run on the *demo origin* (after goto), not about:blank - localStorage
|
|
2717
|
+
// is origin-scoped.
|
|
2718
|
+
const syncAutoplayFromEnv = async () => {
|
|
2719
|
+
if (process.env.WAYGRAPH_AUTOPLAY === undefined) return;
|
|
2455
2720
|
const on = process.env.WAYGRAPH_AUTOPLAY === "1";
|
|
2456
2721
|
await page
|
|
2457
2722
|
.evaluate((want) => {
|
|
@@ -2462,7 +2727,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2462
2727
|
}
|
|
2463
2728
|
}, on)
|
|
2464
2729
|
.catch(() => {});
|
|
2465
|
-
}
|
|
2730
|
+
};
|
|
2731
|
+
await syncAutoplayFromEnv();
|
|
2466
2732
|
// Some real Blocks (e.g. zsign-all's login.block.ts) call
|
|
2467
2733
|
// page.setViewportSize({ width: 1280, height: 720 }) inside their own
|
|
2468
2734
|
// act() - a hardcoded override for THEIR OWN testing consistency, with no
|
|
@@ -2616,6 +2882,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2616
2882
|
// still rendered on screen.
|
|
2617
2883
|
if ((clearSession || r.resetSession) && i > 0) {
|
|
2618
2884
|
await resetPageState(context, page, baseURL);
|
|
2885
|
+
// clear() wiped origin storage - re-apply CLI autoplay default.
|
|
2886
|
+
await syncAutoplayFromEnv();
|
|
2619
2887
|
}
|
|
2620
2888
|
// Episode pace: block > flow > CLI --fast (only when no authored pace).
|
|
2621
2889
|
// Authoring wins: withDemoPace("slow"|2|4500) keeps slow dwell even under --fast.
|
|
@@ -2748,47 +3016,12 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2748
3016
|
delete window.__wgPendingNavClickLabel;
|
|
2749
3017
|
})
|
|
2750
3018
|
.catch(() => {});
|
|
2751
|
-
// stubOnError
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
if (window.__wgRingTrack) {
|
|
2758
|
-
window.removeEventListener("resize", window.__wgRingTrack);
|
|
2759
|
-
window.removeEventListener("scroll", window.__wgRingTrack, true);
|
|
2760
|
-
window.__wgRingTrack = null;
|
|
2761
|
-
}
|
|
2762
|
-
})
|
|
2763
|
-
.catch(() => {});
|
|
2764
|
-
const errPhase = await runStubPhase(r.block, "stubOnError", {
|
|
2765
|
-
fixtures: fixturesOnError,
|
|
2766
|
-
error: err,
|
|
2767
|
-
});
|
|
2768
|
-
const errHighlights = errPhase.highlights.map((h) => {
|
|
2769
|
-
const styled = applyHighlightStyleDefaults(h, r.highlightStyle);
|
|
2770
|
-
return {
|
|
2771
|
-
selector: styled.selector,
|
|
2772
|
-
label: formatHighlightCaption(styled),
|
|
2773
|
-
duration: styled.duration,
|
|
2774
|
-
fastMode: styled.fastMode,
|
|
2775
|
-
tone: styled.tone,
|
|
2776
|
-
size: styled.size,
|
|
2777
|
-
weight: styled.weight,
|
|
2778
|
-
zoom: styled.zoom,
|
|
2779
|
-
};
|
|
2780
|
-
});
|
|
2781
|
-
await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
|
|
2782
|
-
defaultHoldMs: 2000,
|
|
2783
|
-
pace: pacing.demoPace,
|
|
2784
|
-
todos: errPhase.todos,
|
|
2785
|
-
});
|
|
2786
|
-
}
|
|
2787
|
-
// A thrown act()/observe()/a failed verify Trait used to just crash
|
|
2788
|
-
// the whole Node process with a raw stack trace - the browser closes
|
|
2789
|
-
// (main()'s own try/finally) before a human watching ever sees WHY.
|
|
2790
|
-
// Show it on-screen and let a real click acknowledge it first.
|
|
2791
|
-
await renderStepError(page, {
|
|
3019
|
+
// stubOnError + error/expected panel BEFORE acknowledging (PIA RFC).
|
|
3020
|
+
const errEdits = await presentFailPanel(page, {
|
|
3021
|
+
block: r.block,
|
|
3022
|
+
fixtures: r.highlightFixtures,
|
|
3023
|
+
highlightStyle: r.highlightStyle,
|
|
3024
|
+
error: err,
|
|
2792
3025
|
index: i,
|
|
2793
3026
|
total: resolved.length,
|
|
2794
3027
|
blockName: r.block.name,
|
|
@@ -2802,14 +3035,13 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2802
3035
|
episodeTitle: r.episodeTitle,
|
|
2803
3036
|
expectedFailureReason: r.expectedFailureReason,
|
|
2804
3037
|
stepperMode,
|
|
2805
|
-
|
|
3038
|
+
gatesFast: pacing.gatesFast,
|
|
3039
|
+
demoPace: pacing.demoPace,
|
|
3040
|
+
gate,
|
|
2806
3041
|
});
|
|
2807
|
-
const errEdits = await gate();
|
|
2808
3042
|
if (errEdits && errEdits.__wgRetry) {
|
|
2809
|
-
// Same clean slate a normal episode boundary gets - not just
|
|
2810
|
-
// re-running the failed block against whatever broken/half-
|
|
2811
|
-
// navigated state it left the page in.
|
|
2812
3043
|
await resetPageState(context, page, baseURL);
|
|
3044
|
+
await syncAutoplayFromEnv();
|
|
2813
3045
|
i = episodeStartIndex - 1;
|
|
2814
3046
|
continue;
|
|
2815
3047
|
}
|
|
@@ -2831,6 +3063,50 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2831
3063
|
})
|
|
2832
3064
|
.catch(() => {});
|
|
2833
3065
|
result = stepOutcome.result;
|
|
3066
|
+
// withExpectedFailure last block that SUCCEEDS on the intentional fail
|
|
3067
|
+
// branch (e.g. submit-login -> LoginPage + error banner). Still show
|
|
3068
|
+
// stubOnError rings + amber "expected outcome" panel - branching no
|
|
3069
|
+
// longer throws, but the demo must not skip the educational overlay.
|
|
3070
|
+
if (r.expectedFailureReason) {
|
|
3071
|
+
const errEdits = await presentFailPanel(page, {
|
|
3072
|
+
block: r.block,
|
|
3073
|
+
fixtures: r.highlightFixtures,
|
|
3074
|
+
highlightStyle: r.highlightStyle,
|
|
3075
|
+
error: undefined,
|
|
3076
|
+
out: result,
|
|
3077
|
+
index: i,
|
|
3078
|
+
total: resolved.length,
|
|
3079
|
+
blockName: r.block.name,
|
|
3080
|
+
message:
|
|
3081
|
+
"Resolved " +
|
|
3082
|
+
(result && result.__state ? result.__state : JSON.stringify(result)) +
|
|
3083
|
+
" (expected failure path)",
|
|
3084
|
+
allNames: moduleNames,
|
|
3085
|
+
allDescriptions: moduleDescriptions,
|
|
3086
|
+
moduleIndex,
|
|
3087
|
+
allEpisodes,
|
|
3088
|
+
title,
|
|
3089
|
+
episodeNumber: r.episodeNumber,
|
|
3090
|
+
episodeTitle: r.episodeTitle,
|
|
3091
|
+
expectedFailureReason: r.expectedFailureReason,
|
|
3092
|
+
stepperMode,
|
|
3093
|
+
gatesFast: pacing.gatesFast,
|
|
3094
|
+
demoPace: pacing.demoPace,
|
|
3095
|
+
gate,
|
|
3096
|
+
});
|
|
3097
|
+
if (errEdits && errEdits.__wgRetry) {
|
|
3098
|
+
await resetPageState(context, page, baseURL);
|
|
3099
|
+
await syncAutoplayFromEnv();
|
|
3100
|
+
i = episodeStartIndex - 1;
|
|
3101
|
+
continue;
|
|
3102
|
+
}
|
|
3103
|
+
if (process.env.WAYGRAPH_JSON !== "1") {
|
|
3104
|
+
console.log(
|
|
3105
|
+
"waygraph: expected failure on " + r.block.name + " - " + r.expectedFailureReason,
|
|
3106
|
+
);
|
|
3107
|
+
}
|
|
3108
|
+
break;
|
|
3109
|
+
}
|
|
2834
3110
|
const fixturesAfter = r.highlightFixtures;
|
|
2835
3111
|
const slides = resolveSlides(r.block, { out: result, fixtures: fixturesAfter }).map((s) =>
|
|
2836
3112
|
applyHighlightStyleDefaults(s, r.highlightStyle),
|
package/dist/engine.d.ts
CHANGED
|
@@ -292,15 +292,15 @@ export declare function withBlockPace<In extends Checkpoint<string>, Out extends
|
|
|
292
292
|
* {@link withTitle}.
|
|
293
293
|
*
|
|
294
294
|
* By itself this changes nothing about how the Flow runs - it still throws
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
295
|
+
* Marks a Flow whose last step is an intentional product failure (demo overlay).
|
|
296
|
+
* Step mode shows stubOnError rings + an amber "expected outcome" panel when:
|
|
297
|
+
* - the last block **throws**, or
|
|
298
|
+
* - the last block **succeeds** on the fail branch (e.g. LoginPage + error banner
|
|
299
|
+
* after submit-login branched instead of throwing).
|
|
300
|
+
* Headless `run()` is unchanged - branching success returns the checkpoint.
|
|
301
301
|
* @example const viewerBlockedFlow = withExpectedFailure(
|
|
302
|
-
* engine.defineFlow([start, NavLoginBlock,
|
|
303
|
-
* "locked_out_user
|
|
302
|
+
* engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end]),
|
|
303
|
+
* "locked_out_user stays on LoginPage with the error banner - working as intended.",
|
|
304
304
|
* );
|
|
305
305
|
*/
|
|
306
306
|
export declare function withExpectedFailure<Out extends Checkpoint<string>>(flow: Flow<Out>, reason: string): Flow<Out>;
|
package/dist/engine.js
CHANGED
|
@@ -405,15 +405,15 @@ export function withBlockPace(block, pace) {
|
|
|
405
405
|
* {@link withTitle}.
|
|
406
406
|
*
|
|
407
407
|
* By itself this changes nothing about how the Flow runs - it still throws
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
408
|
+
* Marks a Flow whose last step is an intentional product failure (demo overlay).
|
|
409
|
+
* Step mode shows stubOnError rings + an amber "expected outcome" panel when:
|
|
410
|
+
* - the last block **throws**, or
|
|
411
|
+
* - the last block **succeeds** on the fail branch (e.g. LoginPage + error banner
|
|
412
|
+
* after submit-login branched instead of throwing).
|
|
413
|
+
* Headless `run()` is unchanged - branching success returns the checkpoint.
|
|
414
414
|
* @example const viewerBlockedFlow = withExpectedFailure(
|
|
415
|
-
* engine.defineFlow([start, NavLoginBlock,
|
|
416
|
-
* "locked_out_user
|
|
415
|
+
* engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end]),
|
|
416
|
+
* "locked_out_user stays on LoginPage with the error banner - working as intended.",
|
|
417
417
|
* );
|
|
418
418
|
*/
|
|
419
419
|
export function withExpectedFailure(flow, reason) {
|
package/dist/index.d.ts
CHANGED
|
@@ -16,3 +16,5 @@ export { runTraverse } from "./traverse-run.js";
|
|
|
16
16
|
export type { TraverseOptions, TraverseSessionMode, TraverseWorkerResult, } from "./traverse-run.js";
|
|
17
17
|
export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport, } from "./traverse-coverage.js";
|
|
18
18
|
export type { TraverseCoverageReport } from "./traverse-coverage.js";
|
|
19
|
+
export { WG_UI_SEL, WG_READY_PANEL_SEL, WG_READY_AUTO_SEL, WG_READY_BANNER_SEL, readWgOverlayBeacon, readWgOverlayBeaconInPage, assertWgOverlayReady, } from "./overlay-beacon.js";
|
|
20
|
+
export type { WgModalKind, WgBeaconRow } from "./overlay-beacon.js";
|
package/dist/index.js
CHANGED
|
@@ -8,3 +8,4 @@ export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp,
|
|
|
8
8
|
export { EdgeLeaseCoordinator } from "./traverse-lease.js";
|
|
9
9
|
export { runTraverse } from "./traverse-run.js";
|
|
10
10
|
export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport, } from "./traverse-coverage.js";
|
|
11
|
+
export { WG_UI_SEL, WG_READY_PANEL_SEL, WG_READY_AUTO_SEL, WG_READY_BANNER_SEL, readWgOverlayBeacon, readWgOverlayBeaconInPage, assertWgOverlayReady, } from "./overlay-beacon.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind-agent / Playwright checks for waygraph overlay modals.
|
|
3
|
+
*
|
|
4
|
+
* Every live modal root must carry:
|
|
5
|
+
* data-wg-ui="1"
|
|
6
|
+
* data-wg-modal="panel|banner|auto-panel"
|
|
7
|
+
* data-wg-ready="1" when painted and readable
|
|
8
|
+
*
|
|
9
|
+
* Agents: do not claim the stepper works unless
|
|
10
|
+
* `assertWgOverlayReady(page)` (or WAYGRAPH_PROVE_EXIT=1) passes.
|
|
11
|
+
*/
|
|
12
|
+
import type { Page } from "@playwright/test";
|
|
13
|
+
export declare const WG_UI_SEL = "[data-wg-ui=\"1\"]";
|
|
14
|
+
export declare const WG_READY_PANEL_SEL = "[data-wg-modal=\"panel\"][data-wg-ready=\"1\"]";
|
|
15
|
+
export declare const WG_READY_AUTO_SEL = "[data-wg-modal=\"auto-panel\"][data-wg-ready=\"1\"]";
|
|
16
|
+
export declare const WG_READY_BANNER_SEL = "[data-wg-modal=\"banner\"][data-wg-ready=\"1\"]";
|
|
17
|
+
export type WgModalKind = "panel" | "banner" | "auto-panel";
|
|
18
|
+
export type WgBeaconRow = {
|
|
19
|
+
id: string | null;
|
|
20
|
+
modal: string | null;
|
|
21
|
+
ready: boolean;
|
|
22
|
+
phase: string | null;
|
|
23
|
+
step: string | null;
|
|
24
|
+
block: string | null;
|
|
25
|
+
collapsed: boolean;
|
|
26
|
+
opacity: string;
|
|
27
|
+
textLen: number;
|
|
28
|
+
w: number;
|
|
29
|
+
h: number;
|
|
30
|
+
visible: boolean;
|
|
31
|
+
};
|
|
32
|
+
/** Browser-side beacon reader (also installed as window.__wgOverlayBeacon). */
|
|
33
|
+
export declare function readWgOverlayBeaconInPage(): WgBeaconRow[];
|
|
34
|
+
export declare function readWgOverlayBeacon(page: Page): Promise<WgBeaconRow[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Hard gate: a blank / missing / opacity-0 modal fails.
|
|
37
|
+
* Use after demo/auto paints its first overlay.
|
|
38
|
+
*/
|
|
39
|
+
export declare function assertWgOverlayReady(page: Page, opts?: {
|
|
40
|
+
modal?: WgModalKind;
|
|
41
|
+
minTextLen?: number;
|
|
42
|
+
/** When true, collapsed mini chrome is allowed (still needs visible box + text). */
|
|
43
|
+
allowCollapsed?: boolean;
|
|
44
|
+
}): Promise<WgBeaconRow>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind-agent / Playwright checks for waygraph overlay modals.
|
|
3
|
+
*
|
|
4
|
+
* Every live modal root must carry:
|
|
5
|
+
* data-wg-ui="1"
|
|
6
|
+
* data-wg-modal="panel|banner|auto-panel"
|
|
7
|
+
* data-wg-ready="1" when painted and readable
|
|
8
|
+
*
|
|
9
|
+
* Agents: do not claim the stepper works unless
|
|
10
|
+
* `assertWgOverlayReady(page)` (or WAYGRAPH_PROVE_EXIT=1) passes.
|
|
11
|
+
*/
|
|
12
|
+
export const WG_UI_SEL = '[data-wg-ui="1"]';
|
|
13
|
+
export const WG_READY_PANEL_SEL = '[data-wg-modal="panel"][data-wg-ready="1"]';
|
|
14
|
+
export const WG_READY_AUTO_SEL = '[data-wg-modal="auto-panel"][data-wg-ready="1"]';
|
|
15
|
+
export const WG_READY_BANNER_SEL = '[data-wg-modal="banner"][data-wg-ready="1"]';
|
|
16
|
+
/** Browser-side beacon reader (also installed as window.__wgOverlayBeacon). */
|
|
17
|
+
export function readWgOverlayBeaconInPage() {
|
|
18
|
+
const w = globalThis;
|
|
19
|
+
if (typeof w.__wgOverlayBeacon === "function")
|
|
20
|
+
return w.__wgOverlayBeacon();
|
|
21
|
+
return Array.from(document.querySelectorAll(WG_UI_SEL)).map((el) => {
|
|
22
|
+
const r = el.getBoundingClientRect();
|
|
23
|
+
const st = getComputedStyle(el);
|
|
24
|
+
return {
|
|
25
|
+
id: el.id || null,
|
|
26
|
+
modal: el.getAttribute("data-wg-modal"),
|
|
27
|
+
ready: el.getAttribute("data-wg-ready") === "1",
|
|
28
|
+
phase: el.getAttribute("data-wg-phase"),
|
|
29
|
+
step: el.getAttribute("data-wg-step"),
|
|
30
|
+
block: el.getAttribute("data-wg-block"),
|
|
31
|
+
collapsed: el.getAttribute("data-wg-collapsed") === "1" || el.classList.contains("wg-collapsed"),
|
|
32
|
+
opacity: st.opacity,
|
|
33
|
+
textLen: (el.textContent || "").trim().length,
|
|
34
|
+
w: Math.round(r.width),
|
|
35
|
+
h: Math.round(r.height),
|
|
36
|
+
visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export async function readWgOverlayBeacon(page) {
|
|
41
|
+
return page.evaluate(() => {
|
|
42
|
+
const w = window;
|
|
43
|
+
if (typeof w.__wgOverlayBeacon === "function") {
|
|
44
|
+
return w.__wgOverlayBeacon();
|
|
45
|
+
}
|
|
46
|
+
return Array.from(document.querySelectorAll('[data-wg-ui="1"]')).map((el) => {
|
|
47
|
+
const r = el.getBoundingClientRect();
|
|
48
|
+
const st = getComputedStyle(el);
|
|
49
|
+
return {
|
|
50
|
+
id: el.id || null,
|
|
51
|
+
modal: el.getAttribute("data-wg-modal"),
|
|
52
|
+
ready: el.getAttribute("data-wg-ready") === "1",
|
|
53
|
+
phase: el.getAttribute("data-wg-phase"),
|
|
54
|
+
step: el.getAttribute("data-wg-step"),
|
|
55
|
+
block: el.getAttribute("data-wg-block"),
|
|
56
|
+
collapsed: el.getAttribute("data-wg-collapsed") === "1" || el.classList.contains("wg-collapsed"),
|
|
57
|
+
opacity: st.opacity,
|
|
58
|
+
textLen: (el.textContent || "").trim().length,
|
|
59
|
+
w: Math.round(r.width),
|
|
60
|
+
h: Math.round(r.height),
|
|
61
|
+
visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Hard gate: a blank / missing / opacity-0 modal fails.
|
|
68
|
+
* Use after demo/auto paints its first overlay.
|
|
69
|
+
*/
|
|
70
|
+
export async function assertWgOverlayReady(page, opts) {
|
|
71
|
+
const modal = opts?.modal ?? "panel";
|
|
72
|
+
const minText = opts?.minTextLen ?? 12;
|
|
73
|
+
const rows = await readWgOverlayBeacon(page);
|
|
74
|
+
const hit = rows.find((r) => r.modal === modal &&
|
|
75
|
+
r.ready &&
|
|
76
|
+
r.visible &&
|
|
77
|
+
r.textLen >= minText &&
|
|
78
|
+
(opts?.allowCollapsed || !r.collapsed || r.textLen >= minText));
|
|
79
|
+
if (!hit) {
|
|
80
|
+
throw new Error(`waygraph overlay NOT ready (modal=${modal}, minTextLen=${minText}). ` +
|
|
81
|
+
`beacons=${JSON.stringify(rows)}. ` +
|
|
82
|
+
`Do not claim stepper/modal works.`);
|
|
83
|
+
}
|
|
84
|
+
return hit;
|
|
85
|
+
}
|
package/dist/step-overlay.d.ts
CHANGED
|
@@ -18,5 +18,20 @@ declare global {
|
|
|
18
18
|
__wgHideRing?: () => void;
|
|
19
19
|
__wgPendingNavClickLabel?: string;
|
|
20
20
|
__wgWirePanelChrome?: (panel: HTMLElement, storageKey: string, chromeTitle?: string) => void;
|
|
21
|
+
__wgStampModal?: (el: Element, kind: string, meta?: Record<string, unknown>) => void;
|
|
22
|
+
__wgOverlayBeacon?: () => Array<{
|
|
23
|
+
id: string | null;
|
|
24
|
+
modal: string | null;
|
|
25
|
+
ready: boolean;
|
|
26
|
+
phase: string | null;
|
|
27
|
+
step: string | null;
|
|
28
|
+
block: string | null;
|
|
29
|
+
collapsed: boolean;
|
|
30
|
+
opacity: string;
|
|
31
|
+
textLen: number;
|
|
32
|
+
w: number;
|
|
33
|
+
h: number;
|
|
34
|
+
visible: boolean;
|
|
35
|
+
}>;
|
|
21
36
|
}
|
|
22
37
|
}
|
package/dist/step-overlay.js
CHANGED
|
@@ -146,6 +146,17 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
|
|
|
146
146
|
banner.innerHTML =
|
|
147
147
|
"<span class='wg-banner-tag'>waygraph auto</span><span>" + title + "</span>";
|
|
148
148
|
document.documentElement.appendChild(banner);
|
|
149
|
+
banner.setAttribute("data-wg-ui", "1");
|
|
150
|
+
banner.setAttribute("data-wg-modal", "banner");
|
|
151
|
+
banner.setAttribute("data-wg-ready", "1");
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const existing = document.getElementById("wg-banner");
|
|
155
|
+
if (existing) {
|
|
156
|
+
existing.setAttribute("data-wg-ui", "1");
|
|
157
|
+
existing.setAttribute("data-wg-modal", "banner");
|
|
158
|
+
existing.setAttribute("data-wg-ready", "1");
|
|
159
|
+
}
|
|
149
160
|
}
|
|
150
161
|
if (showBanner && !document.title.startsWith("[waygraph] ")) {
|
|
151
162
|
document.title = "[waygraph] " + document.title;
|
|
@@ -78,6 +78,22 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
78
78
|
});
|
|
79
79
|
},
|
|
80
80
|
stubAfter: (ctx) => {
|
|
81
|
+
// Fail branch lands on LoginPage with Epic sadface - do not ring the
|
|
82
|
+
// inventory shelf. Success (LoggedIn) hands off to shopFlow.
|
|
83
|
+
if (ctx.out && ctx.out.__state === "LoginPage") {
|
|
84
|
+
ctx.title("Login blocked");
|
|
85
|
+
ctx.todos(["Enter username", "Enter password", "Click Login"]);
|
|
86
|
+
ctx.todoIndex(2);
|
|
87
|
+
ctx.ring("error", {
|
|
88
|
+
selector: '[data-test="error"]',
|
|
89
|
+
label: "Login error banner",
|
|
90
|
+
tone: "danger",
|
|
91
|
+
focus: true,
|
|
92
|
+
color: "#f87171",
|
|
93
|
+
duration: true,
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
81
97
|
ctx.banner("Inventory");
|
|
82
98
|
ctx.todos(["Enter username", "Enter password", "Click Login"]);
|
|
83
99
|
ctx.todoIndex(2);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Engine, start, end, withTitle, withSessionReset } from "waygraph";
|
|
1
|
+
import { Engine, start, end, withTitle, withSessionReset, withExpectedFailure } from "waygraph";
|
|
2
2
|
import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
|
|
3
3
|
import { SubmitLoginActionBlock } from "../blocks/saucedemo-web/methods/submit-login.method.block.js";
|
|
4
4
|
|
|
@@ -16,10 +16,14 @@ const engine = new Engine();
|
|
|
16
16
|
// genuinely fresh cookies/storage, or the previous user's session would
|
|
17
17
|
// still be live and this login attempt wouldn't mean anything.
|
|
18
18
|
//
|
|
19
|
-
// submit-login
|
|
20
|
-
// resolve -> LoginPage, verify confirms the error banner
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
// submit-login branches: observe sees no /inventory.html navigation,
|
|
20
|
+
// resolve -> LoginPage, verify confirms the error banner. Demo still wraps
|
|
21
|
+
// withExpectedFailure so step 9/9 shows stubOnError rings + amber
|
|
22
|
+
// "expected outcome" panel (branching no longer throws).
|
|
23
|
+
export const viewerBlockedFlow = withExpectedFailure(
|
|
24
|
+
withTitle(
|
|
25
|
+
withSessionReset(engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end])),
|
|
26
|
+
"Viewer: Blocked Login Attempt",
|
|
27
|
+
),
|
|
28
|
+
"locked_out_user stays on LoginPage with the error banner - this is the product working as intended.",
|
|
25
29
|
);
|
package/package.json
CHANGED
|
@@ -78,6 +78,22 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
78
78
|
});
|
|
79
79
|
},
|
|
80
80
|
stubAfter: (ctx) => {
|
|
81
|
+
// Fail branch lands on LoginPage with Epic sadface - do not ring the
|
|
82
|
+
// inventory shelf. Success (LoggedIn) hands off to shopFlow.
|
|
83
|
+
if (ctx.out && ctx.out.__state === "LoginPage") {
|
|
84
|
+
ctx.title("Login blocked");
|
|
85
|
+
ctx.todos(["Enter username", "Enter password", "Click Login"]);
|
|
86
|
+
ctx.todoIndex(2);
|
|
87
|
+
ctx.ring("error", {
|
|
88
|
+
selector: '[data-test="error"]',
|
|
89
|
+
label: "Login error banner",
|
|
90
|
+
tone: "danger",
|
|
91
|
+
focus: true,
|
|
92
|
+
color: "#f87171",
|
|
93
|
+
duration: true,
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
81
97
|
ctx.banner("Inventory");
|
|
82
98
|
ctx.todos(["Enter username", "Enter password", "Click Login"]);
|
|
83
99
|
ctx.todoIndex(2);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Engine, start, end, withTitle, withSessionReset } from "waygraph";
|
|
1
|
+
import { Engine, start, end, withTitle, withSessionReset, withExpectedFailure } from "waygraph";
|
|
2
2
|
import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
|
|
3
3
|
import { SubmitLoginActionBlock } from "../blocks/saucedemo-web/methods/submit-login.method.block.js";
|
|
4
4
|
|
|
@@ -16,10 +16,14 @@ const engine = new Engine();
|
|
|
16
16
|
// genuinely fresh cookies/storage, or the previous user's session would
|
|
17
17
|
// still be live and this login attempt wouldn't mean anything.
|
|
18
18
|
//
|
|
19
|
-
// submit-login
|
|
20
|
-
// resolve -> LoginPage, verify confirms the error banner
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
// submit-login branches: observe sees no /inventory.html navigation,
|
|
20
|
+
// resolve -> LoginPage, verify confirms the error banner. Demo still wraps
|
|
21
|
+
// withExpectedFailure so step 9/9 shows stubOnError rings + amber
|
|
22
|
+
// "expected outcome" panel (branching no longer throws).
|
|
23
|
+
export const viewerBlockedFlow = withExpectedFailure(
|
|
24
|
+
withTitle(
|
|
25
|
+
withSessionReset(engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end])),
|
|
26
|
+
"Viewer: Blocked Login Attempt",
|
|
27
|
+
),
|
|
28
|
+
"locked_out_user stays on LoginPage with the error banner - this is the product working as intended.",
|
|
25
29
|
);
|