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 CHANGED
@@ -1,12 +1,58 @@
1
1
  # Waygraph
2
2
 
3
3
  A typed graph of reusable Blocks for driving a browser through E2E flows, in place of a
4
- flat pile of ad hoc helper functions - see
5
- `openspec/changes/archive/2026-08-27-core-spine/proposal.md` for the full motivation.
4
+ flat pile of ad hoc helper functions.
6
5
 
7
6
  This repo is the `waygraph` npm package itself. Consumer projects proving it against real
8
7
  sites/apps (saucedemo.com, zsign-app) live in the sibling `projects_waygraph/` repo, not here.
9
8
 
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install waygraph @playwright/test
13
+ ```
14
+
15
+ `@playwright/test` is a peer dependency, not bundled - you bring your own version (`^1.40.0`
16
+ or later) since it's also what drives the rest of your test suite. Requires Node 22+ (uses
17
+ `URL`/`URLPattern` from `@types/node` for `Trait.url`'s type checking).
18
+
19
+ ## Quick start
20
+
21
+ A Block wraps one `act`/`resolve` step (add `observe`/`verify` once you need them); a `Flow`
22
+ chains several into a run:
23
+
24
+ ```typescript
25
+ import { Engine, start, end, MemPage, key, checkpoint, Trait } from "waygraph";
26
+ import type { Block, Checkpoint, Start } from "waygraph";
27
+
28
+ type LoggedIn = Checkpoint<"LoggedIn">;
29
+
30
+ const Username = key<string>("username");
31
+
32
+ const Login: Block<Start, LoggedIn> = {
33
+ name: "login",
34
+ instruction: {
35
+ async act(page, _input, mem) {
36
+ await page.goto("https://example.com/login");
37
+ await page.getByLabel("Username").fill(mem.get(Username));
38
+ await page.getByRole("button", { name: "Sign in" }).click();
39
+ },
40
+ resolve: () => checkpoint("LoggedIn"),
41
+ verify: [Trait.url({ pathname: "/dashboard" })],
42
+ },
43
+ };
44
+
45
+ const mem = new MemPage();
46
+ mem.set(Username, "alice");
47
+
48
+ const flow = new Engine().defineFlow([start, Login, end]);
49
+ const result = await flow.run(mem); // Engine launches its own browser
50
+ // result === { __state: "LoggedIn" }
51
+ ```
52
+
53
+ Inside an existing `@playwright/test` file, reuse its `context` fixture instead of having the
54
+ Engine launch its own browser: `await flow.run(context, mem)`.
55
+
10
56
  **Status:** `Checkpoint`, `MemPage`, `Instruction`/`Block` (`act`/`observe`/`resolve`/`verify`),
11
57
  `connect()`, `Trait`, `Engine.defineFlow([start, ...blocks, end])`, and `branch()`/self-loops
12
58
  with a `maxSteps` safety cap. A Block's output type is the only contract the next Block can
@@ -34,17 +80,37 @@ components you leave out default to "match anything," so `Trait.url({ pathname:
34
80
  separate flag needed for that. Once Blocks are composed into a `Flow`, `flow.withBlockVerify`/
35
81
  `flow.modBlockVerify` patch one Block's verify from outside - a spec that only imports the
36
82
  finished flow, addressed by the Block reference (preferred), its name, or its numeric
37
- position in the flow - without editing the flow's own file.
83
+ position in the flow - without editing the flow's own file. `composeBlock(name, steps)`
84
+ chains several Blocks into one named unit the same way `connect()` already does - the
85
+ multi-step-form case (a government form with several stepper pages, say), which is
86
+ naturally "one feature" but still wants each step's own real `verify` independently
87
+ overridable, not one giant Block with no per-step confirmation. `composedBlock.withStepVerify`/
88
+ `modStepVerify` patch one step from outside, addressed the same three ways
89
+ `withBlockVerify`/`modBlockVerify` already are - a `composeBlock` result is a plain Block,
90
+ so it drops straight into `defineFlow([start, ..., composed, ..., end])` like any other.
91
+ `spawnTab(entry, page, mem)` drives a genuinely separate second tab through its own
92
+ Block/Flow, in the same browser context an existing `page` already belongs to - the
93
+ pattern `observe()` could already reach for (it's the only phase allowed to touch
94
+ `page.context()`), now a named, discoverable, tested primitive instead of something
95
+ you'd have to already know to hand-roll. `flow.run(context, mem, options)` also accepts a
96
+ trailing optional options object - additively, so `flow.run(context, mem)` still behaves
97
+ byte-for-byte as before: `options.page` drives that already-open page instead of opening
98
+ a fresh tab (the run won't close a page you handed it unless you say so), and
99
+ `options.closeOnFinish: false` leaves the run's page open and returns `{ result, page }`
100
+ so the caller can keep driving it - hands a run tab back instead of losing it. See
101
+ "Recipes" for capture-on-popup, which stays a documented pattern until a second real use
102
+ case promotes it to an engine API.
103
+
104
+ ```bash
105
+ npx create-waygraph my-project && cd my-project && npm install && npm test
106
+ ```
107
+
108
+ [`create-waygraph`](https://github.com/deviate-dv8/create-waygraph) scaffolds a new
109
+ project (package.json, tsconfig, playwright.config, one working example flow) in one
110
+ command, published on npm - the above runs a real green test with no manual edits.
38
111
 
39
- Deliberately **not yet implemented** (each tracked as its own future OpenSpec change,
40
- building on this one):
112
+ Deliberately **not yet implemented** (tracked on the project board):
41
113
  - Interstitials/Watchers - background overlay handling (cookie banners, popups).
42
- - `spawnTab` - a second browser tab.
43
- - A patchable way to compose several Blocks into one named unit with per-step verify
44
- addressable from outside (`connect()`-composed chains don't get `withVerify`/`modVerify`
45
- attached the way `defineBlock()`/`branch()`/`Flow` outputs do) - useful for a multi-step
46
- form (e.g. a government form with several stepper pages) that's naturally "one feature" but
47
- still wants each step's own confirmation independently overridable.
48
114
 
49
115
  ## Example
50
116
 
@@ -54,9 +120,8 @@ real page:
54
120
 
55
121
  ```typescript
56
122
  import { Engine, start, end, MemPage, key, checkpoint } from "waygraph";
57
- import type { Block, Checkpoint } from "waygraph";
123
+ import type { Block, Checkpoint, Start } from "waygraph";
58
124
 
59
- type Start = Checkpoint<"__start__">;
60
125
  type FormLoaded = Checkpoint<"FormLoaded">;
61
126
  type Submitted = Checkpoint<"Submitted">;
62
127
 
@@ -110,9 +175,52 @@ for real. The lower-level `connect()`/`runGraph()` still exist and are what `def
110
175
  builds on - reach for them directly only if you need a shape `defineFlow`'s array can't
111
176
  express yet.
112
177
 
113
- ## Commands
178
+ ## Recipes
179
+
180
+ ### Detect / drive the surviving tab after a run
181
+
182
+ The default run closes the page it opened, and a page you hand in via `options.page`
183
+ survives. For a flow that must BE the surviving tab after it returns (an interactive demo,
184
+ a hand-back to a caller), ask for the page back explicitly:
185
+
186
+ ```typescript
187
+ const { result, page } = await registerFlow.run(context, mem, { closeOnFinish: false });
188
+ // `result.__state` reached the terminal Checkpoint; `page` is still open where the
189
+ // flow left it - the caller keeps driving it, and closes it whenever IT is done.
190
+ ```
191
+
192
+ ### Capture a popup (`target=_blank` click)
193
+
194
+ Frameworks and email UIs routinely rewrite links to `target="_blank"`, so a click that
195
+ "should navigate" actually opens a second, engine-invisible tab. Until a second real use
196
+ case promotes this to an engine API, capture it in userland with a context-level page
197
+ event armed before the click that is expected to open exactly one popup:
198
+
199
+ ```typescript
200
+ let popup: Page | undefined;
201
+ context.once("page", (p) => { popup = p; });
202
+ await linkHandle.click(); // the block that causes the popup (e.g. the
203
+ // MailHog email's real verify link)
204
+ if (popup) {
205
+ await popup.waitForLoadState(); // fully loaded before calling into the engine
206
+ await verifyLinkFlow.run(context, mem, { page: popup, closeOnFinish: false });
207
+ // popup is now the caller's page - close it when done, or leave it as the
208
+ // surviving tab of a demo.
209
+ }
210
+ ```
211
+
212
+ `context.once` (not `.on`) guarantees a second stray popup won't re-fire the handler and
213
+ clobber your handle; if a page truly can open more than one popup, this recipe needs to
214
+ collect them instead. The popup is a normal Playwright `Page` from the same context, so
215
+ anything `Flow.run(context, mem, { page })` can drive is available to keep driving here,
216
+ and `closeOnFinish: false` hands it back into your ownership instead of the run closing
217
+ it out from under you.
218
+
219
+ ## Developing this package
114
220
 
115
221
  ```bash
222
+ git clone git@github.com:deviate-dv8/waygraph.git
223
+ cd waygraph
116
224
  npm install
117
225
  npm run typecheck # tsc --noEmit
118
226
  npm run test # playwright test
@@ -127,13 +235,12 @@ src/
127
235
  types.ts Checkpoint, Instruction, Block, connect()
128
236
  mem-page.ts MemKey, key(), MemPage
129
237
  trait.ts Trait (type + discoverable Trait.url/.text/.visible), runVerify()
130
- engine.ts runGraph(), Engine, start, end, Flow (+ withBlockVerify/modBlockVerify)
238
+ engine.ts runGraph(), spawnTab(), composeBlock(), Engine, start, end, Flow
131
239
  index.ts public barrel
132
240
  typecheck/ compile-time-only fixtures (assignability, @ts-expect-error cases)
133
241
  tests/ runtime unit + integration tests (@playwright/test)
134
242
  tsconfig.json base config - typecheck script includes src/ + typecheck/ + tests/
135
243
  tsconfig.build.json extends base, src/ only, emits to dist/
136
- openspec/ the OpenSpec change history for this repo (propose -> apply per change)
137
244
  ```
138
245
 
139
246
  Consumer projects (saucedemo.com proof, zsign-app integration) live in the sibling
package/bin/waygraph ADDED
@@ -0,0 +1,13 @@
1
+ #!/bin/sh
2
+ # Two cwd/symlink pitfalls this resolves, both real, both hit in practice:
3
+ # - `--import tsx/esm` is a bare specifier - Node resolves it as if from an
4
+ # unnamed module at the CALLER's cwd, not this script's install
5
+ # directory, so it broke when invoked from anywhere else.
6
+ # - `npm link` makes this script reachable via a SYMLINK (e.g.
7
+ # ~/.nvm/.../bin/waygraph) - `dirname "$0"` alone follows argv[0], not the
8
+ # symlink target, so it resolved to the nvm bin dir instead of this
9
+ # package. `readlink -f` follows the symlink to the real file first.
10
+ SOURCE="$(readlink -f "$0")"
11
+ DIR="$(cd "$(dirname "$SOURCE")/.." && pwd)"
12
+ TSX_ESM="$(cd "$DIR" && node -e "console.log(require.resolve('tsx/esm'))")"
13
+ exec node --import "$TSX_ESM" "$DIR/dist/cli.js" "$@"
package/dist/cli.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * waygraph CLI -- tooling around this same package's engine.
4
+ *
5
+ * Commands:
6
+ * list [project] List discovered flows in a project
7
+ * nav [flow] [project] Print flow chain + navigation-only steps (goto / Trait.url)
8
+ * validate [project] Import and validate all flows
9
+ * run <flow> [project] Run a named flow with a fresh browser
10
+ * chain <spec> [project] Run one or more Blocks by name, ad hoc, no flow file needed
11
+ *
12
+ * "project" defaults to the current directory.
13
+ * A project is any directory containing *.flow.ts files (typically under src/flows/).
14
+ */
15
+ export {};