waygraph 0.3.2 → 0.4.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/dist/cli.js +639 -79
- package/dist/engine.d.ts +86 -1
- package/dist/engine.js +253 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/engine.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowserContext, Page } from "@playwright/test";
|
|
1
|
+
import type { BrowserContext, Page, Locator } 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";
|
|
@@ -68,6 +68,24 @@ export interface RunGraphOptions {
|
|
|
68
68
|
* }
|
|
69
69
|
*/
|
|
70
70
|
export declare function spawnTab<TOut extends Checkpoint<string>>(entry: Block<Checkpoint<"__start__">, TOut>, page: Page, mem: MemPage, maxSteps?: number): Promise<TOut>;
|
|
71
|
+
/**
|
|
72
|
+
* Wraps one `act()` interaction with a caption and highlight, for a Block
|
|
73
|
+
* author who wants to explicitly narrate a specific step rather than rely
|
|
74
|
+
* on the generic per-fill/per-click narration `waygraph chain
|
|
75
|
+
* WAYGRAPH_STEP=1` already does automatically -
|
|
76
|
+
* `await narrate(submitButton, "confirms the order", () => submitButton.click())`
|
|
77
|
+
* reads at the call site instead of being inferred.
|
|
78
|
+
*
|
|
79
|
+
* A true no-op everywhere except inside a `WAYGRAPH_STEP=1` run: it checks
|
|
80
|
+
* for a `window.__wgPositionRing` hook the step-mode overlay installs on
|
|
81
|
+
* the page before doing anything, so a headless/CI/automated run (nobody
|
|
82
|
+
* watching, per-character delays and highlight pauses would just be wasted
|
|
83
|
+
* time) pays zero extra cost - not even the boundingBox() lookup runs. The
|
|
84
|
+
* `action` itself always runs regardless; only the narration is
|
|
85
|
+
* conditional.
|
|
86
|
+
* @example await narrate(submitButton, "confirms the order", () => submitButton.click())
|
|
87
|
+
*/
|
|
88
|
+
export declare function narrate<T>(locator: Locator, caption: string, action: () => Promise<T>): Promise<T>;
|
|
71
89
|
/** Reserved markers bookending a `defineFlow([start, ...blocks, end])` call. */
|
|
72
90
|
export declare const start: unique symbol;
|
|
73
91
|
export declare const end: unique symbol;
|
|
@@ -117,6 +135,14 @@ export interface BlockInfo {
|
|
|
117
135
|
name: string;
|
|
118
136
|
/** `branch()`'s routing table, if this Block has one - see {@link Block.routes}. */
|
|
119
137
|
routes?: Readonly<Record<string, string | null>>;
|
|
138
|
+
/**
|
|
139
|
+
* True when {@link chainFlow} inserted a session-reset boundary
|
|
140
|
+
* immediately before this Block - i.e. this is the first Block of a
|
|
141
|
+
* sub-flow that was wrapped in {@link withSessionReset}, and it isn't
|
|
142
|
+
* the very first sub-flow in the chain. Absent (not just false) from
|
|
143
|
+
* `Flow.blocks()` on an ordinary, non-chained Flow.
|
|
144
|
+
*/
|
|
145
|
+
resetSessionBefore?: boolean;
|
|
120
146
|
/**
|
|
121
147
|
* The actual Block, re-runnable on its own via
|
|
122
148
|
* `engine.defineFlow([start, info.block, end])` - e.g. step-through
|
|
@@ -127,6 +153,22 @@ export interface BlockInfo {
|
|
|
127
153
|
block: Block<any, any>;
|
|
128
154
|
}
|
|
129
155
|
export interface Flow<Out extends Checkpoint<string>> {
|
|
156
|
+
/**
|
|
157
|
+
* True once this Flow has been wrapped in {@link withSessionReset} - a
|
|
158
|
+
* hint for `chainFlow`, not something `run()` itself checks: it marks
|
|
159
|
+
* this Flow as one that expects to start from a genuinely fresh,
|
|
160
|
+
* unauthenticated browser state (e.g. a login flow meant to be
|
|
161
|
+
* re-entered later in a longer chain). `chainFlow` clears cookies and
|
|
162
|
+
* storage right before this Flow's first Block runs, but only when this
|
|
163
|
+
* Flow isn't the very first thing in the chain (nothing to reset yet).
|
|
164
|
+
*/
|
|
165
|
+
readonly resetSession: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Set once this Flow has been wrapped in {@link withTitle} - a display
|
|
168
|
+
* hint for a step-through overlay/showcase (e.g. "Ticket #123"), not
|
|
169
|
+
* something `run()` itself uses.
|
|
170
|
+
*/
|
|
171
|
+
readonly title?: string;
|
|
130
172
|
/**
|
|
131
173
|
* This Flow's constituent Blocks, in order between `start`/`end`, as plain
|
|
132
174
|
* data - for introspection/visualization tools, without running anything.
|
|
@@ -170,6 +212,49 @@ export interface Flow<Out extends Checkpoint<string>> {
|
|
|
170
212
|
*/
|
|
171
213
|
modBlockVerify(block: Block<any, any> | string | number, nameOrIndex: string | number, newCheck: Trait["check"] | Trait): Flow<Out>;
|
|
172
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Marks a Flow as expecting to start from a genuinely fresh, unauthenticated
|
|
217
|
+
* browser state - e.g. a login flow meant to be re-entered later in a longer
|
|
218
|
+
* showcase. Non-destructive, same pattern as {@link withVerify}: returns a
|
|
219
|
+
* new Flow: the original is untouched, and the flag survives any further
|
|
220
|
+
* `withBlockVerify`/`modBlockVerify` patch applied to the result.
|
|
221
|
+
*
|
|
222
|
+
* By itself this does nothing - `Flow.run()` never reads `resetSession`.
|
|
223
|
+
* It's a hint {@link chainFlow} looks for: right before this Flow's first
|
|
224
|
+
* Block runs (skipped if it's the very first thing in the chain - nothing
|
|
225
|
+
* to reset yet), chainFlow clears cookies and storage so this Flow starts
|
|
226
|
+
* exactly as if a human had opened the app fresh, not wherever the
|
|
227
|
+
* previous Flow in the chain happened to leave the page.
|
|
228
|
+
* @example const loginFlow = withSessionReset(engine.defineFlow([start, LoginBlock, end]));
|
|
229
|
+
*/
|
|
230
|
+
export declare function withSessionReset<Out extends Checkpoint<string>>(flow: Flow<Out>): Flow<Out>;
|
|
231
|
+
/**
|
|
232
|
+
* Names a Flow for display - a step-through overlay/showcase (waygraph's
|
|
233
|
+
* `chain --step`) shows this as the running banner while this Flow's own
|
|
234
|
+
* Blocks are executing, instead of one fixed title for the whole run.
|
|
235
|
+
* Non-destructive, same pattern as {@link withSessionReset}.
|
|
236
|
+
* @example const ticket123 = withTitle(loginFlow, "Ticket #123 - Sales rep signs in");
|
|
237
|
+
*/
|
|
238
|
+
export declare function withTitle<Out extends Checkpoint<string>>(flow: Flow<Out>, title: string): Flow<Out>;
|
|
239
|
+
/**
|
|
240
|
+
* Runs several complete, independent Flows one after another, reusing the
|
|
241
|
+
* same page/context/mem in order - each remains its own validated
|
|
242
|
+
* start-to-end unit (a Flow's own `[start, ...blocks, end]` contract is
|
|
243
|
+
* untouched), this only sequences whole Flows. Deliberately NOT the same
|
|
244
|
+
* thing as {@link composeBlock}: composeBlock nests several Blocks into
|
|
245
|
+
* one Block that can drop back into another `defineFlow([...])` - a
|
|
246
|
+
* chainFlow result is a Flow, but not meant to be nested inside another
|
|
247
|
+
* Flow's own Block list. Recursive nesting stays composeBlock's job alone;
|
|
248
|
+
* chainFlow is a flat, one-level sequencing of complete showcases (e.g.
|
|
249
|
+
* "ticket #123 end to end, then ticket #456 end to end").
|
|
250
|
+
*
|
|
251
|
+
* A sub-flow wrapped in {@link withSessionReset} gets its cookies/storage
|
|
252
|
+
* cleared right before it runs (skipped for the very first flow in the
|
|
253
|
+
* chain - nothing to reset yet). The combined result is whatever the LAST
|
|
254
|
+
* flow resolved to.
|
|
255
|
+
* @example chainFlow(ticket123Flow, ticket456Flow).run(context, mem)
|
|
256
|
+
*/
|
|
257
|
+
export declare function chainFlow(...flows: readonly Flow<any>[]): Flow<any>;
|
|
173
258
|
/**
|
|
174
259
|
* A Block guaranteed to have `withStepVerify`/`modStepVerify` attached - what
|
|
175
260
|
* {@link composeBlock} returns. Extends plain `Block`, so it drops straight into
|
package/dist/engine.js
CHANGED
|
@@ -92,6 +92,93 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
|
|
|
92
92
|
export async function spawnTab(entry, page, mem, maxSteps = 5000) {
|
|
93
93
|
return runGraph(entry, undefined, page.context(), mem, maxSteps);
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Wraps one `act()` interaction with a caption and highlight, for a Block
|
|
97
|
+
* author who wants to explicitly narrate a specific step rather than rely
|
|
98
|
+
* on the generic per-fill/per-click narration `waygraph chain
|
|
99
|
+
* WAYGRAPH_STEP=1` already does automatically -
|
|
100
|
+
* `await narrate(submitButton, "confirms the order", () => submitButton.click())`
|
|
101
|
+
* reads at the call site instead of being inferred.
|
|
102
|
+
*
|
|
103
|
+
* A true no-op everywhere except inside a `WAYGRAPH_STEP=1` run: it checks
|
|
104
|
+
* for a `window.__wgPositionRing` hook the step-mode overlay installs on
|
|
105
|
+
* the page before doing anything, so a headless/CI/automated run (nobody
|
|
106
|
+
* watching, per-character delays and highlight pauses would just be wasted
|
|
107
|
+
* time) pays zero extra cost - not even the boundingBox() lookup runs. The
|
|
108
|
+
* `action` itself always runs regardless; only the narration is
|
|
109
|
+
* conditional.
|
|
110
|
+
* @example await narrate(submitButton, "confirms the order", () => submitButton.click())
|
|
111
|
+
*/
|
|
112
|
+
export async function narrate(locator, caption, action) {
|
|
113
|
+
const page = locator.page();
|
|
114
|
+
const active = await page
|
|
115
|
+
.evaluate(() => typeof globalThis.__wgPositionRing === "function")
|
|
116
|
+
.catch(() => false);
|
|
117
|
+
if (!active)
|
|
118
|
+
return action();
|
|
119
|
+
let owning = false;
|
|
120
|
+
try {
|
|
121
|
+
const box = await locator.boundingBox();
|
|
122
|
+
if (box) {
|
|
123
|
+
await page
|
|
124
|
+
.evaluate(({ box, caption }) => {
|
|
125
|
+
const w = globalThis;
|
|
126
|
+
if (w.__wgPositionRing)
|
|
127
|
+
w.__wgPositionRing(box, caption);
|
|
128
|
+
// The CLI's own automatic per-fill/per-click narration checks
|
|
129
|
+
// this before showing (and overwriting) its own guess - an
|
|
130
|
+
// explicitly authored caption should win, not get immediately
|
|
131
|
+
// replaced a moment later by the generic one.
|
|
132
|
+
w.__wgLastNarrate = Date.now();
|
|
133
|
+
// Also claim the ring itself: the auto-highlight click/fill
|
|
134
|
+
// patches each hide the ring right after THEIR own step
|
|
135
|
+
// finishes, which - if action() below does more than one thing
|
|
136
|
+
// (e.g. a click then a wait for a toast) - would wipe this
|
|
137
|
+
// caption after just the first sub-step instead of the whole
|
|
138
|
+
// narrated action. Their hideRing() calls no-op while this is
|
|
139
|
+
// true; narrate() below is the one that actually clears it.
|
|
140
|
+
w.__wgNarrateOwnsRing = true;
|
|
141
|
+
}, { box, caption })
|
|
142
|
+
.catch(() => { });
|
|
143
|
+
owning = true;
|
|
144
|
+
// The ring alone is pointless if action() fires on the very next
|
|
145
|
+
// tick - a click that navigates (e.g. a form submit) wipes the ring
|
|
146
|
+
// before a human can read the caption at all. Give it the same
|
|
147
|
+
// "pop for a few seconds" dwell as the CLI's own auto-highlight
|
|
148
|
+
// clicks before the real action runs.
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 650));
|
|
150
|
+
// Re-stamp __wgLastNarrate right before handing off to action() - the
|
|
151
|
+
// CLI's auto-highlight patches only treat a narrate() as "just
|
|
152
|
+
// handled" within a short window (500ms), which the dwell above
|
|
153
|
+
// already burned through. Without this, action()'s own click/fill
|
|
154
|
+
// (same patched Locator prototype) would stomp this caption with its
|
|
155
|
+
// own generic one a moment after it finally became visible.
|
|
156
|
+
await page
|
|
157
|
+
.evaluate(() => {
|
|
158
|
+
globalThis.__wgLastNarrate = Date.now();
|
|
159
|
+
})
|
|
160
|
+
.catch(() => { });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// best-effort - the real action below still runs either way
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
return await action();
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
if (owning) {
|
|
171
|
+
await page
|
|
172
|
+
.evaluate(() => {
|
|
173
|
+
const w = globalThis;
|
|
174
|
+
w.__wgNarrateOwnsRing = false;
|
|
175
|
+
if (w.__wgHideRing)
|
|
176
|
+
w.__wgHideRing();
|
|
177
|
+
})
|
|
178
|
+
.catch(() => { });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
95
182
|
/** Reserved markers bookending a `defineFlow([start, ...blocks, end])` call. */
|
|
96
183
|
export const start = Symbol("waygraph.start");
|
|
97
184
|
export const end = Symbol("waygraph.end");
|
|
@@ -110,6 +197,11 @@ function findBlockIndex(middle, block) {
|
|
|
110
197
|
return index;
|
|
111
198
|
}
|
|
112
199
|
function buildFlow(middle, engineConfig) {
|
|
200
|
+
// resetSession/title start false/unset here - defineFlow itself carries
|
|
201
|
+
// no such options anymore. They're set by wrapping the result in
|
|
202
|
+
// withSessionReset()/withTitle() instead (non-destructive decorators,
|
|
203
|
+
// same pattern as withVerify), which also re-apply themselves after any
|
|
204
|
+
// further withBlockVerify/modBlockVerify patch so the flag survives.
|
|
113
205
|
const chain = middle.reduce((a, b) => connect(a, b));
|
|
114
206
|
// Declared separately (not as the object-literal method below) so the
|
|
115
207
|
// three-overload `Flow.run` signature can be checked against the actual
|
|
@@ -180,6 +272,7 @@ function buildFlow(middle, engineConfig) {
|
|
|
180
272
|
return result;
|
|
181
273
|
};
|
|
182
274
|
return {
|
|
275
|
+
resetSession: false,
|
|
183
276
|
blocks: () => middle.map((b) => ({ name: b.name, block: b, ...(b.routes ? { routes: b.routes } : {}) })),
|
|
184
277
|
run: run,
|
|
185
278
|
withBlockVerify(block, verify) {
|
|
@@ -196,6 +289,166 @@ function buildFlow(middle, engineConfig) {
|
|
|
196
289
|
},
|
|
197
290
|
};
|
|
198
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Marks a Flow as expecting to start from a genuinely fresh, unauthenticated
|
|
294
|
+
* browser state - e.g. a login flow meant to be re-entered later in a longer
|
|
295
|
+
* showcase. Non-destructive, same pattern as {@link withVerify}: returns a
|
|
296
|
+
* new Flow: the original is untouched, and the flag survives any further
|
|
297
|
+
* `withBlockVerify`/`modBlockVerify` patch applied to the result.
|
|
298
|
+
*
|
|
299
|
+
* By itself this does nothing - `Flow.run()` never reads `resetSession`.
|
|
300
|
+
* It's a hint {@link chainFlow} looks for: right before this Flow's first
|
|
301
|
+
* Block runs (skipped if it's the very first thing in the chain - nothing
|
|
302
|
+
* to reset yet), chainFlow clears cookies and storage so this Flow starts
|
|
303
|
+
* exactly as if a human had opened the app fresh, not wherever the
|
|
304
|
+
* previous Flow in the chain happened to leave the page.
|
|
305
|
+
* @example const loginFlow = withSessionReset(engine.defineFlow([start, LoginBlock, end]));
|
|
306
|
+
*/
|
|
307
|
+
export function withSessionReset(flow) {
|
|
308
|
+
return {
|
|
309
|
+
...flow,
|
|
310
|
+
resetSession: true,
|
|
311
|
+
withBlockVerify: (block, verify) => withSessionReset(flow.withBlockVerify(block, verify)),
|
|
312
|
+
modBlockVerify: (block, nameOrIndex, newCheck) => withSessionReset(flow.modBlockVerify(block, nameOrIndex, newCheck)),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Names a Flow for display - a step-through overlay/showcase (waygraph's
|
|
317
|
+
* `chain --step`) shows this as the running banner while this Flow's own
|
|
318
|
+
* Blocks are executing, instead of one fixed title for the whole run.
|
|
319
|
+
* Non-destructive, same pattern as {@link withSessionReset}.
|
|
320
|
+
* @example const ticket123 = withTitle(loginFlow, "Ticket #123 - Sales rep signs in");
|
|
321
|
+
*/
|
|
322
|
+
export function withTitle(flow, title) {
|
|
323
|
+
return {
|
|
324
|
+
...flow,
|
|
325
|
+
title,
|
|
326
|
+
withBlockVerify: (block, verify) => withTitle(flow.withBlockVerify(block, verify), title),
|
|
327
|
+
modBlockVerify: (block, nameOrIndex, newCheck) => withTitle(flow.modBlockVerify(block, nameOrIndex, newCheck), title),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function locateInChain(flows, block) {
|
|
331
|
+
if (typeof block === "number") {
|
|
332
|
+
let remaining = block;
|
|
333
|
+
for (let fi = 0; fi < flows.length; fi++) {
|
|
334
|
+
const len = flows[fi].blocks().length;
|
|
335
|
+
if (remaining < len)
|
|
336
|
+
return { flowIndex: fi, localIndex: remaining };
|
|
337
|
+
remaining -= len;
|
|
338
|
+
}
|
|
339
|
+
throw new Error(`chainFlow: no Block at index ${block} across ${flows.length} chained flows`);
|
|
340
|
+
}
|
|
341
|
+
const blockName = typeof block === "string" ? block : block.name;
|
|
342
|
+
for (let fi = 0; fi < flows.length; fi++) {
|
|
343
|
+
const localIndex = flows[fi].blocks().findIndex((bi) => bi.name === blockName);
|
|
344
|
+
if (localIndex !== -1)
|
|
345
|
+
return { flowIndex: fi, localIndex };
|
|
346
|
+
}
|
|
347
|
+
throw new Error(`chainFlow: no Block named "${blockName}" in any chained flow`);
|
|
348
|
+
}
|
|
349
|
+
async function clearSessionState(context, page) {
|
|
350
|
+
await context.clearCookies().catch(() => { });
|
|
351
|
+
await page
|
|
352
|
+
.evaluate(() => {
|
|
353
|
+
try {
|
|
354
|
+
localStorage.clear();
|
|
355
|
+
sessionStorage.clear();
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
/* storage blocked (e.g. about:blank) - nothing to clear anyway */
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
.catch(() => { });
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Runs several complete, independent Flows one after another, reusing the
|
|
365
|
+
* same page/context/mem in order - each remains its own validated
|
|
366
|
+
* start-to-end unit (a Flow's own `[start, ...blocks, end]` contract is
|
|
367
|
+
* untouched), this only sequences whole Flows. Deliberately NOT the same
|
|
368
|
+
* thing as {@link composeBlock}: composeBlock nests several Blocks into
|
|
369
|
+
* one Block that can drop back into another `defineFlow([...])` - a
|
|
370
|
+
* chainFlow result is a Flow, but not meant to be nested inside another
|
|
371
|
+
* Flow's own Block list. Recursive nesting stays composeBlock's job alone;
|
|
372
|
+
* chainFlow is a flat, one-level sequencing of complete showcases (e.g.
|
|
373
|
+
* "ticket #123 end to end, then ticket #456 end to end").
|
|
374
|
+
*
|
|
375
|
+
* A sub-flow wrapped in {@link withSessionReset} gets its cookies/storage
|
|
376
|
+
* cleared right before it runs (skipped for the very first flow in the
|
|
377
|
+
* chain - nothing to reset yet). The combined result is whatever the LAST
|
|
378
|
+
* flow resolved to.
|
|
379
|
+
* @example chainFlow(ticket123Flow, ticket456Flow).run(context, mem)
|
|
380
|
+
*/
|
|
381
|
+
export function chainFlow(...flows) {
|
|
382
|
+
if (flows.length === 0) {
|
|
383
|
+
throw new Error("chainFlow: give at least one Flow");
|
|
384
|
+
}
|
|
385
|
+
const run = async function run(contextOrMem, memOrConfig, options) {
|
|
386
|
+
if (contextOrMem instanceof MemPage) {
|
|
387
|
+
const mem = contextOrMem;
|
|
388
|
+
const config = memOrConfig;
|
|
389
|
+
// One browser for the WHOLE chain - each sub-flow's own run(mem,
|
|
390
|
+
// config) would otherwise each launch a separate browser, defeating
|
|
391
|
+
// the point of sequencing them (a fresh browser is itself a full
|
|
392
|
+
// session reset, silently making withSessionReset a no-op).
|
|
393
|
+
const browserName = config?.browserName ?? "chromium";
|
|
394
|
+
const launch = LAUNCHERS[browserName];
|
|
395
|
+
const executablePath = browserName === "chromium" ? process.env.CHROME_PATH || process.env.CHROMIUM_PATH : undefined;
|
|
396
|
+
const browser = await launch.launch({
|
|
397
|
+
headless: config?.headless ?? true,
|
|
398
|
+
...(config?.slowMo !== undefined ? { slowMo: config.slowMo } : {}),
|
|
399
|
+
...(executablePath ? { executablePath } : {}),
|
|
400
|
+
});
|
|
401
|
+
try {
|
|
402
|
+
const context = await browser.newContext();
|
|
403
|
+
return await run(context, mem, { closeOnFinish: true });
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
await browser.close();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const context = contextOrMem;
|
|
410
|
+
const mem = memOrConfig;
|
|
411
|
+
const gotOwnPage = options?.page === undefined;
|
|
412
|
+
const page = options?.page ?? (await context.newPage());
|
|
413
|
+
const closeOnFinish = options?.closeOnFinish ?? gotOwnPage;
|
|
414
|
+
let result;
|
|
415
|
+
for (let i = 0; i < flows.length; i++) {
|
|
416
|
+
const flow = flows[i];
|
|
417
|
+
if (i > 0 && flow.resetSession) {
|
|
418
|
+
await clearSessionState(context, page);
|
|
419
|
+
}
|
|
420
|
+
const outcome = (await flow.run(context, mem, { page, closeOnFinish: false }));
|
|
421
|
+
result = outcome.result;
|
|
422
|
+
}
|
|
423
|
+
if (closeOnFinish) {
|
|
424
|
+
await page.close();
|
|
425
|
+
}
|
|
426
|
+
if (options?.closeOnFinish === false) {
|
|
427
|
+
return { result, page };
|
|
428
|
+
}
|
|
429
|
+
return result;
|
|
430
|
+
};
|
|
431
|
+
return {
|
|
432
|
+
resetSession: false,
|
|
433
|
+
blocks: () => flows.flatMap((flow, idx) => flow.blocks().map((bi, i) => ({
|
|
434
|
+
...bi,
|
|
435
|
+
...(i === 0 && idx > 0 && flow.resetSession ? { resetSessionBefore: true } : {}),
|
|
436
|
+
}))),
|
|
437
|
+
run: run,
|
|
438
|
+
withBlockVerify(block, verify) {
|
|
439
|
+
const { flowIndex, localIndex } = locateInChain(flows, block);
|
|
440
|
+
const patched = [...flows];
|
|
441
|
+
patched[flowIndex] = patched[flowIndex].withBlockVerify(localIndex, verify);
|
|
442
|
+
return chainFlow(...patched);
|
|
443
|
+
},
|
|
444
|
+
modBlockVerify(block, nameOrIndex, newCheck) {
|
|
445
|
+
const { flowIndex, localIndex } = locateInChain(flows, block);
|
|
446
|
+
const patched = [...flows];
|
|
447
|
+
patched[flowIndex] = patched[flowIndex].modBlockVerify(localIndex, nameOrIndex, newCheck);
|
|
448
|
+
return chainFlow(...patched);
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
}
|
|
199
452
|
export function composeBlock(name, steps) {
|
|
200
453
|
if (steps.length === 0) {
|
|
201
454
|
throw new Error(`composeBlock: "${name}" needs at least one step`);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ export type { Checkpoint, Instruction, Block, DefinedBlock, Start } from "./type
|
|
|
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, spawnTab, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, composeBlock, preflight, } from "./engine.js";
|
|
5
|
+
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, composeBlock, preflight, withSessionReset, withTitle, chainFlow, } from "./engine.js";
|
|
6
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, spawnTab, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, composeBlock, preflight, } from "./engine.js";
|
|
4
|
+
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, composeBlock, preflight, withSessionReset, withTitle, chainFlow, } from "./engine.js";
|