waygraph 0.12.5 → 0.12.7
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 +6 -4
- package/dist/agent-dive.d.ts +19 -0
- package/dist/agent-dive.js +162 -0
- package/dist/auto-explore-run.d.ts +5 -0
- package/dist/auto-explore-run.js +15 -5
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +56 -2
- package/package.json +1 -1
- package/templates/agents/waygraph-author.agent.md +36 -0
- package/templates/agents/waygraph-healer.agent.md +33 -0
- package/templates/agents/waygraph-planner.agent.md +35 -0
- package/templates/scaffold/README.md +28 -0
- package/templates/scaffold/STRUCTURE.md +61 -0
- package/templates/scaffold/package.json +13 -7
- package/templates/scaffold/playwright.config.ts +15 -0
- package/templates/scaffold/scripts/fixture-server.mjs +41 -0
- package/templates/scaffold/scripts/with-fixture.mjs +69 -0
- package/templates/scaffold/src/blocks/SITE-MAP.md +12 -0
- package/templates/scaffold/src/blocks/demo-web/NAV.md +29 -0
- package/templates/scaffold/src/blocks/demo-web/demo-sel.ts +14 -0
- package/templates/scaffold/src/blocks/demo-web/home.html +65 -0
- package/templates/scaffold/src/blocks/demo-web/home.page.block.ts +29 -0
- package/templates/scaffold/src/blocks/demo-web/methods/add-item.effect.block.ts +68 -0
- package/templates/scaffold/src/blocks/demo-web/methods/assert-hello.method.block.ts +50 -0
- package/templates/scaffold/src/blocks/demo-web/methods/clear-cart.method.block.ts +26 -0
- package/templates/scaffold/src/blocks/demo-web/methods/remove-item.effect.block.ts +63 -0
- package/templates/scaffold/src/blocks/demo-web/nav-home.block.ts +22 -0
- package/templates/scaffold/src/flows/example.flow.ts +24 -3
- package/templates/scaffold/src/flows/shop.flow.ts +42 -0
- package/templates/scaffold/src/states/demo.mem-keys.ts +7 -0
- package/templates/scaffold/src/states/demo.states.ts +6 -0
- package/templates/scaffold/tests/example.spec.ts +11 -2
- package/templates/scaffold/src/blocks/load-page.block.ts +0 -18
package/README.md
CHANGED
|
@@ -89,7 +89,8 @@ Live reference: `examples/saucedemo/src/blocks/saucedemo-web/inventory/`
|
|
|
89
89
|
|------|---------|
|
|
90
90
|
| Watch Sauce Demo step-through (temp dir only) | `npx waygraph try demo` |
|
|
91
91
|
| Interactive explore (Add/Remove/Open details from live page) | `cd examples/saucedemo && npm i && npx waygraph auto` |
|
|
92
|
-
| Scaffold a new **offline** project (green `npm test` on
|
|
92
|
+
| Scaffold a new **offline** project (green `npm test` on local fixture `:4177`) | `npx create-waygraph my-app` **or** `npx waygraph init my-app` |
|
|
93
|
+
| Install coding-agent defs for diving an app into Blocks | `npx waygraph agent-dive --loop claude` (also `opencode` / `cursor` / `vscode`) |
|
|
93
94
|
| Add waygraph to an existing repo | `npm install waygraph @playwright/test` |
|
|
94
95
|
|
|
95
96
|
**Scaffold is not hidden inside waygraph alone** - the offline starter also ships as
|
|
@@ -248,12 +249,13 @@ an opinionated Playwright," so a stealth-patched or otherwise customized launche
|
|
|
248
249
|
the same `.launch()` shape as Playwright's own `chromium`/`firefox`/`webkit`.
|
|
249
250
|
|
|
250
251
|
See **Getting started** above for `try demo`, `create-waygraph`, and `waygraph init`.
|
|
251
|
-
Both scaffold commands emit the same offline tree (
|
|
252
|
-
|
|
252
|
+
Both scaffold commands emit the same offline tree (route-shaped `demo-web/` +
|
|
253
|
+
`methods/`, stubs, fixtures, one YAP slide). Layout:
|
|
254
|
+
[`docs/scaffold.html`](./docs/scaffold.html) · `templates/scaffold/STRUCTURE.md`.
|
|
253
255
|
|
|
254
256
|
**Consumer layout (Next.js App Router):** block folders mirror `app/` page routes (`/`
|
|
255
257
|
at the namespace root; no invented `landing/`/`root/`; sidebar = chrome + edges). See
|
|
256
|
-
`
|
|
258
|
+
[`docs/consumer.html`](./docs/consumer.html) and mesh handout
|
|
257
259
|
`WAYGRAPH-CONSUMER-CONVENTION.md` - separate from this package API doc.
|
|
258
260
|
|
|
259
261
|
Deliberately **not yet implemented** (tracked on the project board):
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type AgentDiveLoop = "claude" | "opencode" | "cursor" | "vscode";
|
|
2
|
+
export interface AgentSpec {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
color?: string;
|
|
7
|
+
tools: string[];
|
|
8
|
+
instructions: string;
|
|
9
|
+
}
|
|
10
|
+
/** Minimal YAML-ish frontmatter parser for `*.agent.md` templates. */
|
|
11
|
+
export declare function parseAgentSpec(filePath: string): AgentSpec;
|
|
12
|
+
export declare function loadAgentSpecs(): AgentSpec[];
|
|
13
|
+
export interface AgentDiveOptions {
|
|
14
|
+
loop: AgentDiveLoop;
|
|
15
|
+
projectDir: string;
|
|
16
|
+
/** Also copy raw prompts under docs/waygraph-agents/ */
|
|
17
|
+
prompts?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function runAgentDive(opts: AgentDiveOptions): void;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `waygraph agent-dive` — initialize coding-agent definitions for diving an app
|
|
3
|
+
* into waygraph Blocks (Playwright `init-agents` analogue).
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
function packageRoot() {
|
|
9
|
+
// dist/agent-dive.js -> ..
|
|
10
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
}
|
|
12
|
+
function agentsTemplateDir() {
|
|
13
|
+
return join(packageRoot(), "templates", "agents");
|
|
14
|
+
}
|
|
15
|
+
/** Minimal YAML-ish frontmatter parser for `*.agent.md` templates. */
|
|
16
|
+
export function parseAgentSpec(filePath) {
|
|
17
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
18
|
+
if (!raw.startsWith("---\n")) {
|
|
19
|
+
throw new Error(`agent-dive: missing frontmatter in ${filePath}`);
|
|
20
|
+
}
|
|
21
|
+
const end = raw.indexOf("\n---\n", 4);
|
|
22
|
+
if (end < 0)
|
|
23
|
+
throw new Error(`agent-dive: unclosed frontmatter in ${filePath}`);
|
|
24
|
+
const fm = raw.slice(4, end);
|
|
25
|
+
const body = raw.slice(end + 5).trim();
|
|
26
|
+
const meta = {};
|
|
27
|
+
let listKey = null;
|
|
28
|
+
const lists = {};
|
|
29
|
+
for (const line of fm.split("\n")) {
|
|
30
|
+
if (/^\s+-\s+/.test(line) && listKey) {
|
|
31
|
+
lists[listKey].push(line.replace(/^\s+-\s+/, "").trim());
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
listKey = null;
|
|
35
|
+
const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
36
|
+
if (!m)
|
|
37
|
+
continue;
|
|
38
|
+
const key = m[1];
|
|
39
|
+
const val = m[2].trim();
|
|
40
|
+
if (val === "") {
|
|
41
|
+
listKey = key;
|
|
42
|
+
lists[key] = [];
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
meta[key] = val;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const name = meta.name || basename(filePath).replace(/\.agent\.md$/, "");
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
description: meta.description || name,
|
|
52
|
+
...(meta.model ? { model: meta.model } : {}),
|
|
53
|
+
...(meta.color ? { color: meta.color } : {}),
|
|
54
|
+
tools: lists.tools ?? [],
|
|
55
|
+
instructions: body,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function loadAgentSpecs() {
|
|
59
|
+
const dir = agentsTemplateDir();
|
|
60
|
+
if (!existsSync(dir)) {
|
|
61
|
+
throw new Error(`agent-dive: templates missing at ${dir}`);
|
|
62
|
+
}
|
|
63
|
+
return readdirSync(dir)
|
|
64
|
+
.filter((f) => f.endsWith(".agent.md"))
|
|
65
|
+
.sort()
|
|
66
|
+
.map((f) => parseAgentSpec(join(dir, f)));
|
|
67
|
+
}
|
|
68
|
+
function writeFileLogged(path, content, kind) {
|
|
69
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
70
|
+
writeFileSync(path, content.endsWith("\n") ? content : content + "\n");
|
|
71
|
+
console.log(` wrote ${path} (${kind})`);
|
|
72
|
+
}
|
|
73
|
+
function claudeAgentMd(agent) {
|
|
74
|
+
const tools = agent.tools.join(", ");
|
|
75
|
+
const lines = [
|
|
76
|
+
"---",
|
|
77
|
+
`name: ${agent.name}`,
|
|
78
|
+
`description: ${agent.description}`,
|
|
79
|
+
...(agent.model ? [`model: ${agent.model}`] : []),
|
|
80
|
+
...(agent.color ? [`color: ${agent.color}`] : []),
|
|
81
|
+
...(tools ? [`tools: ${tools}`] : []),
|
|
82
|
+
"---",
|
|
83
|
+
"",
|
|
84
|
+
agent.instructions,
|
|
85
|
+
"",
|
|
86
|
+
];
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
function opencodePromptMd(agent) {
|
|
90
|
+
return `${agent.instructions}\n`;
|
|
91
|
+
}
|
|
92
|
+
function cursorRuleMdc(agent) {
|
|
93
|
+
return `---
|
|
94
|
+
description: ${agent.description}
|
|
95
|
+
globs:
|
|
96
|
+
alwaysApply: false
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
# ${agent.name}
|
|
100
|
+
|
|
101
|
+
${agent.instructions}
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
function vscodeAgentMd(agent) {
|
|
105
|
+
return claudeAgentMd(agent);
|
|
106
|
+
}
|
|
107
|
+
export function runAgentDive(opts) {
|
|
108
|
+
const agents = loadAgentSpecs();
|
|
109
|
+
const root = opts.projectDir;
|
|
110
|
+
console.log(`waygraph agent-dive (${opts.loop}) -> ${root}`);
|
|
111
|
+
console.log(` ${agents.length} agent(s) from package templates`);
|
|
112
|
+
switch (opts.loop) {
|
|
113
|
+
case "claude": {
|
|
114
|
+
const dir = join(root, ".claude", "agents");
|
|
115
|
+
mkdirSync(dir, { recursive: true });
|
|
116
|
+
for (const a of agents) {
|
|
117
|
+
writeFileLogged(join(dir, `${a.name}.md`), claudeAgentMd(a), "claude agent");
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "opencode": {
|
|
122
|
+
const dir = join(root, ".opencode", "prompts");
|
|
123
|
+
mkdirSync(dir, { recursive: true });
|
|
124
|
+
for (const a of agents) {
|
|
125
|
+
writeFileLogged(join(dir, `${a.name}.md`), opencodePromptMd(a), "opencode prompt");
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "cursor": {
|
|
130
|
+
const dir = join(root, ".cursor", "rules");
|
|
131
|
+
mkdirSync(dir, { recursive: true });
|
|
132
|
+
for (const a of agents) {
|
|
133
|
+
writeFileLogged(join(dir, `${a.name}.mdc`), cursorRuleMdc(a), "cursor rule");
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "vscode": {
|
|
138
|
+
const dir = join(root, ".github", "agents");
|
|
139
|
+
mkdirSync(dir, { recursive: true });
|
|
140
|
+
for (const a of agents) {
|
|
141
|
+
writeFileLogged(join(dir, `${a.name}.md`), vscodeAgentMd(a), "vscode agent");
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
default: {
|
|
146
|
+
const _exhaustive = opts.loop;
|
|
147
|
+
throw new Error(`agent-dive: unsupported loop ${_exhaustive}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (opts.prompts) {
|
|
151
|
+
const dir = join(root, "docs", "waygraph-agents");
|
|
152
|
+
mkdirSync(dir, { recursive: true });
|
|
153
|
+
for (const a of agents) {
|
|
154
|
+
writeFileLogged(join(dir, `${a.name}.md`), `# ${a.name}\n\n${a.description}\n\n${a.instructions}\n`, "prompt copy");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
console.log("");
|
|
158
|
+
console.log("Next: point your coding agent at those files, then:");
|
|
159
|
+
console.log(" npx waygraph check .");
|
|
160
|
+
console.log(" npx waygraph auto --cli");
|
|
161
|
+
console.log(" (scaffold: npx waygraph init my-app)");
|
|
162
|
+
}
|
|
@@ -5,6 +5,11 @@ export interface AutoExploreOptions {
|
|
|
5
5
|
/** Initial URL when the page is blank (defaults to baseURL). */
|
|
6
6
|
startUrl?: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Saucedemo-only defaults. Exact key names only — never substring `"login"`
|
|
10
|
+
* (PIA `login-email` string must stay unset; see docs/proposals/auto-login-email-seed-bug.md).
|
|
11
|
+
*/
|
|
12
|
+
export declare function defaultMemValueForKey(keyName: string): unknown | undefined;
|
|
8
13
|
/**
|
|
9
14
|
* Interactive waygraph auto: locate current Checkpoint, list runnable Blocks, pick one, repeat.
|
|
10
15
|
*/
|
package/dist/auto-explore-run.js
CHANGED
|
@@ -77,12 +77,24 @@ function missingMemKeys(entry, mem) {
|
|
|
77
77
|
return requires.filter((k) => !mem.has(k));
|
|
78
78
|
}
|
|
79
79
|
const DEFAULT_CREDENTIALS = { username: "standard_user", password: "secret_sauce" };
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Saucedemo-only defaults. Exact key names only — never substring `"login"`
|
|
82
|
+
* (PIA `login-email` string must stay unset; see docs/proposals/auto-login-email-seed-bug.md).
|
|
83
|
+
*/
|
|
84
|
+
export function defaultMemValueForKey(keyName) {
|
|
85
|
+
if (keyName === "login-credentials" ||
|
|
86
|
+
keyName === "credentials" ||
|
|
87
|
+
keyName === "saucedemo.credentials") {
|
|
82
88
|
return DEFAULT_CREDENTIALS;
|
|
83
89
|
}
|
|
84
90
|
return undefined;
|
|
85
91
|
}
|
|
92
|
+
function defaultMemJsonPrompt(keyName) {
|
|
93
|
+
const def = defaultMemValueForKey(keyName);
|
|
94
|
+
if (def !== undefined)
|
|
95
|
+
return JSON.stringify(def);
|
|
96
|
+
return "{}";
|
|
97
|
+
}
|
|
86
98
|
function seedMemFromEnv(mem, entry) {
|
|
87
99
|
const raw = process.env.WAYGRAPH_DATA ?? process.env.WAYGRAPH_AUTO_MEM;
|
|
88
100
|
if (!raw)
|
|
@@ -129,9 +141,7 @@ async function promptMemCli(entry, mem) {
|
|
|
129
141
|
const rl = createInterface({ input, output });
|
|
130
142
|
try {
|
|
131
143
|
for (const k of missingMemKeys(entry, mem)) {
|
|
132
|
-
const def =
|
|
133
|
-
? '{"username":"standard_user","password":"secret_sauce"}'
|
|
134
|
-
: "{}";
|
|
144
|
+
const def = defaultMemJsonPrompt(k.name);
|
|
135
145
|
const line = await rl.question(` ${k.name} JSON [${def}]: `);
|
|
136
146
|
const json = line.trim() || def;
|
|
137
147
|
mem.set(k, JSON.parse(json));
|
package/dist/cli.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand`
|
|
8
8
|
* run Execute; `--blocks` `--data` `--non-headless` `--video`
|
|
9
9
|
*
|
|
10
|
-
* Also: list / nav / validate / check / graph / init / try
|
|
10
|
+
* Also: list / nav / validate / check / graph / init / agent-dive / try
|
|
11
11
|
* Aliases (one release): `chain` -> run/demo --blocks; `--autoplay` -> `--auto-next`
|
|
12
12
|
*
|
|
13
13
|
* "project" defaults to cwd. Flags beat WAYGRAPH_* env.
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full` `--ff-expand`
|
|
8
8
|
* run Execute; `--blocks` `--data` `--non-headless` `--video`
|
|
9
9
|
*
|
|
10
|
-
* Also: list / nav / validate / check / graph / init / try
|
|
10
|
+
* Also: list / nav / validate / check / graph / init / agent-dive / try
|
|
11
11
|
* Aliases (one release): `chain` -> run/demo --blocks; `--autoplay` -> `--auto-next`
|
|
12
12
|
*
|
|
13
13
|
* "project" defaults to cwd. Flags beat WAYGRAPH_* env.
|
|
@@ -19,6 +19,7 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
import { spawn } from "node:child_process";
|
|
20
20
|
import { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
|
|
21
21
|
import { runAutoExplore } from "./auto-explore-run.js";
|
|
22
|
+
import { runAgentDive } from "./agent-dive.js";
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Filesystem
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
@@ -3237,8 +3238,10 @@ function initCommand(projectName) {
|
|
|
3237
3238
|
console.log(" waygraph check # nav hygiene + orphan Blocks");
|
|
3238
3239
|
console.log(" waygraph auto # interactive explore (headed panel)");
|
|
3239
3240
|
console.log(" waygraph auto --cli # same menus in the terminal");
|
|
3240
|
-
console.log(" waygraph demo
|
|
3241
|
+
console.log(" waygraph demo --blocks exampleFlow");
|
|
3241
3242
|
console.log(" waygraph graph # static state graph JSON");
|
|
3243
|
+
console.log("");
|
|
3244
|
+
console.log(" Layout: see STRUCTURE.md (or https://deviate-dv8.github.io/waygraph/scaffold.html)");
|
|
3242
3245
|
}
|
|
3243
3246
|
async function checkCommand(projectDir) {
|
|
3244
3247
|
const files = discoverBlocks(projectDir);
|
|
@@ -3658,6 +3661,8 @@ Primary (less is more):
|
|
|
3658
3661
|
|
|
3659
3662
|
Also:
|
|
3660
3663
|
waygraph list | nav | validate | check | graph | init <name>
|
|
3664
|
+
waygraph agent-dive [--loop claude|opencode|cursor|vscode] [--prompts]
|
|
3665
|
+
Initialize coding-agent defs (Playwright init-agents analogue)
|
|
3661
3666
|
waygraph try [demo|auto|auto:cli] One-shot saucedemo in a temp dir
|
|
3662
3667
|
try auto Headed browser panel (default)
|
|
3663
3668
|
try auto:cli / --cli Terminal menu instead
|
|
@@ -3754,6 +3759,55 @@ async function main() {
|
|
|
3754
3759
|
initCommand(args[1] ?? "");
|
|
3755
3760
|
break;
|
|
3756
3761
|
}
|
|
3762
|
+
case "agent-dive":
|
|
3763
|
+
case "init-agents": {
|
|
3764
|
+
// Playwright analogue: npx playwright init-agents --loop <provider>
|
|
3765
|
+
const rest = args.slice(1);
|
|
3766
|
+
let loop = "claude";
|
|
3767
|
+
let prompts = false;
|
|
3768
|
+
let projectDir = process.cwd();
|
|
3769
|
+
for (let i = 0; i < rest.length; i++) {
|
|
3770
|
+
const a = rest[i];
|
|
3771
|
+
if (a === "--prompts") {
|
|
3772
|
+
prompts = true;
|
|
3773
|
+
continue;
|
|
3774
|
+
}
|
|
3775
|
+
if (a === "--loop" || a.startsWith("--loop=")) {
|
|
3776
|
+
const v = a.startsWith("--loop=") ? a.slice("--loop=".length) : rest[++i];
|
|
3777
|
+
if (!v || !["claude", "opencode", "cursor", "vscode"].includes(v)) {
|
|
3778
|
+
console.error("waygraph agent-dive: --loop must be claude | opencode | cursor | vscode");
|
|
3779
|
+
process.exit(1);
|
|
3780
|
+
}
|
|
3781
|
+
loop = v;
|
|
3782
|
+
continue;
|
|
3783
|
+
}
|
|
3784
|
+
if (a === "--help" || a === "-h") {
|
|
3785
|
+
console.log(`waygraph agent-dive [--loop claude|opencode|cursor|vscode] [--prompts] [project]
|
|
3786
|
+
|
|
3787
|
+
Initialize coding-agent definitions for diving an app into waygraph Blocks
|
|
3788
|
+
(Playwright \`init-agents\` analogue).
|
|
3789
|
+
|
|
3790
|
+
--loop claude write .claude/agents/*.md (default)
|
|
3791
|
+
--loop opencode write .opencode/prompts/*.md
|
|
3792
|
+
--loop cursor write .cursor/rules/waygraph-*.mdc
|
|
3793
|
+
--loop vscode write .github/agents/*.md
|
|
3794
|
+
--prompts also copy docs/waygraph-agents/*.md
|
|
3795
|
+
|
|
3796
|
+
Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
|
|
3797
|
+
`);
|
|
3798
|
+
process.exit(0);
|
|
3799
|
+
}
|
|
3800
|
+
if (!a.startsWith("-")) {
|
|
3801
|
+
projectDir = resolve(a);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
if (!existsSync(projectDir) || !statSync(projectDir).isDirectory()) {
|
|
3805
|
+
console.error(`waygraph agent-dive: no such directory: ${projectDir}`);
|
|
3806
|
+
process.exit(1);
|
|
3807
|
+
}
|
|
3808
|
+
runAgentDive({ loop, projectDir, prompts });
|
|
3809
|
+
break;
|
|
3810
|
+
}
|
|
3757
3811
|
case "chain": {
|
|
3758
3812
|
// Compat alias: chain auto A B -> auto --blocks A B; else -> run --blocks.
|
|
3759
3813
|
const flags = parseRunFlags(args.slice(1));
|
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: waygraph-author
|
|
3
|
+
description: Author waygraph Nav/Page/Method/Effect blocks and flows from an approved SITE-MAP. Use after planning, or when extending coverage for a named route.
|
|
4
|
+
model: sonnet
|
|
5
|
+
color: green
|
|
6
|
+
tools:
|
|
7
|
+
- search
|
|
8
|
+
- edit
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are a waygraph block author. Implement Blocks and Flows that match the consumer SITE-MAP.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
|
|
15
|
+
1. One concern per file: `nav-*.block.ts`, `*.page.block.ts`, `*.method.block.ts`, `*.effect.block.ts`, `*.flow.ts`.
|
|
16
|
+
2. Navigation only inside `defineNavBlock` / `defineNavClickBlock`. Regular Method/Effect `act` uses ActionPage (no goto).
|
|
17
|
+
3. Effects that appear in `waygraph auto` menus need `instanceOptions` that scan the live DOM (or mem) for labeled rows.
|
|
18
|
+
4. Stubs: `stubBefore` / `stubAfter` / `stubOnError` for demo narration; YAP slides on Methods when useful; flow fixtures via `withHighlightFixtures` when AC copy is flow-specific.
|
|
19
|
+
5. Mem: `requires` lists keys the block needs; seed via `--data` / `WAYGRAPH_DATA` / project defaults — never assume saucedemo creds for email keys.
|
|
20
|
+
6. External Mailpit/MailHog: nav to inbox URL from env; Methods open message / click verify link; never put mail UI under `*-web/`.
|
|
21
|
+
|
|
22
|
+
## Verify locally
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx waygraph check .
|
|
26
|
+
npx waygraph list
|
|
27
|
+
npm run typecheck
|
|
28
|
+
# offline fixtures or live:
|
|
29
|
+
npx playwright test
|
|
30
|
+
# explore:
|
|
31
|
+
npm run auto # or: npx waygraph auto --cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Output
|
|
35
|
+
|
|
36
|
+
Ship compiling TypeScript under `src/blocks/` + `src/flows/` + `src/states/`. Update NAV.md when edges change. Keep STRUCTURE / SITE-MAP honest.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: waygraph-healer
|
|
3
|
+
description: Debug and fix failing waygraph flows, auto menus, Mem seeds, and Mailpit/email verify hops. Use when npm test, waygraph run, or waygraph auto fails.
|
|
4
|
+
model: sonnet
|
|
5
|
+
color: red
|
|
6
|
+
tools:
|
|
7
|
+
- search
|
|
8
|
+
- edit
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are a waygraph healer. Fix failing Blocks/Flows with evidence — do not rewrite the architecture.
|
|
12
|
+
|
|
13
|
+
## Diagnose order
|
|
14
|
+
|
|
15
|
+
1. Read the error (selector timeout, Trait fail, Mem missing, wrong checkpoint).
|
|
16
|
+
2. Confirm base URL / fixture server / env (`WAYGRAPH_BASE_URL`, Mailpit URL).
|
|
17
|
+
3. Check Mem: wrong type for a key (object vs string) is a common auto bug — `login-email` must be a string address, not `{username,password}`.
|
|
18
|
+
4. Nav vs Method: accidental `page.goto` outside NavBlock → move to `defineNavBlock`.
|
|
19
|
+
5. Effect auto menus: `instanceOptions` must match live DOM; empty menu usually means wrong selector or not on the page yet.
|
|
20
|
+
6. Mailpit: filter by the real recipient email in Mem; ambiguous `.msglist-message` with multiple rows → tighten locator.
|
|
21
|
+
|
|
22
|
+
## Fix loop
|
|
23
|
+
|
|
24
|
+
1. Reproduce with the smallest command (`waygraph run --blocks …` or one Playwright spec).
|
|
25
|
+
2. Patch the owning block (or fixture), re-run the same command.
|
|
26
|
+
3. Keep stubs/fixtures working after selector changes.
|
|
27
|
+
4. Report proof: command + exit 0 / expected checkpoint.
|
|
28
|
+
|
|
29
|
+
## Do not
|
|
30
|
+
|
|
31
|
+
- Invent new route folders that are not app URLs
|
|
32
|
+
- Seed credential objects into email Mem keys
|
|
33
|
+
- Clear shared remote MailHog/Mailpit unless the operator said so
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: waygraph-planner
|
|
3
|
+
description: Plan waygraph SITE-MAP and route coverage for an existing web app. Use when diving into a product codebase to map URLs to Nav/Page/Method/Effect blocks before writing code.
|
|
4
|
+
model: sonnet
|
|
5
|
+
color: blue
|
|
6
|
+
tools:
|
|
7
|
+
- search
|
|
8
|
+
- edit
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are a waygraph planner. Your job is to dive an existing app and produce a SITE-MAP + NAV.md plan — not dump ad hoc Playwright helpers.
|
|
12
|
+
|
|
13
|
+
## Hard conventions
|
|
14
|
+
|
|
15
|
+
1. **Route folders = app URL folders.** A folder gets `NAV.md` + a nav block iff the app has a real page at that URL. Do not invent `landing/`, `root/`, or shell folders that are not routes.
|
|
16
|
+
2. **Namespaces by origin:** `<app>-web/` for the product FE; `<app>-external/` for Mailpit/MailHog, Studio, probes (never mix).
|
|
17
|
+
3. **Block kinds:**
|
|
18
|
+
- Nav — `defineNavBlock` / `defineNavClickBlock` — only place for `page.goto` / click-nav
|
|
19
|
+
- Page — `definePageBlock` hub + `methods` registry
|
|
20
|
+
- Method — forms, asserts, same-URL actions without instance menus
|
|
21
|
+
- Effect — `defineEffectBlock` + `instanceOptions` for per-row auto menus (add/remove/toggle)
|
|
22
|
+
4. **Mem keys:** typed `key` / `keyGroup`. Never rely on substring `"login"` defaults — email keys like `login-email` stay strings; credential objects use exact names such as `saucedemo.credentials` / `login-credentials`.
|
|
23
|
+
5. Prefer `click` NavBlocks when the product has a real control; use `url` for deep links / email tokens.
|
|
24
|
+
|
|
25
|
+
## Workflow
|
|
26
|
+
|
|
27
|
+
1. Read the app router (Next.js `app/`, etc.) and list real page URLs.
|
|
28
|
+
2. Draft `src/blocks/SITE-MAP.md` with namespaces + route folders.
|
|
29
|
+
3. For each route: list nav-to edges, methods/effects, checkpoints, mem keys in `NAV.md` (plan only).
|
|
30
|
+
4. Call out external surfaces (Mailpit inbox, magic-link verify) under `*-external/`.
|
|
31
|
+
5. Propose 1-3 starter flows (happy path, login/verify, one feature) — do not implement unless asked.
|
|
32
|
+
|
|
33
|
+
## Output
|
|
34
|
+
|
|
35
|
+
Write/update markdown under `src/blocks/` (`SITE-MAP.md`, route `NAV.md` stubs). ASCII only. No localhost ports in shared docs.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
Offline waygraph scaffold - Nav, Page hub, Method, Effect (`instanceOptions` for
|
|
4
|
+
`waygraph auto`), stubs/fixtures/YAP, mem keys.
|
|
5
|
+
|
|
6
|
+
## Layout
|
|
7
|
+
|
|
8
|
+
See [STRUCTURE.md](./STRUCTURE.md). Docs:
|
|
9
|
+
https://deviate-dv8.github.io/waygraph/scaffold.html
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
npx playwright install chromium
|
|
16
|
+
npm test
|
|
17
|
+
npm run list && npm run check && npm run graph
|
|
18
|
+
npm run demo # exampleFlow (hello + YAP)
|
|
19
|
+
npm run demo:shop # Effect path (needs --data selectedItem)
|
|
20
|
+
npm run auto # headed explore - pick Add Alpha / …
|
|
21
|
+
npm run auto:cli
|
|
22
|
+
npx waygraph agent-dive --loop claude # coding-agent defs (optional)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Point `package.json` `waygraph.baseUrl` at your real app when you leave the
|
|
26
|
+
fixture (and drop `scripts/with-fixture.mjs` wrappers). Keep route folders =
|
|
27
|
+
app URLs (consumer layout).
|
|
28
|
+
Offline fixture is HTTP `http://127.0.0.1:4177/home.html` (Flatpak-safe).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Offline scaffold layout (`waygraph init` / `create-waygraph`)
|
|
2
|
+
|
|
3
|
+
Shipped under `templates/scaffold/` in the **waygraph** package.
|
|
4
|
+
Docs: [docs/scaffold.html](../../docs/scaffold.html)
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
.
|
|
8
|
+
├── package.json
|
|
9
|
+
├── STRUCTURE.md # this file
|
|
10
|
+
├── README.md
|
|
11
|
+
├── src/
|
|
12
|
+
│ ├── blocks/
|
|
13
|
+
│ │ ├── SITE-MAP.md
|
|
14
|
+
│ │ └── demo-web/ # synthetic "/" (HTTP fixture :4177)
|
|
15
|
+
│ │ ├── NAV.md
|
|
16
|
+
│ │ ├── home.html # offline fixture (catalog + cart)
|
|
17
|
+
│ │ ├── demo-sel.ts
|
|
18
|
+
│ │ ├── nav-home.block.ts # Nav
|
|
19
|
+
│ │ ├── home.page.block.ts # Page hub (methods registry)
|
|
20
|
+
│ │ └── methods/
|
|
21
|
+
│ │ ├── assert-hello.method.block.ts # Method + stubs + YAP
|
|
22
|
+
│ │ ├── add-item.effect.block.ts # Effect + instanceOptions (auto)
|
|
23
|
+
│ │ ├── remove-item.effect.block.ts # Effect + instanceOptions
|
|
24
|
+
│ │ └── clear-cart.method.block.ts # Method
|
|
25
|
+
│ ├── states/
|
|
26
|
+
│ │ ├── demo.states.ts
|
|
27
|
+
│ │ └── demo.mem-keys.ts # SelectedItem for Effect / auto
|
|
28
|
+
│ └── flows/
|
|
29
|
+
│ ├── example.flow.ts # nav + assert (smoke)
|
|
30
|
+
│ └── shop.flow.ts # nav + add-item + clear (Effect path)
|
|
31
|
+
├── scripts/
|
|
32
|
+
│ ├── fixture-server.mjs # serves home.html on :4177
|
|
33
|
+
│ └── with-fixture.mjs # start server + run waygraph auto/demo/run
|
|
34
|
+
└── tests/
|
|
35
|
+
└── example.spec.ts
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Features covered
|
|
39
|
+
|
|
40
|
+
| Feature | Where |
|
|
41
|
+
|---------|--------|
|
|
42
|
+
| Nav (`defineNavBlock`) | `nav-home.block.ts` |
|
|
43
|
+
| Page hub (`definePageBlock` + `methods`) | `home.page.block.ts` |
|
|
44
|
+
| Method (`defineMethodBlock`) | `assert-hello`, `clear-cart` |
|
|
45
|
+
| Effect + `instanceOptions` (auto menus) | `add-item`, `remove-item` |
|
|
46
|
+
| Mem (`keyGroup`) | `demo.mem-keys.ts` |
|
|
47
|
+
| `stubBefore` / `stubAfter` / `stubOnError` | all instruction blocks |
|
|
48
|
+
| Flow fixtures + title | `example.flow.ts`, `shop.flow.ts` |
|
|
49
|
+
| YAP slides | `assert-hello` |
|
|
50
|
+
| `withSessionReset` | `shop.flow.ts` |
|
|
51
|
+
| `waygraph auto` / `auto --cli` | `npm run auto` |
|
|
52
|
+
| `demo` / `run` / `list` / `check` / `graph` | `package.json` scripts |
|
|
53
|
+
|
|
54
|
+
## Auto
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm run auto # headed explore - pick Add Alpha / Add Beta from live DOM
|
|
58
|
+
npm run auto:cli # same menus in the terminal
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
After `nav-home`, Effect blocks expose one menu row per visible Add/Remove button.
|
|
@@ -6,19 +6,25 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"typecheck": "tsc --noEmit",
|
|
8
8
|
"test": "playwright test",
|
|
9
|
+
"fixture": "node scripts/fixture-server.mjs",
|
|
9
10
|
"list": "waygraph list",
|
|
10
11
|
"check": "waygraph check",
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
12
|
+
"graph": "waygraph graph",
|
|
13
|
+
"graph:mermaid": "waygraph graph --mermaid",
|
|
14
|
+
"auto": "node scripts/with-fixture.mjs waygraph auto",
|
|
15
|
+
"auto:cli": "node scripts/with-fixture.mjs waygraph auto --cli",
|
|
16
|
+
"demo": "node scripts/with-fixture.mjs waygraph demo --blocks exampleFlow",
|
|
17
|
+
"demo:auto-next": "node scripts/with-fixture.mjs waygraph demo --blocks exampleFlow --auto-next --title \"Scaffold hello\"",
|
|
18
|
+
"demo:shop": "node scripts/with-fixture.mjs waygraph demo --blocks shopFlow --data \"{\\\"demo.selectedItem\\\":{\\\"id\\\":\\\"alpha\\\",\\\"name\\\":\\\"Alpha\\\"}}\" --title \"Scaffold shop\"",
|
|
19
|
+
"demo:shop:auto-next": "node scripts/with-fixture.mjs waygraph demo --blocks shopFlow --auto-next --data \"{\\\"demo.selectedItem\\\":{\\\"id\\\":\\\"alpha\\\",\\\"name\\\":\\\"Alpha\\\"}}\"",
|
|
20
|
+
"run": "node scripts/with-fixture.mjs waygraph run --blocks exampleFlow",
|
|
21
|
+
"run:shop": "node scripts/with-fixture.mjs waygraph run --blocks shopFlow --data \"{\\\"demo.selectedItem\\\":{\\\"id\\\":\\\"alpha\\\",\\\"name\\\":\\\"Alpha\\\"}}\""
|
|
16
22
|
},
|
|
17
23
|
"waygraph": {
|
|
18
|
-
"baseUrl": "
|
|
24
|
+
"baseUrl": "http://127.0.0.1:4177"
|
|
19
25
|
},
|
|
20
26
|
"dependencies": {
|
|
21
|
-
"waygraph": "^0.
|
|
27
|
+
"waygraph": "^0.12.7"
|
|
22
28
|
},
|
|
23
29
|
"devDependencies": {
|
|
24
30
|
"@playwright/test": "^1.55.0",
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import { defineConfig } from "@playwright/test";
|
|
2
2
|
|
|
3
|
+
const FIXTURE_PORT = process.env.WAYGRAPH_FIXTURE_PORT || "4177";
|
|
4
|
+
const FIXTURE_HOST = process.env.WAYGRAPH_FIXTURE_HOST || "127.0.0.1";
|
|
5
|
+
const FIXTURE_ORIGIN = `http://${FIXTURE_HOST}:${FIXTURE_PORT}`;
|
|
6
|
+
|
|
3
7
|
export default defineConfig({
|
|
4
8
|
testDir: "./tests",
|
|
5
9
|
fullyParallel: true,
|
|
6
10
|
reporter: "list",
|
|
7
11
|
use: {
|
|
12
|
+
baseURL: FIXTURE_ORIGIN,
|
|
8
13
|
launchOptions: {
|
|
9
14
|
executablePath: process.env.CHROME_PATH || process.env.CHROMIUM_PATH,
|
|
10
15
|
},
|
|
11
16
|
},
|
|
17
|
+
webServer: {
|
|
18
|
+
command: "node scripts/fixture-server.mjs",
|
|
19
|
+
url: `${FIXTURE_ORIGIN}/home.html`,
|
|
20
|
+
reuseExistingServer: !process.env.CI,
|
|
21
|
+
env: {
|
|
22
|
+
WAYGRAPH_FIXTURE_PORT: FIXTURE_PORT,
|
|
23
|
+
WAYGRAPH_FIXTURE_HOST: FIXTURE_HOST,
|
|
24
|
+
WAYGRAPH_FIXTURE_QUIET: "1",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
12
27
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tiny static server for the offline scaffold fixture (home.html).
|
|
4
|
+
* Flatpak Chromium cannot open file:// under /tmp — HTTP localhost works.
|
|
5
|
+
*/
|
|
6
|
+
import { createServer } from "node:http";
|
|
7
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { join, dirname, extname, normalize } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "../src/blocks/demo-web");
|
|
12
|
+
const PORT = Number(process.env.WAYGRAPH_FIXTURE_PORT || 4177);
|
|
13
|
+
const HOST = process.env.WAYGRAPH_FIXTURE_HOST || "127.0.0.1";
|
|
14
|
+
|
|
15
|
+
const TYPES = {
|
|
16
|
+
".html": "text/html; charset=utf-8",
|
|
17
|
+
".css": "text/css; charset=utf-8",
|
|
18
|
+
".js": "text/javascript; charset=utf-8",
|
|
19
|
+
".svg": "image/svg+xml",
|
|
20
|
+
".png": "image/png",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const server = createServer((req, res) => {
|
|
24
|
+
const raw = (req.url || "/").split("?")[0] || "/";
|
|
25
|
+
const rel = raw === "/" ? "home.html" : raw.replace(/^\//, "");
|
|
26
|
+
const full = normalize(join(ROOT, rel));
|
|
27
|
+
if (!full.startsWith(ROOT) || !existsSync(full)) {
|
|
28
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
29
|
+
res.end("not found");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const type = TYPES[extname(full)] || "application/octet-stream";
|
|
33
|
+
res.writeHead(200, { "content-type": type });
|
|
34
|
+
res.end(readFileSync(full));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
server.listen(PORT, HOST, () => {
|
|
38
|
+
if (process.env.WAYGRAPH_FIXTURE_QUIET !== "1") {
|
|
39
|
+
console.error(`waygraph fixture: http://${HOST}:${PORT}/home.html`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Start fixture-server, run a child command, tear down on exit.
|
|
4
|
+
* Usage: node scripts/with-fixture.mjs [--] <cmd> [args...]
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const PORT = process.env.WAYGRAPH_FIXTURE_PORT || "4177";
|
|
12
|
+
const HOST = process.env.WAYGRAPH_FIXTURE_HOST || "127.0.0.1";
|
|
13
|
+
|
|
14
|
+
const argv = process.argv.slice(2).filter((a) => a !== "--");
|
|
15
|
+
if (argv.length === 0) {
|
|
16
|
+
console.error("usage: node scripts/with-fixture.mjs <cmd> [args...]");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const env = {
|
|
21
|
+
...process.env,
|
|
22
|
+
WAYGRAPH_FIXTURE_PORT: PORT,
|
|
23
|
+
WAYGRAPH_FIXTURE_HOST: HOST,
|
|
24
|
+
WAYGRAPH_FIXTURE_QUIET: "1",
|
|
25
|
+
WAYGRAPH_BASE_URL: process.env.WAYGRAPH_BASE_URL || `http://${HOST}:${PORT}`,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const server = spawn(process.execPath, [join(HERE, "fixture-server.mjs")], {
|
|
29
|
+
env,
|
|
30
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function killServer() {
|
|
34
|
+
try {
|
|
35
|
+
server.kill("SIGTERM");
|
|
36
|
+
} catch {
|
|
37
|
+
/* ignore */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function waitReady(ms = 5000) {
|
|
42
|
+
const url = `http://${HOST}:${PORT}/home.html`;
|
|
43
|
+
const start = Date.now();
|
|
44
|
+
while (Date.now() - start < ms) {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(url);
|
|
47
|
+
if (res.ok) return;
|
|
48
|
+
} catch {
|
|
49
|
+
/* retry */
|
|
50
|
+
}
|
|
51
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`fixture server not ready at ${url}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let code = 1;
|
|
57
|
+
try {
|
|
58
|
+
await waitReady();
|
|
59
|
+
const child = spawn(argv[0], argv.slice(1), { env, stdio: "inherit", shell: false });
|
|
60
|
+
code = await new Promise((resolve) => {
|
|
61
|
+
child.on("exit", (c, signal) => resolve(signal ? 1 : (c ?? 1)));
|
|
62
|
+
});
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
65
|
+
code = 1;
|
|
66
|
+
} finally {
|
|
67
|
+
killServer();
|
|
68
|
+
}
|
|
69
|
+
process.exit(code);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Site map (scaffold)
|
|
2
|
+
|
|
3
|
+
Offline catalog uses one synthetic namespace (`demo-web/`) and a local
|
|
4
|
+
`home.html` fixture so `npm test` / `waygraph auto` need no network.
|
|
5
|
+
|
|
6
|
+
| Folder | URL | Notes |
|
|
7
|
+
|--------|-----|-------|
|
|
8
|
+
| `demo-web/` | `/` (synthetic) | Nav + Page hub + methods/effects |
|
|
9
|
+
| `demo-web/methods/` | same URL | assert, add/remove Effects, clear |
|
|
10
|
+
|
|
11
|
+
When you wire a real app, rename `demo-web/` to your FE origin namespace and
|
|
12
|
+
add one folder per real `page.tsx` route. See **Consumer layout** docs.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# demo-web / (synthetic home)
|
|
2
|
+
|
|
3
|
+
**URL:** `http://127.0.0.1:4177/home.html` (offline catalog via `scripts/fixture-server.mjs` - replace with your real `/`)
|
|
4
|
+
|
|
5
|
+
## Nav-to
|
|
6
|
+
|
|
7
|
+
| From | Block | How |
|
|
8
|
+
|------|-------|-----|
|
|
9
|
+
| start | `nav-home` | `defineNavBlock` `url` → home.html |
|
|
10
|
+
|
|
11
|
+
## Page hub
|
|
12
|
+
|
|
13
|
+
| Block | Checkpoint | Methods |
|
|
14
|
+
|-------|------------|---------|
|
|
15
|
+
| `page-home` | `Home` | add-item, remove-item, clear-cart, assert-hello |
|
|
16
|
+
|
|
17
|
+
## Methods / Effects
|
|
18
|
+
|
|
19
|
+
| Block | Kind | Auto |
|
|
20
|
+
|-------|------|------|
|
|
21
|
+
| `assert-hello` | Method | single row |
|
|
22
|
+
| `add-item` | Effect | one row per live Add button (`instanceOptions`) |
|
|
23
|
+
| `remove-item` | Effect | one row per live Remove button |
|
|
24
|
+
| `clear-cart` | Method | single row |
|
|
25
|
+
|
|
26
|
+
## States / mem-keys
|
|
27
|
+
|
|
28
|
+
- Checkpoints: `Home`, `HomeVerified`, `ItemInCart`, `CartEmpty`
|
|
29
|
+
- Mem: `demo.selectedItem` (`SelectedItem`) - filled by auto or `--data`
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Offline catalog URL — HTTP fixture server (not file://; Flatpak Chromium blocks /tmp file URLs). */
|
|
2
|
+
export const HOME_URL =
|
|
3
|
+
process.env.WAYGRAPH_HOME_URL ??
|
|
4
|
+
`http://${process.env.WAYGRAPH_FIXTURE_HOST || "127.0.0.1"}:${process.env.WAYGRAPH_FIXTURE_PORT || "4177"}/home.html`;
|
|
5
|
+
|
|
6
|
+
export const DemoSel = {
|
|
7
|
+
title: "h1",
|
|
8
|
+
sub: "#sub",
|
|
9
|
+
cartCount: "[data-test=cart-count]",
|
|
10
|
+
catalog: "#catalog",
|
|
11
|
+
clear: "[data-test=clear-cart]",
|
|
12
|
+
addBtn: (id: string) => `[data-test=add-${id}]`,
|
|
13
|
+
removeBtn: (id: string) => `[data-test=remove-${id}]`,
|
|
14
|
+
} as const;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Waygraph scaffold demo</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 32rem; }
|
|
8
|
+
h1 { margin: 0 0 0.5rem; }
|
|
9
|
+
#cart { margin: 1rem 0; font-weight: 600; }
|
|
10
|
+
.item { display: flex; gap: 0.75rem; align-items: center; margin: 0.5rem 0; }
|
|
11
|
+
button { cursor: pointer; }
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<h1>Hello Waygraph</h1>
|
|
16
|
+
<p id="sub">Scaffold demo - Effect / auto / Page hub</p>
|
|
17
|
+
<div id="cart">Cart: <span data-test="cart-count">0</span></div>
|
|
18
|
+
<ul id="catalog">
|
|
19
|
+
<li class="item" data-id="alpha">
|
|
20
|
+
<span class="name">Alpha</span>
|
|
21
|
+
<button type="button" data-test="add-alpha" class="add">Add Alpha</button>
|
|
22
|
+
<button type="button" data-test="remove-alpha" class="remove" hidden>Remove Alpha</button>
|
|
23
|
+
</li>
|
|
24
|
+
<li class="item" data-id="beta">
|
|
25
|
+
<span class="name">Beta</span>
|
|
26
|
+
<button type="button" data-test="add-beta" class="add">Add Beta</button>
|
|
27
|
+
<button type="button" data-test="remove-beta" class="remove" hidden>Remove Beta</button>
|
|
28
|
+
</li>
|
|
29
|
+
<li class="item" data-id="gamma">
|
|
30
|
+
<span class="name">Gamma</span>
|
|
31
|
+
<button type="button" data-test="add-gamma" class="add">Add Gamma</button>
|
|
32
|
+
<button type="button" data-test="remove-gamma" class="remove" hidden>Remove Gamma</button>
|
|
33
|
+
</li>
|
|
34
|
+
</ul>
|
|
35
|
+
<button type="button" data-test="clear-cart" id="clear">Clear cart</button>
|
|
36
|
+
<script>
|
|
37
|
+
const countEl = document.querySelector("[data-test=cart-count]");
|
|
38
|
+
const sync = () => {
|
|
39
|
+
const n = document.querySelectorAll(".remove:not([hidden])").length;
|
|
40
|
+
countEl.textContent = String(n);
|
|
41
|
+
};
|
|
42
|
+
document.querySelectorAll(".item").forEach((row) => {
|
|
43
|
+
const add = row.querySelector(".add");
|
|
44
|
+
const remove = row.querySelector(".remove");
|
|
45
|
+
add.addEventListener("click", () => {
|
|
46
|
+
add.hidden = true;
|
|
47
|
+
remove.hidden = false;
|
|
48
|
+
sync();
|
|
49
|
+
});
|
|
50
|
+
remove.addEventListener("click", () => {
|
|
51
|
+
remove.hidden = true;
|
|
52
|
+
add.hidden = false;
|
|
53
|
+
sync();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
document.getElementById("clear").addEventListener("click", () => {
|
|
57
|
+
document.querySelectorAll(".item").forEach((row) => {
|
|
58
|
+
row.querySelector(".add").hidden = false;
|
|
59
|
+
row.querySelector(".remove").hidden = true;
|
|
60
|
+
});
|
|
61
|
+
sync();
|
|
62
|
+
});
|
|
63
|
+
</script>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { definePageBlock, Trait } from "waygraph";
|
|
2
|
+
import type { Home } from "../../states/demo.states.js";
|
|
3
|
+
import { DemoSel } from "./demo-sel.js";
|
|
4
|
+
import { AddItemBlock } from "./methods/add-item.effect.block.js";
|
|
5
|
+
import { RemoveItemBlock } from "./methods/remove-item.effect.block.js";
|
|
6
|
+
import { ClearCartBlock } from "./methods/clear-cart.method.block.js";
|
|
7
|
+
import { AssertHelloBlock } from "./methods/assert-hello.method.block.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Kind: Page
|
|
11
|
+
* Helper: definePageBlock
|
|
12
|
+
* Route: demo-web/
|
|
13
|
+
*
|
|
14
|
+
* Hub for the offline home screen. Methods hang here for docs/auto grouping;
|
|
15
|
+
* edges in flows still reference the method Blocks directly.
|
|
16
|
+
* Arrival-only (nav-home already landed on Home).
|
|
17
|
+
*/
|
|
18
|
+
export const HomePageBlock = definePageBlock<Home>({
|
|
19
|
+
name: "page-home",
|
|
20
|
+
description: "Offline home hub - add/remove effects, clear, assert-hello.",
|
|
21
|
+
checkpoint: "Home",
|
|
22
|
+
verify: [Trait.visible(DemoSel.catalog)],
|
|
23
|
+
methods: {
|
|
24
|
+
addItem: () => AddItemBlock,
|
|
25
|
+
removeItem: () => RemoveItemBlock,
|
|
26
|
+
clearCart: () => ClearCartBlock,
|
|
27
|
+
assertHello: () => AssertHelloBlock,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineEffectBlock,
|
|
3
|
+
checkpoint,
|
|
4
|
+
type Checkpoint,
|
|
5
|
+
type WaygraphInstanceOption,
|
|
6
|
+
} from "waygraph";
|
|
7
|
+
import type { ItemInCart } from "../../../states/demo.states.js";
|
|
8
|
+
import { SelectedItem } from "../../../states/demo.mem-keys.js";
|
|
9
|
+
import { DemoSel } from "../demo-sel.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Kind: Effect
|
|
13
|
+
* Helper: defineEffectBlock
|
|
14
|
+
* Route: demo-web/methods/
|
|
15
|
+
*
|
|
16
|
+
* Mutates one catalog row from mem. `waygraph auto` lists one "Add …" row per
|
|
17
|
+
* live Add button via instanceOptions.
|
|
18
|
+
*/
|
|
19
|
+
export const AddItemBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>({
|
|
20
|
+
name: "add-item",
|
|
21
|
+
description: "Adds the mem-selected catalog item to the cart.",
|
|
22
|
+
requires: [SelectedItem.key],
|
|
23
|
+
instruction: {
|
|
24
|
+
async act(page, _input, mem) {
|
|
25
|
+
const { id } = mem.get(SelectedItem.key);
|
|
26
|
+
await page.locator(DemoSel.addBtn(id)).click();
|
|
27
|
+
},
|
|
28
|
+
resolve: () => checkpoint("ItemInCart"),
|
|
29
|
+
verify: [
|
|
30
|
+
{
|
|
31
|
+
name: "item-added",
|
|
32
|
+
async check(page, mem) {
|
|
33
|
+
const { id } = mem.get(SelectedItem.key);
|
|
34
|
+
return page.locator(DemoSel.removeBtn(id)).isVisible();
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
stubBefore: {
|
|
39
|
+
add: { selector: DemoSel.catalog, label: "Catalog" },
|
|
40
|
+
},
|
|
41
|
+
stubAfter: {
|
|
42
|
+
cart: { selector: DemoSel.cartCount, label: "Cart count", duration: true },
|
|
43
|
+
},
|
|
44
|
+
stubOnError: {
|
|
45
|
+
cart: { selector: DemoSel.cartCount, label: "Cart at failure", tag: "FAIL" },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
|
|
49
|
+
const buttons = page.locator("button.add:not([hidden])");
|
|
50
|
+
const n = await buttons.count();
|
|
51
|
+
const out: WaygraphInstanceOption[] = [];
|
|
52
|
+
for (let i = 0; i < n; i++) {
|
|
53
|
+
const btn = buttons.nth(i);
|
|
54
|
+
const label = ((await btn.textContent()) || "").trim();
|
|
55
|
+
const testId = await btn.getAttribute("data-test");
|
|
56
|
+
const id = (testId || "").replace(/^add-/, "");
|
|
57
|
+
if (!id) continue;
|
|
58
|
+
out.push({
|
|
59
|
+
id,
|
|
60
|
+
label: label || `Add ${id}`,
|
|
61
|
+
key: SelectedItem.key,
|
|
62
|
+
value: { id, name: id },
|
|
63
|
+
highlight: DemoSel.addBtn(id),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineMethodBlock, checkpoint, Trait } from "waygraph";
|
|
2
|
+
import { DemoSel } from "../demo-sel.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kind: Method
|
|
6
|
+
* Helper: defineMethodBlock
|
|
7
|
+
* Route: demo-web/methods/
|
|
8
|
+
*
|
|
9
|
+
* Same-URL assert after nav - stubs + fixtures + YAP slide.
|
|
10
|
+
*/
|
|
11
|
+
export const AssertHelloBlock = defineMethodBlock({
|
|
12
|
+
name: "assert-hello",
|
|
13
|
+
description: "Confirms the hello heading is still visible after arrival.",
|
|
14
|
+
instruction: {
|
|
15
|
+
async act(_page) {},
|
|
16
|
+
async observe(page) {
|
|
17
|
+
await page.locator(DemoSel.title).first().waitFor({ state: "visible", timeout: 5_000 });
|
|
18
|
+
return "ok" as const;
|
|
19
|
+
},
|
|
20
|
+
resolve: () => checkpoint("HomeVerified"),
|
|
21
|
+
verify: [Trait.text(DemoSel.title, "Hello Waygraph"), Trait.visible(DemoSel.sub)],
|
|
22
|
+
stubBefore: {
|
|
23
|
+
title: { selector: DemoSel.title, label: "Heading about to check" },
|
|
24
|
+
},
|
|
25
|
+
stubAfter: {
|
|
26
|
+
title: {
|
|
27
|
+
selector: DemoSel.title,
|
|
28
|
+
label: "Hello confirmed",
|
|
29
|
+
detail: "Offline scaffold proof point.",
|
|
30
|
+
duration: true,
|
|
31
|
+
},
|
|
32
|
+
sub: { selector: DemoSel.sub, label: "Subcopy", duration: 1500 },
|
|
33
|
+
},
|
|
34
|
+
stubOnError: {
|
|
35
|
+
title: {
|
|
36
|
+
selector: DemoSel.title,
|
|
37
|
+
label: "Heading at failure",
|
|
38
|
+
tag: "FAIL",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
slides: [
|
|
42
|
+
{
|
|
43
|
+
caption: "Scaffold covers auto + narration",
|
|
44
|
+
detail: "Page hub, Effect instanceOptions, stubs, fixtures, YAP.",
|
|
45
|
+
tag: "YAP",
|
|
46
|
+
duration: true,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineMethodBlock, checkpoint, Trait } from "waygraph";
|
|
2
|
+
import { DemoSel } from "../demo-sel.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kind: Method
|
|
6
|
+
* Helper: defineMethodBlock
|
|
7
|
+
* Route: demo-web/methods/
|
|
8
|
+
*/
|
|
9
|
+
export const ClearCartBlock = defineMethodBlock({
|
|
10
|
+
name: "clear-cart",
|
|
11
|
+
description: "Clears every item via the Clear cart control.",
|
|
12
|
+
instruction: {
|
|
13
|
+
async act(page) {
|
|
14
|
+
await page.locator(DemoSel.clear).click();
|
|
15
|
+
},
|
|
16
|
+
resolve: () => checkpoint("CartEmpty"),
|
|
17
|
+
verify: [Trait.text(DemoSel.cartCount, "0")],
|
|
18
|
+
stubBefore: {
|
|
19
|
+
clear: { selector: DemoSel.clear, label: "Clear cart" },
|
|
20
|
+
},
|
|
21
|
+
stubAfter: {
|
|
22
|
+
cart: { selector: DemoSel.cartCount, label: "Empty cart", duration: true },
|
|
23
|
+
},
|
|
24
|
+
stubOnError: {},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineEffectBlock,
|
|
3
|
+
checkpoint,
|
|
4
|
+
type Checkpoint,
|
|
5
|
+
type WaygraphInstanceOption,
|
|
6
|
+
} from "waygraph";
|
|
7
|
+
import type { CartEmpty } from "../../../states/demo.states.js";
|
|
8
|
+
import { SelectedItem } from "../../../states/demo.mem-keys.js";
|
|
9
|
+
import { DemoSel } from "../demo-sel.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Kind: Effect
|
|
13
|
+
* Helper: defineEffectBlock
|
|
14
|
+
* Route: demo-web/methods/
|
|
15
|
+
*
|
|
16
|
+
* Removes one in-cart row. Auto menus one row per visible Remove button.
|
|
17
|
+
*/
|
|
18
|
+
export const RemoveItemBlock = defineEffectBlock<Checkpoint<string>, CartEmpty>({
|
|
19
|
+
name: "remove-item",
|
|
20
|
+
description: "Removes the mem-selected catalog item from the cart.",
|
|
21
|
+
requires: [SelectedItem.key],
|
|
22
|
+
instruction: {
|
|
23
|
+
async act(page, _input, mem) {
|
|
24
|
+
const { id } = mem.get(SelectedItem.key);
|
|
25
|
+
await page.locator(DemoSel.removeBtn(id)).click();
|
|
26
|
+
},
|
|
27
|
+
resolve: () => checkpoint("CartEmpty"),
|
|
28
|
+
verify: [
|
|
29
|
+
{
|
|
30
|
+
name: "item-removed",
|
|
31
|
+
async check(page, mem) {
|
|
32
|
+
const { id } = mem.get(SelectedItem.key);
|
|
33
|
+
return page.locator(DemoSel.addBtn(id)).isVisible();
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
stubBefore: {},
|
|
38
|
+
stubAfter: {
|
|
39
|
+
cart: { selector: DemoSel.cartCount, label: "Cart count", duration: true },
|
|
40
|
+
},
|
|
41
|
+
stubOnError: {},
|
|
42
|
+
},
|
|
43
|
+
async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
|
|
44
|
+
const buttons = page.locator("button.remove:not([hidden])");
|
|
45
|
+
const n = await buttons.count();
|
|
46
|
+
const out: WaygraphInstanceOption[] = [];
|
|
47
|
+
for (let i = 0; i < n; i++) {
|
|
48
|
+
const btn = buttons.nth(i);
|
|
49
|
+
const label = ((await btn.textContent()) || "").trim();
|
|
50
|
+
const testId = await btn.getAttribute("data-test");
|
|
51
|
+
const id = (testId || "").replace(/^remove-/, "");
|
|
52
|
+
if (!id) continue;
|
|
53
|
+
out.push({
|
|
54
|
+
id,
|
|
55
|
+
label: label || `Remove ${id}`,
|
|
56
|
+
key: SelectedItem.key,
|
|
57
|
+
value: { id, name: id },
|
|
58
|
+
highlight: DemoSel.removeBtn(id),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineNavBlock, Trait } from "waygraph";
|
|
2
|
+
import { DemoSel, HOME_URL } from "./demo-sel.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Kind: Nav
|
|
6
|
+
* Helper: defineNavBlock
|
|
7
|
+
* Route: demo-web/ (synthetic "/")
|
|
8
|
+
*/
|
|
9
|
+
export const NavHomeBlock = defineNavBlock({
|
|
10
|
+
name: "nav-home",
|
|
11
|
+
description: "Opens the offline catalog page (HTTP fixture :4177).",
|
|
12
|
+
checkpoint: "Home",
|
|
13
|
+
url: HOME_URL,
|
|
14
|
+
verify: [Trait.text(DemoSel.title, "Hello Waygraph"), Trait.visible(DemoSel.catalog)],
|
|
15
|
+
stubBefore: {},
|
|
16
|
+
stubAfter: {
|
|
17
|
+
title: { selector: DemoSel.title, label: "Home heading", duration: true },
|
|
18
|
+
},
|
|
19
|
+
stubOnError: {
|
|
20
|
+
title: { selector: DemoSel.title, label: "Home heading at failure" },
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import { Engine, start, end } from "waygraph";
|
|
2
|
-
import {
|
|
1
|
+
import { Engine, start, end, withTitle, withHighlightFixtures } from "waygraph";
|
|
2
|
+
import { NavHomeBlock } from "../blocks/demo-web/nav-home.block.js";
|
|
3
|
+
import { AssertHelloBlock } from "../blocks/demo-web/methods/assert-hello.method.block.js";
|
|
4
|
+
// Page hub registered for auto/docs grouping (arrival-only after nav).
|
|
5
|
+
import "../blocks/demo-web/home.page.block.js";
|
|
3
6
|
|
|
4
7
|
const engine = new Engine();
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
/** Smoke: nav + assert (no mem). Green offline. */
|
|
10
|
+
export const exampleFlow = withHighlightFixtures(
|
|
11
|
+
withTitle(
|
|
12
|
+
engine.defineFlow([start, NavHomeBlock, AssertHelloBlock, end]),
|
|
13
|
+
"Scaffold: Hello Waygraph",
|
|
14
|
+
),
|
|
15
|
+
{
|
|
16
|
+
"assert-hello": {
|
|
17
|
+
stubAfter: {
|
|
18
|
+
title: {
|
|
19
|
+
label: "AC · Hello visible",
|
|
20
|
+
detail: "Replace this fixture copy with your ticket lines.",
|
|
21
|
+
tag: "AC",
|
|
22
|
+
duration: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Engine,
|
|
3
|
+
start,
|
|
4
|
+
end,
|
|
5
|
+
withTitle,
|
|
6
|
+
withHighlightFixtures,
|
|
7
|
+
withSessionReset,
|
|
8
|
+
} from "waygraph";
|
|
9
|
+
import { NavHomeBlock } from "../blocks/demo-web/nav-home.block.js";
|
|
10
|
+
import { AddItemBlock } from "../blocks/demo-web/methods/add-item.effect.block.js";
|
|
11
|
+
import { ClearCartBlock } from "../blocks/demo-web/methods/clear-cart.method.block.js";
|
|
12
|
+
import "../blocks/demo-web/home.page.block.js";
|
|
13
|
+
|
|
14
|
+
const engine = new Engine();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Effect + Method demo (set mem for add-item, or use `waygraph auto` which
|
|
18
|
+
* fills SelectedItem via instanceOptions).
|
|
19
|
+
*
|
|
20
|
+
* waygraph demo --blocks shopFlow --data '{"demo.selectedItem":{"id":"alpha","name":"Alpha"}}'
|
|
21
|
+
* waygraph auto # after landing on home, pick Add Alpha / Add Beta / …
|
|
22
|
+
*/
|
|
23
|
+
export const shopFlow = withHighlightFixtures(
|
|
24
|
+
withTitle(
|
|
25
|
+
withSessionReset(
|
|
26
|
+
engine.defineFlow([start, NavHomeBlock, AddItemBlock, ClearCartBlock, end]),
|
|
27
|
+
),
|
|
28
|
+
"Scaffold: Shop (Effect + clear)",
|
|
29
|
+
),
|
|
30
|
+
{
|
|
31
|
+
"add-item": {
|
|
32
|
+
stubAfter: {
|
|
33
|
+
cart: {
|
|
34
|
+
label: "AC · item in cart",
|
|
35
|
+
detail: "Cart count bumped after Add.",
|
|
36
|
+
tag: "AC",
|
|
37
|
+
duration: true,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { keyGroup } from "waygraph";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which catalog row `add-item` / `remove-item` act on.
|
|
5
|
+
* `waygraph auto` fills this via Effect `instanceOptions` (one menu row per live button).
|
|
6
|
+
*/
|
|
7
|
+
export const SelectedItem = keyGroup<{ id: string; name: string }>("demo.selectedItem");
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { test, expect } from "@playwright/test";
|
|
2
2
|
import { MemPage, checkpoint } from "waygraph";
|
|
3
3
|
import { exampleFlow } from "../src/flows/example.flow.js";
|
|
4
|
+
import { shopFlow } from "../src/flows/shop.flow.js";
|
|
5
|
+
import { SelectedItem } from "../src/states/demo.mem-keys.js";
|
|
4
6
|
|
|
5
|
-
test("example flow reaches
|
|
7
|
+
test("example flow reaches HomeVerified", async ({ context }) => {
|
|
6
8
|
const result = await exampleFlow.run(context, new MemPage());
|
|
7
|
-
expect(result).toEqual(checkpoint("
|
|
9
|
+
expect(result).toEqual(checkpoint("HomeVerified"));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("shop flow adds then clears (Effect + Method)", async ({ context }) => {
|
|
13
|
+
const mem = new MemPage();
|
|
14
|
+
mem.set(SelectedItem.key, { id: "alpha", name: "Alpha" });
|
|
15
|
+
const result = await shopFlow.run(context, mem);
|
|
16
|
+
expect(result).toEqual(checkpoint("CartEmpty"));
|
|
8
17
|
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { defineBlock, checkpoint, Trait } from "waygraph";
|
|
2
|
-
import type { Checkpoint } from "waygraph";
|
|
3
|
-
|
|
4
|
-
export type Start = Checkpoint<"__start__">;
|
|
5
|
-
export type Loaded = Checkpoint<"Loaded">;
|
|
6
|
-
|
|
7
|
-
const PAGE_URL = "data:text/html,<h1>Hello Waygraph</h1>";
|
|
8
|
-
|
|
9
|
-
export const LoadPageBlock = defineBlock<Start, Loaded>({
|
|
10
|
-
name: "load-page",
|
|
11
|
-
instruction: {
|
|
12
|
-
async act(page) {
|
|
13
|
-
await page.goto(PAGE_URL);
|
|
14
|
-
},
|
|
15
|
-
resolve: () => checkpoint("Loaded"),
|
|
16
|
-
verify: [Trait.text("h1", "Hello Waygraph")],
|
|
17
|
-
},
|
|
18
|
-
});
|