waygraph 0.15.52 → 0.15.54

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 (156) hide show
  1. package/README.md +8 -1
  2. package/dist/analysis/graph.d.ts +3 -0
  3. package/dist/analysis/map-check.d.ts +3 -2
  4. package/dist/auto-session/helpers.d.ts +10 -0
  5. package/dist/blocks-select.d.ts +3 -0
  6. package/dist/blocks-select.js +2 -0
  7. package/dist/cli/flags.js +3 -1
  8. package/dist/cli/usage.js +4 -1
  9. package/dist/cli.js +4 -1
  10. package/dist/commands/agent-dive-scaffold.d.ts +0 -20
  11. package/dist/commands/agent-dive-scaffold.js +3 -3
  12. package/dist/commands/init.js +1 -1
  13. package/dist/commands/upgrade.d.ts +1 -1
  14. package/dist/commands/upgrade.js +7 -1
  15. package/dist/config.d.ts +55 -19
  16. package/dist/config.js +55 -13
  17. package/dist/core/block.d.ts +7 -2
  18. package/dist/core/block.js +7 -3
  19. package/dist/core/blocks/api.d.ts +1 -0
  20. package/dist/core/blocks/method.d.ts +2 -0
  21. package/dist/core/blocks/method.js +1 -0
  22. package/dist/core/blocks/nav.d.ts +1 -0
  23. package/dist/core/blocks/page.d.ts +1 -0
  24. package/dist/core/branch-regression.d.ts +1 -0
  25. package/dist/core/compose.d.ts +1 -0
  26. package/dist/core/engine.d.ts +2 -0
  27. package/dist/core/engine.js +3 -1
  28. package/dist/core/flow.d.ts +11 -0
  29. package/dist/core/flow.js +32 -8
  30. package/dist/core/map-builder.d.ts +34 -1
  31. package/dist/core/map-builder.js +42 -4
  32. package/dist/core/run-graph.d.ts +2 -0
  33. package/dist/core/run-graph.js +1 -0
  34. package/dist/errors.d.ts +1 -0
  35. package/dist/extensions.d.ts +1 -0
  36. package/dist/extensions.js +1 -0
  37. package/dist/highlights/demo-pace.d.ts +4 -1
  38. package/dist/highlights/demo-pace.js +28 -3
  39. package/dist/highlights/device.d.ts +2 -0
  40. package/dist/highlights/device.js +2 -0
  41. package/dist/highlights/types.d.ts +6 -0
  42. package/dist/index.d.ts +2 -2
  43. package/dist/pilot-overlay/action-ring.d.ts +0 -9
  44. package/dist/pilot-overlay/action-ring.js +1 -1
  45. package/dist/pilot-overlay/panel.d.ts +2 -1
  46. package/dist/runner/interaction-patch.js +32 -1
  47. package/dist/runner/main.js +5 -2
  48. package/dist/runner/step-mode.js +10 -4
  49. package/dist/session/auto-explore-run.js +6 -0
  50. package/dist/session/auto-explore.d.ts +2 -1
  51. package/dist/session/auto-session-ipc.d.ts +1 -2
  52. package/dist/session/auto-session-ipc.js +1 -1
  53. package/dist/session/block-inject.d.ts +0 -13
  54. package/dist/session/block-inject.js +3 -3
  55. package/dist/session/overlay-beacon.d.ts +7 -0
  56. package/dist/session/overlay-beacon.js +5 -0
  57. package/dist/session/pilot.d.ts +1 -2
  58. package/dist/session/pilot.js +1 -2
  59. package/dist/traverse/coverage.d.ts +4 -4
  60. package/dist/traverse/coverage.js +4 -1
  61. package/dist/traverse/lease.d.ts +1 -0
  62. package/dist/traverse/lease.js +1 -0
  63. package/dist/traverse/run.d.ts +3 -0
  64. package/dist/types.d.ts +1 -1
  65. package/examples/routed-demo/src/flows/routed-demo-map.flow.ts +1 -1
  66. package/examples/routed-demo/src/flows/routed-demo.flow.ts +2 -2
  67. package/examples/saucedemo/README.md +11 -12
  68. package/examples/saucedemo/config/waygraph.config.ts +17 -0
  69. package/examples/saucedemo/docs/CONVENTION.md +1 -2
  70. package/examples/saucedemo/docs/HELPERS.md +1 -1
  71. package/examples/saucedemo/package-lock.json +10 -3
  72. package/examples/saucedemo/package.json +12 -29
  73. package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-cart.block.ts +0 -49
  74. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +22 -25
  75. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +0 -6
  76. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +38 -96
  77. package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +1 -26
  78. package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +1 -45
  79. package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +14 -77
  80. package/examples/saucedemo/src/flows/api-demo.flow.ts +17 -11
  81. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +19 -14
  82. package/examples/saucedemo/src/flows/cart-continue.flow.ts +10 -8
  83. package/examples/saucedemo/src/flows/cart-mem.flow.ts +7 -7
  84. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +13 -8
  85. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +26 -48
  86. package/examples/saucedemo/src/flows/checkout.flow.ts +70 -86
  87. package/examples/saucedemo/src/flows/device-showcase.flow.ts +24 -25
  88. package/examples/saucedemo/src/flows/login.flow.ts +11 -18
  89. package/examples/saucedemo/src/flows/mem-stub-demo.flow.ts +9 -20
  90. package/examples/saucedemo/src/flows/shop.flow.ts +15 -20
  91. package/examples/saucedemo/src/flows/smoke.flow.ts +12 -27
  92. package/examples/saucedemo/src/flows/stub-on-error.flow.ts +24 -43
  93. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +26 -34
  94. package/examples/saucedemo/src/states/checkout.mem-stubs.ts +11 -0
  95. package/examples/saucedemo/src/stubs/device-showcase.ts +193 -0
  96. package/examples/saucedemo/src/stubs/login.ts +37 -0
  97. package/examples/saucedemo/tests/chain-flow-episodes.spec.ts +7 -9
  98. package/examples/saucedemo/tests/submit-login-branch.spec.ts +18 -5
  99. package/package.json +12 -5
  100. package/templates/quickstart/README.md +4 -7
  101. package/templates/quickstart/config/waygraph.config.ts +17 -0
  102. package/templates/quickstart/docs/CONVENTION.md +1 -2
  103. package/templates/quickstart/docs/HELPERS.md +1 -1
  104. package/templates/quickstart/package.json +14 -14
  105. package/templates/quickstart/src/blocks/SITE-MAP.md +14 -3
  106. package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +0 -49
  107. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +22 -25
  108. package/templates/quickstart/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +0 -6
  109. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +38 -96
  110. package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +1 -26
  111. package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +1 -45
  112. package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +14 -77
  113. package/templates/quickstart/src/flows/api-demo.flow.ts +43 -0
  114. package/templates/quickstart/src/flows/cart-bulk.flow.ts +19 -14
  115. package/templates/quickstart/src/flows/cart-continue.flow.ts +10 -8
  116. package/templates/quickstart/src/flows/cart-mem.flow.ts +7 -7
  117. package/templates/quickstart/src/flows/catalog-logout.flow.ts +13 -8
  118. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +26 -48
  119. package/templates/quickstart/src/flows/checkout.flow.ts +70 -63
  120. package/templates/quickstart/src/flows/device-showcase.flow.ts +24 -25
  121. package/templates/quickstart/src/flows/login.flow.ts +11 -18
  122. package/templates/quickstart/src/flows/mem-stub-demo.flow.ts +12 -0
  123. package/templates/quickstart/src/flows/shop.flow.ts +15 -20
  124. package/templates/quickstart/src/flows/smoke.flow.ts +12 -27
  125. package/templates/quickstart/src/flows/stub-on-error.flow.ts +31 -0
  126. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +26 -34
  127. package/templates/quickstart/src/states/checkout.mem-stubs.ts +11 -0
  128. package/templates/quickstart/src/stubs/device-showcase.ts +193 -0
  129. package/templates/quickstart/src/stubs/login.ts +37 -0
  130. package/templates/quickstart/tests/chain-flow.spec.ts +7 -10
  131. package/templates/scaffold/README.md +1 -1
  132. package/templates/scaffold/STRUCTURE.md +5 -2
  133. package/templates/scaffold/config/waygraph.config.ts +24 -5
  134. package/templates/scaffold/package.json +6 -6
  135. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +36 -0
  136. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +24 -0
  137. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +34 -0
  138. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +47 -0
  139. package/templates/scaffold/src/flows/README.md +35 -14
  140. package/templates/scaffold/src/flows/{shop.flow.ts → home/cart.flow.ts} +18 -17
  141. package/templates/scaffold/src/flows/{example.flow.ts → home/hello.flow.ts} +12 -13
  142. package/templates/scaffold/tests/example.spec.ts +5 -5
  143. package/templates/scaffold/tests/hoppscotch.spec.ts +3 -3
  144. package/templates/scaffold/tests/mail-verify.spec.ts +7 -7
  145. package/templates/skills/waygraph-convention.skill.md +14 -6
  146. package/examples/saucedemo/playwright.config.ts +0 -19
  147. package/examples/saucedemo/scripts/drive-auto-cli.mjs +0 -93
  148. package/examples/saucedemo/scripts/prove-add-to-cart-menu.mjs +0 -126
  149. package/examples/saucedemo/scripts/prove-auto-graph.mjs +0 -92
  150. package/examples/saucedemo/scripts/prove-wrong-login-runtime.mjs +0 -63
  151. package/templates/quickstart/playwright.config.ts +0 -14
  152. package/templates/quickstart/scripts/drive-auto-cli.mjs +0 -93
  153. package/templates/scaffold/src/flows/hoppscotch-auth.flow.ts +0 -35
  154. package/templates/scaffold/src/flows/hoppscotch-get.flow.ts +0 -23
  155. package/templates/scaffold/src/flows/hoppscotch-post.flow.ts +0 -33
  156. package/templates/scaffold/src/flows/mail-verify.flow.ts +0 -46
@@ -0,0 +1,193 @@
1
+ import type { StubCtx } from "waygraph";
2
+ import { LoginCreds } from "../states/checkout.mem-keys.js";
3
+ import { LoginSel } from "../blocks/saucedemo-web/methods/login.sel.js";
4
+ import { CartSel } from "../blocks/saucedemo-web/cart/cart.sel.js";
5
+ import { InventorySel } from "../blocks/saucedemo-web/inventory/methods/inventory-items.js";
6
+
7
+ const PRODUCT_ID = "sauce-labs-backpack";
8
+
9
+ /** The six-step checklist of the device showcase; `done` is how many are ticked, `index` the highlighted one. */
10
+ function checklist(done: number, index: number) {
11
+ const steps = [
12
+ "Enter username",
13
+ "Enter password",
14
+ "Tap Login",
15
+ "Find the product",
16
+ "Add to cart",
17
+ "Open cart",
18
+ ];
19
+ const ids = [
20
+ "login-user",
21
+ "login-pass",
22
+ "login-submit",
23
+ "cart-find",
24
+ "cart-add",
25
+ "cart-open",
26
+ ];
27
+ return [
28
+ ids.map((id, i) => ({
29
+ id,
30
+ text: steps[i]!,
31
+ ...(i < done ? { done: true } : {}),
32
+ })),
33
+ { title: "Device showcase", index },
34
+ ] as const;
35
+ }
36
+
37
+ /** Narration for the device showcase flow: phone login, tablet shelf, landscape cart. Each is a stub to chain on its Block. */
38
+ export const deviceShowcase = {
39
+ username: {
40
+ before: (ctx: StubCtx) => {
41
+ ctx.title("Signing in · mobile");
42
+ ctx.device("mobile");
43
+ ctx.portrait();
44
+ ctx.todos("saucedemo", ...checklist(0, 0));
45
+ ctx.zoom(1.35);
46
+ const fromMem = ctx.mem?.get(LoginCreds.key)?.username;
47
+ ctx.ring("username", {
48
+ selector: LoginSel.username,
49
+ label: "Username",
50
+ detail: fromMem
51
+ ? `tone: info (blue) - mem ${fromMem}`
52
+ : "tone: info (blue)",
53
+ tone: "info",
54
+ todo: "login-user",
55
+ focus: true,
56
+ zoom: 1.4,
57
+ });
58
+ },
59
+ after: (ctx: StubCtx) => {
60
+ ctx.todos("saucedemo", ...checklist(1, 1));
61
+ },
62
+ },
63
+ password: {
64
+ before: (ctx: StubCtx) => {
65
+ ctx.ring("password", {
66
+ selector: LoginSel.password,
67
+ label: "Password",
68
+ detail: "tone: planned (purple)",
69
+ todo: "login-pass",
70
+ tone: "planned",
71
+ focus: true,
72
+ zoom: 1.4,
73
+ });
74
+ },
75
+ after: (ctx: StubCtx) => {
76
+ ctx.todos("saucedemo", ...checklist(2, 2));
77
+ },
78
+ },
79
+ submit: {
80
+ before: (ctx: StubCtx) => {
81
+ ctx.highlights({
82
+ submit: {
83
+ selector: LoginSel.loginButton,
84
+ label: "Login",
85
+ detail: "tone: warning (yellow)",
86
+ todo: "login-submit",
87
+ tone: "warning",
88
+ focus: true,
89
+ zoom: 1.55,
90
+ weight: "bold",
91
+ gesture: "tap",
92
+ },
93
+ });
94
+ },
95
+ after: (ctx: StubCtx) => {
96
+ ctx.banner("Inventory · landscape");
97
+ ctx.landscape();
98
+ ctx.todos("saucedemo", ...checklist(3, 3));
99
+ ctx.ring("shelf", {
100
+ selector: ".inventory_list",
101
+ label: "Product shelf",
102
+ detail: "tone: success (green) - ready to shop",
103
+ tone: "success",
104
+ focus: true,
105
+ zoom: 1.25,
106
+ duration: true,
107
+ });
108
+ },
109
+ },
110
+ addToCart: {
111
+ before: (ctx: StubCtx) => {
112
+ ctx.title("Pick a product · tablet");
113
+ ctx.device("tablet");
114
+ ctx.portrait();
115
+ ctx.todos("saucedemo", ...checklist(3, 3));
116
+ ctx.zoom(1.35);
117
+ ctx.zoomOut(false);
118
+ ctx.highlights({
119
+ product: {
120
+ selector: InventorySel.title(PRODUCT_ID),
121
+ label: "Sauce Labs Backpack",
122
+ detail: "tone: info (blue)",
123
+ tone: "info",
124
+ focus: true,
125
+ zoom: 1.45,
126
+ zoomOut: false,
127
+ },
128
+ add: {
129
+ selector: InventorySel.addBtn(PRODUCT_ID),
130
+ label: "Add to cart",
131
+ detail: "tone: success (green)",
132
+ tone: "success",
133
+ focus: true,
134
+ zoom: 1.5,
135
+ zoomOut: false,
136
+ weight: "bold",
137
+ },
138
+ });
139
+ },
140
+ after: (ctx: StubCtx) => {
141
+ ctx.banner("In the cart");
142
+ ctx.todos("saucedemo", ...checklist(4, 4));
143
+ ctx.zoomOut(false);
144
+ ctx.ring("badge", {
145
+ selector: InventorySel.cartBadge,
146
+ label: "Cart badge",
147
+ detail: "tone: success (green) - count updated",
148
+ tone: "success",
149
+ focus: true,
150
+ zoomOut: false,
151
+ duration: true,
152
+ });
153
+ },
154
+ },
155
+ openCart: {
156
+ before: (ctx: StubCtx) => {
157
+ ctx.title("Open cart · landscape");
158
+ ctx.landscape();
159
+ ctx.todos("saucedemo", ...checklist(5, 5));
160
+ ctx.zoom(1.4);
161
+ ctx.zoomOut(false);
162
+ ctx.highlights({
163
+ cart: {
164
+ selector: CartSel.cartLink,
165
+ label: "Cart",
166
+ detail: "tone: planned (purple) - open cart",
167
+ tone: "planned",
168
+ focus: true,
169
+ zoomOut: false,
170
+ followMouse: false,
171
+ weight: "bold",
172
+ gesture: "tap",
173
+ },
174
+ });
175
+ },
176
+ after: (ctx: StubCtx) => {
177
+ ctx.banner("Your cart · desktop");
178
+ ctx.clearDevice();
179
+ ctx.hideTodos();
180
+ ctx.zoomOut(true);
181
+ ctx.ring("items", {
182
+ selector: InventorySel.cartItem,
183
+ label: "Cart items",
184
+ detail: "tone: success (green) - product in cart",
185
+ tone: "success",
186
+ focus: true,
187
+ zoomOut: true,
188
+ followMouse: false,
189
+ duration: true,
190
+ });
191
+ },
192
+ },
193
+ };
@@ -0,0 +1,37 @@
1
+ import type { StubCtx } from "waygraph";
2
+ import { LoginSel } from "../blocks/saucedemo-web/methods/login.sel.js";
3
+
4
+ /** What a refused login looks like on screen: the title, a finished checklist and the error banner. */
5
+ export function loginRefused(ctx: StubCtx): void {
6
+ ctx.title("Login blocked");
7
+ ctx.todos(
8
+ "saucedemo",
9
+ [
10
+ { id: "login-user", text: "Enter username", done: true },
11
+ { id: "login-pass", text: "Enter password", done: true },
12
+ { id: "login-submit", text: "Click Login", done: true },
13
+ ],
14
+ { title: "Sign in", style: "checklist" },
15
+ );
16
+ ctx.ring("error", {
17
+ selector: LoginSel.errorBanner,
18
+ label: "Login error banner",
19
+ detail: "tone: danger (red)",
20
+ tone: "danger",
21
+ focus: true,
22
+ duration: true,
23
+ });
24
+ }
25
+
26
+ /** Rings the error banner when the login step itself throws. */
27
+ export function loginFailed(ctx: StubCtx): void {
28
+ ctx.title("Login failed");
29
+ ctx.ring("error", {
30
+ selector: LoginSel.errorBanner,
31
+ label: "Login error banner",
32
+ detail: "tone: danger (red)",
33
+ tone: "danger",
34
+ focus: true,
35
+ duration: true,
36
+ });
37
+ }
@@ -1,5 +1,6 @@
1
1
  import { test, expect } from "@playwright/test";
2
- import { MemPage, checkpoint, chainFlow } from "waygraph";
2
+ import { MemPage, checkpoint } from "waygraph";
3
+ import { engine } from "../config/waygraph.config.js";
3
4
  import { loginFlow } from "../src/flows/login.flow.js";
4
5
  import { shopFlow } from "../src/flows/shop.flow.js";
5
6
  import { viewerBlockedFlow } from "../src/flows/viewer-blocked.flow.js";
@@ -7,12 +8,12 @@ import { LoginCreds, SelectedItem } from "../src/states/checkout.mem-keys.js";
7
8
 
8
9
  const BACKPACK = SelectedItem({ id: "sauce-labs-backpack", name: "Sauce Labs Backpack" });
9
10
 
10
- test("chainFlow: Episode 1 signs in + shops, Episode 2 locked-out user stays on LoginPage", async () => {
11
+ test("nested flows: Episode 1 signs in + shops, Episode 2 locked-out user stays on LoginPage", async () => {
11
12
  const mem = new MemPage();
12
13
  mem.set(LoginCreds({ username: "standard_user", password: "secret_sauce" }));
13
14
  mem.set(BACKPACK);
14
15
 
15
- const ep1 = chainFlow(loginFlow, shopFlow);
16
+ const ep1 = engine.map().start().flow(loginFlow).flow(shopFlow).end();
16
17
  const result1 = await ep1.run(mem);
17
18
  expect(result1).toEqual(checkpoint("OrderComplete"));
18
19
 
@@ -26,16 +27,12 @@ test("shopFlow alone: already-seeded SelectedItem completes checkout after login
26
27
  mem.set(LoginCreds({ username: "standard_user", password: "secret_sauce" }));
27
28
  mem.set(BACKPACK);
28
29
 
29
- const result = await chainFlow(loginFlow, shopFlow).run(mem);
30
+ const result = await engine.map().start().flow(loginFlow).flow(shopFlow).end().run(mem);
30
31
  expect(result).toEqual(checkpoint("OrderComplete"));
31
32
  });
32
33
 
33
- test("chainFlow blocks() lists the flattened episode chain in order", () => {
34
- expect(loginFlow.title).toBe("Sign In");
35
- expect(shopFlow.title).toBe("Shop & Checkout");
36
- expect(viewerBlockedFlow.title).toBe("Viewer: Blocked Login Attempt");
37
-
38
- const blocks = chainFlow(loginFlow, shopFlow, viewerBlockedFlow).blocks();
34
+ test("blocks() lists the flattened episode chain in order", () => {
35
+ const blocks = engine.map().start().flow(loginFlow).flow(shopFlow).flow(viewerBlockedFlow).end().blocks();
39
36
  expect(blocks.map((b) => b.name)).toEqual([
40
37
  "nav-login",
41
38
  "fill-username",
@@ -20,7 +20,7 @@ More: `npm run check` (lint your Blocks), `npm run graph` (see the whole graph),
20
20
  ## Where things live
21
21
 
22
22
  See [STRUCTURE.md](./STRUCTURE.md). Short version: one folder per page under `src/map/`,
23
- one Block per action, flows in `src/flows/`.
23
+ one Block per action, one flow per use case in `src/flows/` (a folder per page: its success, its failures, its bad inputs).
24
24
 
25
25
  ## Point it at your real app
26
26
 
@@ -27,7 +27,10 @@ Next.js route groups) and never appear in a URL or Checkpoint name.
27
27
  │ │ ├── (external)/mailpit/ # a different origin (mail catcher)
28
28
  │ │ └── (external)/hoppscotch/ # API client, REST client, needs the hoppscotch extension
29
29
  │ ├── states/ # Checkpoint types + mem keys
30
- │ └── flows/ # example / shop / mail-verify
30
+ │ └── flows/ # use cases, one file per scenario (see flows/README.md)
31
+ │ ├── home/ # a page: hello, cart
32
+ │ ├── (user-stories)/ # flows that cross pages
33
+ │ └── (external)/hoppscotch/ # a tool on another origin
31
34
  └── tests/example.spec.ts
32
35
  ```
33
36
 
@@ -41,7 +44,7 @@ page folder uses, so any agent or teammate knows where to look without asking.
41
44
  | Add a page | copy a folder under `src/map/`, keep the `_nav` / `_page` / `_sel` names |
42
45
  | Add an action on a page | one file in that page's `_methods/` |
43
46
  | Verify a state (button disabled, error shown) | `defineAssertBlock` + `Trait.disabled` / `Trait.enabled` |
44
- | Wire steps together | `src/flows/*.flow.ts` |
47
+ | Define a use case (success, failed, bad input) | one `*.flow.ts` in `src/flows/<page>/`, or `(user-stories)/` for one that crosses pages |
45
48
  | Give a flow fake data | `registerMemStub(key, fake)` + `memStub: true` in the flow's options (docs: docs/REFERENCE.md, "memStub") |
46
49
  | Explore by hand | `npm run auto` (headed) or `npm run auto:cli` |
47
50
  | Check your work | `npm run check`, `npm run graph`, `waygraph map` |
@@ -1,6 +1,6 @@
1
- import { defineConfig } from "waygraph";
1
+ import { Engine, defineConfig } from "waygraph";
2
2
 
3
- export default defineConfig(({ env }) => ({
3
+ const config = defineConfig(({ env }) => ({
4
4
  // Where the app under test lives. BASE_URL comes from .env / .env.<mode>: waygraph test --env staging
5
5
  baseUrl: env.BASE_URL ?? "__APP_BASE_URL__",
6
6
 
@@ -10,11 +10,25 @@ export default defineConfig(({ env }) => ({
10
10
  // dir: "~/Downloads/waygraph-videos", // where they are saved (default: Downloads/waygraph-videos/<project>)
11
11
  // viewport: "1920x1080", // recording size: demo defaults to 1920x1080, run to 1280x720
12
12
  // },
13
- // slowMo: 250, // ms to pause before each browser action, so a run is watchable
14
- // headed: true, // show the browser window (demo and auto already do)
15
- // demo: { autoNext: true, fast: false }, // advance demo steps without clicking Next; blitz fast-forward steps
13
+ // slowMo: 250, // shared by run and demo: ms to pause around each browser action
14
+ // headed: true, // shared: show the browser window (demo and auto already do)
15
+ // run: { // `waygraph run` only; overrides the shared values above
16
+ // slowMo: 0,
17
+ // video: { viewport: "1280x720" },
18
+ // },
19
+ // demo: { // `waygraph demo` only; overrides the shared values above
20
+ // pace: "fast", // default pace for steps that set none: "fast" | "slow" | "blitz" | 0.5 | 2
21
+ // speeds: { blitz: 0.05, fast: 0.33, slow: 2 }, // what each pace means, x normal dwell (lower = faster)
22
+ // typingMs: 30, // typing animation, ms per character (0 types instantly)
23
+ // slowMo: 350, // pause after each action, once per action (a step with pace "blitz" skips it)
24
+ // autoNext: true, // advance steps without clicking Next
25
+ // fast: false, // shorter Next / auto-next gates
26
+ // video: { viewport: "1920x1080" },
27
+ // },
16
28
  // webServer: { command: "npm start", port: 3000 }, // started around test / run / demo / auto
17
29
 
30
+ // Another config for another environment: config/waygraph.staging.config.ts, chosen with `waygraph test --config staging`.
31
+
18
32
  // ── test runner (Playwright) ──────────────────────────────────────────────────────────────────
19
33
  // testDir: "tests",
20
34
  // playwright: { // any Playwright test option, merged over waygraph's defaults
@@ -24,3 +38,8 @@ export default defineConfig(({ env }) => ({
24
38
  // use: { video: "on", trace: "on", screenshot: "only-on-failure", viewport: { width: 1280, height: 720 } },
25
39
  // },
26
40
  }));
41
+
42
+ export default config;
43
+
44
+ // Flows import this, so they run with the config above already applied.
45
+ export const engine = new Engine(config);
@@ -13,12 +13,12 @@
13
13
  "graph:mermaid": "waygraph graph --mermaid",
14
14
  "auto": "waygraph auto",
15
15
  "auto:cli": "waygraph auto --cli",
16
- "demo": "waygraph demo --blocks exampleFlow",
17
- "demo:auto-next": "waygraph demo --blocks exampleFlow --auto-next",
18
- "demo:shop": "waygraph demo --blocks shopFlow",
19
- "demo:shop:auto-next": "waygraph demo --blocks shopFlow --auto-next",
20
- "run": "waygraph run --blocks exampleFlow",
21
- "run:shop": "waygraph run --blocks shopFlow"
16
+ "demo": "waygraph demo --blocks homeHelloFlow",
17
+ "demo:auto-next": "waygraph demo --blocks homeHelloFlow --auto-next",
18
+ "demo:shop": "waygraph demo --blocks homeCartFlow",
19
+ "demo:shop:auto-next": "waygraph demo --blocks homeCartFlow --auto-next",
20
+ "run": "waygraph run --blocks homeHelloFlow",
21
+ "run:shop": "waygraph run --blocks homeCartFlow"
22
22
  },
23
23
  "dependencies": {
24
24
  "waygraph": "latest"
@@ -0,0 +1,36 @@
1
+ import { engine } from "../../../../config/waygraph.config.js";
2
+ import { NavHoppscotchSettingsBlock } from "../../../map/(external)/hoppscotch/settings/_nav.block.js";
3
+ import { PickExtensionInterceptorBlock } from "../../../map/(external)/hoppscotch/_methods/pick-extension-interceptor.method.block.js";
4
+ import { NavHoppscotchRestBlock } from "../../../map/(external)/hoppscotch/_nav.block.js";
5
+ import { SetQueryParamsBlock } from "../../../map/(external)/hoppscotch/_methods/set-query-params.method.block.js";
6
+ import { SetUrlBlock } from "../../../map/(external)/hoppscotch/_methods/set-url.method.block.js";
7
+ import { SetBearerTokenBlock } from "../../../map/(external)/hoppscotch/_methods/set-bearer-token.method.block.js";
8
+ import { SendRequestBlock } from "../../../map/(external)/hoppscotch/_methods/send-request.method.block.js";
9
+ import { AssertStatusBlock } from "../../../map/(external)/hoppscotch/_methods/assert-status.method.block.js";
10
+ import { ReadResponseHeadersBlock } from "../../../map/(external)/hoppscotch/_methods/read-response-headers.method.block.js";
11
+ import { AssertResponseHeaderBlock } from "../../../map/(external)/hoppscotch/_methods/assert-response-header.method.block.js";
12
+ import { ReadResponseBodyBlock } from "../../../map/(external)/hoppscotch/_methods/read-response-body.method.block.js";
13
+ import { AssertResponseBodyBlock } from "../../../map/(external)/hoppscotch/_methods/assert-response-body.method.block.js";
14
+ import "../../../map/(external)/hoppscotch/_page.block.js";
15
+
16
+ /** GET with query params and a bearer token, check the status, that the API received them, and a response header. Needs `extensions: ["hoppscotch"]`. */
17
+ export const hoppscotchAuthFlow = engine
18
+ .map()
19
+ .start()
20
+ .gotoExternal(
21
+ NavHoppscotchSettingsBlock.stubBefore((ctx) => {
22
+ ctx.title("Scaffold: Hoppscotch GET with query params and bearer token");
23
+ }),
24
+ )
25
+ .method(PickExtensionInterceptorBlock)
26
+ .gotoExternal(NavHoppscotchRestBlock)
27
+ .method(SetUrlBlock)
28
+ .method(SetQueryParamsBlock)
29
+ .method(SetBearerTokenBlock)
30
+ .method(SendRequestBlock)
31
+ .assert(AssertStatusBlock)
32
+ .method(ReadResponseBodyBlock)
33
+ .assert(AssertResponseBodyBlock)
34
+ .method(ReadResponseHeadersBlock)
35
+ .assert(AssertResponseHeaderBlock)
36
+ .end();
@@ -0,0 +1,24 @@
1
+ import { engine } from "../../../../config/waygraph.config.js";
2
+ import { NavHoppscotchSettingsBlock } from "../../../map/(external)/hoppscotch/settings/_nav.block.js";
3
+ import { PickExtensionInterceptorBlock } from "../../../map/(external)/hoppscotch/_methods/pick-extension-interceptor.method.block.js";
4
+ import { NavHoppscotchRestBlock } from "../../../map/(external)/hoppscotch/_nav.block.js";
5
+ import { SetUrlBlock } from "../../../map/(external)/hoppscotch/_methods/set-url.method.block.js";
6
+ import { SendRequestBlock } from "../../../map/(external)/hoppscotch/_methods/send-request.method.block.js";
7
+ import { AssertStatusBlock } from "../../../map/(external)/hoppscotch/_methods/assert-status.method.block.js";
8
+ import "../../../map/(external)/hoppscotch/_page.block.js";
9
+
10
+ /** GET a URL and check the status. Needs `extensions: ["hoppscotch"]` in waygraph.config.ts. */
11
+ export const hoppscotchGetFlow = engine
12
+ .map()
13
+ .start()
14
+ .gotoExternal(
15
+ NavHoppscotchSettingsBlock.stubBefore((ctx) => {
16
+ ctx.title("Scaffold: Hoppscotch GET");
17
+ }),
18
+ )
19
+ .method(PickExtensionInterceptorBlock)
20
+ .gotoExternal(NavHoppscotchRestBlock)
21
+ .method(SetUrlBlock)
22
+ .method(SendRequestBlock)
23
+ .assert(AssertStatusBlock)
24
+ .end();
@@ -0,0 +1,34 @@
1
+ import { engine } from "../../../../config/waygraph.config.js";
2
+ import { NavHoppscotchSettingsBlock } from "../../../map/(external)/hoppscotch/settings/_nav.block.js";
3
+ import { PickExtensionInterceptorBlock } from "../../../map/(external)/hoppscotch/_methods/pick-extension-interceptor.method.block.js";
4
+ import { NavHoppscotchRestBlock } from "../../../map/(external)/hoppscotch/_nav.block.js";
5
+ import { SetMethodBlock } from "../../../map/(external)/hoppscotch/_methods/set-method.method.block.js";
6
+ import { SetUrlBlock } from "../../../map/(external)/hoppscotch/_methods/set-url.method.block.js";
7
+ import { SetHeadersBlock } from "../../../map/(external)/hoppscotch/_methods/set-headers.method.block.js";
8
+ import { SetJsonBodyBlock } from "../../../map/(external)/hoppscotch/_methods/set-json-body.method.block.js";
9
+ import { SendRequestBlock } from "../../../map/(external)/hoppscotch/_methods/send-request.method.block.js";
10
+ import { AssertStatusBlock } from "../../../map/(external)/hoppscotch/_methods/assert-status.method.block.js";
11
+ import { ReadResponseBodyBlock } from "../../../map/(external)/hoppscotch/_methods/read-response-body.method.block.js";
12
+ import { AssertResponseBodyBlock } from "../../../map/(external)/hoppscotch/_methods/assert-response-body.method.block.js";
13
+ import "../../../map/(external)/hoppscotch/_page.block.js";
14
+
15
+ /** POST JSON with a header, check the status and the response body. Needs `extensions: ["hoppscotch"]`. */
16
+ export const hoppscotchPostFlow = engine
17
+ .map()
18
+ .start()
19
+ .gotoExternal(
20
+ NavHoppscotchSettingsBlock.stubBefore((ctx) => {
21
+ ctx.title("Scaffold: Hoppscotch POST with header and JSON body");
22
+ }),
23
+ )
24
+ .method(PickExtensionInterceptorBlock)
25
+ .gotoExternal(NavHoppscotchRestBlock)
26
+ .method(SetMethodBlock)
27
+ .method(SetUrlBlock)
28
+ .method(SetHeadersBlock)
29
+ .method(SetJsonBodyBlock)
30
+ .method(SendRequestBlock)
31
+ .assert(AssertStatusBlock)
32
+ .method(ReadResponseBodyBlock)
33
+ .assert(AssertResponseBodyBlock)
34
+ .end();
@@ -0,0 +1,47 @@
1
+ import { engine } from "../../../config/waygraph.config.js";
2
+ import { NavMailpitInboxBlock } from "../../map/(external)/mailpit/_nav.block.js";
3
+ import { AssertEmailReceivedBlock } from "../../map/(external)/mailpit/_methods/assert-email-received.method.block.js";
4
+ import { OpenMessageBlock } from "../../map/(external)/mailpit/_methods/open-message.method.block.js";
5
+ import { AssertEmailContentBlock } from "../../map/(external)/mailpit/_methods/assert-email-content.method.block.js";
6
+ import { ExtractEmailLinkBlock } from "../../map/(external)/mailpit/_methods/extract-email-link.method.block.js";
7
+ import { NavVerificationLinkBlock } from "../../map/(app_base)/home/nav-verification-link.block.js";
8
+ import "../../map/(external)/mailpit/_page.block.js";
9
+ import { MailpitSel } from "../../map/(external)/mailpit/_sel.js";
10
+
11
+ /** Needs a Mailpit at WAYGRAPH_MAIL_URL (default http://127.0.0.1:8025); tests/mail-verify.spec.ts starts one. */
12
+ export const emailVerificationFlow = engine
13
+ .map()
14
+ .start()
15
+ .gotoExternal(
16
+ NavMailpitInboxBlock.stubBefore((ctx) => {
17
+ ctx.title("Scaffold: Cross-origin mail verification (QA-rich)");
18
+ }),
19
+ )
20
+ .assert(
21
+ AssertEmailReceivedBlock.stubAfter((ctx) => {
22
+ ctx.ring("note", {
23
+ selector: MailpitSel.messageRow,
24
+ label: "AC · email arrived",
25
+ detail: "Confirmed before opening anything.",
26
+ tag: "AC",
27
+ });
28
+ }),
29
+ )
30
+ .method(OpenMessageBlock)
31
+ .assert(
32
+ AssertEmailContentBlock.stubAfter((ctx) => {
33
+ ctx.ring("note", { selector: MailpitSel.previewIframe, label: "AC · email body matches expected copy", tag: "AC" });
34
+ }),
35
+ )
36
+ .method(
37
+ ExtractEmailLinkBlock.stubAfter((ctx) => {
38
+ ctx.ring("note", {
39
+ selector: MailpitSel.previewIframe,
40
+ label: "AC · link found",
41
+ detail: "Read from the message preview iframe.",
42
+ tag: "AC",
43
+ });
44
+ }),
45
+ )
46
+ .gotoPage(NavVerificationLinkBlock)
47
+ .end();
@@ -1,18 +1,39 @@
1
1
  # `src/flows/`
2
2
 
3
- A `*.flow.ts` composes Blocks from `src/map/` into a runnable path - `waygraph run`/`demo`/`test`
4
- all point at an export here by name (`waygraph run --blocks shopFlow`).
3
+ A flow is one **use case**: a path a user takes through the app, with its own pass or fail. `waygraph run`, `demo` and
4
+ `test` all point at an export here by name (`waygraph run --blocks homeCartFlow`).
5
5
 
6
- - Prefer `new Engine().map()...end()` (the Map builder: `.gotoPage()`/`.method()`/`.assert()`/
7
- `.branch()`) over hand-composing raw Blocks - it checks each step's Block kind at runtime and
8
- the Checkpoint chain at compile time, so a wrong step is a build/test failure, not a
9
- mid-run surprise.
10
- - Flow settings go in `.end({ title, memStub, sessionReset })`. No nested `withTitle(withMemStub(...))`.
11
- - Demo highlights are ctx stubs chained on the Block where the flow uses it:
12
- `AddItemBlock.stubAfter((ctx) => { ctx.ring("cart", { selector, label }); })`.
13
- - One flow, one scenario. A flow that needs a genuinely different starting session
14
- (re-entering the login page mid-chain) sets `sessionReset: true`, rather than hand-clearing
15
- cookies inside a Block.
6
+ ## The folder mocks the front end
16
7
 
17
- New flow → new file here, named for the scenario (`checkout.flow.ts`), not for the Blocks it
18
- happens to use.
8
+ `src/flows/` mirrors the app's endpoints the way `src/map/` does, but where the map holds the *pieces* of a page, a flow
9
+ folder holds the *stories* of it. One folder per page or area, one file per use case. The scaffold only has the demo pages, so `auth/login/` below is the shape to copy for a real login page:
10
+
11
+ ```text
12
+ src/flows/
13
+ auth/login/
14
+ success.flow.ts # valid credentials -> dashboard
15
+ wrong-password.flow.ts # refused, error banner, stays on login
16
+ empty-fields.flow.ts # each required field flags itself
17
+ home/
18
+ hello.flow.ts
19
+ cart.flow.ts
20
+ (user-stories)/ # not a page: a story that crosses pages
21
+ email-verification.flow.ts
22
+ (external)/hoppscotch/ # a tool on another origin
23
+ post.flow.ts
24
+ ```
25
+
26
+ - **`<area>/<page>/`** matches the app URL, like the map. Open the folder and you see every way a user can go through that
27
+ page: the happy path, each failure, each bad input.
28
+ - **`(user-stories)/`** (any `(group)` name) is for flows that don't belong to one page: checkout end to end, onboarding,
29
+ "a viewer is locked out". Parentheses mean organizational only, as in the map.
30
+ - **A file is one scenario**, named for the outcome (`success`, `wrong-password`), not for the Blocks it happens to use.
31
+ An export is `<page><Outcome>Flow` (`loginWrongPasswordFlow`) so `--blocks` names stay unique across the project; a file path works too (`waygraph run src/flows/auth/login/success.flow.ts`).
32
+
33
+ ## Writing one
34
+
35
+ - Build it with `engine.map({ memStub })…end()`; nest another with `.flow(sub, { reset: true })`. No `defineFlow([...])`,
36
+ no nested `withTitle(withMemStub(...))`, and `.end()` takes nothing.
37
+ - The flow's name and narration are ctx stubs chained on the Block where they apply: `ctx.title("…")`, `ctx.ring(…)`.
38
+ - A flow expected to fail (a locked-out user) says so in `map({ expectedFailure: "…" })`.
39
+ - A step's speed in the demo is `{ pace }`: `.method(FillBlock, { pace: "blitz" })`.
@@ -1,17 +1,14 @@
1
- import { Engine } from "waygraph";
2
- import config from "../../config/waygraph.config.js";
3
- import { DemoSel } from "../map/(app_base)/home/_sel.js";
4
- import { NavHomeBlock } from "../map/(app_base)/home/_nav.block.js";
5
- import { AddItemBlock } from "../map/(app_base)/home/_methods/add-item.effect.block.js";
6
- import { AssertItemAddedBlock } from "../map/(app_base)/home/_methods/assert-item-added.method.block.js";
7
- import { RemoveItemBlock } from "../map/(app_base)/home/_methods/remove-item.effect.block.js";
8
- import { ClearCartBlock } from "../map/(app_base)/home/_methods/clear-cart.method.block.js";
9
- import "../states/demo.mem-stubs.js";
10
- import "../map/(app_base)/home/_page.block.js";
1
+ import { engine } from "../../../config/waygraph.config.js";
2
+ import { DemoSel } from "../../map/(app_base)/home/_sel.js";
3
+ import { NavHomeBlock } from "../../map/(app_base)/home/_nav.block.js";
4
+ import { AddItemBlock } from "../../map/(app_base)/home/_methods/add-item.effect.block.js";
5
+ import { AssertItemAddedBlock } from "../../map/(app_base)/home/_methods/assert-item-added.method.block.js";
6
+ import { RemoveItemBlock } from "../../map/(app_base)/home/_methods/remove-item.effect.block.js";
7
+ import { ClearCartBlock } from "../../map/(app_base)/home/_methods/clear-cart.method.block.js";
8
+ import "../../states/demo.mem-stubs.js";
9
+ import "../../map/(app_base)/home/_page.block.js";
11
10
 
12
- const engine = new Engine(config);
13
-
14
- // `memStub: true` fills SelectedItem (alpha) from src/states/demo.mem-stubs.ts; --data overrides it.
11
+ // `map({ memStub: true })` fills SelectedItem (alpha) from src/states/demo.mem-stubs.ts; --data overrides it.
15
12
  const AddShown = AddItemBlock.stubBefore((ctx) => {
16
13
  ctx.ring("add", { selector: DemoSel.catalog, label: "Catalog" });
17
14
  })
@@ -38,13 +35,17 @@ const ClearShown = ClearCartBlock.stubBefore((ctx) => {
38
35
  ctx.ring("cart", { selector: DemoSel.cartCount, label: "Empty cart", duration: true });
39
36
  });
40
37
 
41
- export const shopFlow = engine
42
- .map()
38
+ export const homeCartFlow = engine
39
+ .map({ memStub: true })
43
40
  .start()
44
- .gotoPage(NavHomeBlock)
41
+ .gotoPage(
42
+ NavHomeBlock.stubBefore((ctx) => {
43
+ ctx.title("Scaffold: Shop (Effect + clear)");
44
+ }),
45
+ )
45
46
  .method(AddShown)
46
47
  .assert(AssertItemAddedBlock)
47
48
  .method(RemoveShown)
48
49
  .method(AddShown)
49
50
  .method(ClearShown)
50
- .end({ title: "Scaffold: Shop (Effect + clear)", sessionReset: true, memStub: true });
51
+ .end();