waygraph 0.0.1 → 0.2.0
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 +123 -16
- package/bin/waygraph +13 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +998 -0
- package/dist/engine.d.ts +136 -2
- package/dist/engine.js +125 -27
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/types.d.ts +22 -0
- package/package.json +9 -2
package/dist/engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowserContext } from "@playwright/test";
|
|
1
|
+
import type { BrowserContext, Page } from "@playwright/test";
|
|
2
2
|
import type { Block, Checkpoint, Instruction, DefinedBlock } from "./types.js";
|
|
3
3
|
import { MemPage } from "./mem-page.js";
|
|
4
4
|
import type { Trait } from "./trait.js";
|
|
@@ -29,7 +29,45 @@ export declare function preflight(mem: MemPage, block: Block<any, any>): void;
|
|
|
29
29
|
* reachable outcome by construction (as with a `defineFlow`-built Flow, which has
|
|
30
30
|
* no routing at all) - there is nothing to validate against.
|
|
31
31
|
*/
|
|
32
|
-
export declare function runGraph<TOut extends Checkpoint<string>>(entry: Block<Checkpoint<"__start__">, TOut>, terminals: ReadonlySet<string> | undefined, context: BrowserContext, mem: MemPage, maxSteps?: number): Promise<TOut>;
|
|
32
|
+
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>;
|
|
33
|
+
/** Options for `runGraph`'s trailing `options` arg and for {@link Flow.run}'s `options` overload. */
|
|
34
|
+
export interface RunGraphOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Drive this already-open page instead of opening a new one via
|
|
37
|
+
* `context.newPage()`. Lets a run continue on a tab the caller already
|
|
38
|
+
* has open (e.g. one that's already logged in), instead of every run
|
|
39
|
+
* being forced to start its own fresh tab and lose access to whatever was
|
|
40
|
+
* already there.
|
|
41
|
+
*/
|
|
42
|
+
page?: Page;
|
|
43
|
+
/**
|
|
44
|
+
* `Flow.run` defaults this to `true` only when it opened the page itself
|
|
45
|
+
* (no `page` given) - matching every call from before this option
|
|
46
|
+
* existed. If you pass your own `page`, it defaults to `false` instead:
|
|
47
|
+
* a page you opened is yours to close, not this run's to close out from
|
|
48
|
+
* under you. Either way an explicit value here always wins. `runGraph`
|
|
49
|
+
* itself has no such page-aware default (its caller already decided
|
|
50
|
+
* `page` and `closeOnFinish` together) - it just does what you ask.
|
|
51
|
+
*/
|
|
52
|
+
closeOnFinish?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Drives a genuinely separate second tab through its own Block/Flow, in the
|
|
56
|
+
* same browser context an existing `page` already belongs to - the primitive
|
|
57
|
+
* `observe()` is specifically allowed to reach for (see {@link Instruction.observe}),
|
|
58
|
+
* since `act`/`resolve` can't touch `page.context()` at all. A thin, named
|
|
59
|
+
* wrapper around `runGraph` (which already opens its own page from whatever
|
|
60
|
+
* context it's given) rather than a new mechanism - the point is making the
|
|
61
|
+
* pattern discoverable and giving it a real name, not inventing new tab-lifecycle
|
|
62
|
+
* logic. The spawned tab is closed (by `runGraph`'s own `finally`) before this
|
|
63
|
+
* resolves; the original `page` is never touched.
|
|
64
|
+
* @example
|
|
65
|
+
* async observe(page, mem) {
|
|
66
|
+
* const receipt = await spawnTab(EmailReceiptFlow, page, mem);
|
|
67
|
+
* mem.set(ReceiptCode, receipt.__state);
|
|
68
|
+
* }
|
|
69
|
+
*/
|
|
70
|
+
export declare function spawnTab<TOut extends Checkpoint<string>>(entry: Block<Checkpoint<"__start__">, TOut>, page: Page, mem: MemPage, maxSteps?: number): Promise<TOut>;
|
|
33
71
|
/** Reserved markers bookending a `defineFlow([start, ...blocks, end])` call. */
|
|
34
72
|
export declare const start: unique symbol;
|
|
35
73
|
export declare const end: unique symbol;
|
|
@@ -67,8 +105,53 @@ type EndMarker = typeof end;
|
|
|
67
105
|
* each call, so it's still correct after a patch too. Returns a new Flow -
|
|
68
106
|
* the original is untouched, same as every other decorator in this module.
|
|
69
107
|
*/
|
|
108
|
+
/**
|
|
109
|
+
* One Block's identity as exposed by graph introspection - {@link Flow.blocks},
|
|
110
|
+
* {@link ComposedBlock.steps} - without needing to execute anything. Does not
|
|
111
|
+
* include the Block's literal Checkpoint `In`/`Out` tags: those are TypeScript
|
|
112
|
+
* literal types, erased at runtime, and can only be recovered by a build-time
|
|
113
|
+
* static-analysis tool reading the source's own type annotations - see the
|
|
114
|
+
* `graph-visualization` openspec change for that piece.
|
|
115
|
+
*/
|
|
116
|
+
export interface BlockInfo {
|
|
117
|
+
name: string;
|
|
118
|
+
/** `branch()`'s routing table, if this Block has one - see {@link Block.routes}. */
|
|
119
|
+
routes?: Readonly<Record<string, string | null>>;
|
|
120
|
+
/**
|
|
121
|
+
* The actual Block, re-runnable on its own via
|
|
122
|
+
* `engine.defineFlow([start, info.block, end])` - e.g. step-through
|
|
123
|
+
* tooling that runs a Flow's Blocks one at a time. Introspection that only
|
|
124
|
+
* wants to read the shape (a visualizer) can ignore this and use `name`/
|
|
125
|
+
* `routes` alone.
|
|
126
|
+
*/
|
|
127
|
+
block: Block<any, any>;
|
|
128
|
+
}
|
|
70
129
|
export interface Flow<Out extends Checkpoint<string>> {
|
|
130
|
+
/**
|
|
131
|
+
* This Flow's constituent Blocks, in order between `start`/`end`, as plain
|
|
132
|
+
* data - for introspection/visualization tools, without running anything.
|
|
133
|
+
* @example loginFlow.blocks() // [{ name: "login" }, { name: "add-to-cart", routes: {...} }]
|
|
134
|
+
*/
|
|
135
|
+
blocks(): readonly BlockInfo[];
|
|
71
136
|
run(context: BrowserContext, mem: MemPage): Promise<Out>;
|
|
137
|
+
/**
|
|
138
|
+
* `options.closeOnFinish: false` hands the driven page back instead of
|
|
139
|
+
* closing it - use this overload (a literal `false`, not a `boolean`) to
|
|
140
|
+
* get the `{ result, page }` shape typed correctly.
|
|
141
|
+
* @example const { result, page } = await loginFlow.run(context, mem, { closeOnFinish: false });
|
|
142
|
+
*/
|
|
143
|
+
run(context: BrowserContext, mem: MemPage, options: RunGraphOptions & {
|
|
144
|
+
closeOnFinish: false;
|
|
145
|
+
}): Promise<{
|
|
146
|
+
result: Out;
|
|
147
|
+
page: Page;
|
|
148
|
+
}>;
|
|
149
|
+
/**
|
|
150
|
+
* `options.page` drives that already-open page instead of opening a new
|
|
151
|
+
* one - see {@link RunGraphOptions}.
|
|
152
|
+
* @example await loginFlow.run(context, mem, { page: existingPage });
|
|
153
|
+
*/
|
|
154
|
+
run(context: BrowserContext, mem: MemPage, options?: RunGraphOptions): Promise<Out>;
|
|
72
155
|
run(mem: MemPage, config?: EngineConfig): Promise<Out>;
|
|
73
156
|
/**
|
|
74
157
|
* Replaces the given Block's whole verify list, wherever it sits in this
|
|
@@ -87,6 +170,56 @@ export interface Flow<Out extends Checkpoint<string>> {
|
|
|
87
170
|
*/
|
|
88
171
|
modBlockVerify(block: Block<any, any> | string | number, nameOrIndex: string | number, newCheck: Trait["check"] | Trait): Flow<Out>;
|
|
89
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* A Block guaranteed to have `withStepVerify`/`modStepVerify` attached - what
|
|
175
|
+
* {@link composeBlock} returns. Extends plain `Block`, so it drops straight into
|
|
176
|
+
* `defineFlow([start, ..., composed, ..., end])` like any other Block; no special
|
|
177
|
+
* casing needed anywhere that only expects a `Block`.
|
|
178
|
+
*/
|
|
179
|
+
export interface ComposedBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>> extends Block<In, Out> {
|
|
180
|
+
/**
|
|
181
|
+
* This composed Block's constituent steps, in order, as plain data - same
|
|
182
|
+
* shape and purpose as {@link Flow.blocks}.
|
|
183
|
+
* @example GovFormBlock.steps() // [{ name: "step-1-name" }, { name: "step-2-address" }, ...]
|
|
184
|
+
*/
|
|
185
|
+
steps(): readonly BlockInfo[];
|
|
186
|
+
/**
|
|
187
|
+
* Replaces one step's whole verify list, wherever it sits in this composed
|
|
188
|
+
* Block - same rules as {@link withVerify}. Throws if no step matches.
|
|
189
|
+
* @example GovFormBlock.withStepVerify("step-2-address", [])
|
|
190
|
+
*/
|
|
191
|
+
withStepVerify(step: Block<any, any> | string | number, verify: Trait[] | ((out: any) => Trait[])): ComposedBlock<In, Out>;
|
|
192
|
+
/**
|
|
193
|
+
* Replaces one Trait inside one step's verify list - same rules as
|
|
194
|
+
* {@link modVerify}. Throws if no step matches, or that step has no verify
|
|
195
|
+
* trait at the given name/index.
|
|
196
|
+
* @example GovFormBlock.modStepVerify("step-2-address", "zip-visible", Trait.visible("#zip-confirmed"))
|
|
197
|
+
*/
|
|
198
|
+
modStepVerify(step: Block<any, any> | string | number, nameOrIndex: string | number, newCheck: Trait["check"] | Trait): ComposedBlock<In, Out>;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Chains several Blocks into one named, exported unit - what a naturally
|
|
202
|
+
* multi-step feature (a stepper form with several pages, say) wants instead of
|
|
203
|
+
* either one giant Block with no per-step confirmation, or several separately
|
|
204
|
+
* exported Blocks the caller has to wire together by hand every time. Each
|
|
205
|
+
* step keeps its own real `act`/`resolve`/`verify`, chained the same way
|
|
206
|
+
* `connect()` already chains any two Blocks - `composeBlock` is not a new
|
|
207
|
+
* execution mechanism, just `connect()` reduced over the list plus a name and
|
|
208
|
+
* the same step-list closure `Flow` already keeps for its own Block list, so
|
|
209
|
+
* `withStepVerify`/`modStepVerify` can patch one step from outside without the
|
|
210
|
+
* caller needing to hand-rebuild the chain. Resolution is by `.name`, not
|
|
211
|
+
* object identity, so a step reference stays valid even after an earlier patch
|
|
212
|
+
* swapped that slot for a new object - see {@link Flow.withBlockVerify} for
|
|
213
|
+
* the identical guarantee one level up. Each call returns a new composed
|
|
214
|
+
* Block; the original is untouched.
|
|
215
|
+
* @example composeBlock("gov-form", [step1, step2, step3, step4])
|
|
216
|
+
*/
|
|
217
|
+
export declare function composeBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, Out>]): ComposedBlock<In, Out>;
|
|
218
|
+
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, Out>]): ComposedBlock<In, Out>;
|
|
219
|
+
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, Out>]): ComposedBlock<In, Out>;
|
|
220
|
+
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, Out>]): ComposedBlock<In, Out>;
|
|
221
|
+
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, Out>]): ComposedBlock<In, Out>;
|
|
222
|
+
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, F>, Block<F, Out>]): ComposedBlock<In, Out>;
|
|
90
223
|
/** Configures how an Engine launches its own browser - only used by `Flow.run(mem)` (no context given); ignored by `Flow.run(context, mem)`, since that context is already launched. */
|
|
91
224
|
export interface EngineConfig {
|
|
92
225
|
/** Default `true`, matching Playwright's own default. */
|
|
@@ -169,6 +302,7 @@ export declare function defineBlock<In extends Checkpoint<string>, Out extends C
|
|
|
169
302
|
instruction: Instruction<In, Out, any>;
|
|
170
303
|
next?: Block<In, Out>["next"];
|
|
171
304
|
requires?: Block<In, Out>["requires"];
|
|
305
|
+
routes?: Block<In, Out>["routes"];
|
|
172
306
|
}): DefinedBlock<In, Out>;
|
|
173
307
|
/**
|
|
174
308
|
* Returns a new Block that runs exactly like `block` - same act/observe/resolve,
|
package/dist/engine.js
CHANGED
|
@@ -37,9 +37,10 @@ export function preflight(mem, block) {
|
|
|
37
37
|
* reachable outcome by construction (as with a `defineFlow`-built Flow, which has
|
|
38
38
|
* no routing at all) - there is nothing to validate against.
|
|
39
39
|
*/
|
|
40
|
-
export async function runGraph(entry, terminals, context, mem, maxSteps = 5000) {
|
|
40
|
+
export async function runGraph(entry, terminals, context, mem, maxSteps = 5000, options) {
|
|
41
41
|
preflight(mem, entry);
|
|
42
|
-
const page = await context.newPage();
|
|
42
|
+
const page = options?.page ?? (await context.newPage());
|
|
43
|
+
const closeOnFinish = options?.closeOnFinish ?? true;
|
|
43
44
|
try {
|
|
44
45
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
46
|
let current = entry;
|
|
@@ -67,9 +68,30 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000)
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
finally {
|
|
70
|
-
|
|
71
|
+
if (closeOnFinish) {
|
|
72
|
+
await page.close();
|
|
73
|
+
}
|
|
71
74
|
}
|
|
72
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Drives a genuinely separate second tab through its own Block/Flow, in the
|
|
78
|
+
* same browser context an existing `page` already belongs to - the primitive
|
|
79
|
+
* `observe()` is specifically allowed to reach for (see {@link Instruction.observe}),
|
|
80
|
+
* since `act`/`resolve` can't touch `page.context()` at all. A thin, named
|
|
81
|
+
* wrapper around `runGraph` (which already opens its own page from whatever
|
|
82
|
+
* context it's given) rather than a new mechanism - the point is making the
|
|
83
|
+
* pattern discoverable and giving it a real name, not inventing new tab-lifecycle
|
|
84
|
+
* logic. The spawned tab is closed (by `runGraph`'s own `finally`) before this
|
|
85
|
+
* resolves; the original `page` is never touched.
|
|
86
|
+
* @example
|
|
87
|
+
* async observe(page, mem) {
|
|
88
|
+
* const receipt = await spawnTab(EmailReceiptFlow, page, mem);
|
|
89
|
+
* mem.set(ReceiptCode, receipt.__state);
|
|
90
|
+
* }
|
|
91
|
+
*/
|
|
92
|
+
export async function spawnTab(entry, page, mem, maxSteps = 5000) {
|
|
93
|
+
return runGraph(entry, undefined, page.context(), mem, maxSteps);
|
|
94
|
+
}
|
|
73
95
|
/** Reserved markers bookending a `defineFlow([start, ...blocks, end])` call. */
|
|
74
96
|
export const start = Symbol("waygraph.start");
|
|
75
97
|
export const end = Symbol("waygraph.end");
|
|
@@ -89,31 +111,77 @@ function findBlockIndex(middle, block) {
|
|
|
89
111
|
}
|
|
90
112
|
function buildFlow(middle, engineConfig) {
|
|
91
113
|
const chain = middle.reduce((a, b) => connect(a, b));
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
// Declared separately (not as the object-literal method below) so the
|
|
115
|
+
// three-overload `Flow.run` signature can be checked against the actual
|
|
116
|
+
// per-call-shape behavior, then attached with a cast - object-literal
|
|
117
|
+
// methods can't satisfy TS's per-overload implementation check the way a
|
|
118
|
+
// standalone function assigned to the property can.
|
|
119
|
+
const run = async function run(contextOrMem, memOrConfig, options) {
|
|
120
|
+
if (contextOrMem instanceof MemPage) {
|
|
121
|
+
const mem = contextOrMem;
|
|
122
|
+
const config = { ...engineConfig, ...memOrConfig };
|
|
123
|
+
// Checked again inside runGraph too, but doing it here first means a
|
|
124
|
+
// missing key fails before a browser process even launches, not just
|
|
125
|
+
// before a page opens - the same "fail loud, cheaply" guarantee
|
|
126
|
+
// run(context, mem) already has via an existing context.
|
|
127
|
+
preflight(mem, chain);
|
|
128
|
+
const browserName = config.browserName ?? "chromium";
|
|
129
|
+
const launch = LAUNCHERS[browserName];
|
|
130
|
+
// If CHROME_PATH/CHROMIUM_PATH is set in the environment (e.g. a system
|
|
131
|
+
// or Flatpak Chromium), launch that instead of Playwright's own bundled
|
|
132
|
+
// binary - only for chromium (the env var names a Chromium build, not a
|
|
133
|
+
// Firefox/WebKit one), and a no-op default anywhere that env var isn't set.
|
|
134
|
+
const executablePath = browserName === "chromium" ? process.env.CHROME_PATH || process.env.CHROMIUM_PATH : undefined;
|
|
135
|
+
const browser = await launch.launch({
|
|
136
|
+
headless: config.headless ?? true,
|
|
137
|
+
...(config.slowMo !== undefined ? { slowMo: config.slowMo } : {}),
|
|
138
|
+
...(executablePath ? { executablePath } : {}),
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
const context = await browser.newContext();
|
|
142
|
+
return await runGraph(chain, undefined, context, mem);
|
|
114
143
|
}
|
|
115
|
-
|
|
116
|
-
|
|
144
|
+
finally {
|
|
145
|
+
await browser.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const context = contextOrMem;
|
|
149
|
+
const mem = memOrConfig;
|
|
150
|
+
// Open (or reuse) the page here, not inside runGraph, so this method -
|
|
151
|
+
// not runGraph - decides whether to close it once the run is over; the
|
|
152
|
+
// page reference has to survive past runGraph's own return either way.
|
|
153
|
+
const gotOwnPage = options?.page === undefined;
|
|
154
|
+
const page = options?.page ?? (await context.newPage());
|
|
155
|
+
// Defaulting close-on-finish to `true` unconditionally would close a
|
|
156
|
+
// page the *caller* opened and handed in via `options.page` - the exact
|
|
157
|
+
// thing this option exists to let a caller keep driving. Only default to
|
|
158
|
+
// closing when this call opened the page itself (no `options.page`
|
|
159
|
+
// given), which is the one case that matches every `run(context, mem)`
|
|
160
|
+
// call from before this option existed. An explicit `closeOnFinish`
|
|
161
|
+
// always wins either way.
|
|
162
|
+
const closeOnFinish = options?.closeOnFinish ?? gotOwnPage;
|
|
163
|
+
const result = await runGraph(chain, undefined, context, mem, 5000, {
|
|
164
|
+
page,
|
|
165
|
+
closeOnFinish: false,
|
|
166
|
+
});
|
|
167
|
+
if (closeOnFinish) {
|
|
168
|
+
await page.close();
|
|
169
|
+
}
|
|
170
|
+
// The return *shape* only changes on an explicit `closeOnFinish: false`
|
|
171
|
+
// (the literal the `{ result, page }` overload is typed against) - not
|
|
172
|
+
// on the computed `closeOnFinish` default above. A caller who passed
|
|
173
|
+
// their own `page` and left `closeOnFinish` unset already holds that
|
|
174
|
+
// page reference; handing it back again in a different return shape
|
|
175
|
+
// would silently break the plain-`Out` overload their call actually
|
|
176
|
+
// matched at the type level.
|
|
177
|
+
if (options?.closeOnFinish === false) {
|
|
178
|
+
return { result, page };
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
};
|
|
182
|
+
return {
|
|
183
|
+
blocks: () => middle.map((b) => ({ name: b.name, block: b, ...(b.routes ? { routes: b.routes } : {}) })),
|
|
184
|
+
run: run,
|
|
117
185
|
withBlockVerify(block, verify) {
|
|
118
186
|
const index = findBlockIndex(middle, block);
|
|
119
187
|
const patched = [...middle];
|
|
@@ -128,6 +196,29 @@ function buildFlow(middle, engineConfig) {
|
|
|
128
196
|
},
|
|
129
197
|
};
|
|
130
198
|
}
|
|
199
|
+
export function composeBlock(name, steps) {
|
|
200
|
+
if (steps.length === 0) {
|
|
201
|
+
throw new Error(`composeBlock: "${name}" needs at least one step`);
|
|
202
|
+
}
|
|
203
|
+
const chain = steps.reduce((a, b) => connect(a, b));
|
|
204
|
+
return {
|
|
205
|
+
...chain,
|
|
206
|
+
name,
|
|
207
|
+
steps: () => steps.map((b) => ({ name: b.name, block: b, ...(b.routes ? { routes: b.routes } : {}) })),
|
|
208
|
+
withStepVerify(step, verify) {
|
|
209
|
+
const index = findBlockIndex(steps, step);
|
|
210
|
+
const patched = [...steps];
|
|
211
|
+
patched[index] = withVerify(steps[index], verify);
|
|
212
|
+
return composeBlock(name, patched);
|
|
213
|
+
},
|
|
214
|
+
modStepVerify(step, nameOrIndex, newCheck) {
|
|
215
|
+
const index = findBlockIndex(steps, step);
|
|
216
|
+
const patched = [...steps];
|
|
217
|
+
patched[index] = modVerify(steps[index], nameOrIndex, newCheck);
|
|
218
|
+
return composeBlock(name, patched);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
131
222
|
/**
|
|
132
223
|
* Holds engine-level configuration shared across every flow defined from it -
|
|
133
224
|
* for now, just how to launch a browser when a Flow owns its own
|
|
@@ -160,6 +251,10 @@ export function branch(block, routes) {
|
|
|
160
251
|
const base = stripMethods(block);
|
|
161
252
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
162
253
|
let routed;
|
|
254
|
+
const routesAsData = {};
|
|
255
|
+
for (const [tag, target] of Object.entries(routes)) {
|
|
256
|
+
routesAsData[tag] = target === null ? null : target === block ? base.name : target.name;
|
|
257
|
+
}
|
|
163
258
|
routed = defineBlock({
|
|
164
259
|
...base,
|
|
165
260
|
next: (checkpoint) => {
|
|
@@ -171,6 +266,7 @@ export function branch(block, routes) {
|
|
|
171
266
|
// the routed Block, not the original with no `next` of its own.
|
|
172
267
|
return target === block ? routed : target;
|
|
173
268
|
},
|
|
269
|
+
routes: routesAsData,
|
|
174
270
|
});
|
|
175
271
|
return routed;
|
|
176
272
|
}
|
|
@@ -190,6 +286,7 @@ function stripMethods(block) {
|
|
|
190
286
|
instruction: block.instruction,
|
|
191
287
|
...(block.next ? { next: block.next } : {}),
|
|
192
288
|
...(block.requires ? { requires: block.requires } : {}),
|
|
289
|
+
...(block.routes ? { routes: block.routes } : {}),
|
|
193
290
|
};
|
|
194
291
|
}
|
|
195
292
|
/**
|
|
@@ -207,6 +304,7 @@ export function defineBlock(base) {
|
|
|
207
304
|
instruction: base.instruction,
|
|
208
305
|
...(base.next ? { next: base.next } : {}),
|
|
209
306
|
...(base.requires ? { requires: base.requires } : {}),
|
|
307
|
+
...(base.routes ? { routes: base.routes } : {}),
|
|
210
308
|
};
|
|
211
309
|
return {
|
|
212
310
|
...plain,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export type { Checkpoint, Instruction, Block, DefinedBlock } from "./types.js";
|
|
1
|
+
export type { Checkpoint, Instruction, Block, DefinedBlock, Start } from "./types.js";
|
|
2
2
|
export { connect, checkpoint } from "./types.js";
|
|
3
3
|
export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
|
|
4
4
|
export { Trait, urlMatches, textEquals, visible } from "./trait.js";
|
|
5
|
-
export { runGraph, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, preflight, } from "./engine.js";
|
|
6
|
-
export type { Flow, EngineConfig } from "./engine.js";
|
|
5
|
+
export { runGraph, spawnTab, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, composeBlock, preflight, } from "./engine.js";
|
|
6
|
+
export type { Flow, EngineConfig, ComposedBlock, RunGraphOptions, BlockInfo } from "./engine.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { connect, checkpoint } from "./types.js";
|
|
2
2
|
export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
|
|
3
3
|
export { Trait, urlMatches, textEquals, visible } from "./trait.js";
|
|
4
|
-
export { runGraph, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, preflight, } from "./engine.js";
|
|
4
|
+
export { runGraph, spawnTab, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, composeBlock, preflight, } from "./engine.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -13,6 +13,16 @@ export type Checkpoint<Tag extends string> = {
|
|
|
13
13
|
* @example resolve: () => checkpoint("LoggedIn")
|
|
14
14
|
*/
|
|
15
15
|
export declare function checkpoint<Tag extends string>(tag: Tag): Checkpoint<Tag>;
|
|
16
|
+
/**
|
|
17
|
+
* The type every entry Block's `In` must be - `runGraph` always seeds a fresh
|
|
18
|
+
* run with `checkpoint("__start__")` as the fake initial input, so a Block
|
|
19
|
+
* meant to lead off a Flow needs exactly this type. Exported so no project
|
|
20
|
+
* has to know or hand-type the `"__start__"` literal itself - the same reason
|
|
21
|
+
* `checkpoint()` exists instead of every call site spelling `{ __state: tag }`
|
|
22
|
+
* by hand.
|
|
23
|
+
* @example export const LoginBlock: Block<Start, LoggedIn> = { ... }
|
|
24
|
+
*/
|
|
25
|
+
export type Start = Checkpoint<"__start__">;
|
|
16
26
|
/**
|
|
17
27
|
* The four-phase pipeline every Block runs. Most Blocks only need `act` + a
|
|
18
28
|
* one-line `resolve` - add `observe` only once a Block genuinely branches, add
|
|
@@ -69,6 +79,18 @@ export interface Block<In extends Checkpoint<string>, Out extends Checkpoint<str
|
|
|
69
79
|
* self-loop - see `runGraph`'s `maxSteps` for the safety net that needs.
|
|
70
80
|
*/
|
|
71
81
|
next?(checkpoint: Out): Block<any, any> | null | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* `branch()`'s routing table as plain, readable data - Checkpoint tag ->
|
|
84
|
+
* target Block's `.name` (`null` for a terminal tag) - alongside {@link next}
|
|
85
|
+
* itself, which stays the actual runtime dispatch. `next` is an opaque
|
|
86
|
+
* closure a graph-introspection tool can't read; this is `next`'s own
|
|
87
|
+
* routing decisions exposed as data instead, purely additive and never
|
|
88
|
+
* consulted by `runGraph`/`connect` - it exists only for tools like
|
|
89
|
+
* `Flow`'s inspection method (and, eventually, `toMermaid`) to walk without
|
|
90
|
+
* executing anything. A route back to the Block itself (a self-loop) maps
|
|
91
|
+
* to that same Block's own name, not `null`.
|
|
92
|
+
*/
|
|
93
|
+
routes?: Readonly<Record<string, string | null>>;
|
|
72
94
|
/**
|
|
73
95
|
* MemKeys this Block's `act` reads from `mem` and needs already set.
|
|
74
96
|
* Declaring it lets {@link preflight} (built into `runGraph`) catch a missing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waygraph",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"waygraph": "bin/waygraph"
|
|
16
|
+
},
|
|
14
17
|
"files": [
|
|
15
|
-
"dist"
|
|
18
|
+
"dist",
|
|
19
|
+
"bin"
|
|
16
20
|
],
|
|
17
21
|
"repository": {
|
|
18
22
|
"type": "git",
|
|
@@ -26,6 +30,9 @@
|
|
|
26
30
|
"peerDependencies": {
|
|
27
31
|
"@playwright/test": "^1.40.0"
|
|
28
32
|
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"tsx": "^4.0.0"
|
|
35
|
+
},
|
|
29
36
|
"devDependencies": {
|
|
30
37
|
"@playwright/test": "^1.55.0",
|
|
31
38
|
"@types/node": "^26.0.0",
|