waygraph 0.7.15 → 0.10.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.
Files changed (93) hide show
  1. package/README.md +90 -52
  2. package/dist/auto-explore-run.js +13 -7
  3. package/dist/auto-explore.js +47 -36
  4. package/dist/cli.d.ts +8 -12
  5. package/dist/cli.js +229 -249
  6. package/dist/engine.d.ts +77 -7
  7. package/dist/engine.js +108 -7
  8. package/dist/graph.d.ts +2 -1
  9. package/dist/graph.js +51 -11
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +1 -1
  12. package/examples/saucedemo/README.md +19 -5
  13. package/examples/saucedemo/docs/CONVENTION.md +22 -18
  14. package/examples/saucedemo/docs/HELPERS.md +186 -0
  15. package/examples/saucedemo/package-lock.json +1 -1
  16. package/examples/saucedemo/package.json +11 -5
  17. package/examples/saucedemo/scripts/capture-wrong-login.mjs +1 -1
  18. package/examples/saucedemo/scripts/drive-auto-cli.mjs +93 -0
  19. package/examples/saucedemo/scripts/prove-wrong-login-runtime.mjs +1 -1
  20. package/examples/saucedemo/scripts/show-auto-menu.mjs +72 -0
  21. package/examples/saucedemo/src/blocks/SITE-MAP.md +11 -8
  22. package/examples/saucedemo/src/blocks/saucedemo-web/NAV.md +3 -3
  23. package/examples/saucedemo/src/blocks/saucedemo-web/cart/NAV.md +1 -1
  24. package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-cart.block.ts +3 -3
  25. package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts +3 -3
  26. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/NAV.md +1 -1
  27. package/{templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/actions/submit-checkout-info.action.block.ts → examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts} +5 -5
  28. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/NAV.md +1 -1
  29. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/{actions/finish-order.action.block.ts → methods/finish-order.method.block.ts} +5 -5
  30. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/NAV.md +17 -5
  31. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +37 -0
  32. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts +55 -0
  33. package/{templates/quickstart/src/blocks/saucedemo-web/inventory/actions → examples/saucedemo/src/blocks/saucedemo-web/inventory/methods}/add-to-cart.effect.block.ts +6 -6
  34. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts +40 -20
  35. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts +55 -0
  36. package/{templates/quickstart/src/blocks/saucedemo-web/inventory/actions → examples/saucedemo/src/blocks/saucedemo-web/inventory/methods}/remove-from-cart.effect.block.ts +5 -5
  37. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +4 -4
  38. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.ts +3 -3
  39. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts +3 -3
  40. package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts +3 -3
  41. package/examples/saucedemo/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts +2 -2
  42. package/examples/saucedemo/src/blocks/saucedemo-web/{actions/submit-login.action.block.ts → methods/submit-login.method.block.ts} +5 -5
  43. package/{templates/quickstart/src/blocks/saucedemo-web/actions/submit-logout.action.block.ts → examples/saucedemo/src/blocks/saucedemo-web/methods/submit-logout.method.block.ts} +5 -5
  44. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +19 -0
  45. package/examples/saucedemo/src/flows/cart-mem.flow.ts +1 -1
  46. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
  47. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +4 -4
  48. package/examples/saucedemo/src/flows/checkout.flow.ts +4 -4
  49. package/examples/saucedemo/src/flows/login.flow.ts +1 -1
  50. package/examples/saucedemo/src/flows/shop.flow.ts +3 -3
  51. package/examples/saucedemo/src/flows/smoke.flow.ts +2 -2
  52. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
  53. package/examples/saucedemo/tests/flow-block-verify.spec.ts +1 -1
  54. package/examples/saucedemo/tests/submit-login-branch.spec.ts +1 -1
  55. package/package.json +1 -1
  56. package/templates/quickstart/README-saucedemo-commands.md +72 -0
  57. package/templates/quickstart/docs/CONVENTION.md +5 -5
  58. package/templates/quickstart/docs/HELPERS.md +186 -0
  59. package/templates/quickstart/package.json +9 -5
  60. package/templates/quickstart/scripts/drive-auto-cli.mjs +93 -0
  61. package/templates/quickstart/src/blocks/SITE-MAP.md +11 -8
  62. package/templates/quickstart/src/blocks/saucedemo-web/NAV.md +3 -3
  63. package/templates/quickstart/src/blocks/saucedemo-web/cart/NAV.md +1 -1
  64. package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +3 -3
  65. package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts +3 -3
  66. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/NAV.md +1 -1
  67. package/{examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/actions/submit-checkout-info.action.block.ts → templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts} +5 -5
  68. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/NAV.md +1 -1
  69. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/{actions/finish-order.action.block.ts → methods/finish-order.method.block.ts} +5 -5
  70. package/templates/quickstart/src/blocks/saucedemo-web/inventory/NAV.md +17 -5
  71. package/templates/quickstart/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +37 -0
  72. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts +55 -0
  73. package/{examples/saucedemo/src/blocks/saucedemo-web/inventory/actions → templates/quickstart/src/blocks/saucedemo-web/inventory/methods}/add-to-cart.effect.block.ts +6 -6
  74. package/templates/quickstart/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts +40 -20
  75. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts +55 -0
  76. package/{examples/saucedemo/src/blocks/saucedemo-web/inventory/actions → templates/quickstart/src/blocks/saucedemo-web/inventory/methods}/remove-from-cart.effect.block.ts +5 -5
  77. package/templates/quickstart/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +1 -1
  78. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.ts +3 -3
  79. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts +3 -3
  80. package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts +3 -3
  81. package/templates/quickstart/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts +2 -2
  82. package/templates/quickstart/src/blocks/saucedemo-web/{actions/submit-login.action.block.ts → methods/submit-login.method.block.ts} +5 -5
  83. package/{examples/saucedemo/src/blocks/saucedemo-web/actions/submit-logout.action.block.ts → templates/quickstart/src/blocks/saucedemo-web/methods/submit-logout.method.block.ts} +5 -5
  84. package/templates/quickstart/src/flows/cart-bulk.flow.ts +19 -0
  85. package/templates/quickstart/src/flows/cart-mem.flow.ts +1 -1
  86. package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
  87. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +4 -4
  88. package/templates/quickstart/src/flows/checkout.flow.ts +4 -4
  89. package/templates/quickstart/src/flows/login.flow.ts +1 -1
  90. package/templates/quickstart/src/flows/shop.flow.ts +3 -3
  91. package/templates/quickstart/src/flows/smoke.flow.ts +2 -2
  92. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
  93. package/templates/scaffold/package.json +1 -1
@@ -0,0 +1,186 @@
1
+ # Block helpers (convention)
2
+
3
+ Runtime is always a `Block`. Helpers are TypeScript salt + clear intent.
4
+
5
+ | Helper | Use when | File |
6
+ |--------|----------|------|
7
+ | `definePageBlock` | Screen hub (checkpoint + methods + optional Sel) | `*.page.block.ts` |
8
+ | `defineNavBlock` | URL / `goto` only | `nav-*.block.ts` |
9
+ | `defineNavClickBlock` | Click-to-navigate only | `nav-*.block.ts` |
10
+ | `defineMemNavBlock` | Nav + per-row `instanceOptions` | `nav-*.block.ts` |
11
+ | `defineMethodBlock` | One-shot non-nav app step | `*.method.block.ts` under `methods/` |
12
+ | `defineEffectBlock` | Instance mutate + auto menu rows | `*.effect.block.ts` under `methods/` |
13
+ | `defineBlock` | Escape hatch: tests, unsure, probes | any |
14
+ | `defineActionBlock` | **Deprecated** alias of `defineMethodBlock` | — |
15
+
16
+ `defineNavBlock({ click })` still works (compat) - prefer `defineNavClickBlock` in app code.
17
+
18
+ ---
19
+
20
+ ## Page hub + methods + Sel
21
+
22
+ One **PageBlock** = this screen. Methods hang off it. DOM strings live in `*Sel` (not in mem).
23
+
24
+ ```ts
25
+ export const InventorySel = {
26
+ list: ".inventory_list",
27
+ addBtn: (id: string) => `[data-test="add-to-cart-${id}"]`,
28
+ // …
29
+ };
30
+
31
+ export const InventoryPageBlock = definePageBlock({
32
+ name: "page-inventory",
33
+ checkpoint: "LoggedIn",
34
+ verify: [Trait.url({ pathname: "/inventory.html" }), Trait.visible(InventorySel.list)],
35
+ methods: {
36
+ addToCart: () => AddToCartBlock,
37
+ removeFromCart: () => RemoveFromCartBlock,
38
+ addAllToCart: () => AddAllToCartBlock,
39
+ removeAllFromCart: () => RemoveAllFromCartBlock,
40
+ openDetail: () => NavItemDetailBlock,
41
+ },
42
+ });
43
+ ```
44
+
45
+ | Piece | Stores |
46
+ |-------|--------|
47
+ | `*Sel` | DOM selectors only (static + `(id) => …`) |
48
+ | Mem keys | Typed values (which item / queue) — not selectors |
49
+ | `instanceOptions` | Live DOM → menu rows → `mem.set` on pick |
50
+ | Predefined method | No `instanceOptions`; flow seeds mem first |
51
+
52
+ Live: `inventory/inventory.page.block.ts`, `inventory/methods/`.
53
+
54
+ **Round-trip (not forward-only):** cart Methods/Effects use `In = Checkpoint<string>`
55
+ so the graph edge is `from: "*"`. Auto lists them whenever the live page has matching
56
+ buttons (Add vs Remove), including after continue-shopping / back to inventory. Bulk
57
+ `add-all-to-cart` / `remove-all-from-cart` are one menu row each (no `instanceOptions`);
58
+ Out is `ItemInCart` vs `LoggedIn` from cart emptiness.
59
+
60
+ ---
61
+
62
+ ## Checkpoint vs `branch()`
63
+
64
+ | | **Checkpoint** | **`branch()`** |
65
+ |--|----------------|----------------|
66
+ | Answers | *Where* are you? (`LoggedIn`, `LoginPage`) | *Which Block runs next?* |
67
+ | How | `observe` + `resolve` -> union `Out` | `branch(block, { Tag: nextBlock \| null })` |
68
+ | Alone can | Label state; graph edges; verify | Route a `runGraph` walk |
69
+ | Alone cannot | Pick the next Block | Decide the state |
70
+
71
+ Checkpoint = place on the map. `branch()` = which road to take from that place.
72
+
73
+ Conditional Out (no `branch()` needed for auto/graph):
74
+
75
+ ```ts
76
+ defineMethodBlock<LoginPage, LoggedIn | LoginPage>({
77
+ name: "submit-login",
78
+ instruction: {
79
+ async act(page, _in, mem) { /* fill + click */ },
80
+ async observe(page) {
81
+ try {
82
+ await page.waitForURL(/\/inventory\.html/, { timeout: 8000 });
83
+ return "success";
84
+ } catch {
85
+ return "failure";
86
+ }
87
+ },
88
+ resolve: (o) => checkpoint(o === "success" ? "LoggedIn" : "LoginPage"),
89
+ },
90
+ });
91
+ ```
92
+
93
+ Optional routing for `runGraph`:
94
+
95
+ ```ts
96
+ branch(SubmitLoginActionBlock, {
97
+ LoggedIn: AddToCartBlock,
98
+ LoginPage: null, // stop
99
+ });
100
+ ```
101
+
102
+ Linear `defineFlow([start, …, end])` usually skips `branch()` (one happy path).
103
+
104
+ ---
105
+
106
+ ## Mem / `instanceOptions` (spawns menu rows)
107
+
108
+ Not a separate runtime type. One Block + `requires` + `instanceOptions(page)` ->
109
+ `waygraph auto` lists **one row per live DOM match**. On pick: `mem.set(key, value)`
110
+ then run the Block (act reads mem as usual).
111
+
112
+ ### Effect - spawn Method-like rows (Add / Remove)
113
+
114
+ ```ts
115
+ defineEffectBlock<Checkpoint<string>, ItemInCart>({
116
+ name: "add-to-cart",
117
+ requires: [SelectedItem.key],
118
+ instruction: {
119
+ async act(page, _in, mem) {
120
+ const { id } = mem.get(SelectedItem.key);
121
+ await page.locator(InventorySel.addBtn(id)).click();
122
+ },
123
+ resolve: () => checkpoint("ItemInCart"),
124
+ },
125
+ async instanceOptions(page) {
126
+ const items = await collectAddableItems(page);
127
+ return items.map((item) => ({
128
+ id: item.id,
129
+ label: `Add "${item.name}" to cart`,
130
+ key: SelectedItem.key,
131
+ value: item,
132
+ highlight: InventorySel.addBtn(item.id),
133
+ }));
134
+ },
135
+ });
136
+ ```
137
+
138
+ Auto shows: `Add "Sauce Labs Backpack" to cart`, `Add "Bike Light"…`, …
139
+
140
+ ### MemNav - spawn Nav rows (Open details)
141
+
142
+ ```ts
143
+ defineMemNavBlock<ItemDetailPage>({
144
+ name: "nav-item-detail",
145
+ checkpoint: "ItemDetailPage",
146
+ requires: [SelectedItem.key],
147
+ click: (mem) => InventorySel.title(mem.get(SelectedItem.key).id),
148
+ verify: [Trait.url({ pathname: "/inventory-item.html" })],
149
+ async instanceOptions(page) {
150
+ const items = await collectInventoryItems(page);
151
+ return items.map((item) => ({
152
+ id: item.id,
153
+ label: `Open "${item.name}" details`,
154
+ key: SelectedItem.key,
155
+ value: item,
156
+ highlight: InventorySel.title(item.id),
157
+ }));
158
+ },
159
+ });
160
+ ```
161
+
162
+ Auto shows: `Open "Backpack" details`, … - still a NavBlock (click nav), salt only.
163
+
164
+ | | Effect | MemNav |
165
+ |--|--------|--------|
166
+ | Helper | `defineEffectBlock` | `defineMemNavBlock` |
167
+ | Spawns | mutate rows | navigate rows |
168
+ | `act` / nav | click Add/Remove | `click: (mem) => …` title link |
169
+ | Mem | same pattern: `requires` + row `key`/`value` | same |
170
+
171
+ Live source: `inventory/methods/add-to-cart.effect.block.ts`,
172
+ `inventory/nav-item-detail.block.ts`.
173
+
174
+ ---
175
+
176
+ ## Cart bulk: demo / run / auto
177
+
178
+ | | Command |
179
+ |--|---------|
180
+ | **Run (Block names)** | `npm run run:bulk` — `run --blocks "loginFlow then add-all…" --data '{…}'` |
181
+ | **Demo (named flow)** | `npm run demo:bulk` — `--blocks cartBulkFlow --data '{…}'` |
182
+ | **Demo QA video** | `npm run demo:bulk:video` — `--auto-play-video` |
183
+ | **Auto headed / CLI** | `npm run auto` / `auto:cli` |
184
+ | **Auto path-find** | `waygraph auto --blocks LoginPage OrderComplete` |
185
+
186
+
@@ -17,7 +17,7 @@
17
17
  }
18
18
  },
19
19
  "../..": {
20
- "version": "0.7.11",
20
+ "version": "0.10.0",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "tsx": "^4.0.0"
@@ -8,18 +8,24 @@
8
8
  "typecheck": "tsc --noEmit",
9
9
  "test": "playwright test",
10
10
  "flows": "waygraph list",
11
- "demo": "waygraph demo shopFlow",
12
- "demo:step": "waygraph demo shopFlow --step --no-autoplay --title \"Shop demo\"",
13
- "demo:autoplay": "waygraph demo shopFlow --autoplay --title \"Shop demo\"",
11
+ "demo": "waygraph demo --blocks shopFlow",
12
+ "demo:step": "waygraph demo --blocks shopFlow --title \"Shop demo\"",
13
+ "demo:auto-next": "waygraph demo --blocks shopFlow --auto-next --title \"Shop demo\"",
14
14
  "auto": "waygraph auto",
15
- "auto:cli": "waygraph auto --cli",
15
+ "auto:cli": "waygraph auto --cli --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
16
+ "prove:auto-cli": "node scripts/drive-auto-cli.mjs",
16
17
  "graph": "waygraph graph",
17
18
  "graph:mermaid": "waygraph graph --mermaid",
18
19
  "prove:login-branch": "node scripts/prove-wrong-login-runtime.mjs",
19
20
  "prove:graph": "node scripts/prove-auto-graph.mjs",
20
21
  "prove:add-to-cart-menu": "node scripts/prove-add-to-cart-menu.mjs",
21
22
  "prove": "npm run prove:login-branch && npm run prove:graph && npm run prove:add-to-cart-menu && npm test",
22
- "record": "waygraph chain \"loginFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}) then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}})\" --video --no-step"
23
+ "record": "waygraph run --blocks \"loginFlow then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}})\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --video --no-step",
24
+ "demo:bulk": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
25
+ "demo:bulk:auto-next": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-next --title \"Cart bulk demo\"",
26
+ "demo:bulk:video": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-play-video --title \"Cart bulk demo\"",
27
+ "run:bulk": "waygraph run --blocks \"loginFlow then add-all-to-cart then remove-all-from-cart\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
28
+ "chain:bulk": "waygraph run --blocks \"loginFlow then add-all-to-cart then remove-all-from-cart\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --non-headless"
23
29
  },
24
30
  "waygraph": {
25
31
  "baseUrl": "https://www.saucedemo.com"
@@ -88,7 +88,7 @@ const nodeNames = graph.nodes.map((n) => n.checkpoint);
88
88
 
89
89
  const { NavLoginBlock } = await import(pathToFileURL(join(root, "src/blocks/saucedemo-web/nav-login.block.js")).href);
90
90
  const { SubmitLoginActionBlock } = await import(
91
- pathToFileURL(join(root, "src/blocks/saucedemo-web/actions/submit-login.action.block.js")).href
91
+ pathToFileURL(join(root, "src/blocks/saucedemo-web/methods/submit-login.method.block.js")).href
92
92
  );
93
93
  const { LoginCreds } = await import(pathToFileURL(join(root, "src/states/checkout.mem-keys.js")).href);
94
94
 
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Drive `waygraph auto --cli` through login -> add-all -> remove-all -> quit.
4
+ * Waits for each Choose: prompt so picks are not sent early.
5
+ *
6
+ * Usage (from examples/saucedemo):
7
+ * node scripts/drive-auto-cli.mjs
8
+ */
9
+ import { spawn } from "node:child_process";
10
+ import { createInterface } from "node:readline";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, join } from "node:path";
13
+
14
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
15
+ const waygraphBin = join(root, "../../bin/waygraph");
16
+ const DATA =
17
+ process.env.WAYGRAPH_DATA ??
18
+ '{"saucedemo.credentials":{"username":"standard_user","password":"secret_sauce"}}';
19
+
20
+ function pickByLabel(menuText, label) {
21
+ const re = new RegExp(`\\[(\\d+)\\]\\s+${label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
22
+ const m = menuText.match(re);
23
+ if (!m) throw new Error(`menu missing "${label}"\n${menuText}`);
24
+ return m[1];
25
+ }
26
+
27
+ async function main() {
28
+ const child = spawn(waygraphBin, ["auto", "--cli", "--data", DATA, "."], {
29
+ cwd: root,
30
+ stdio: ["pipe", "pipe", "pipe"],
31
+ env: process.env,
32
+ });
33
+
34
+ let buf = "";
35
+ const dump = (chunk, stream) => {
36
+ const s = chunk.toString();
37
+ buf += s;
38
+ process[stream].write(s);
39
+ };
40
+ child.stdout.on("data", (c) => dump(c, "stdout"));
41
+ child.stderr.on("data", (c) => dump(c, "stderr"));
42
+
43
+ const waitChoose = () =>
44
+ new Promise((resolve, reject) => {
45
+ const start = Date.now();
46
+ const tick = () => {
47
+ if (buf.includes("Choose:")) {
48
+ const i = buf.lastIndexOf("Choose:");
49
+ const slice = buf.slice(Math.max(0, i - 4000), i + 8);
50
+ resolve(slice);
51
+ return;
52
+ }
53
+ if (Date.now() - start > 120_000) {
54
+ reject(new Error(`timeout waiting for Choose:\n${buf.slice(-2000)}`));
55
+ return;
56
+ }
57
+ setTimeout(tick, 100);
58
+ };
59
+ tick();
60
+ });
61
+
62
+ const send = (line) => {
63
+ console.error(`\n>> pick ${line.trim()}`);
64
+ buf = buf.replace(/Choose:/g, "Choose(done):");
65
+ child.stdin.write(line);
66
+ };
67
+
68
+ try {
69
+ let menu = await waitChoose();
70
+ send("1\n"); // submit-login
71
+ menu = await waitChoose();
72
+ if (!/LoggedIn|Add all to cart/.test(menu)) {
73
+ throw new Error(`expected inventory after login, got:\n${menu}`);
74
+ }
75
+ send(`${pickByLabel(menu, "Add all to cart")}\n`);
76
+ menu = await waitChoose();
77
+ send(`${pickByLabel(menu, "Remove all from cart")}\n`);
78
+ menu = await waitChoose();
79
+ if (!/Add all to cart/.test(menu)) {
80
+ throw new Error(`expected Add all again after remove-all, got:\n${menu}`);
81
+ }
82
+ send("q\n");
83
+ const code = await new Promise((res) => child.on("exit", res));
84
+ if (code !== 0 && code !== null) process.exitCode = code;
85
+ else console.error("\ndrive-auto-cli: PASS login -> add-all -> remove-all -> quit");
86
+ } catch (err) {
87
+ console.error(err instanceof Error ? err.message : String(err));
88
+ child.kill("SIGTERM");
89
+ process.exitCode = 1;
90
+ }
91
+ }
92
+
93
+ main();
@@ -15,7 +15,7 @@ const waygraphDist = join(root, "../../dist");
15
15
  const { Engine, start, end, MemPage } = await import(pathToFileURL(join(waygraphDist, "index.js")).href);
16
16
  const { NavLoginBlock } = await import(pathToFileURL(join(root, "src/blocks/saucedemo-web/nav-login.block.js")).href);
17
17
  const { SubmitLoginActionBlock } = await import(
18
- pathToFileURL(join(root, "src/blocks/saucedemo-web/actions/submit-login.action.block.js")).href
18
+ pathToFileURL(join(root, "src/blocks/saucedemo-web/methods/submit-login.method.block.js")).href
19
19
  );
20
20
  const { LoginCreds } = await import(pathToFileURL(join(root, "src/states/checkout.mem-keys.js")).href);
21
21
 
@@ -0,0 +1,72 @@
1
+ // Dump waygraph auto menu sections on inventory (incl. bulk add/remove-all).
2
+ import { chromium } from "@playwright/test";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { createRequire } from "node:module";
6
+
7
+ const require = createRequire(import.meta.url);
8
+ await import(require.resolve("tsx/esm"));
9
+
10
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
11
+ const waygraphDist = join(root, "../../dist");
12
+
13
+ const { buildExploreContext, buildExploreMenu } = await import(
14
+ pathToFileURL(join(waygraphDist, "auto-explore.js")).href
15
+ );
16
+ const { MemPage, Engine, start, end } = await import(
17
+ pathToFileURL(join(waygraphDist, "index.js")).href
18
+ );
19
+
20
+ const exe = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
21
+
22
+ async function runBlock(engine, entry, context, page, mem) {
23
+ const flow = engine.defineFlow([start, entry.block, end]);
24
+ await flow.run(context, mem, { page, closeOnFinish: false });
25
+ }
26
+
27
+ function dump(title, menu) {
28
+ console.log(`\n=== ${title} (here=${menu.here}) ===`);
29
+ for (const sec of menu.sections) {
30
+ console.log(`\n[${sec.title}]`);
31
+ for (const e of sec.edges) {
32
+ const label = e.label ?? `${e.block} -> ${e.to}`;
33
+ console.log(` • ${label}`);
34
+ }
35
+ }
36
+ }
37
+
38
+ const { graph, library } = await buildExploreContext(root);
39
+ const mem = new MemPage();
40
+ mem.set(library.byName.get("submit-login").block.requires[0], {
41
+ username: "standard_user",
42
+ password: "secret_sauce",
43
+ });
44
+
45
+ const browser = await chromium.launch({
46
+ headless: true,
47
+ ...(exe ? { executablePath: exe, args: ["--no-sandbox"] } : {}),
48
+ });
49
+ const context = await browser.newContext({
50
+ baseURL: "https://www.saucedemo.com",
51
+ viewport: { width: 1280, height: 900 },
52
+ });
53
+ const page = await context.newPage();
54
+ const engine = new Engine({ headless: true });
55
+
56
+ await page.goto("/", { waitUntil: "load" });
57
+ await runBlock(engine, library.byName.get("submit-login"), context, page, mem);
58
+ await page.waitForURL(/inventory\.html/);
59
+
60
+ const menu1 = await buildExploreMenu(page, graph, library, "LoggedIn");
61
+ dump("AFTER LOGIN — cart empty", menu1);
62
+
63
+ const bike = menu1.flat.find(
64
+ (e) => e.block === "add-to-cart" && e.instanceOption?.value?.id === "sauce-labs-bike-light",
65
+ );
66
+ mem.set(bike.instanceOption.key, bike.instanceOption.value);
67
+ await runBlock(engine, library.byName.get("add-to-cart"), context, page, mem);
68
+
69
+ const menu2 = await buildExploreMenu(page, graph, library, "LoggedIn");
70
+ dump("AFTER ADD BIKE LIGHT — mixed", menu2);
71
+
72
+ await browser.close();
@@ -9,16 +9,19 @@ src/blocks/
9
9
  saucedemo-web/ # URL /
10
10
  NAV.md
11
11
  nav-login.block.ts # Nav defineNavBlock url:/
12
- actions/
13
- submit-login.action.block.ts # Action defineBlock
12
+ methods/
13
+ submit-login.method.block.ts # Method defineMethodBlock
14
14
  submit-login-for-flow.ts # helper (not a discoverable *.block.ts alone)
15
- submit-logout.action.block.ts # Action
15
+ submit-logout.method.block.ts # Method defineMethodBlock
16
16
  inventory/ # URL /inventory.html
17
+ inventory.page.block.ts # Page definePageBlock
17
18
  NAV.md
18
19
  nav-item-detail.block.ts # Nav defineMemNavBlock (instance row)
19
- actions/
20
+ methods/
20
21
  inventory-items.ts # DOM helpers for Effects (not a Block)
21
22
  add-to-cart.effect.block.ts # Effect defineEffectBlock
23
+ add-all-to-cart.method.block.ts
24
+ remove-all-from-cart.method.block.ts
22
25
  remove-from-cart.effect.block.ts
23
26
  inventory-item/ # URL /inventory-item.html
24
27
  NAV.md
@@ -31,12 +34,12 @@ src/blocks/
31
34
  nav-checkout-info.block.ts
32
35
  checkout-step-one/ # URL /checkout-step-one.html
33
36
  NAV.md
34
- actions/
35
- submit-checkout-info.action.block.ts
37
+ methods/
38
+ submit-checkout-info.method.block.ts
36
39
  checkout-step-two/ # URL /checkout-step-two.html
37
40
  NAV.md
38
- actions/
39
- finish-order.action.block.ts
41
+ methods/
42
+ finish-order.method.block.ts
40
43
  ```
41
44
 
42
45
  Checkpoints and mem keys live under `src/states/`. Flows under `src/flows/`
@@ -16,9 +16,9 @@ Login page for Swag Labs. This folder is the namespace root (`/` in the browser)
16
16
 
17
17
  | File | Kind | Notes |
18
18
  |------|------|-------|
19
- | `actions/submit-login.action.block.ts` | Action | Form submit; branches LoggedIn vs LoginPage |
20
- | `actions/submit-login-for-flow.ts` | helper | Typed alias for linear Flows |
21
- | `actions/submit-logout.action.block.ts` | Action | Burger menu logout -> LoginPage |
19
+ | `methods/submit-login.method.block.ts` | Method | Form submit; branches LoggedIn vs LoginPage |
20
+ | `methods/submit-login-for-flow.ts` | helper | Typed alias for linear Flows |
21
+ | `methods/submit-logout.method.block.ts` | Method | Burger menu logout -> LoginPage |
22
22
 
23
23
  ## Related mem keys / checkpoints
24
24
 
@@ -15,7 +15,7 @@ Shopping cart page and checkout entry.
15
15
 
16
16
  ## Effects / actions
17
17
 
18
- (none on this route; cart mutations live under `inventory/actions/`)
18
+ (none on this route; cart mutations live under `inventory/methods/`)
19
19
 
20
20
  ## Related mem keys / checkpoints
21
21
 
@@ -1,14 +1,14 @@
1
- import { defineNavBlock, Trait } from "waygraph";
1
+ import { defineNavClickBlock, Trait } from "waygraph";
2
2
  import type { CartPage } from "../../../states/checkout.states.js";
3
3
 
4
4
  /**
5
5
  * Kind: Nav
6
- * Helper: defineNavBlock
6
+ * Helper: defineNavClickBlock
7
7
  * Route: saucedemo-web/cart/ (= /cart.html)
8
8
  *
9
9
  * click, not url - header cart link from inventory-adjacent pages.
10
10
  */
11
- export const NavCartBlock = defineNavBlock<CartPage>({
11
+ export const NavCartBlock = defineNavClickBlock<CartPage>({
12
12
  name: "nav-cart",
13
13
  description: "Clicks the cart link in the header.",
14
14
  checkpoint: "CartPage",
@@ -1,14 +1,14 @@
1
- import { defineNavBlock, Trait } from "waygraph";
1
+ import { defineNavClickBlock, Trait } from "waygraph";
2
2
  import type { CheckoutInfoPage } from "../../../states/checkout.states.js";
3
3
 
4
4
  /**
5
5
  * Kind: Nav
6
- * Helper: defineNavBlock
6
+ * Helper: defineNavClickBlock
7
7
  * Route: saucedemo-web/cart/ (click leaves /cart.html -> /checkout-step-one.html)
8
8
  *
9
9
  * Cart page "Checkout" button - owns the nav into step one.
10
10
  */
11
- export const NavCheckoutInfoBlock = defineNavBlock<CheckoutInfoPage>({
11
+ export const NavCheckoutInfoBlock = defineNavClickBlock<CheckoutInfoPage>({
12
12
  name: "nav-checkout-info",
13
13
  description: "Clicks 'Checkout' on the cart page.",
14
14
  checkpoint: "CheckoutInfoPage",
@@ -14,7 +14,7 @@ Shipping info form before order overview.
14
14
 
15
15
  | File | Kind | Notes |
16
16
  |------|------|-------|
17
- | `actions/submit-checkout-info.action.block.ts` | Action | Fill form + Continue -> overview |
17
+ | `methods/submit-checkout-info.method.block.ts` | Method | Fill form + Continue -> overview |
18
18
 
19
19
  ## Related mem keys / checkpoints
20
20
 
@@ -1,15 +1,15 @@
1
- import { defineBlock, checkpoint, Trait } from "waygraph";
1
+ import { defineMethodBlock, checkpoint, Trait } from "waygraph";
2
2
  import type { CheckoutInfoPage, CheckoutOverviewPage } from "../../../../states/checkout.states.js";
3
3
 
4
4
  /**
5
- * Kind: Action
6
- * Helper: defineBlock
7
- * Route: saucedemo-web/checkout-step-one/actions/ (= /checkout-step-one.html)
5
+ * Kind: Method
6
+ * Helper: defineMethodBlock
7
+ * Route: saucedemo-web/checkout-step-one/methods/ (= /checkout-step-one.html)
8
8
  *
9
9
  * Fills shipping form and continues - never navigates itself
10
10
  * (nav-checkout-info already got here).
11
11
  */
12
- export const SubmitCheckoutInfoBlock = defineBlock<CheckoutInfoPage, CheckoutOverviewPage>({
12
+ export const SubmitCheckoutInfoBlock = defineMethodBlock<CheckoutInfoPage, CheckoutOverviewPage>({
13
13
  name: "submit-checkout-info",
14
14
  description: "Fills in the shipping info form and continues to the order overview.",
15
15
  instruction: {
@@ -14,7 +14,7 @@ Order overview / finish page.
14
14
 
15
15
  | File | Kind | Notes |
16
16
  |------|------|-------|
17
- | `actions/finish-order.action.block.ts` | Action | Finish -> thank-you / OrderComplete |
17
+ | `methods/finish-order.method.block.ts` | Method | Finish -> thank-you / OrderComplete |
18
18
 
19
19
  ## Related mem keys / checkpoints
20
20
 
@@ -1,14 +1,14 @@
1
- import { defineBlock, checkpoint, Trait, narrate } from "waygraph";
1
+ import { defineMethodBlock, checkpoint, Trait, narrate } from "waygraph";
2
2
  import type { CheckoutOverviewPage, OrderComplete } from "../../../../states/checkout.states.js";
3
3
 
4
4
  /**
5
- * Kind: Action
6
- * Helper: defineBlock
7
- * Route: saucedemo-web/checkout-step-two/actions/ (= /checkout-step-two.html)
5
+ * Kind: Method
6
+ * Helper: defineMethodBlock
7
+ * Route: saucedemo-web/checkout-step-two/methods/ (= /checkout-step-two.html)
8
8
  *
9
9
  * Places the order on the overview page - form/finish click, not a NavBlock.
10
10
  */
11
- export const FinishOrderBlock = defineBlock<CheckoutOverviewPage, OrderComplete>({
11
+ export const FinishOrderBlock = defineMethodBlock<CheckoutOverviewPage, OrderComplete>({
12
12
  name: "finish-order",
13
13
  description: "Places the order and confirms the 'Thank you for your order!' completion page.",
14
14
  instruction: {
@@ -6,21 +6,33 @@ Product catalog after a successful login.
6
6
 
7
7
  - `/inventory.html`
8
8
 
9
+ ## Page hub
10
+
11
+ | File | Helper | Notes |
12
+ |------|--------|-------|
13
+ | `inventory.page.block.ts` | `definePageBlock` | Hub `LoggedIn`; registers add/remove/open methods; `InventorySel` for DOM |
14
+
9
15
  ## Nav blocks
10
16
 
11
17
  | File | Helper | Notes |
12
18
  |------|--------|-------|
13
19
  | `nav-item-detail.block.ts` | `defineMemNavBlock` | Opens mem-picked product detail; instance rows per card |
14
20
 
15
- ## Effects / actions
21
+ ## Methods
16
22
 
17
23
  | File | Kind | Notes |
18
24
  |------|------|-------|
19
- | `actions/add-to-cart.effect.block.ts` | Effect | Add mem-picked item; instance menu |
20
- | `actions/remove-from-cart.effect.block.ts` | Effect | Remove mem-picked item; may empty cart |
21
- | `actions/inventory-items.ts` | helper | DOM collectors (not a Block) |
25
+ | `methods/add-to-cart.effect.block.ts` | Effect | Add mem-picked item; instance menu |
26
+ | `methods/remove-from-cart.effect.block.ts` | Effect | Remove mem-picked item; may empty cart |
27
+ | `methods/add-all-to-cart.method.block.ts` | Method | Bulk add every Add button; one menu row |
28
+ | `methods/remove-all-from-cart.method.block.ts` | Method | Bulk remove every Remove button; one menu row |
29
+ | `methods/inventory-items.ts` | helper | `InventorySel` + DOM collectors (not a Block) |
22
30
 
23
31
  ## Related mem keys / checkpoints
24
32
 
25
- - Mem: `saucedemo.selectedItem` (`SelectedItem`)
33
+ - Mem: `saucedemo.selectedItem` (`SelectedItem`) - per-item Effects only
26
34
  - Checkpoints: `LoggedIn`, `ItemInCart`, `ItemDetailPage` (via MemNav)
35
+ - Sel: `InventorySel` (DOM only — not mem)
36
+ - Round-trip: cart Methods/Effects use `In = Checkpoint<string>` (`from: "*"`).
37
+ Auto menu follows live Add/Remove buttons after leave/return; URL sniff on
38
+ `/inventory.html` still reports `LoggedIn` (same URL; cart is status).
@@ -0,0 +1,37 @@
1
+ import { definePageBlock, Trait } from "waygraph";
2
+ import type { LoggedIn } from "../../../states/checkout.states.js";
3
+ import { AddToCartBlock } from "./methods/add-to-cart.effect.block.js";
4
+ import { RemoveFromCartBlock } from "./methods/remove-from-cart.effect.block.js";
5
+ import { AddAllToCartBlock } from "./methods/add-all-to-cart.method.block.js";
6
+ import { RemoveAllFromCartBlock } from "./methods/remove-all-from-cart.method.block.js";
7
+ import { NavItemDetailBlock } from "./nav-item-detail.block.js";
8
+ import { InventorySel } from "./methods/inventory-items.js";
9
+
10
+ /**
11
+ * Kind: Page
12
+ * Helper: definePageBlock
13
+ * Route: saucedemo-web/inventory/ (= /inventory.html)
14
+ *
15
+ * Hub for the inventory screen after login. Methods (per-item + bulk
16
+ * add/remove, open detail) hang off this page; DOM strings live in
17
+ * InventorySel. Arrival-only (no url/click) - submit-login already resolved
18
+ * to LoggedIn. Cart Effects/Methods use In Checkpoint<string> (from: "*") so
19
+ * auto still offers them after leave/return - menu follows live DOM buttons.
20
+ */
21
+ export const InventoryPageBlock = definePageBlock<LoggedIn>({
22
+ name: "page-inventory",
23
+ description:
24
+ "Inventory catalog hub - per-item and bulk add/remove, open item detail.",
25
+ checkpoint: "LoggedIn",
26
+ verify: [
27
+ Trait.url({ pathname: "/inventory.html" }),
28
+ Trait.visible(InventorySel.list),
29
+ ],
30
+ methods: {
31
+ addToCart: () => AddToCartBlock,
32
+ removeFromCart: () => RemoveFromCartBlock,
33
+ addAllToCart: () => AddAllToCartBlock,
34
+ removeAllFromCart: () => RemoveAllFromCartBlock,
35
+ openDetail: () => NavItemDetailBlock,
36
+ },
37
+ });