waygraph 0.16.1 → 0.16.4
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 +12 -0
- package/dist/analysis/practices-check.d.ts +1 -1
- package/dist/analysis/practices-check.js +36 -0
- package/dist/beta.d.ts +6 -0
- package/dist/beta.js +5 -0
- package/dist/cli/commands/codemod-flows.js +2 -2
- package/dist/cli/commands/codemod-stubs.js +132 -2
- package/dist/cli/commands/pilot.js +3 -16
- package/dist/cli/commands/session-ops.js +26 -1
- package/dist/cli/flags.d.ts +1 -1
- package/dist/cli/flags.js +1 -1
- package/dist/cli/run-chain.js +2 -2
- package/dist/cli/usage.js +6 -0
- package/dist/cli/util.js +1 -1
- package/dist/demo/narration/demo-pace.d.ts +1 -1
- package/dist/demo/narration/demo-pace.js +1 -1
- package/dist/demo/narration/merge.d.ts +1 -1
- package/dist/demo/narration/merge.js +1 -1
- package/dist/demo/narration/narration.d.ts +2 -6
- package/dist/demo/narration/narration.js +5 -17
- package/dist/demo/narration/stub-ctx.js +126 -124
- package/dist/demo/overlay/cursor.js +1 -1
- package/dist/demo/overlay/inpage/banner.js +5 -0
- package/dist/demo/overlay/inpage/core.js +1 -6
- package/dist/demo/overlay/inpage/network.js +1 -1
- package/dist/demo/overlay/inpage/todos.js +0 -15
- package/dist/demo/overlay/network-capture.js +2 -2
- package/dist/demo/overlay/overlay-css.js +19 -76
- package/dist/demo/overlay/overlay-stage.js +1 -1
- package/dist/demo/overlay/overlay.js +3 -2
- package/dist/demo/overlay/phase-fixtures.js +5 -5
- package/dist/demo/overlay/pilot/shell.js +7 -5
- package/dist/demo/overlay/rings.d.ts +0 -8
- package/dist/demo/overlay/rings.js +0 -15
- package/dist/demo/overlay/ui/components/ring.js +2 -2
- package/dist/demo/overlay/ui/overlay.css +136 -33
- package/dist/demo/overlay/ui/shadow.js +69 -2
- package/dist/demo/stepper/after.js +4 -17
- package/dist/demo/stepper/before.js +3 -1
- package/dist/demo/stepper/demo-log.js +8 -8
- package/dist/demo/stepper/interaction-patch.js +3 -15
- package/dist/demo/stepper/panel-after.js +1 -1
- package/dist/demo/stepper/slides.d.ts +3 -0
- package/dist/demo/stepper/slides.js +18 -2
- package/dist/demo/stepper/stepper.js +1 -1
- package/dist/demo/stepper/teardown.js +0 -1
- package/dist/demo/verbose-log.js +1 -1
- package/dist/engine/block/block.d.ts +1 -12
- package/dist/engine/block/kind.d.ts +3 -0
- package/dist/engine/block/kind.js +11 -0
- package/dist/engine/block/kinds/method.d.ts +0 -2
- package/dist/engine/block/kinds/method.js +0 -1
- package/dist/engine/block/kinds/nav.js +1 -1
- package/dist/engine/errors.d.ts +12 -0
- package/dist/engine/errors.js +12 -0
- package/dist/engine/read/read-block.d.ts +61 -0
- package/dist/engine/read/read-block.js +94 -0
- package/dist/engine/read/read-key.d.ts +32 -0
- package/dist/engine/read/read-key.js +34 -0
- package/dist/engine/read/read-page.d.ts +19 -0
- package/dist/engine/read/read-page.js +50 -0
- package/dist/engine/read/readers.d.ts +82 -0
- package/dist/engine/read/readers.js +43 -0
- package/dist/engine/read/run-read.d.ts +17 -0
- package/dist/engine/read/run-read.js +156 -0
- package/dist/engine/run/connect.js +3 -2
- package/dist/engine/run/events.d.ts +28 -10
- package/dist/engine/run/events.js +52 -38
- package/dist/engine/run/run.d.ts +1 -19
- package/dist/engine/run/run.js +21 -97
- package/dist/engine/stub/ring.d.ts +2 -4
- package/dist/engine/stub/ring.js +1 -0
- package/dist/engine/stub/stub.d.ts +38 -25
- package/dist/index.d.ts +9 -7
- package/dist/index.js +11 -6
- package/dist/live/auto/auto.js +2 -3
- package/dist/live/auto/menu.js +3 -2
- package/dist/live/auto-session/auto-session.d.ts +9 -2
- package/dist/live/auto-session/auto-session.js +23 -20
- package/dist/live/auto-session/feed.d.ts +16 -0
- package/dist/live/auto-session/feed.js +71 -0
- package/dist/live/auto-session/helpers.d.ts +7 -0
- package/dist/live/auto-session/helpers.js +9 -0
- package/dist/live/auto-session/ipc.d.ts +15 -47
- package/dist/live/auto-session/ipc.js +34 -4
- package/dist/live/auto-session/protocol.d.ts +127 -0
- package/dist/live/auto-session/protocol.js +6 -0
- package/dist/live/auto-session/read.d.ts +25 -0
- package/dist/live/auto-session/read.js +50 -0
- package/dist/live/traverse/worker.d.ts +1 -0
- package/dist/live/traverse/worker.js +6 -1
- package/dist/manifest/manifest.d.ts +22 -2
- package/dist/manifest/manifest.js +27 -1
- package/dist/manifest/project.d.ts +10 -1
- package/dist/manifest/project.js +9 -12
- package/dist/manifest/read-blocks.d.ts +2 -0
- package/dist/manifest/read-blocks.js +23 -0
- package/dist/manifest/scan.d.ts +3 -2
- package/dist/manifest/scan.js +8 -4
- package/dist/runner/plan/find.js +2 -2
- package/dist/runner/plan/plan.js +1 -1
- package/dist/runner/plan/seed-mem.js +5 -5
- package/dist/runner/plan/spec.js +1 -1
- package/dist/runner/runner.d.ts +0 -1
- package/dist/runner/runner.js +6 -6
- package/dist/runtime/full-launch.d.ts +2 -1
- package/dist/runtime/full-launch.js +29 -27
- package/dist/runtime/run-log.d.ts +2 -0
- package/dist/runtime/run-log.js +13 -6
- package/examples/saucedemo/README.md +2 -2
- package/examples/saucedemo/src/blocks/SITE-MAP.md +3 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/cart.read.block.ts +26 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
- package/examples/saucedemo/src/blocks/saucedemo-web/header.read.block.ts +25 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.read.block.ts +28 -0
- package/examples/saucedemo/src/flows/api-demo.flow.ts +1 -1
- package/examples/saucedemo/src/flows/cart-bulk.flow.ts +1 -1
- package/examples/saucedemo/src/flows/cart-continue.flow.ts +1 -1
- package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
- package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +3 -2
- package/examples/saucedemo/src/flows/checkout.flow.ts +13 -12
- package/examples/saucedemo/src/flows/login.flow.ts +1 -1
- package/examples/saucedemo/src/flows/shop.flow.ts +3 -2
- package/examples/saucedemo/src/flows/stub-on-error.flow.ts +1 -1
- package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
- package/examples/saucedemo/src/states/checkout.reads.ts +9 -0
- package/examples/saucedemo/src/stubs/device-showcase.ts +70 -76
- package/examples/saucedemo/src/stubs/finish-order.ts +38 -0
- package/examples/saucedemo/src/stubs/login.ts +5 -5
- package/package.json +5 -1
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
- package/templates/quickstart/src/flows/api-demo.flow.ts +1 -1
- package/templates/quickstart/src/flows/cart-bulk.flow.ts +1 -1
- package/templates/quickstart/src/flows/cart-continue.flow.ts +1 -1
- package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
- package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +3 -2
- package/templates/quickstart/src/flows/checkout.flow.ts +13 -12
- package/templates/quickstart/src/flows/login.flow.ts +1 -1
- package/templates/quickstart/src/flows/shop.flow.ts +3 -2
- package/templates/quickstart/src/flows/stub-on-error.flow.ts +1 -1
- package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
- package/templates/quickstart/src/stubs/device-showcase.ts +70 -76
- package/templates/quickstart/src/stubs/finish-order.ts +38 -0
- package/templates/quickstart/src/stubs/login.ts +5 -5
- package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +1 -1
- package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +1 -1
- package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +1 -1
- package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +4 -4
- package/templates/scaffold/src/flows/README.md +1 -1
- package/templates/scaffold/src/flows/home/cart.flow.ts +7 -7
- package/templates/scaffold/src/flows/home/hello.flow.ts +10 -7
- package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +1 -9
- package/templates/skills/waygraph-convention.skill.md +10 -2
- package/templates/skills/waygraph-pilot.skill.md +7 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { ignorePageGone, ignoreTimeout } from "../page-gone.js";
|
|
2
|
+
import { Reader } from "./readers.js";
|
|
3
|
+
/** In the page (self-contained, shipped by evaluateAll): reads a field, list rows or a table from the matched elements. */
|
|
4
|
+
function readInPage(matched, ask) {
|
|
5
|
+
const shown = (el) => {
|
|
6
|
+
const box = el.getBoundingClientRect();
|
|
7
|
+
const style = getComputedStyle(el);
|
|
8
|
+
return box.width > 0 && box.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
9
|
+
};
|
|
10
|
+
// textContent, whitespace collapsed: innerText forces layout per element (125ms vs 3ms for 8,000 table cells).
|
|
11
|
+
const textOf = (el) => (el.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
12
|
+
const take = (els, plan) => {
|
|
13
|
+
const first = els[0];
|
|
14
|
+
switch (plan.kind) {
|
|
15
|
+
case "count":
|
|
16
|
+
return els.length;
|
|
17
|
+
case "visible":
|
|
18
|
+
return els.some(shown);
|
|
19
|
+
case "texts":
|
|
20
|
+
return els.map(textOf);
|
|
21
|
+
case "attr":
|
|
22
|
+
return first ? first.getAttribute(plan.attr ?? "") : null;
|
|
23
|
+
case "value":
|
|
24
|
+
return first && "value" in first ? String(first.value) : null;
|
|
25
|
+
default:
|
|
26
|
+
// text, and number (parsed in Node from the text)
|
|
27
|
+
return first ? textOf(first) : null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const within = (root, plan) => (plan.selector ? [...root.querySelectorAll(plan.selector)] : [root]);
|
|
31
|
+
if (ask.mode === "field")
|
|
32
|
+
return take(matched, ask.plan);
|
|
33
|
+
if (ask.mode === "list") {
|
|
34
|
+
return matched.map((row) => {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const [name, plan] of Object.entries(ask.fields))
|
|
37
|
+
out[name] = take(within(row, plan), plan);
|
|
38
|
+
return out;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const table = matched[0];
|
|
42
|
+
if (!table)
|
|
43
|
+
return null;
|
|
44
|
+
const HEAD = "th, [role=columnheader]";
|
|
45
|
+
const CELL = "td, th, [role=cell], [role=gridcell], [role=rowheader], [role=columnheader]";
|
|
46
|
+
const rows = [...table.querySelectorAll("tr, [role=row]")];
|
|
47
|
+
const headRow = rows.find((r) => r.querySelector(HEAD) && !r.querySelector("td, [role=cell], [role=gridcell]"));
|
|
48
|
+
const headers = headRow ? [...headRow.querySelectorAll(CELL)].map(textOf) : [];
|
|
49
|
+
const index = {};
|
|
50
|
+
const absent = [];
|
|
51
|
+
for (const [name, header] of Object.entries(ask.columns)) {
|
|
52
|
+
const at = headers.findIndex((h) => h.toLowerCase() === header.toLowerCase());
|
|
53
|
+
if (at < 0)
|
|
54
|
+
absent.push(name);
|
|
55
|
+
else
|
|
56
|
+
index[name] = at;
|
|
57
|
+
}
|
|
58
|
+
const body = rows.filter((r) => r !== headRow && r.querySelector("td, [role=cell], [role=gridcell], [role=rowheader]"));
|
|
59
|
+
const out = body.map((r) => {
|
|
60
|
+
const cells = [...r.querySelectorAll(CELL)];
|
|
61
|
+
const row = {};
|
|
62
|
+
for (const name of Object.keys(ask.columns)) {
|
|
63
|
+
const cell = index[name] === undefined ? undefined : cells[index[name]];
|
|
64
|
+
row[name] = cell ? textOf(cell) : null;
|
|
65
|
+
}
|
|
66
|
+
return row;
|
|
67
|
+
});
|
|
68
|
+
const declared = Number(table.getAttribute("aria-rowcount"));
|
|
69
|
+
return { rows: out, absent, rowCount: Number.isFinite(declared) && declared > 0 ? declared - (headRow ? 1 : 0) : out.length };
|
|
70
|
+
}
|
|
71
|
+
/** In the page: resolves once the DOM has had no change for `quietMs` (or after `capMs`, whichever comes first). */
|
|
72
|
+
function domQuiet({ quietMs, capMs }) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const done = () => {
|
|
75
|
+
observer.disconnect();
|
|
76
|
+
clearTimeout(quiet);
|
|
77
|
+
clearTimeout(cap);
|
|
78
|
+
resolve();
|
|
79
|
+
};
|
|
80
|
+
let quiet = setTimeout(done, quietMs);
|
|
81
|
+
const cap = setTimeout(done, capMs);
|
|
82
|
+
const observer = new MutationObserver(() => {
|
|
83
|
+
clearTimeout(quiet);
|
|
84
|
+
quiet = setTimeout(done, quietMs);
|
|
85
|
+
});
|
|
86
|
+
observer.observe(document, { subtree: true, childList: true, characterData: true, attributes: true });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Waits for the page to stop changing before a read. A step's verify can pass before the app has rendered where it
|
|
91
|
+
* landed (a URL check passes the moment a client-side route changes), so a read right after it can see the old page.
|
|
92
|
+
*/
|
|
93
|
+
export async function settlePage(page, quietMs = 150, capMs = 2000) {
|
|
94
|
+
await page.waitForLoadState("domcontentloaded", { timeout: capMs }).catch(ignoreTimeout);
|
|
95
|
+
await page.evaluate(domQuiet, { quietMs, capMs }).catch(ignorePageGone);
|
|
96
|
+
}
|
|
97
|
+
/** The first number in a display text: `"$1,299.00"` -> 1299, `"41 likes"` -> 41, no number -> null. */
|
|
98
|
+
export function firstNumber(text) {
|
|
99
|
+
if (typeof text !== "string")
|
|
100
|
+
return null;
|
|
101
|
+
const m = /-?\d[\d,]*(?:\.\d+)?/.exec(text);
|
|
102
|
+
return m ? Number(m[0].replace(/,/g, "")) : null;
|
|
103
|
+
}
|
|
104
|
+
function finishField(plan, raw) {
|
|
105
|
+
return plan.kind === "number" ? firstNumber(raw) : raw;
|
|
106
|
+
}
|
|
107
|
+
async function readOne(page, mem, plan, path, notes) {
|
|
108
|
+
if (plan.kind === "fn")
|
|
109
|
+
return (await plan.fn(page, mem)) ?? null;
|
|
110
|
+
const matched = page.locator(plan.selector);
|
|
111
|
+
if (plan.kind === "list") {
|
|
112
|
+
const rows = (await matched.evaluateAll(readInPage, { mode: "list", fields: plan.fields }));
|
|
113
|
+
return rows.map((row) => {
|
|
114
|
+
for (const [name, field] of Object.entries(plan.fields))
|
|
115
|
+
row[name] = finishField(field, row[name]);
|
|
116
|
+
return row;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (plan.kind === "table") {
|
|
120
|
+
const got = (await matched.evaluateAll(readInPage, { mode: "table", columns: plan.columns }));
|
|
121
|
+
if (!got)
|
|
122
|
+
return null;
|
|
123
|
+
const why = [];
|
|
124
|
+
if (got.absent.length)
|
|
125
|
+
why.push(`no column ${got.absent.map((c) => JSON.stringify(plan.columns[c])).join(", ")}`);
|
|
126
|
+
if (got.rowCount > got.rows.length)
|
|
127
|
+
why.push(`${got.rows.length} of ${got.rowCount} rows rendered (scroll or page to read more)`);
|
|
128
|
+
if (why.length)
|
|
129
|
+
notes.set(path, why.join("; "));
|
|
130
|
+
return got.rows;
|
|
131
|
+
}
|
|
132
|
+
return finishField(plan, await matched.evaluateAll(readInPage, { mode: "field", plan }));
|
|
133
|
+
}
|
|
134
|
+
/** Reads every field of every spec, in parallel, into leaves by path. */
|
|
135
|
+
export async function runReadSpecs(page, mem, specs) {
|
|
136
|
+
const values = new Map();
|
|
137
|
+
const notes = new Map();
|
|
138
|
+
const jobs = [];
|
|
139
|
+
const walk = (fields, prefix) => {
|
|
140
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
141
|
+
if (value === undefined)
|
|
142
|
+
continue;
|
|
143
|
+
const path = `${prefix}.${name}`;
|
|
144
|
+
if (value instanceof Reader) {
|
|
145
|
+
values.set(path, null);
|
|
146
|
+
jobs.push(readOne(page, mem, value.plan, path, notes).then((v) => void values.set(path, v)));
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
walk(value, path);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
for (const spec of specs)
|
|
153
|
+
walk(spec.fields, spec.namespace);
|
|
154
|
+
await Promise.all(jobs);
|
|
155
|
+
return { values, notes };
|
|
156
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runVerify, runPrecondition } from "../trait/trait.js";
|
|
2
2
|
import { isConnected, lastLeaf, stepEnded, stepStarted } from "./events.js";
|
|
3
|
+
import { blockKindOf } from "../block/kind.js";
|
|
3
4
|
/**
|
|
4
5
|
* Composes two Blocks into one, typechecked so this only compiles when A's output
|
|
5
6
|
* tag equals B's input tag. The composed Block's act runs A's full instruction
|
|
@@ -25,7 +26,7 @@ export function connect(a, b, layouts) {
|
|
|
25
26
|
instruction: {
|
|
26
27
|
async act(page, input, mem) {
|
|
27
28
|
if (!isConnected(a))
|
|
28
|
-
stepStarted(a.name, input.__state);
|
|
29
|
+
stepStarted(a.name, blockKindOf(a), input.__state);
|
|
29
30
|
await runPrecondition(a.instruction.precondition, input, page, mem, a.name);
|
|
30
31
|
await a.instruction.act(page, input, mem);
|
|
31
32
|
const aObserved = a.instruction.observe
|
|
@@ -42,7 +43,7 @@ export function connect(a, b, layouts) {
|
|
|
42
43
|
}
|
|
43
44
|
stepEnded(lastLeaf(a), mid.__state);
|
|
44
45
|
if (!isConnected(b))
|
|
45
|
-
stepStarted(b.name, mid.__state);
|
|
46
|
+
stepStarted(b.name, blockKindOf(b), mid.__state);
|
|
46
47
|
await runPrecondition(b.instruction.precondition, mid, page, mem, b.name);
|
|
47
48
|
await b.instruction.act(page, mid, mem);
|
|
48
49
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BlockKind } from "../block/kind.js";
|
|
1
2
|
export type RunEvent = {
|
|
2
3
|
type: "run:start";
|
|
3
4
|
runId: string;
|
|
@@ -9,6 +10,7 @@ export type RunEvent = {
|
|
|
9
10
|
at: number;
|
|
10
11
|
index: number;
|
|
11
12
|
block: string;
|
|
13
|
+
kind: BlockKind;
|
|
12
14
|
from: string;
|
|
13
15
|
} | {
|
|
14
16
|
type: "step:end";
|
|
@@ -16,6 +18,7 @@ export type RunEvent = {
|
|
|
16
18
|
at: number;
|
|
17
19
|
index: number;
|
|
18
20
|
block: string;
|
|
21
|
+
kind: BlockKind;
|
|
19
22
|
to: string;
|
|
20
23
|
ms: number;
|
|
21
24
|
} | {
|
|
@@ -24,9 +27,18 @@ export type RunEvent = {
|
|
|
24
27
|
at: number;
|
|
25
28
|
index: number;
|
|
26
29
|
block: string;
|
|
30
|
+
kind: BlockKind;
|
|
27
31
|
error: string;
|
|
28
32
|
ms: number;
|
|
29
|
-
} | {
|
|
33
|
+
} | ({
|
|
34
|
+
type: "step:read";
|
|
35
|
+
runId: string;
|
|
36
|
+
at: number;
|
|
37
|
+
index: number;
|
|
38
|
+
block: string;
|
|
39
|
+
kind: BlockKind;
|
|
40
|
+
checkpoint: string;
|
|
41
|
+
} & StepRead) | {
|
|
30
42
|
type: "run:end";
|
|
31
43
|
runId: string;
|
|
32
44
|
at: number;
|
|
@@ -41,22 +53,28 @@ export type RunEvent = {
|
|
|
41
53
|
kind: "video" | "trace" | "screenshot";
|
|
42
54
|
path: string;
|
|
43
55
|
};
|
|
56
|
+
/** What the page showed after a step, from the Read Blocks for the Checkpoint it landed on (beta). */
|
|
57
|
+
export interface StepRead {
|
|
58
|
+
read: Record<string, unknown>;
|
|
59
|
+
missing: string[];
|
|
60
|
+
notes: Record<string, string>;
|
|
61
|
+
readBlocks: string[];
|
|
62
|
+
/** A Read Block that failed (two reading one key, say): the step still ran; the read is empty. */
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
|
44
65
|
/** What {@link onRunEvent} calls with each event. */
|
|
45
66
|
export type RunEventListener = (event: RunEvent) => void;
|
|
46
67
|
/** Subscribes to every run event in this process; returns the unsubscribe function. */
|
|
47
68
|
export declare function onRunEvent(listener: RunEventListener): () => void;
|
|
48
69
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
70
|
+
* Runs `body` as a run: opens one (`run:start` ... `run:end`), or joins the run its caller is already in (a flow nested in
|
|
71
|
+
* another, a runner driving a chain step by step). `resultOf` names the outcome for `run:end`.
|
|
51
72
|
*/
|
|
52
|
-
export declare function
|
|
53
|
-
|
|
54
|
-
export declare function endRun(opened: boolean, outcome: {
|
|
55
|
-
result?: string;
|
|
56
|
-
error?: unknown;
|
|
57
|
-
}): void;
|
|
58
|
-
export declare function stepStarted(block: string, from: string): void;
|
|
73
|
+
export declare function inRun<T>(label: string | undefined, body: () => Promise<T>, resultOf?: (value: T) => string | undefined): Promise<T>;
|
|
74
|
+
export declare function stepStarted(block: string, kind: BlockKind, from: string): void;
|
|
59
75
|
export declare function stepEnded(block: string, to: string): void;
|
|
76
|
+
/** What the page showed after the current step (emitted between its verify and its `step:end`). */
|
|
77
|
+
export declare function stepRead(checkpoint: string, read: StepRead): void;
|
|
60
78
|
/** The step that started last failed. */
|
|
61
79
|
export declare function stepFailed(error: unknown): void;
|
|
62
80
|
/** The id of the run open right now, if any. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Run events: what happened, step by step, during a run. Core emits them; subscribers (the run log, the demo overlay,
|
|
2
2
|
// reports, Waygraph UI) listen. Emitting never throws into the run.
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
4
|
const listeners = new Set();
|
|
4
5
|
/** Subscribes to every run event in this process; returns the unsubscribe function. */
|
|
5
6
|
export function onRunEvent(listener) {
|
|
@@ -16,68 +17,81 @@ function emit(event) {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
|
-
let open;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* The run each async call chain is in. Per chain, not per process: two runs at once (parallel traverse workers, a UI
|
|
22
|
+
* hosting several) each get their own id and step counter; a flow nested in another joins its parent's run.
|
|
23
23
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
/** Closes the run when the matching `beginRun` opened it. */
|
|
35
|
-
export function endRun(opened, outcome) {
|
|
36
|
-
if (!open)
|
|
37
|
-
return;
|
|
38
|
-
open.depth -= 1;
|
|
39
|
-
if (!opened)
|
|
40
|
-
return;
|
|
24
|
+
const scope = new AsyncLocalStorage();
|
|
25
|
+
/**
|
|
26
|
+
* Runs `body` as a run: opens one (`run:start` ... `run:end`), or joins the run its caller is already in (a flow nested in
|
|
27
|
+
* another, a runner driving a chain step by step). `resultOf` names the outcome for `run:end`.
|
|
28
|
+
*/
|
|
29
|
+
export async function inRun(label, body, resultOf) {
|
|
30
|
+
if (scope.getStore())
|
|
31
|
+
return body();
|
|
41
32
|
const at = Date.now();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
33
|
+
const run = { runId: `${new Date(at).toISOString().replace(/[:.]/g, "-")}-${crypto.randomUUID().slice(0, 8)}`, startedAt: at, index: 0 };
|
|
34
|
+
emit({ type: "run:start", runId: run.runId, at, ...(label ? { label } : {}) });
|
|
35
|
+
const end = (outcome) => {
|
|
36
|
+
const done = Date.now();
|
|
37
|
+
emit({
|
|
38
|
+
type: "run:end",
|
|
39
|
+
runId: run.runId,
|
|
40
|
+
at: done,
|
|
41
|
+
ok: !("error" in outcome),
|
|
42
|
+
...(outcome.result !== undefined ? { result: outcome.result } : {}),
|
|
43
|
+
...("error" in outcome ? { error: outcome.error instanceof Error ? outcome.error.message : String(outcome.error) } : {}),
|
|
44
|
+
ms: done - run.startedAt,
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
const value = await scope.run(run, body);
|
|
49
|
+
end({ result: resultOf?.(value) });
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
end({ error: err });
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
|
-
export function stepStarted(block, from) {
|
|
57
|
+
export function stepStarted(block, kind, from) {
|
|
58
|
+
const open = scope.getStore();
|
|
54
59
|
if (!open)
|
|
55
60
|
return;
|
|
56
61
|
const at = Date.now();
|
|
57
|
-
open.step = { index: open.index++, block, startedAt: at };
|
|
58
|
-
emit({ type: "step:start", runId: open.runId, at, index: open.step.index, block, from });
|
|
62
|
+
open.step = { index: open.index++, block, kind, startedAt: at };
|
|
63
|
+
emit({ type: "step:start", runId: open.runId, at, index: open.step.index, block, kind, from });
|
|
59
64
|
}
|
|
60
65
|
export function stepEnded(block, to) {
|
|
66
|
+
const open = scope.getStore();
|
|
61
67
|
if (!open?.step)
|
|
62
68
|
return;
|
|
63
69
|
const at = Date.now();
|
|
64
|
-
emit({ type: "step:end", runId: open.runId, at, index: open.step.index, block, to, ms: at - open.step.startedAt });
|
|
70
|
+
emit({ type: "step:end", runId: open.runId, at, index: open.step.index, block, kind: open.step.kind, to, ms: at - open.step.startedAt });
|
|
65
71
|
open.step = undefined;
|
|
66
72
|
}
|
|
73
|
+
/** What the page showed after the current step (emitted between its verify and its `step:end`). */
|
|
74
|
+
export function stepRead(checkpoint, read) {
|
|
75
|
+
const open = scope.getStore();
|
|
76
|
+
if (!open?.step)
|
|
77
|
+
return;
|
|
78
|
+
emit({ type: "step:read", runId: open.runId, at: Date.now(), index: open.step.index, block: open.step.block, kind: open.step.kind, checkpoint, ...read });
|
|
79
|
+
}
|
|
67
80
|
/** The step that started last failed. */
|
|
68
81
|
export function stepFailed(error) {
|
|
82
|
+
const open = scope.getStore();
|
|
69
83
|
if (!open?.step)
|
|
70
84
|
return;
|
|
71
85
|
const at = Date.now();
|
|
72
|
-
const { index, block, startedAt } = open.step;
|
|
73
|
-
emit({ type: "step:error", runId: open.runId, at, index, block, error: error instanceof Error ? error.message : String(error), ms: at - startedAt });
|
|
86
|
+
const { index, block, kind, startedAt } = open.step;
|
|
87
|
+
emit({ type: "step:error", runId: open.runId, at, index, block, kind, error: error instanceof Error ? error.message : String(error), ms: at - startedAt });
|
|
74
88
|
open.step = undefined;
|
|
75
89
|
}
|
|
76
90
|
/** The id of the run open right now, if any. */
|
|
77
91
|
export function currentRunId() {
|
|
78
|
-
return
|
|
92
|
+
return scope.getStore()?.runId;
|
|
79
93
|
}
|
|
80
|
-
export function recordArtifact(kind, path, runId =
|
|
94
|
+
export function recordArtifact(kind, path, runId = scope.getStore()?.runId) {
|
|
81
95
|
if (runId)
|
|
82
96
|
emit({ type: "run:artifact", runId, at: Date.now(), kind, path });
|
|
83
97
|
}
|
package/dist/engine/run/run.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { MemPage } from "../mem/mem.js";
|
|
|
2
2
|
import type { Block } from "../block/block.js";
|
|
3
3
|
import type { Checkpoint } from "../checkpoint/checkpoint.js";
|
|
4
4
|
import type { Trait } from "../trait/trait.js";
|
|
5
|
-
import type { BrowserContext,
|
|
5
|
+
import type { BrowserContext, Page } from "@playwright/test";
|
|
6
6
|
import type { SomeBlock } from "../block/block.js";
|
|
7
7
|
/**
|
|
8
8
|
* Checks `block.requires` against `mem` and throws, naming every missing key at
|
|
@@ -125,21 +125,3 @@ export interface RunGraphOptions {
|
|
|
125
125
|
* }
|
|
126
126
|
*/
|
|
127
127
|
export declare function spawnTab<TOut extends Checkpoint<string>>(entry: Block<Checkpoint<"__start__">, TOut>, page: Page, mem: MemPage, maxSteps?: number): Promise<TOut>;
|
|
128
|
-
/**
|
|
129
|
-
* Wraps one `act()` interaction with a caption and highlight, for a Block
|
|
130
|
-
* author who wants to explicitly narrate a specific step rather than rely
|
|
131
|
-
* on the generic per-fill/per-click narration `waygraph chain
|
|
132
|
-
* WAYGRAPH_STEP=1` already does automatically -
|
|
133
|
-
* `await narrate(submitButton, "confirms the order", () => submitButton.click())`
|
|
134
|
-
* reads at the call site instead of being inferred.
|
|
135
|
-
*
|
|
136
|
-
* A true no-op everywhere except inside a `WAYGRAPH_STEP=1` run: it checks
|
|
137
|
-
* for a `window.__wgPositionRing` hook the step-mode overlay installs on
|
|
138
|
-
* the page before doing anything, so a headless/CI/automated run (nobody
|
|
139
|
-
* watching, per-character delays and highlight pauses would just be wasted
|
|
140
|
-
* time) pays zero extra cost - not even the boundingBox() lookup runs. The
|
|
141
|
-
* `action` itself always runs regardless; only the narration is
|
|
142
|
-
* conditional.
|
|
143
|
-
* @example await narrate(submitButton, "confirms the order", () => submitButton.click())
|
|
144
|
-
*/
|
|
145
|
-
export declare function narrate<T>(locator: Locator, caption: string, action: () => Promise<T>): Promise<T>;
|
package/dist/engine/run/run.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { WaygraphError, MSG_PREFLIGHT_MISSING_KEY, MSG_RUNGRAPH_MAX_STEPS, MSG_RUNGRAPH_NOT_TERMINAL } from "../errors.js";
|
|
2
2
|
import { checkpoint } from "../checkpoint/checkpoint.js";
|
|
3
|
-
import {
|
|
3
|
+
import { inRun, isConnected, lastLeaf, stepEnded, stepFailed, stepRead, stepStarted } from "./events.js";
|
|
4
|
+
import { readBlocksFor, readCheckpoint } from "../read/read-block.js";
|
|
5
|
+
import { blockKindOf } from "../block/kind.js";
|
|
4
6
|
import { runPrecondition, runVerify } from "../trait/trait.js";
|
|
5
|
-
import { ignorePageGone } from "../page-gone.js";
|
|
6
7
|
/**
|
|
7
8
|
* Checks `block.requires` against `mem` and throws, naming every missing key at
|
|
8
9
|
* once, if any are unset. `connect()` unions each Block's `requires` into the
|
|
@@ -79,9 +80,13 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
|
|
|
79
80
|
preflight(mem, entry);
|
|
80
81
|
const page = options?.page ?? (await context.newPage());
|
|
81
82
|
const closeOnFinish = options?.closeOnFinish ?? true;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
return inRun(undefined, () => playGraph(entry, terminals, page, mem, maxSteps, options), (out) => out.__state).finally(async () => {
|
|
84
|
+
if (closeOnFinish)
|
|
85
|
+
await page.close();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** The step loop of {@link runGraph}, inside its run: act, observe, resolve, verify, then the next Block. */
|
|
89
|
+
async function playGraph(entry, terminals, page, mem, maxSteps, options) {
|
|
85
90
|
try {
|
|
86
91
|
let current = entry;
|
|
87
92
|
let input = checkpoint("__start__");
|
|
@@ -91,7 +96,7 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
|
|
|
91
96
|
throw new WaygraphError("WG_RUNGRAPH_MAX_STEPS", MSG_RUNGRAPH_MAX_STEPS(maxSteps));
|
|
92
97
|
}
|
|
93
98
|
if (!isConnected(current))
|
|
94
|
-
stepStarted(current.name, input.__state);
|
|
99
|
+
stepStarted(current.name, blockKindOf(current), input.__state);
|
|
95
100
|
await runPrecondition(current.instruction.precondition, input, page, mem, current.name);
|
|
96
101
|
await current.instruction.act(page, input, mem);
|
|
97
102
|
const observed = current.instruction.observe
|
|
@@ -100,13 +105,13 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
|
|
|
100
105
|
const checkpoint = await current.instruction.resolve(observed);
|
|
101
106
|
await runVerify(current.instruction.verify, checkpoint, page, mem, current.name);
|
|
102
107
|
await runLayouts(options?.layouts, checkpoint.__state, checkpoint, page, mem, current.name);
|
|
108
|
+
await readLanding(page, mem, checkpoint.__state);
|
|
103
109
|
stepEnded(lastLeaf(current), checkpoint.__state);
|
|
104
110
|
const next = current.next?.(checkpoint);
|
|
105
111
|
if (!next) {
|
|
106
112
|
if (terminals && !terminals.has(checkpoint.__state)) {
|
|
107
113
|
throw new WaygraphError("WG_RUNGRAPH_NOT_TERMINAL", MSG_RUNGRAPH_NOT_TERMINAL(current.name, checkpoint.__state, [...terminals].join(", ")));
|
|
108
114
|
}
|
|
109
|
-
result = checkpoint.__state;
|
|
110
115
|
return checkpoint;
|
|
111
116
|
}
|
|
112
117
|
current = next;
|
|
@@ -114,16 +119,9 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
|
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
catch (err) {
|
|
117
|
-
failure = err;
|
|
118
122
|
stepFailed(err);
|
|
119
123
|
throw err;
|
|
120
124
|
}
|
|
121
|
-
finally {
|
|
122
|
-
endRun(opened, failure === undefined ? { ...(result !== undefined ? { result } : {}) } : { error: failure });
|
|
123
|
-
if (closeOnFinish) {
|
|
124
|
-
await page.close();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
125
|
}
|
|
128
126
|
/**
|
|
129
127
|
* Drives a genuinely separate second tab through its own Block/Flow, in the
|
|
@@ -144,90 +142,16 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
|
|
|
144
142
|
export async function spawnTab(entry, page, mem, maxSteps = 5000) {
|
|
145
143
|
return runGraph(entry, undefined, page.context(), mem, maxSteps);
|
|
146
144
|
}
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* WAYGRAPH_STEP=1` already does automatically -
|
|
152
|
-
* `await narrate(submitButton, "confirms the order", () => submitButton.click())`
|
|
153
|
-
* reads at the call site instead of being inferred.
|
|
154
|
-
*
|
|
155
|
-
* A true no-op everywhere except inside a `WAYGRAPH_STEP=1` run: it checks
|
|
156
|
-
* for a `window.__wgPositionRing` hook the step-mode overlay installs on
|
|
157
|
-
* the page before doing anything, so a headless/CI/automated run (nobody
|
|
158
|
-
* watching, per-character delays and highlight pauses would just be wasted
|
|
159
|
-
* time) pays zero extra cost - not even the boundingBox() lookup runs. The
|
|
160
|
-
* `action` itself always runs regardless; only the narration is
|
|
161
|
-
* conditional.
|
|
162
|
-
* @example await narrate(submitButton, "confirms the order", () => submitButton.click())
|
|
163
|
-
*/
|
|
164
|
-
export async function narrate(locator, caption, action) {
|
|
165
|
-
const page = locator.page();
|
|
166
|
-
const active = await page
|
|
167
|
-
.evaluate(() => typeof globalThis.__wgPositionRing === "function")
|
|
168
|
-
.catch(() => false);
|
|
169
|
-
if (!active)
|
|
170
|
-
return action();
|
|
171
|
-
let owning = false;
|
|
172
|
-
try {
|
|
173
|
-
const box = await locator.boundingBox();
|
|
174
|
-
if (box) {
|
|
175
|
-
await page
|
|
176
|
-
.evaluate(({ box, caption }) => {
|
|
177
|
-
const w = globalThis;
|
|
178
|
-
if (w.__wgPositionRing)
|
|
179
|
-
w.__wgPositionRing(box, caption, "planned");
|
|
180
|
-
// The CLI's own automatic per-fill/per-click narration checks
|
|
181
|
-
// this before showing (and overwriting) its own guess - an
|
|
182
|
-
// explicitly authored caption should win, not get immediately
|
|
183
|
-
// replaced a moment later by the generic one.
|
|
184
|
-
w.__wgLastNarrate = Date.now();
|
|
185
|
-
// Also claim the ring itself: the auto-highlight click/fill
|
|
186
|
-
// patches each hide the ring right after THEIR own step
|
|
187
|
-
// finishes, which - if action() below does more than one thing
|
|
188
|
-
// (e.g. a click then a wait for a toast) - would wipe this
|
|
189
|
-
// caption after just the first sub-step instead of the whole
|
|
190
|
-
// narrated action. Their hideRing() calls no-op while this is
|
|
191
|
-
// true; narrate() below is the one that actually clears it.
|
|
192
|
-
w.__wgNarrateOwnsRing = true;
|
|
193
|
-
}, { box, caption })
|
|
194
|
-
.catch(ignorePageGone);
|
|
195
|
-
owning = true;
|
|
196
|
-
// The ring alone is pointless if action() fires on the very next
|
|
197
|
-
// tick - a click that navigates (e.g. a form submit) wipes the ring
|
|
198
|
-
// before a human can read the caption at all. Give it the same
|
|
199
|
-
// "pop for a few seconds" dwell as the CLI's own auto-highlight
|
|
200
|
-
// clicks before the real action runs.
|
|
201
|
-
await new Promise((resolve) => setTimeout(resolve, 650));
|
|
202
|
-
// Re-stamp __wgLastNarrate right before handing off to action() - the
|
|
203
|
-
// CLI's auto-highlight patches only treat a narrate() as "just
|
|
204
|
-
// handled" within a short window (500ms), which the dwell above
|
|
205
|
-
// already burned through. Without this, action()'s own click/fill
|
|
206
|
-
// (same patched Locator prototype) would stomp this caption with its
|
|
207
|
-
// own generic one a moment after it finally became visible.
|
|
208
|
-
await page
|
|
209
|
-
.evaluate(() => {
|
|
210
|
-
globalThis.__wgLastNarrate = Date.now();
|
|
211
|
-
})
|
|
212
|
-
.catch(ignorePageGone);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
// best-effort - the real action below still runs either way
|
|
217
|
-
}
|
|
145
|
+
/** After a step's verify: what the page shows, from the Read Blocks for where it landed (none loaded: nothing to do). */
|
|
146
|
+
async function readLanding(page, mem, checkpoint) {
|
|
147
|
+
if (readBlocksFor(checkpoint).length === 0)
|
|
148
|
+
return;
|
|
218
149
|
try {
|
|
219
|
-
|
|
150
|
+
const { read, blocks } = await readCheckpoint(page, mem, checkpoint);
|
|
151
|
+
stepRead(checkpoint, { read: read.toJSON(), missing: [...read.missing], notes: { ...read.notes }, readBlocks: blocks });
|
|
220
152
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
.evaluate(() => {
|
|
225
|
-
const w = globalThis;
|
|
226
|
-
w.__wgNarrateOwnsRing = false;
|
|
227
|
-
if (w.__wgHideRing)
|
|
228
|
-
w.__wgHideRing();
|
|
229
|
-
})
|
|
230
|
-
.catch(ignorePageGone);
|
|
231
|
-
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
// A broken Read Block is reported on the step, never fails the run it observes.
|
|
155
|
+
stepRead(checkpoint, { read: {}, missing: [], notes: {}, readBlocks: [], error: err instanceof Error ? err.message : String(err) });
|
|
232
156
|
}
|
|
233
157
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Checkpoint } from "../checkpoint/checkpoint.js";
|
|
2
1
|
/** Shared dwell fields on stubs, fixtures, and yap slides. */
|
|
3
2
|
export type FixtureDurationFields = {
|
|
4
3
|
/**
|
|
@@ -83,8 +82,8 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
|
|
|
83
82
|
/** A static stub phase: named ring slots to paint before or after a step. */
|
|
84
83
|
export type HighlightStubPhase = Record<string, WaygraphHighlightStub>;
|
|
85
84
|
/**
|
|
86
|
-
* One yap/slide in a multi-step demo caption sequence.
|
|
87
|
-
*
|
|
85
|
+
* One yap/slide in a multi-step demo caption sequence, painted with {@link StubCtx.slides}.
|
|
86
|
+
* The demo pauses on each (Next, or auto-next) before moving on.
|
|
88
87
|
*/
|
|
89
88
|
export interface WaygraphSlide extends FixtureDurationFields {
|
|
90
89
|
/** Primary caption (required). */
|
|
@@ -119,7 +118,6 @@ export type HighlightStyleDefaults = {
|
|
|
119
118
|
size?: HighlightSize;
|
|
120
119
|
weight?: HighlightWeight;
|
|
121
120
|
};
|
|
122
|
-
export type WaygraphSlidesOrFn<Out extends Checkpoint<string>> = readonly WaygraphSlide[] | ((out: Out) => readonly WaygraphSlide[]);
|
|
123
121
|
/** A ring or caption after its defaults and overrides are merged, ready to paint. */
|
|
124
122
|
export type ResolvedHighlight = WaygraphHighlightStub;
|
|
125
123
|
/** Map author aliases (`error` -> danger) to a canonical tone. */
|
package/dist/engine/stub/ring.js
CHANGED