waygraph 0.6.0 → 0.7.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 +10 -3
- package/dist/cli.js +82 -27
- package/dist/engine.d.ts +43 -15
- package/dist/engine.js +16 -15
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,9 +103,16 @@ a fresh tab (the run won't close a page you handed it unless you say so), and
|
|
|
103
103
|
so the caller can keep driving it - hands a run tab back instead of losing it. See
|
|
104
104
|
"Recipes" for capture-on-popup, which stays a documented pattern until a second real use
|
|
105
105
|
case promotes it to an engine API.
|
|
106
|
-
`defineNavBlock({ name, checkpoint, url })`
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
`defineNavBlock({ name, checkpoint, url })` or `defineNavBlock({ name, checkpoint, click })`
|
|
107
|
+
builds a Block whose only possible action is navigating - exactly one of `url`/`click` is
|
|
108
|
+
required, enforced at compile time. Both accept a plain string or `(mem) => string`/
|
|
109
|
+
`(mem) => selector` for parameterized routes; the generated `act()` is always exactly
|
|
110
|
+
`page.goto(url)` or `page.locator(click).click()`, never author-supplied. `click` is the
|
|
111
|
+
opinionated default - navigating by clicking a real element already on the page (a nav
|
|
112
|
+
link, a sidebar item) instead of teleporting straight to a URL, so a Block actually proves
|
|
113
|
+
the app's own navigation path works instead of skipping past it. `url` still exists for the
|
|
114
|
+
real cases with no click path to get there (a deep link from an email, a public share
|
|
115
|
+
link). A
|
|
109
116
|
regular `defineBlock`'s `act()` receives its `page` typed as `ActionPage` instead of raw
|
|
110
117
|
`Page` - structurally identical, every method still fully present and callable, except
|
|
111
118
|
`goto`/`reload`/`goBack`/`goForward` are re-declared `@deprecated`, so a TypeScript-aware
|
package/dist/cli.js
CHANGED
|
@@ -2003,11 +2003,9 @@ function packageRoot() {
|
|
|
2003
2003
|
* directory the real demo files get copied into, so it resolves "waygraph"
|
|
2004
2004
|
* and "@playwright/test" exactly as they will - and actually launches (then
|
|
2005
2005
|
* immediately closes) chromium, a real check rather than a guess at whether
|
|
2006
|
-
* the browser's installed.
|
|
2007
|
-
* friendly, actionable message naming exactly what to install when it
|
|
2008
|
-
* isn't. Never leaves the probe script behind.
|
|
2006
|
+
* the browser's installed. Never leaves the probe script behind.
|
|
2009
2007
|
*/
|
|
2010
|
-
async function
|
|
2008
|
+
async function probeTryPrereqs(projectDir) {
|
|
2011
2009
|
const tsxEsm = import.meta.resolve("tsx/esm");
|
|
2012
2010
|
const probeScript = [
|
|
2013
2011
|
"try {",
|
|
@@ -2050,27 +2048,80 @@ async function checkTryPrereqs(projectDir) {
|
|
|
2050
2048
|
child.on("exit", (code) => res({ code: code ?? 1, stderr: stderrBuf }));
|
|
2051
2049
|
});
|
|
2052
2050
|
if (result.code === 0)
|
|
2053
|
-
return
|
|
2051
|
+
return { ok: true };
|
|
2054
2052
|
const stderr = result.stderr;
|
|
2055
|
-
if (stderr.includes("WAYGRAPH_TRY_MISSING_WAYGRAPH"))
|
|
2056
|
-
return
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
return "Playwright's chromium browser isn't installed yet.\nRun this first:\n npx playwright install chromium";
|
|
2063
|
-
}
|
|
2064
|
-
return `couldn't verify prerequisites:\n${stderr.trim()}`;
|
|
2053
|
+
if (stderr.includes("WAYGRAPH_TRY_MISSING_WAYGRAPH"))
|
|
2054
|
+
return { ok: false, missing: "waygraph" };
|
|
2055
|
+
if (stderr.includes("WAYGRAPH_TRY_MISSING_PLAYWRIGHT_TEST"))
|
|
2056
|
+
return { ok: false, missing: "playwright-test" };
|
|
2057
|
+
if (stderr.includes("WAYGRAPH_TRY_MISSING_BROWSER"))
|
|
2058
|
+
return { ok: false, missing: "browser" };
|
|
2059
|
+
return { ok: false, missing: "unknown", detail: stderr.trim() || `probe exited with code ${result.code}` };
|
|
2065
2060
|
}
|
|
2066
2061
|
finally {
|
|
2067
2062
|
rmSync(probePath, { force: true });
|
|
2068
2063
|
}
|
|
2069
2064
|
}
|
|
2065
|
+
/** Spawns with inherited stdio (the real npm/playwright output, visible) and resolves to its exit code. */
|
|
2066
|
+
function runInherited(cmd, args, cwd) {
|
|
2067
|
+
return new Promise((res, rej) => {
|
|
2068
|
+
const child = spawn(cmd, args, { cwd, stdio: "inherit", env: process.env });
|
|
2069
|
+
child.on("error", rej);
|
|
2070
|
+
child.on("exit", (code) => res(code ?? 1));
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* `waygraph try` is meant to be a genuine one-shot `npx waygraph try` -
|
|
2075
|
+
* no separate init/install step the human has to remember, the same shape
|
|
2076
|
+
* as `npm create vite@latest`. Probes first; anything missing gets
|
|
2077
|
+
* installed right here (a minimal package.json if none exists, then
|
|
2078
|
+
* `npm install waygraph @playwright/test`, then Playwright's own chromium
|
|
2079
|
+
* download), with the real install output visible the whole time - not
|
|
2080
|
+
* silent, just not a separate step you have to run yourself first. Returns
|
|
2081
|
+
* null once everything's actually ready, or a message naming whatever
|
|
2082
|
+
* still isn't after really trying.
|
|
2083
|
+
*/
|
|
2084
|
+
async function ensureTryPrereqs(projectDir) {
|
|
2085
|
+
let status = await probeTryPrereqs(projectDir);
|
|
2086
|
+
if (status.ok)
|
|
2087
|
+
return null;
|
|
2088
|
+
if (status.missing === "unknown") {
|
|
2089
|
+
return `couldn't verify prerequisites:\n${status.detail}`;
|
|
2090
|
+
}
|
|
2091
|
+
if (status.missing === "waygraph" || status.missing === "playwright-test") {
|
|
2092
|
+
if (!existsSync(join(projectDir, "package.json"))) {
|
|
2093
|
+
writeFileSync(join(projectDir, "package.json"), JSON.stringify({ name: basename(projectDir) || "waygraph-quickstart", private: true, type: "module" }, null, 2) + "\n");
|
|
2094
|
+
console.log(`waygraph try: no package.json in ${projectDir} - created a minimal one.`);
|
|
2095
|
+
}
|
|
2096
|
+
console.log(`waygraph try: installing waygraph + @playwright/test in ${projectDir} ...`);
|
|
2097
|
+
const installCode = await runInherited("npm", ["install", "waygraph", "@playwright/test"], projectDir);
|
|
2098
|
+
if (installCode !== 0) {
|
|
2099
|
+
return `npm install exited with code ${installCode} - see the output above.`;
|
|
2100
|
+
}
|
|
2101
|
+
status = await probeTryPrereqs(projectDir);
|
|
2102
|
+
if (status.ok)
|
|
2103
|
+
return null;
|
|
2104
|
+
if (status.missing === "unknown")
|
|
2105
|
+
return `couldn't verify prerequisites:\n${status.detail}`;
|
|
2106
|
+
}
|
|
2107
|
+
// Only the browser can still be missing at this point.
|
|
2108
|
+
console.log("waygraph try: downloading Playwright's chromium browser ...");
|
|
2109
|
+
const localPlaywrightBin = join(projectDir, "node_modules", ".bin", "playwright");
|
|
2110
|
+
const browserCode = existsSync(localPlaywrightBin)
|
|
2111
|
+
? await runInherited(localPlaywrightBin, ["install", "chromium"], projectDir)
|
|
2112
|
+
: await runInherited("npx", ["--yes", "playwright", "install", "chromium"], projectDir);
|
|
2113
|
+
if (browserCode !== 0) {
|
|
2114
|
+
return `"playwright install chromium" exited with code ${browserCode} - see the output above.`;
|
|
2115
|
+
}
|
|
2116
|
+
status = await probeTryPrereqs(projectDir);
|
|
2117
|
+
if (status.ok)
|
|
2118
|
+
return null;
|
|
2119
|
+
return status.detail ?? "prerequisites still aren't ready after attempting to install them.";
|
|
2120
|
+
}
|
|
2070
2121
|
async function runTry(projectDir) {
|
|
2071
|
-
const problem = await
|
|
2122
|
+
const problem = await ensureTryPrereqs(projectDir);
|
|
2072
2123
|
if (problem) {
|
|
2073
|
-
console.error(`waygraph try:
|
|
2124
|
+
console.error(`waygraph try: couldn't get ready.\n${problem}`);
|
|
2074
2125
|
process.exitCode = 1;
|
|
2075
2126
|
return;
|
|
2076
2127
|
}
|
|
@@ -2208,16 +2259,20 @@ Usage:
|
|
|
2208
2259
|
waygraph run <flow> [project] Run a named flow
|
|
2209
2260
|
waygraph chain <spec> [project] Run one or more Blocks by name, ad hoc
|
|
2210
2261
|
waygraph check [project] Warn about navigation outside a NavBlock
|
|
2211
|
-
waygraph try [project]
|
|
2212
|
-
"waygraph"
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2262
|
+
waygraph try [project] One-shot quickstart showcase - genuinely
|
|
2263
|
+
"npx waygraph try", no separate init step.
|
|
2264
|
+
Checks (a real launch+close, not a guess)
|
|
2265
|
+
whether "waygraph" + "@playwright/test"
|
|
2266
|
+
resolve from [project] (default: cwd) and
|
|
2267
|
+
its chromium browser is downloaded; if
|
|
2268
|
+
anything's missing, installs it right
|
|
2269
|
+
there (writes a minimal package.json if
|
|
2270
|
+
[project] doesn't have one yet, then
|
|
2271
|
+
npm install, then the browser download),
|
|
2272
|
+
real output visible the whole time. Once
|
|
2273
|
+
ready, copies a small real Flow
|
|
2274
|
+
(nav/action Blocks, Trait verify,
|
|
2275
|
+
narrate()) into
|
|
2221
2276
|
[project]/.waygraph-quickstart/ and runs
|
|
2222
2277
|
it in step mode against a public demo
|
|
2223
2278
|
site - then prints the absolute path to
|
package/dist/engine.d.ts
CHANGED
|
@@ -444,31 +444,59 @@ export declare function defineBlock<In extends Checkpoint<string>, Out extends C
|
|
|
444
444
|
*/
|
|
445
445
|
export type NavBlock<Out extends Checkpoint<string>> = DefinedBlock<Checkpoint<string>, Out>;
|
|
446
446
|
/**
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
447
|
+
* `defineNavBlock`'s own options - exactly one of `url`/`click` required,
|
|
448
|
+
* enforced at compile time (giving both, or neither, is a type error - see
|
|
449
|
+
* `typecheck/nav-block.types.ts`). `checkpoint` is the tag this NavBlock
|
|
450
450
|
* resolves to once navigation completes - a NavBlock never branches on
|
|
451
451
|
* observed evidence the way a regular Block can, so there's no `resolve`
|
|
452
452
|
* logic to author; it always just declares "arrived."
|
|
453
|
-
*
|
|
454
|
-
* The generated `act()` is always exactly `page.goto(url)` - never anything
|
|
455
|
-
* else, never author-supplied. Callers get a real `Page` here internally
|
|
456
|
-
* (the one legitimate place navigation belongs); `defineBlock`'s own
|
|
457
|
-
* `ActionPage` narrowing is what discourages navigation everywhere else,
|
|
458
|
-
* and never applies to this generated function since no author ever writes
|
|
459
|
-
* it by hand.
|
|
460
|
-
* @example defineNavBlock({ name: "nav-web-login", checkpoint: "LoginForm", url: "/login" })
|
|
461
|
-
* @example defineNavBlock({ name: "nav-web-request-detail", checkpoint: "RequestDetail", url: (mem) => `/dashboard/requests/${mem.get(RequestId.key)}` })
|
|
462
453
|
*/
|
|
463
|
-
export
|
|
454
|
+
export type NavBlockOptions<Out extends Checkpoint<string>> = {
|
|
464
455
|
name: string;
|
|
465
456
|
description?: string;
|
|
466
457
|
checkpoint: Out["__state"];
|
|
467
|
-
url: string | ((mem: MemPage) => string);
|
|
468
458
|
requires?: readonly MemKey<any>[];
|
|
469
459
|
verify?: Trait[] | ((out: Out) => Trait[]);
|
|
470
460
|
highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
|
|
471
|
-
}
|
|
461
|
+
} & ({
|
|
462
|
+
/**
|
|
463
|
+
* Direct URL navigation. Accepts a plain string or a function
|
|
464
|
+
* `(mem) => string` for parameterized routes (e.g.
|
|
465
|
+
* `/dashboard/requests/:id` with a real id read from mem). The
|
|
466
|
+
* escape hatch for the few real cases with no click path to get
|
|
467
|
+
* there (a deep link from an email, a public share link) - not for
|
|
468
|
+
* skipping past navigation an agent didn't feel like finding. Prefer
|
|
469
|
+
* `click` whenever the target app actually has one.
|
|
470
|
+
*/
|
|
471
|
+
url: string | ((mem: MemPage) => string);
|
|
472
|
+
click?: never;
|
|
473
|
+
} | {
|
|
474
|
+
/**
|
|
475
|
+
* Navigates by clicking a real element already on the page (a nav
|
|
476
|
+
* link, a sidebar item) instead of teleporting straight to a URL -
|
|
477
|
+
* the opinionated default. Accepts a Playwright selector string, or
|
|
478
|
+
* a function `(mem) => selector` for a target that depends on mem
|
|
479
|
+
* (e.g. clicking the row for a specific id). Verifying arrival is
|
|
480
|
+
* `verify`'s job (typically `Trait.url(...)`), same as a `url`-based
|
|
481
|
+
* NavBlock - the click itself is only ever awaited for its own
|
|
482
|
+
* completion, not for whatever navigation it triggers.
|
|
483
|
+
*/
|
|
484
|
+
click: string | ((mem: MemPage) => string);
|
|
485
|
+
url?: never;
|
|
486
|
+
});
|
|
487
|
+
/**
|
|
488
|
+
* Builds a {@link NavBlock}. The generated `act()` is always exactly
|
|
489
|
+
* `page.goto(url)` or `page.locator(click).click()` - never anything else,
|
|
490
|
+
* never author-supplied. Callers get a real `Page` here internally (the one
|
|
491
|
+
* legitimate place navigation belongs); `defineBlock`'s own `ActionPage`
|
|
492
|
+
* narrowing is what discourages navigation everywhere else, and never
|
|
493
|
+
* applies to this generated function since no author ever writes it by
|
|
494
|
+
* hand.
|
|
495
|
+
* @example defineNavBlock({ name: "nav-web-login", checkpoint: "LoginForm", url: "/login" })
|
|
496
|
+
* @example defineNavBlock({ name: "nav-web-request-detail", checkpoint: "RequestDetail", url: (mem) => `/dashboard/requests/${mem.get(RequestId.key)}` })
|
|
497
|
+
* @example defineNavBlock({ name: "nav-web-documents", checkpoint: "DocumentsList", click: "nav >> text=Documents" })
|
|
498
|
+
*/
|
|
499
|
+
export declare function defineNavBlock<Out extends Checkpoint<string>>(options: NavBlockOptions<Out>): NavBlock<Out>;
|
|
472
500
|
/**
|
|
473
501
|
* Returns a new Block that runs exactly like `block` - same act/observe/resolve,
|
|
474
502
|
* same routing - but with `verify` replaced. For giving one flow stricter (or
|
package/dist/engine.js
CHANGED
|
@@ -609,21 +609,16 @@ export function defineBlock(base) {
|
|
|
609
609
|
};
|
|
610
610
|
}
|
|
611
611
|
/**
|
|
612
|
-
* Builds a {@link NavBlock}. `
|
|
613
|
-
* `(
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
* The generated `act()` is always exactly `page.goto(url)` - never anything
|
|
620
|
-
* else, never author-supplied. Callers get a real `Page` here internally
|
|
621
|
-
* (the one legitimate place navigation belongs); `defineBlock`'s own
|
|
622
|
-
* `ActionPage` narrowing is what discourages navigation everywhere else,
|
|
623
|
-
* and never applies to this generated function since no author ever writes
|
|
624
|
-
* it by hand.
|
|
612
|
+
* Builds a {@link NavBlock}. The generated `act()` is always exactly
|
|
613
|
+
* `page.goto(url)` or `page.locator(click).click()` - never anything else,
|
|
614
|
+
* never author-supplied. Callers get a real `Page` here internally (the one
|
|
615
|
+
* legitimate place navigation belongs); `defineBlock`'s own `ActionPage`
|
|
616
|
+
* narrowing is what discourages navigation everywhere else, and never
|
|
617
|
+
* applies to this generated function since no author ever writes it by
|
|
618
|
+
* hand.
|
|
625
619
|
* @example defineNavBlock({ name: "nav-web-login", checkpoint: "LoginForm", url: "/login" })
|
|
626
620
|
* @example defineNavBlock({ name: "nav-web-request-detail", checkpoint: "RequestDetail", url: (mem) => `/dashboard/requests/${mem.get(RequestId.key)}` })
|
|
621
|
+
* @example defineNavBlock({ name: "nav-web-documents", checkpoint: "DocumentsList", click: "nav >> text=Documents" })
|
|
627
622
|
*/
|
|
628
623
|
export function defineNavBlock(options) {
|
|
629
624
|
const built = defineBlock({
|
|
@@ -632,8 +627,14 @@ export function defineNavBlock(options) {
|
|
|
632
627
|
...(options.requires ? { requires: options.requires } : {}),
|
|
633
628
|
instruction: {
|
|
634
629
|
async act(page, _input, mem) {
|
|
635
|
-
|
|
636
|
-
|
|
630
|
+
if (options.url !== undefined) {
|
|
631
|
+
const url = typeof options.url === "function" ? options.url(mem) : options.url;
|
|
632
|
+
await page.goto(url);
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
const selector = typeof options.click === "function" ? options.click(mem) : options.click;
|
|
636
|
+
await page.locator(selector).click();
|
|
637
|
+
}
|
|
637
638
|
},
|
|
638
639
|
resolve: () => checkpoint(options.checkpoint),
|
|
639
640
|
...(options.verify ? { verify: options.verify } : {}),
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ 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
5
|
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, chainFlow, } from "./engine.js";
|
|
6
|
-
export type { Flow, EngineConfig, ComposedBlock, RunGraphOptions, BlockInfo, NavBlock } from "./engine.js";
|
|
6
|
+
export type { Flow, EngineConfig, ComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions } from "./engine.js";
|
|
7
7
|
export type { ActionPage } from "./types.js";
|