waygraph 0.15.24 → 0.15.26
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/engine/blocks/effect.d.ts +41 -0
- package/dist/engine/blocks/effect.js +23 -0
- package/dist/engine/blocks/method.d.ts +90 -0
- package/dist/engine/blocks/method.js +70 -0
- package/dist/engine/blocks/nav.d.ts +138 -0
- package/dist/engine/blocks/nav.js +110 -0
- package/dist/engine/blocks/page.d.ts +59 -0
- package/dist/engine/blocks/page.js +91 -0
- package/dist/engine/compose.d.ts +75 -0
- package/dist/engine/compose.js +76 -0
- package/dist/engine/config.d.ts +55 -0
- package/dist/engine/config.js +17 -0
- package/dist/engine/core.d.ts +92 -0
- package/dist/engine/core.js +256 -0
- package/dist/engine/engine-class.d.ts +289 -0
- package/dist/engine/engine-class.js +257 -0
- package/dist/engine/flow.d.ts +266 -0
- package/dist/engine/flow.js +387 -0
- package/dist/engine/locate.d.ts +26 -0
- package/dist/engine/locate.js +62 -0
- package/dist/engine/run-graph.d.ts +147 -0
- package/dist/engine/run-graph.js +223 -0
- package/dist/engine.d.ts +11 -1224
- package/dist/engine.js +12 -1550
- package/dist/highlights/css.d.ts +14 -0
- package/dist/highlights/css.js +58 -0
- package/dist/highlights/demo-pace.d.ts +56 -0
- package/dist/highlights/demo-pace.js +184 -0
- package/dist/highlights/device.d.ts +29 -0
- package/dist/highlights/device.js +171 -0
- package/dist/highlights/merge.d.ts +8 -0
- package/dist/highlights/merge.js +173 -0
- package/dist/highlights/run-phase.d.ts +43 -0
- package/dist/highlights/run-phase.js +211 -0
- package/dist/highlights/stub-ctx.d.ts +10 -0
- package/dist/highlights/stub-ctx.js +281 -0
- package/dist/highlights/style.d.ts +27 -0
- package/dist/highlights/style.js +84 -0
- package/dist/highlights/todo-dock.d.ts +52 -0
- package/dist/highlights/todo-dock.js +248 -0
- package/dist/highlights/types.d.ts +536 -0
- package/dist/highlights/types.js +1 -0
- package/dist/highlights.d.ts +9 -734
- package/dist/highlights.js +10 -1416
- package/examples/saucedemo/.waygraph-auto/008f4e6e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/22fefa13.log +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NavBlock } from "./blocks/nav.js";
|
|
2
|
+
import type { Page } from "@playwright/test";
|
|
3
|
+
/**
|
|
4
|
+
* "Where am I" - reverse-matches a live `page` against every NavBlock /
|
|
5
|
+
* PageBlock in `library`, in order, returning the first one whose own `verify`
|
|
6
|
+
* Traits all pass (its `checkpoint`), or `null` if none match. Deliberately
|
|
7
|
+
* Nav/Page hubs only: each has exactly one fixed `checkpoint` and a `verify`
|
|
8
|
+
* list whose whole job is already "confirm arrival here," so it's a reliable
|
|
9
|
+
* fingerprint - a regular `defineBlock` can branch to different `Out` tags
|
|
10
|
+
* depending on runtime evidence, so its `verify` alone doesn't identify a
|
|
11
|
+
* single state the way a hub's does.
|
|
12
|
+
*
|
|
13
|
+
* The primitive `waygraph auto`'s discovered graph is *for* - the graph says
|
|
14
|
+
* what pages exist and how to reach them; `locate()` is how an autonomous
|
|
15
|
+
* run figures out which one it's actually looking at right now, without a
|
|
16
|
+
* human telling it.
|
|
17
|
+
*
|
|
18
|
+
* More than one NavBlock matching is a real possibility (two Checkpoints
|
|
19
|
+
* whose `verify` Traits are both satisfied by the same page) - `locate()`
|
|
20
|
+
* returns the first match rather than treating that as an error; resolving
|
|
21
|
+
* genuine ambiguity is a follow-up, not solved here.
|
|
22
|
+
* @example const here = await locate(page, [NavLoginBlock, NavDashboardBlock]);
|
|
23
|
+
*/
|
|
24
|
+
export declare function locate(page: Page, library: readonly NavBlock<any>[], options?: {
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
}): Promise<string | null>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { MemPage } from "../mem-page.js";
|
|
2
|
+
/**
|
|
3
|
+
* "Where am I" - reverse-matches a live `page` against every NavBlock /
|
|
4
|
+
* PageBlock in `library`, in order, returning the first one whose own `verify`
|
|
5
|
+
* Traits all pass (its `checkpoint`), or `null` if none match. Deliberately
|
|
6
|
+
* Nav/Page hubs only: each has exactly one fixed `checkpoint` and a `verify`
|
|
7
|
+
* list whose whole job is already "confirm arrival here," so it's a reliable
|
|
8
|
+
* fingerprint - a regular `defineBlock` can branch to different `Out` tags
|
|
9
|
+
* depending on runtime evidence, so its `verify` alone doesn't identify a
|
|
10
|
+
* single state the way a hub's does.
|
|
11
|
+
*
|
|
12
|
+
* The primitive `waygraph auto`'s discovered graph is *for* - the graph says
|
|
13
|
+
* what pages exist and how to reach them; `locate()` is how an autonomous
|
|
14
|
+
* run figures out which one it's actually looking at right now, without a
|
|
15
|
+
* human telling it.
|
|
16
|
+
*
|
|
17
|
+
* More than one NavBlock matching is a real possibility (two Checkpoints
|
|
18
|
+
* whose `verify` Traits are both satisfied by the same page) - `locate()`
|
|
19
|
+
* returns the first match rather than treating that as an error; resolving
|
|
20
|
+
* genuine ambiguity is a follow-up, not solved here.
|
|
21
|
+
* @example const here = await locate(page, [NavLoginBlock, NavDashboardBlock]);
|
|
22
|
+
*/
|
|
23
|
+
export async function locate(page, library, options) {
|
|
24
|
+
const mem = new MemPage();
|
|
25
|
+
const timeoutMs = options?.timeoutMs;
|
|
26
|
+
if (timeoutMs !== undefined) {
|
|
27
|
+
page.setDefaultTimeout(timeoutMs);
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
for (const block of library) {
|
|
31
|
+
try {
|
|
32
|
+
// NavBlock's own resolve() ignores whatever it's called with - always
|
|
33
|
+
// exactly `checkpoint(options.checkpoint)` - so this is safe to call
|
|
34
|
+
// before verify, unlike a regular Block where resolve depends on
|
|
35
|
+
// observed evidence.
|
|
36
|
+
const resolved = await block.instruction.resolve(undefined);
|
|
37
|
+
const verify = block.instruction.verify;
|
|
38
|
+
const traits = typeof verify === "function" ? verify(resolved) : (verify ?? []);
|
|
39
|
+
let allPass = true;
|
|
40
|
+
for (const trait of traits) {
|
|
41
|
+
// eslint-disable-next-line no-await-in-loop
|
|
42
|
+
if (!(await trait.check(page, mem))) {
|
|
43
|
+
allPass = false;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (allPass)
|
|
48
|
+
return resolved.__state;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// This NavBlock's own verify threw (e.g. a waitFor timing out) -
|
|
52
|
+
// treat that the same as "didn't match," try the next candidate.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (timeoutMs !== undefined) {
|
|
59
|
+
page.setDefaultTimeout(30_000);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { MemPage } from "../mem-page.js";
|
|
2
|
+
import type { Block, Checkpoint } from "../types.js";
|
|
3
|
+
import type { Trait } from "../trait.js";
|
|
4
|
+
import type { BrowserContext, Locator, Page } from "@playwright/test";
|
|
5
|
+
/**
|
|
6
|
+
* Checks `block.requires` against `mem` and throws, naming every missing key at
|
|
7
|
+
* once, if any are unset. `connect()` unions each Block's `requires` into the
|
|
8
|
+
* composed Block, so calling this on a `defineFlow`-built Flow's entry - which
|
|
9
|
+
* `runGraph` does automatically, before opening a tab - covers every Block in
|
|
10
|
+
* that flow. It does not see Blocks only reachable through `branch()` routing,
|
|
11
|
+
* since those depend on a runtime decision that hasn't happened yet.
|
|
12
|
+
*/
|
|
13
|
+
export declare function preflight(mem: MemPage, block: Block<any, any>): void;
|
|
14
|
+
/**
|
|
15
|
+
* A cross-cutting, automatically-enforced verify - the real engine
|
|
16
|
+
* primitive behind "persistent UI" (a sidebar, a header, anything that's
|
|
17
|
+
* supposed to be present on many/most Checkpoints, not just one page's own
|
|
18
|
+
* Block). Deliberately NOT tied to any folder convention - `appliesTo`
|
|
19
|
+
* matches by Checkpoint tag, explicitly, so this works the same whether a
|
|
20
|
+
* project uses the Waygraph Map folder convention or the older freeform
|
|
21
|
+
* "manual mode" layout real production consumers (pia-waygraph,
|
|
22
|
+
* zsign-atomic-waygraph) already use - registering a Layout with an Engine
|
|
23
|
+
* is the only requirement, folder structure is irrelevant to it.
|
|
24
|
+
*
|
|
25
|
+
* Not a Block: no `act`/`resolve`/Checkpoint of its own, never a graph
|
|
26
|
+
* node - it only ever adds extra `verify` Traits to Checkpoints something
|
|
27
|
+
* else already resolved to, the same way `runVerify` already runs after
|
|
28
|
+
* any Block's own `resolve()` (see `runGraph`'s own verify step, just
|
|
29
|
+
* below) - a Layout is that mechanism made reusable across many
|
|
30
|
+
* Checkpoints instead of hand-repeated per Block.
|
|
31
|
+
*/
|
|
32
|
+
export interface Layout {
|
|
33
|
+
name: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
appliesTo: (tag: string) => boolean;
|
|
36
|
+
/** Function form gets the full resolved Checkpoint, same shape `runVerify`/a Block's own `verify` already use - not just the tag. */
|
|
37
|
+
verify: Trait[] | ((out: Checkpoint<string>) => Trait[]);
|
|
38
|
+
}
|
|
39
|
+
export interface LayoutOptions {
|
|
40
|
+
name: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
/** A fixed tag list, or a predicate for open-ended matching (e.g. a naming pattern). */
|
|
43
|
+
appliesTo: readonly string[] | ((tag: string) => boolean);
|
|
44
|
+
verify: Trait[] | ((out: Checkpoint<string>) => Trait[]);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @example
|
|
48
|
+
* const AppShellLayout = defineLayout({
|
|
49
|
+
* name: "app-shell",
|
|
50
|
+
* appliesTo: ["AppHome", "Chats", "Notifications", "Settings"],
|
|
51
|
+
* verify: [Trait.visible(SidebarSel.root)],
|
|
52
|
+
* });
|
|
53
|
+
* const engine = new Engine({ layouts: [AppShellLayout] });
|
|
54
|
+
* // Now every Block resolving to one of those four Checkpoints, in any
|
|
55
|
+
* // Flow this engine defines, also gets AppShellLayout's own verify run
|
|
56
|
+
* // automatically - a page that silently drops the sidebar fails loud,
|
|
57
|
+
* // even though no individual Block's own `verify` mentions it.
|
|
58
|
+
*/
|
|
59
|
+
export declare function defineLayout(options: LayoutOptions): Layout;
|
|
60
|
+
/**
|
|
61
|
+
* Runs `entry` against a fresh tab and returns the resulting terminal Checkpoint.
|
|
62
|
+
* The engine owns the tab's lifecycle, not any individual Block: the tab is
|
|
63
|
+
* created before the first `act` runs and closed in `finally`, so it closes even
|
|
64
|
+
* if a step throws.
|
|
65
|
+
*
|
|
66
|
+
* After each Block resolves, its `next` (if any, from `branch()`) is followed to
|
|
67
|
+
* the following Block, in the same tab - a Block with no `next` runs exactly once,
|
|
68
|
+
* unchanged from before `branch()` existed. `next` returning the same Block is a
|
|
69
|
+
* self-loop; `maxSteps` bounds the loop so a routing bug fails loud instead of
|
|
70
|
+
* hanging.
|
|
71
|
+
*
|
|
72
|
+
* `terminals`, if given, is a sanity check: the Checkpoint the run stops on must be
|
|
73
|
+
* one of the tags the caller actually expects, or the run fails loud instead of
|
|
74
|
+
* silently returning an unexpected result. Omit it when there is only ever one
|
|
75
|
+
* reachable outcome by construction (as with a `defineFlow`-built Flow, which has
|
|
76
|
+
* no routing at all) - there is nothing to validate against.
|
|
77
|
+
*/
|
|
78
|
+
export declare function runGraph<TOut extends Checkpoint<string>>(entry: Block<Checkpoint<"__start__">, TOut>, terminals: ReadonlySet<string> | undefined, context: BrowserContext, mem: MemPage, maxSteps?: number, options?: RunGraphOptions): Promise<TOut>;
|
|
79
|
+
/** Options for `runGraph`'s trailing `options` arg and for {@link Flow.run}'s `options` overload. */
|
|
80
|
+
export interface RunGraphOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Drive this already-open page instead of opening a new one via
|
|
83
|
+
* `context.newPage()`. Lets a run continue on a tab the caller already
|
|
84
|
+
* has open (e.g. one that's already logged in), instead of every run
|
|
85
|
+
* being forced to start its own fresh tab and lose access to whatever was
|
|
86
|
+
* already there.
|
|
87
|
+
*/
|
|
88
|
+
page?: Page;
|
|
89
|
+
/**
|
|
90
|
+
* `Flow.run` defaults this to `true` only when it opened the page itself
|
|
91
|
+
* (no `page` given) - matching every call from before this option
|
|
92
|
+
* existed. If you pass your own `page`, it defaults to `false` instead:
|
|
93
|
+
* a page you opened is yours to close, not this run's to close out from
|
|
94
|
+
* under you. Either way an explicit value here always wins. `runGraph`
|
|
95
|
+
* itself has no such page-aware default (its caller already decided
|
|
96
|
+
* `page` and `closeOnFinish` together) - it just does what you ask.
|
|
97
|
+
*/
|
|
98
|
+
closeOnFinish?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Layouts (see {@link defineLayout}) whose `verify` runs automatically
|
|
101
|
+
* after any Block in this run resolves to a Checkpoint they apply to -
|
|
102
|
+
* `Flow.run` threads this in from `EngineConfig.layouts` by default; pass
|
|
103
|
+
* it directly here to call `runGraph` standalone (outside a `Flow`) with
|
|
104
|
+
* layouts still enforced.
|
|
105
|
+
*/
|
|
106
|
+
layouts?: readonly Layout[];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Drives a genuinely separate second tab through its own Block/Flow, in the
|
|
110
|
+
* same browser context an existing `page` already belongs to - the primitive
|
|
111
|
+
* `observe()` is specifically allowed to reach for (see {@link Instruction.observe}),
|
|
112
|
+
* since `act`/`resolve` can't touch `page.context()` at all. A thin, named
|
|
113
|
+
* wrapper around `runGraph` (which already opens its own page from whatever
|
|
114
|
+
* context it's given) rather than a new mechanism - the point is making the
|
|
115
|
+
* pattern discoverable and giving it a real name, not inventing new tab-lifecycle
|
|
116
|
+
* logic. The spawned tab is closed (by `runGraph`'s own `finally`) before this
|
|
117
|
+
* resolves; the original `page` is never touched.
|
|
118
|
+
* @example
|
|
119
|
+
* async observe(page, mem) {
|
|
120
|
+
* const receipt = await spawnTab(EmailReceiptFlow, page, mem);
|
|
121
|
+
* mem.set(ReceiptCode, receipt.__state);
|
|
122
|
+
* }
|
|
123
|
+
*/
|
|
124
|
+
export declare function spawnTab<TOut extends Checkpoint<string>>(entry: Block<Checkpoint<"__start__">, TOut>, page: Page, mem: MemPage, maxSteps?: number): Promise<TOut>;
|
|
125
|
+
/**
|
|
126
|
+
* Wraps one `act()` interaction with a caption and highlight, for a Block
|
|
127
|
+
* author who wants to explicitly narrate a specific step rather than rely
|
|
128
|
+
* on the generic per-fill/per-click narration `waygraph chain
|
|
129
|
+
* WAYGRAPH_STEP=1` already does automatically -
|
|
130
|
+
* `await narrate(submitButton, "confirms the order", () => submitButton.click())`
|
|
131
|
+
* reads at the call site instead of being inferred.
|
|
132
|
+
*
|
|
133
|
+
* A true no-op everywhere except inside a `WAYGRAPH_STEP=1` run: it checks
|
|
134
|
+
* for a `window.__wgPositionRing` hook the step-mode overlay installs on
|
|
135
|
+
* the page before doing anything, so a headless/CI/automated run (nobody
|
|
136
|
+
* watching, per-character delays and highlight pauses would just be wasted
|
|
137
|
+
* time) pays zero extra cost - not even the boundingBox() lookup runs. The
|
|
138
|
+
* `action` itself always runs regardless; only the narration is
|
|
139
|
+
* conditional.
|
|
140
|
+
* @example await narrate(submitButton, "confirms the order", () => submitButton.click())
|
|
141
|
+
*/
|
|
142
|
+
export declare function narrate<T>(locator: Locator, caption: string, action: () => Promise<T>): Promise<T>;
|
|
143
|
+
/** Reserved markers bookending a `defineFlow([start, ...blocks, end])` call. */
|
|
144
|
+
export declare const start: unique symbol;
|
|
145
|
+
export declare const end: unique symbol;
|
|
146
|
+
export type StartMarker = typeof start;
|
|
147
|
+
export type EndMarker = typeof end;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { checkpoint } from "../types.js";
|
|
2
|
+
import { runPrecondition, runVerify } from "../trait.js";
|
|
3
|
+
/**
|
|
4
|
+
* Checks `block.requires` against `mem` and throws, naming every missing key at
|
|
5
|
+
* once, if any are unset. `connect()` unions each Block's `requires` into the
|
|
6
|
+
* composed Block, so calling this on a `defineFlow`-built Flow's entry - which
|
|
7
|
+
* `runGraph` does automatically, before opening a tab - covers every Block in
|
|
8
|
+
* that flow. It does not see Blocks only reachable through `branch()` routing,
|
|
9
|
+
* since those depend on a runtime decision that hasn't happened yet.
|
|
10
|
+
*/
|
|
11
|
+
export function preflight(mem, block) {
|
|
12
|
+
const missing = (block.requires ?? []).filter((k) => !mem.has(k));
|
|
13
|
+
if (missing.length > 0) {
|
|
14
|
+
throw new Error(`preflight: MemPage is missing required key(s) before "${block.name}" can run: ${missing
|
|
15
|
+
.map((k) => `"${k.name}"`)
|
|
16
|
+
.join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* @example
|
|
21
|
+
* const AppShellLayout = defineLayout({
|
|
22
|
+
* name: "app-shell",
|
|
23
|
+
* appliesTo: ["AppHome", "Chats", "Notifications", "Settings"],
|
|
24
|
+
* verify: [Trait.visible(SidebarSel.root)],
|
|
25
|
+
* });
|
|
26
|
+
* const engine = new Engine({ layouts: [AppShellLayout] });
|
|
27
|
+
* // Now every Block resolving to one of those four Checkpoints, in any
|
|
28
|
+
* // Flow this engine defines, also gets AppShellLayout's own verify run
|
|
29
|
+
* // automatically - a page that silently drops the sidebar fails loud,
|
|
30
|
+
* // even though no individual Block's own `verify` mentions it.
|
|
31
|
+
*/
|
|
32
|
+
export function defineLayout(options) {
|
|
33
|
+
const appliesTo = typeof options.appliesTo === "function"
|
|
34
|
+
? options.appliesTo
|
|
35
|
+
: (tag) => options.appliesTo.includes(tag);
|
|
36
|
+
return {
|
|
37
|
+
name: options.name,
|
|
38
|
+
...(options.description ? { description: options.description } : {}),
|
|
39
|
+
appliesTo,
|
|
40
|
+
verify: options.verify,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Runs every registered layout whose `appliesTo(tag)` matches the just-
|
|
45
|
+
* resolved Checkpoint, in registration order, right after the Block's own
|
|
46
|
+
* `verify` already ran. A layout's own Trait failing throws the same way
|
|
47
|
+
* any other `runVerify` failure does, naming the layout so it's clearly
|
|
48
|
+
* distinguishable from the Block's own verify in the error message.
|
|
49
|
+
*/
|
|
50
|
+
async function runLayouts(layouts, tag, out, page, mem, blockName) {
|
|
51
|
+
if (!layouts)
|
|
52
|
+
return;
|
|
53
|
+
for (const layout of layouts) {
|
|
54
|
+
if (!layout.appliesTo(tag))
|
|
55
|
+
continue;
|
|
56
|
+
await runVerify(layout.verify, out, page, mem, `${blockName} (layout: "${layout.name}")`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Runs `entry` against a fresh tab and returns the resulting terminal Checkpoint.
|
|
61
|
+
* The engine owns the tab's lifecycle, not any individual Block: the tab is
|
|
62
|
+
* created before the first `act` runs and closed in `finally`, so it closes even
|
|
63
|
+
* if a step throws.
|
|
64
|
+
*
|
|
65
|
+
* After each Block resolves, its `next` (if any, from `branch()`) is followed to
|
|
66
|
+
* the following Block, in the same tab - a Block with no `next` runs exactly once,
|
|
67
|
+
* unchanged from before `branch()` existed. `next` returning the same Block is a
|
|
68
|
+
* self-loop; `maxSteps` bounds the loop so a routing bug fails loud instead of
|
|
69
|
+
* hanging.
|
|
70
|
+
*
|
|
71
|
+
* `terminals`, if given, is a sanity check: the Checkpoint the run stops on must be
|
|
72
|
+
* one of the tags the caller actually expects, or the run fails loud instead of
|
|
73
|
+
* silently returning an unexpected result. Omit it when there is only ever one
|
|
74
|
+
* reachable outcome by construction (as with a `defineFlow`-built Flow, which has
|
|
75
|
+
* no routing at all) - there is nothing to validate against.
|
|
76
|
+
*/
|
|
77
|
+
export async function runGraph(entry, terminals, context, mem, maxSteps = 5000, options) {
|
|
78
|
+
preflight(mem, entry);
|
|
79
|
+
const page = options?.page ?? (await context.newPage());
|
|
80
|
+
const closeOnFinish = options?.closeOnFinish ?? true;
|
|
81
|
+
try {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
+
let current = entry;
|
|
84
|
+
let input = checkpoint("__start__");
|
|
85
|
+
let steps = 0;
|
|
86
|
+
for (;;) {
|
|
87
|
+
if (++steps > maxSteps) {
|
|
88
|
+
throw new Error(`runGraph: exceeded ${maxSteps} steps - check for an unintended self-loop`);
|
|
89
|
+
}
|
|
90
|
+
await runPrecondition(current.instruction.precondition, input, page, mem, current.name);
|
|
91
|
+
await current.instruction.act(page, input, mem);
|
|
92
|
+
const observed = current.instruction.observe
|
|
93
|
+
? await current.instruction.observe(page, mem)
|
|
94
|
+
: undefined;
|
|
95
|
+
const checkpoint = await current.instruction.resolve(observed);
|
|
96
|
+
await runVerify(current.instruction.verify, checkpoint, page, mem, current.name);
|
|
97
|
+
await runLayouts(options?.layouts, checkpoint.__state, checkpoint, page, mem, current.name);
|
|
98
|
+
const next = current.next?.(checkpoint);
|
|
99
|
+
if (!next) {
|
|
100
|
+
if (terminals && !terminals.has(checkpoint.__state)) {
|
|
101
|
+
throw new Error(`runGraph: "${current.name}" resolved to "${checkpoint.__state}", which is not a registered terminal (expected one of: ${[...terminals].join(", ")})`);
|
|
102
|
+
}
|
|
103
|
+
return checkpoint;
|
|
104
|
+
}
|
|
105
|
+
current = next;
|
|
106
|
+
input = checkpoint;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (closeOnFinish) {
|
|
111
|
+
await page.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Drives a genuinely separate second tab through its own Block/Flow, in the
|
|
117
|
+
* same browser context an existing `page` already belongs to - the primitive
|
|
118
|
+
* `observe()` is specifically allowed to reach for (see {@link Instruction.observe}),
|
|
119
|
+
* since `act`/`resolve` can't touch `page.context()` at all. A thin, named
|
|
120
|
+
* wrapper around `runGraph` (which already opens its own page from whatever
|
|
121
|
+
* context it's given) rather than a new mechanism - the point is making the
|
|
122
|
+
* pattern discoverable and giving it a real name, not inventing new tab-lifecycle
|
|
123
|
+
* logic. The spawned tab is closed (by `runGraph`'s own `finally`) before this
|
|
124
|
+
* resolves; the original `page` is never touched.
|
|
125
|
+
* @example
|
|
126
|
+
* async observe(page, mem) {
|
|
127
|
+
* const receipt = await spawnTab(EmailReceiptFlow, page, mem);
|
|
128
|
+
* mem.set(ReceiptCode, receipt.__state);
|
|
129
|
+
* }
|
|
130
|
+
*/
|
|
131
|
+
export async function spawnTab(entry, page, mem, maxSteps = 5000) {
|
|
132
|
+
return runGraph(entry, undefined, page.context(), mem, maxSteps);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Wraps one `act()` interaction with a caption and highlight, for a Block
|
|
136
|
+
* author who wants to explicitly narrate a specific step rather than rely
|
|
137
|
+
* on the generic per-fill/per-click narration `waygraph chain
|
|
138
|
+
* WAYGRAPH_STEP=1` already does automatically -
|
|
139
|
+
* `await narrate(submitButton, "confirms the order", () => submitButton.click())`
|
|
140
|
+
* reads at the call site instead of being inferred.
|
|
141
|
+
*
|
|
142
|
+
* A true no-op everywhere except inside a `WAYGRAPH_STEP=1` run: it checks
|
|
143
|
+
* for a `window.__wgPositionRing` hook the step-mode overlay installs on
|
|
144
|
+
* the page before doing anything, so a headless/CI/automated run (nobody
|
|
145
|
+
* watching, per-character delays and highlight pauses would just be wasted
|
|
146
|
+
* time) pays zero extra cost - not even the boundingBox() lookup runs. The
|
|
147
|
+
* `action` itself always runs regardless; only the narration is
|
|
148
|
+
* conditional.
|
|
149
|
+
* @example await narrate(submitButton, "confirms the order", () => submitButton.click())
|
|
150
|
+
*/
|
|
151
|
+
export async function narrate(locator, caption, action) {
|
|
152
|
+
const page = locator.page();
|
|
153
|
+
const active = await page
|
|
154
|
+
.evaluate(() => typeof globalThis.__wgPositionRing === "function")
|
|
155
|
+
.catch(() => false);
|
|
156
|
+
if (!active)
|
|
157
|
+
return action();
|
|
158
|
+
let owning = false;
|
|
159
|
+
try {
|
|
160
|
+
const box = await locator.boundingBox();
|
|
161
|
+
if (box) {
|
|
162
|
+
await page
|
|
163
|
+
.evaluate(({ box, caption }) => {
|
|
164
|
+
const w = globalThis;
|
|
165
|
+
if (w.__wgPositionRing)
|
|
166
|
+
w.__wgPositionRing(box, caption, "planned");
|
|
167
|
+
// The CLI's own automatic per-fill/per-click narration checks
|
|
168
|
+
// this before showing (and overwriting) its own guess - an
|
|
169
|
+
// explicitly authored caption should win, not get immediately
|
|
170
|
+
// replaced a moment later by the generic one.
|
|
171
|
+
w.__wgLastNarrate = Date.now();
|
|
172
|
+
// Also claim the ring itself: the auto-highlight click/fill
|
|
173
|
+
// patches each hide the ring right after THEIR own step
|
|
174
|
+
// finishes, which - if action() below does more than one thing
|
|
175
|
+
// (e.g. a click then a wait for a toast) - would wipe this
|
|
176
|
+
// caption after just the first sub-step instead of the whole
|
|
177
|
+
// narrated action. Their hideRing() calls no-op while this is
|
|
178
|
+
// true; narrate() below is the one that actually clears it.
|
|
179
|
+
w.__wgNarrateOwnsRing = true;
|
|
180
|
+
}, { box, caption })
|
|
181
|
+
.catch(() => { });
|
|
182
|
+
owning = true;
|
|
183
|
+
// The ring alone is pointless if action() fires on the very next
|
|
184
|
+
// tick - a click that navigates (e.g. a form submit) wipes the ring
|
|
185
|
+
// before a human can read the caption at all. Give it the same
|
|
186
|
+
// "pop for a few seconds" dwell as the CLI's own auto-highlight
|
|
187
|
+
// clicks before the real action runs.
|
|
188
|
+
await new Promise((resolve) => setTimeout(resolve, 650));
|
|
189
|
+
// Re-stamp __wgLastNarrate right before handing off to action() - the
|
|
190
|
+
// CLI's auto-highlight patches only treat a narrate() as "just
|
|
191
|
+
// handled" within a short window (500ms), which the dwell above
|
|
192
|
+
// already burned through. Without this, action()'s own click/fill
|
|
193
|
+
// (same patched Locator prototype) would stomp this caption with its
|
|
194
|
+
// own generic one a moment after it finally became visible.
|
|
195
|
+
await page
|
|
196
|
+
.evaluate(() => {
|
|
197
|
+
globalThis.__wgLastNarrate = Date.now();
|
|
198
|
+
})
|
|
199
|
+
.catch(() => { });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// best-effort - the real action below still runs either way
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
return await action();
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
if (owning) {
|
|
210
|
+
await page
|
|
211
|
+
.evaluate(() => {
|
|
212
|
+
const w = globalThis;
|
|
213
|
+
w.__wgNarrateOwnsRing = false;
|
|
214
|
+
if (w.__wgHideRing)
|
|
215
|
+
w.__wgHideRing();
|
|
216
|
+
})
|
|
217
|
+
.catch(() => { });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Reserved markers bookending a `defineFlow([start, ...blocks, end])` call. */
|
|
222
|
+
export const start = Symbol("waygraph.start");
|
|
223
|
+
export const end = Symbol("waygraph.end");
|