waygraph 0.15.21 → 0.15.23
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/auto-explore-run.d.ts +2 -1
- package/dist/auto-explore-run.js +2 -1
- package/dist/auto-session.js +2 -1
- package/dist/cli.js +21 -6067
- package/dist/runner/chain-run.js +102 -0
- package/dist/runner/cursor.js +140 -0
- package/dist/runner/demo-log.js +314 -0
- package/dist/runner/device-stage.js +400 -0
- package/dist/runner/discovery.js +105 -0
- package/dist/runner/index.js +2 -0
- package/dist/runner/interaction-patch.js +353 -0
- package/dist/runner/main.js +397 -0
- package/dist/runner/overlay-css.js +342 -0
- package/dist/runner/overlay-install.js +1301 -0
- package/dist/runner/rings.js +490 -0
- package/dist/runner/seed-mem.js +110 -0
- package/dist/runner/slides.js +272 -0
- package/dist/runner/step-mode.js +776 -0
- package/dist/runner/step-panels.js +723 -0
- package/dist/runner/teardown.js +195 -0
- package/dist/runner/todo-dock.js +142 -0
- package/examples/saucedemo/.waygraph-auto/19b9eee5.log +0 -0
- package/examples/saucedemo/.waygraph-auto/422d4390.log +0 -0
- package/examples/saucedemo/.waygraph-auto/6c9f8e10.log +0 -0
- package/examples/saucedemo/.waygraph-auto/74ce745a.log +0 -0
- package/examples/saucedemo/.waygraph-auto/89477e92.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b7e545f0.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cb2726d7.log +0 -0
- package/package.json +8 -4
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
// Moved verbatim from the former CHAIN_RUNNER_SCRIPT template string in cli.ts (see src/ARCHITECTURE.md).
|
|
2
|
+
// Runs inside the target project's own waygraph copy - keep it dependency-light and self-contained.
|
|
3
|
+
import { ANSI, ansiPaint, demoFixture, demoHighlight, demoLog } from "./demo-log.js";
|
|
4
|
+
import { playSwipeTrail } from "./cursor.js";
|
|
5
|
+
import { advanceTodoDock, formatHighlightCaption, normalizeHighlightSize, normalizeHighlightTone, normalizeHighlightWeight, normalizeTodos, resolveFixtureDwellMs } from "../highlights.js";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Scroll a locator into view for demo theater.
|
|
10
|
+
* Prefer CSS smooth scroll + scrollend wait. Do NOT call Playwright's
|
|
11
|
+
* scrollIntoViewIfNeeded first - that jumps instantly and makes the
|
|
12
|
+
* following smooth scroll a no-op (looks like a teleport), especially
|
|
13
|
+
* noticeable under --fast when dwell is shorter.
|
|
14
|
+
* Pass opts.instant=true only for skipTheater / blitz.
|
|
15
|
+
*/
|
|
16
|
+
export async function ensureLocatorInView(locator, opts) {
|
|
17
|
+
const instant = !!(opts && opts.instant);
|
|
18
|
+
try {
|
|
19
|
+
if (instant) {
|
|
20
|
+
await locator.scrollIntoViewIfNeeded().catch(() => {});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Touch mode: if the target is off-screen, show a swipe trail in the
|
|
24
|
+
// finger direction that would scroll it into view (not device morph).
|
|
25
|
+
const scrollPlan = await locator
|
|
26
|
+
.evaluate((el) => {
|
|
27
|
+
if (!el || typeof el.getBoundingClientRect !== "function") return null;
|
|
28
|
+
const touch =
|
|
29
|
+
window.__wgDeviceTouch === true ||
|
|
30
|
+
document.documentElement.dataset.wgTouch === "1" ||
|
|
31
|
+
window.__wgDevicePreset === "mobile" ||
|
|
32
|
+
window.__wgDevicePreset === "tablet";
|
|
33
|
+
if (!touch) return null;
|
|
34
|
+
const r = el.getBoundingClientRect();
|
|
35
|
+
const vw = window.innerWidth || 390;
|
|
36
|
+
const vh = window.innerHeight || 844;
|
|
37
|
+
const margin = 48;
|
|
38
|
+
const below = r.top > vh - margin;
|
|
39
|
+
const above = r.bottom < margin;
|
|
40
|
+
const right = r.left > vw - margin;
|
|
41
|
+
const left = r.right < margin;
|
|
42
|
+
if (!below && !above && !left && !right) return null;
|
|
43
|
+
// Finger direction to reveal the target (swipe up => content moves up).
|
|
44
|
+
let dir = "up";
|
|
45
|
+
if (below) dir = "up";
|
|
46
|
+
else if (above) dir = "down";
|
|
47
|
+
else if (right) dir = "left";
|
|
48
|
+
else if (left) dir = "right";
|
|
49
|
+
return { dir, hint: "scroll" };
|
|
50
|
+
})
|
|
51
|
+
.catch(() => null);
|
|
52
|
+
if (scrollPlan && scrollPlan.dir) {
|
|
53
|
+
demoLog(" swipe trail dir=" + scrollPlan.dir + " hint=scroll");
|
|
54
|
+
await playSwipeTrail(locator.page(), {
|
|
55
|
+
dir: scrollPlan.dir,
|
|
56
|
+
label: "swipe \u00b7 scroll",
|
|
57
|
+
hint: "scroll",
|
|
58
|
+
ms: Number(process.env.WAYGRAPH_SWIPE_MS || 650),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
await locator
|
|
62
|
+
.evaluate(async (el) => {
|
|
63
|
+
if (!el || typeof el.scrollIntoView !== "function") return;
|
|
64
|
+
const waitScroll = (node, ms) =>
|
|
65
|
+
new Promise((resolve) => {
|
|
66
|
+
let done = false;
|
|
67
|
+
const finish = () => {
|
|
68
|
+
if (done) return;
|
|
69
|
+
done = true;
|
|
70
|
+
try {
|
|
71
|
+
node.removeEventListener("scrollend", finish);
|
|
72
|
+
} catch {
|
|
73
|
+
/* ignore */
|
|
74
|
+
}
|
|
75
|
+
clearTimeout(t);
|
|
76
|
+
resolve();
|
|
77
|
+
};
|
|
78
|
+
const t = setTimeout(finish, ms);
|
|
79
|
+
try {
|
|
80
|
+
node.addEventListener("scrollend", finish, { once: true });
|
|
81
|
+
} catch {
|
|
82
|
+
/* scrollend unsupported - timeout only */
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
el.scrollIntoView({ block: "center", inline: "center", behavior: "smooth" });
|
|
87
|
+
} catch {
|
|
88
|
+
try {
|
|
89
|
+
el.scrollIntoView({ block: "center", inline: "center" });
|
|
90
|
+
} catch {
|
|
91
|
+
try {
|
|
92
|
+
el.scrollIntoView(true);
|
|
93
|
+
} catch {
|
|
94
|
+
/* ignore */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await waitScroll(document.scrollingElement || document.documentElement, 650);
|
|
99
|
+
let p = el.parentElement;
|
|
100
|
+
while (p && p !== document.documentElement && p !== document.body) {
|
|
101
|
+
const st = getComputedStyle(p);
|
|
102
|
+
const ox = st.overflowX;
|
|
103
|
+
const oy = st.overflowY;
|
|
104
|
+
if (/(auto|scroll|overlay)/.test(ox) || /(auto|scroll|overlay)/.test(oy)) {
|
|
105
|
+
const er = el.getBoundingClientRect();
|
|
106
|
+
const pr = p.getBoundingClientRect();
|
|
107
|
+
if (er.left < pr.left || er.right > pr.right || er.top < pr.top || er.bottom > pr.bottom) {
|
|
108
|
+
const left = p.scrollLeft + (er.left + er.width / 2 - (pr.left + pr.width / 2));
|
|
109
|
+
const top = p.scrollTop + (er.top + er.height / 2 - (pr.top + pr.height / 2));
|
|
110
|
+
try {
|
|
111
|
+
p.scrollTo({ left, top, behavior: "smooth" });
|
|
112
|
+
} catch {
|
|
113
|
+
p.scrollLeft = left;
|
|
114
|
+
p.scrollTop = top;
|
|
115
|
+
}
|
|
116
|
+
await waitScroll(p, 650);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
p = p.parentElement;
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
.catch(() => {});
|
|
123
|
+
} catch {
|
|
124
|
+
/* best-effort */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
export async function ensureSelectorInView(page, selector, opts) {
|
|
130
|
+
await ensureLocatorInView(page.locator(selector).first(), opts);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
export async function applyHighlightZoom(page, selector, zoom, zoomOut) {
|
|
135
|
+
const z = Number(zoom);
|
|
136
|
+
if (!Number.isFinite(z) || z <= 1.001) {
|
|
137
|
+
await page.evaluate(() => {
|
|
138
|
+
window.__wgZoomOutOnHide = true;
|
|
139
|
+
if (window.__wgClearZoom) window.__wgClearZoom();
|
|
140
|
+
}).catch(() => {});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
demoFixture("zoom", "ctx/ring zoom(" + z + ") zoomOut=" + (zoomOut !== false), selector);
|
|
144
|
+
await page
|
|
145
|
+
.evaluate(
|
|
146
|
+
({ sel, scale, zoomOut }) => {
|
|
147
|
+
// false = keep camera after ring hide (ctx.zoomOut(false) / ring.zoomOut).
|
|
148
|
+
window.__wgZoomOutOnHide = zoomOut !== false;
|
|
149
|
+
if (window.__wgApplyZoom) window.__wgApplyZoom(sel, scale);
|
|
150
|
+
},
|
|
151
|
+
{ sel: selector, scale: z, zoomOut: zoomOut !== false },
|
|
152
|
+
)
|
|
153
|
+
.catch(() => {});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async function applyHighlightFocus(page, box, focus) {
|
|
158
|
+
if (!focus || !box) {
|
|
159
|
+
await page
|
|
160
|
+
.evaluate(() => {
|
|
161
|
+
if (window.__wgClearFocus) window.__wgClearFocus();
|
|
162
|
+
})
|
|
163
|
+
.catch(() => {});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
demoFixture("focus", "ctx/ring focus(true)", {
|
|
167
|
+
x: Math.round(box.x),
|
|
168
|
+
y: Math.round(box.y),
|
|
169
|
+
w: Math.round(box.width),
|
|
170
|
+
h: Math.round(box.height),
|
|
171
|
+
});
|
|
172
|
+
await page
|
|
173
|
+
.evaluate((b) => {
|
|
174
|
+
if (window.__wgApplyFocus) window.__wgApplyFocus(b);
|
|
175
|
+
}, box)
|
|
176
|
+
.catch(() => {});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
export async function cycleHighlightRings(page, highlights, gatesFast, opts) {
|
|
181
|
+
const list = highlights || [];
|
|
182
|
+
// defaultHoldMs: when set (fail path), use instead of legacy 900/200 so BUG
|
|
183
|
+
// rings stay visible ~2s before the error panel (PIA stubOnError RFC).
|
|
184
|
+
const defaultHoldMs = opts && opts.defaultHoldMs != null ? opts.defaultHoldMs : null;
|
|
185
|
+
const pace = opts && opts.pace !== undefined ? opts.pace : undefined;
|
|
186
|
+
let dock = opts && opts.todoDock;
|
|
187
|
+
const advanceTodos = !(opts && opts.advanceTodos === false);
|
|
188
|
+
// Push stubAfter/before dock before the first ring so Method after-panels
|
|
189
|
+
// show the updated checklist immediately (not only after the ring cycle).
|
|
190
|
+
if (dock) {
|
|
191
|
+
await page
|
|
192
|
+
.evaluate((d) => {
|
|
193
|
+
if (window.__wgSyncTodos) window.__wgSyncTodos({ sync: "set", dock: d });
|
|
194
|
+
}, dock)
|
|
195
|
+
.catch(() => {});
|
|
196
|
+
} else if (opts && opts.todoSync === "clear") {
|
|
197
|
+
await page
|
|
198
|
+
.evaluate(() => {
|
|
199
|
+
if (window.__wgSyncTodos) window.__wgSyncTodos({ sync: "clear" });
|
|
200
|
+
})
|
|
201
|
+
.catch(() => {});
|
|
202
|
+
}
|
|
203
|
+
let lastRingEndedAt = null;
|
|
204
|
+
for (let i = 0; i < list.length; i++) {
|
|
205
|
+
const h = list[i];
|
|
206
|
+
const ringStarted = Date.now();
|
|
207
|
+
try {
|
|
208
|
+
await ensureSelectorInView(page, h.selector);
|
|
209
|
+
await applyHighlightZoom(page, h.selector, h.zoom, h.zoomOut);
|
|
210
|
+
// Prefer live in-page rect (honors device-shell scale) over Playwright box.
|
|
211
|
+
const box = await page
|
|
212
|
+
.evaluate((sel) => {
|
|
213
|
+
const el = document.querySelector(sel);
|
|
214
|
+
if (!el) return null;
|
|
215
|
+
const r = el.getBoundingClientRect();
|
|
216
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
217
|
+
}, h.selector)
|
|
218
|
+
.catch(() => null);
|
|
219
|
+
if (box && box.width > 0 && box.height > 0) {
|
|
220
|
+
await showRing(page, box, h.label, h.tone || "planned", {
|
|
221
|
+
size: h.size,
|
|
222
|
+
weight: h.weight,
|
|
223
|
+
selector: h.selector,
|
|
224
|
+
focus: !!h.focus,
|
|
225
|
+
});
|
|
226
|
+
// focus is applied by FollowRing / showRing - no second apply needed
|
|
227
|
+
// Do NOT advanceTodoDock by ring index after stubAfter already set
|
|
228
|
+
// progress - that reset current back to 0 and the next block looked
|
|
229
|
+
// blank. Mid-act Method advance still uses syncTodoDockAdvance.
|
|
230
|
+
if (dock && advanceTodos) {
|
|
231
|
+
dock = advanceTodoDock(dock, i);
|
|
232
|
+
if (opts && opts.todoDockRef) opts.todoDockRef.current = dock;
|
|
233
|
+
await page
|
|
234
|
+
.evaluate((d) => {
|
|
235
|
+
if (window.__wgSyncTodos) window.__wgSyncTodos({ sync: "set", dock: d });
|
|
236
|
+
}, dock)
|
|
237
|
+
.catch(() => {});
|
|
238
|
+
} else if (!dock && ((opts && opts.todos && opts.todos.length) || (h.todos && h.todos.length))) {
|
|
239
|
+
const rows =
|
|
240
|
+
opts && opts.todos && opts.todos.length
|
|
241
|
+
? opts.todos
|
|
242
|
+
: normalizeTodos(h.todos, h.todoIndex);
|
|
243
|
+
await page
|
|
244
|
+
.evaluate((todos) => {
|
|
245
|
+
if (window.__wgSyncTodos) {
|
|
246
|
+
window.__wgSyncTodos({ sync: "set", list: todos });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Fallback for pages that never got installOverlay sync helper.
|
|
250
|
+
const ul = document.getElementById("wg-todos");
|
|
251
|
+
if (!ul) return;
|
|
252
|
+
ul.innerHTML = todos
|
|
253
|
+
.map((t) => {
|
|
254
|
+
const cls = t.current
|
|
255
|
+
? "wg-todo-current"
|
|
256
|
+
: t.done
|
|
257
|
+
? "wg-todo-done"
|
|
258
|
+
: "wg-todo-pending";
|
|
259
|
+
const mark = t.done ? "\u2713" : t.current ? "\u2192" : "\u25CB";
|
|
260
|
+
const esc = (s) =>
|
|
261
|
+
String(s)
|
|
262
|
+
.replace(/&/g, "&")
|
|
263
|
+
.replace(/</g, "<")
|
|
264
|
+
.replace(/>/g, ">");
|
|
265
|
+
return (
|
|
266
|
+
"<li class=\"" +
|
|
267
|
+
cls +
|
|
268
|
+
"\"><span class=\"wg-todo-mark\">" +
|
|
269
|
+
mark +
|
|
270
|
+
"</span><span>" +
|
|
271
|
+
esc(t.text || "") +
|
|
272
|
+
"</span></li>"
|
|
273
|
+
);
|
|
274
|
+
})
|
|
275
|
+
.join("");
|
|
276
|
+
}, rows)
|
|
277
|
+
.catch(() => {});
|
|
278
|
+
}
|
|
279
|
+
const authored = resolveFixtureDwellMs(h, { gatesFast, pace });
|
|
280
|
+
const legacyMs = i === list.length - 1 ? 200 : 900;
|
|
281
|
+
const holdMs =
|
|
282
|
+
authored != null ? authored : defaultHoldMs != null ? defaultHoldMs : legacyMs;
|
|
283
|
+
const gap = lastRingEndedAt != null ? ringStarted - lastRingEndedAt : null;
|
|
284
|
+
const holdExtra =
|
|
285
|
+
"hold=" +
|
|
286
|
+
holdMs +
|
|
287
|
+
"ms" +
|
|
288
|
+
(authored != null ? " authored" : " default") +
|
|
289
|
+
" box=" +
|
|
290
|
+
Math.round(box.x) +
|
|
291
|
+
"," +
|
|
292
|
+
Math.round(box.y) +
|
|
293
|
+
" " +
|
|
294
|
+
Math.round(box.width) +
|
|
295
|
+
"x" +
|
|
296
|
+
Math.round(box.height) +
|
|
297
|
+
(gap != null ? " gap=" + gap + "ms" : "");
|
|
298
|
+
demoHighlight(h, holdExtra);
|
|
299
|
+
demoLog(
|
|
300
|
+
" highlight " +
|
|
301
|
+
(i + 1) +
|
|
302
|
+
"/" +
|
|
303
|
+
list.length +
|
|
304
|
+
(gap != null && gap < 120 ? ansiPaint(ANSI.yellow, " WARN gap<120ms") : "") +
|
|
305
|
+
(holdMs < 250 && list.length > 1 ? ansiPaint(ANSI.yellow, " WARN short hold") : ""),
|
|
306
|
+
);
|
|
307
|
+
await new Promise((res) => setTimeout(res, holdMs));
|
|
308
|
+
lastRingEndedAt = Date.now();
|
|
309
|
+
} else {
|
|
310
|
+
demoHighlight(h, "SKIP no boundingBox");
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
demoHighlight(
|
|
314
|
+
h || { selector: h && h.selector, label: "(error)" },
|
|
315
|
+
"SKIP " + String(err && err.message ? err.message : err).slice(0, 80),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (list.length > 0) {
|
|
320
|
+
const last = list[list.length - 1];
|
|
321
|
+
await page
|
|
322
|
+
.evaluate((h) => {
|
|
323
|
+
if (!window.__wgFollowRing && !window.__wgPositionRing) return;
|
|
324
|
+
if (window.__wgFollowRing) {
|
|
325
|
+
window.__wgFollowRing(
|
|
326
|
+
h.selector,
|
|
327
|
+
h.label,
|
|
328
|
+
h.tone || "planned",
|
|
329
|
+
{ size: h.size || "md", weight: h.weight || "normal" },
|
|
330
|
+
!!h.focus,
|
|
331
|
+
);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const reposition = () => {
|
|
335
|
+
const el = document.querySelector(h.selector);
|
|
336
|
+
if (!el) {
|
|
337
|
+
if (window.__wgHideRing) window.__wgHideRing();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const box = el.getBoundingClientRect();
|
|
341
|
+
window.__wgPositionRing(box, h.label, h.tone || "planned", {
|
|
342
|
+
size: h.size || "md",
|
|
343
|
+
weight: h.weight || "normal",
|
|
344
|
+
});
|
|
345
|
+
if (h.focus && window.__wgApplyFocus) {
|
|
346
|
+
window.__wgApplyFocus({
|
|
347
|
+
x: box.x,
|
|
348
|
+
y: box.y,
|
|
349
|
+
width: box.width,
|
|
350
|
+
height: box.height,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
reposition();
|
|
355
|
+
window.__wgRingTrack = reposition;
|
|
356
|
+
window.addEventListener("resize", reposition);
|
|
357
|
+
window.addEventListener("scroll", reposition, true);
|
|
358
|
+
}, last)
|
|
359
|
+
.catch(() => {});
|
|
360
|
+
} else {
|
|
361
|
+
await hideRing(page);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
export async function showRing(page, box, label, tone, style) {
|
|
367
|
+
const size = normalizeHighlightSize(style && style.size);
|
|
368
|
+
const weight = normalizeHighlightWeight(style && style.weight);
|
|
369
|
+
const selector = style && style.selector ? String(style.selector) : "";
|
|
370
|
+
const focus = !!(style && style.focus);
|
|
371
|
+
await page
|
|
372
|
+
.evaluate(
|
|
373
|
+
({ box, label, tone, size, weight, selector, focus }) => {
|
|
374
|
+
if (selector && window.__wgFollowRing) {
|
|
375
|
+
window.__wgFollowRing(selector, label, tone || "planned", { size, weight }, focus);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (window.__wgStopRingFollow) window.__wgStopRingFollow();
|
|
379
|
+
if (window.__wgPositionRing) {
|
|
380
|
+
window.__wgPositionRing(box, label, tone || "planned", { size, weight });
|
|
381
|
+
}
|
|
382
|
+
if (focus && box && window.__wgApplyFocus) window.__wgApplyFocus(box);
|
|
383
|
+
else if (!focus && window.__wgClearFocus) window.__wgClearFocus();
|
|
384
|
+
},
|
|
385
|
+
{ box, label, tone: tone || "planned", size, weight, selector, focus },
|
|
386
|
+
)
|
|
387
|
+
.catch(() => {});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
/** Match a locator to a stubBefore slot by overlapping bounding boxes. */
|
|
392
|
+
async function matchStubForLocator(page, locator, stubs) {
|
|
393
|
+
if (!stubs || stubs.length === 0) return null;
|
|
394
|
+
const box = await locator.boundingBox().catch(() => null);
|
|
395
|
+
if (!box) return null;
|
|
396
|
+
for (const s of stubs) {
|
|
397
|
+
try {
|
|
398
|
+
const b = await page.locator(s.selector).first().boundingBox();
|
|
399
|
+
if (
|
|
400
|
+
b &&
|
|
401
|
+
Math.abs(b.x - box.x) < 4 &&
|
|
402
|
+
Math.abs(b.y - box.y) < 4 &&
|
|
403
|
+
Math.abs(b.width - box.width) < 8
|
|
404
|
+
) {
|
|
405
|
+
return s;
|
|
406
|
+
}
|
|
407
|
+
} catch {
|
|
408
|
+
/* skip */
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
/** 0-based stub index for Method fill/click todo advance; -1 if none. */
|
|
416
|
+
export async function matchStubIndexForLocator(page, locator, stubs) {
|
|
417
|
+
if (!stubs || stubs.length === 0) return -1;
|
|
418
|
+
const box = await locator.boundingBox().catch(() => null);
|
|
419
|
+
if (!box) return -1;
|
|
420
|
+
for (let i = 0; i < stubs.length; i++) {
|
|
421
|
+
const s = stubs[i];
|
|
422
|
+
try {
|
|
423
|
+
const b = await page.locator(s.selector).first().boundingBox();
|
|
424
|
+
if (
|
|
425
|
+
b &&
|
|
426
|
+
Math.abs(b.x - box.x) < 4 &&
|
|
427
|
+
Math.abs(b.y - box.y) < 4 &&
|
|
428
|
+
Math.abs(b.width - box.width) < 8
|
|
429
|
+
) {
|
|
430
|
+
return i;
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
/* skip */
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return -1;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
export async function captionForLocator(page, locator, stubs, fallback) {
|
|
441
|
+
const matched = await matchStubForLocator(page, locator, stubs);
|
|
442
|
+
if (matched) {
|
|
443
|
+
return {
|
|
444
|
+
label: formatHighlightCaption(matched),
|
|
445
|
+
tone: normalizeHighlightTone(matched.tone),
|
|
446
|
+
size: normalizeHighlightSize(matched.size),
|
|
447
|
+
weight: normalizeHighlightWeight(matched.weight),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return { label: fallback, tone: "auto", size: "md", weight: "normal" };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
export async function dwellMatchedStub(page, locator, stubs, pacing) {
|
|
455
|
+
const matched = await matchStubForLocator(page, locator, stubs);
|
|
456
|
+
if (!matched) return;
|
|
457
|
+
const ms = resolveFixtureDwellMs(matched, {
|
|
458
|
+
gatesFast: !!(pacing && pacing.gatesFast),
|
|
459
|
+
pace: pacing && pacing.demoPace,
|
|
460
|
+
});
|
|
461
|
+
if (ms != null && ms > 0) {
|
|
462
|
+
await new Promise((res) => setTimeout(res, ms));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
export async function hideRing(page) {
|
|
468
|
+
await page
|
|
469
|
+
.evaluate(() => {
|
|
470
|
+
if (window.__wgHideRing) window.__wgHideRing();
|
|
471
|
+
})
|
|
472
|
+
.catch(() => {});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* True if the engine's own narrate() (waygraph's public export, for a Block
|
|
478
|
+
* author explicitly captioning one action) JUST positioned the ring for
|
|
479
|
+
* THIS action, moments ago - the automatic per-fill/per-click narration
|
|
480
|
+
* below should not immediately overwrite an author's own explicit caption
|
|
481
|
+
* with its generic guess.
|
|
482
|
+
*/
|
|
483
|
+
export async function wasJustNarrated(page) {
|
|
484
|
+
return page
|
|
485
|
+
.evaluate(() => {
|
|
486
|
+
const w = window;
|
|
487
|
+
return typeof w.__wgLastNarrate === "number" && Date.now() - w.__wgLastNarrate < 500;
|
|
488
|
+
})
|
|
489
|
+
.catch(() => false);
|
|
490
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Moved verbatim from the former CHAIN_RUNNER_SCRIPT template string in cli.ts (see src/ARCHITECTURE.md).
|
|
2
|
+
// Runs inside the target project's own waygraph copy - keep it dependency-light and self-contained.
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getMemStub } from "../mem-stub.js";
|
|
5
|
+
|
|
6
|
+
function seedMemFromRequires(mem, requires, json, label, stubActive) {
|
|
7
|
+
if (requires.length === 0) {
|
|
8
|
+
if (json !== undefined) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"waygraph chain: \"" + label + "\" takes no input (empty requires) but got a payload: " + json,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (json === undefined) {
|
|
16
|
+
// --data / WAYGRAPH_DATA supplies the same payload shape as blockName({...}).
|
|
17
|
+
const fromEnv = process.env.WAYGRAPH_DATA;
|
|
18
|
+
if (fromEnv && fromEnv.trim()) json = fromEnv;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let parsed;
|
|
22
|
+
let haveParsed = false;
|
|
23
|
+
if (json !== undefined) {
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(json);
|
|
26
|
+
haveParsed = true;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new Error("waygraph chain: \"" + label + "\" payload is not valid JSON - " + String(err));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!haveParsed) {
|
|
33
|
+
// No payload at all - only a hard failure when memStub isn't active for
|
|
34
|
+
// this flow; when it is, fall through with an empty object so the
|
|
35
|
+
// per-key loop below gets a chance to fill each one from the registry.
|
|
36
|
+
if (!stubActive) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"waygraph chain: \"" + label + "\" requires " + requires.map((k) => k.name).join(", ") +
|
|
39
|
+
" - give a JSON payload (inline blockName({...}) or --data '{...}'), or enable --mem-stub" +
|
|
40
|
+
" with registerMemStub for these keys",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
parsed = {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Keyed-by-name when every require name is a top-level key (preferred for
|
|
47
|
+
// --data and for {"saucedemo.credentials":{...}} even with one require).
|
|
48
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
49
|
+
const keyed = requires.every((k) => k.name in parsed);
|
|
50
|
+
if (keyed) {
|
|
51
|
+
for (const k of requires) mem.set(k, parsed[k.name]);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Single-key shorthand: the whole payload IS that one key's value - only
|
|
56
|
+
// when a real payload was actually given (the stub-only "{}" fallback
|
|
57
|
+
// above must fall through to per-key stub resolution instead).
|
|
58
|
+
if (haveParsed && requires.length === 1) {
|
|
59
|
+
mem.set(requires[0], parsed);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (haveParsed && (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"waygraph chain: \"" + label + "\" requires " + requires.length + " keys (" +
|
|
65
|
+
requires.map((k) => k.name).join(", ") + ") - payload must be an object keyed by each key's name",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const stillMissing = [];
|
|
69
|
+
for (const k of requires) {
|
|
70
|
+
if (k.name in parsed) {
|
|
71
|
+
mem.set(k, parsed[k.name]);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const fake = stubActive ? getMemStub(k) : undefined;
|
|
75
|
+
if (fake) {
|
|
76
|
+
mem.set(k, fake());
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
stillMissing.push(k);
|
|
80
|
+
}
|
|
81
|
+
if (stillMissing.length > 0) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
"waygraph chain: \"" + label + "\" payload is missing required key(s): " +
|
|
84
|
+
stillMissing.map((k) => "\"" + k.name + "\"").join(", "),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
export function seedMemForBlock(mem, resolved, json, stubActive) {
|
|
91
|
+
seedMemFromRequires(mem, resolved.block.requires ?? [], json, resolved.exportName, stubActive);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Same idea as seedMemForBlock, but for a whole Flow segment
|
|
97
|
+
* ("loginFlow({...}) then ...") - a Flow has no single .requires of its
|
|
98
|
+
* own, so this unions every constituent Block's requires (deduped by key
|
|
99
|
+
* name) and seeds them all from one JSON payload, same keyed-by-name shape
|
|
100
|
+
* as a multi-key Block payload already uses.
|
|
101
|
+
*/
|
|
102
|
+
export function seedMemForFlow(mem, flowBlocks, json, label, stubActive) {
|
|
103
|
+
const seen = new Map();
|
|
104
|
+
for (const bi of flowBlocks) {
|
|
105
|
+
for (const k of bi.block.requires ?? []) {
|
|
106
|
+
if (!seen.has(k.name)) seen.set(k.name, k);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
seedMemFromRequires(mem, Array.from(seen.values()), json, label, stubActive);
|
|
110
|
+
}
|