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/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
|
|
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** (
|
|
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
|
-
##
|
|
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
|
|
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 {};
|