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
@@ -1,21 +1,20 @@
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 { AssertHelloBlock } from "../map/(app_base)/home/_methods/assert-hello.method.block.js";
6
- import { NavDocsBlock } from "../map/(app_base)/docs/_nav.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 { AssertHelloBlock } from "../../map/(app_base)/home/_methods/assert-hello.method.block.js";
5
+ import { NavDocsBlock } from "../../map/(app_base)/docs/_nav.block.js";
7
6
  // Page hubs registered for auto/docs grouping (arrival-only after nav).
8
- import "../map/(app_base)/home/_page.block.js";
9
- import "../map/(app_base)/docs/_page.block.js";
10
-
11
- const engine = new Engine(config);
7
+ import "../../map/(app_base)/home/_page.block.js";
8
+ import "../../map/(app_base)/docs/_page.block.js";
12
9
 
13
10
  /** Smoke: nav + assert (no mem), then nav to the sibling docs/ page. Green offline. */
14
- export const exampleFlow = engine
11
+ export const homeHelloFlow = engine
15
12
  .map()
16
13
  .start()
17
14
  .gotoPage(
18
- NavHomeBlock.stubAfter((ctx) => {
15
+ NavHomeBlock.stubBefore((ctx) => {
16
+ ctx.title("Scaffold: Hello Waygraph");
17
+ }).stubAfter((ctx) => {
19
18
  ctx.ring("title", { selector: DemoSel.title, label: "Home heading", duration: true });
20
19
  }).stubOnError((ctx) => {
21
20
  ctx.ring("title", { selector: DemoSel.title, label: "Home heading at failure" });
@@ -40,4 +39,4 @@ export const exampleFlow = engine
40
39
  }),
41
40
  )
42
41
  .gotoPage(NavDocsBlock)
43
- .end({ title: "Scaffold: Hello Waygraph" });
42
+ .end();
@@ -1,16 +1,16 @@
1
1
  import { test, expect } from "@playwright/test";
2
2
  import { MemPage, checkpoint, seedMemStub } from "waygraph";
3
- import { exampleFlow } from "../src/flows/example.flow.js";
4
- import { shopFlow } from "../src/flows/shop.flow.js";
3
+ import { homeHelloFlow } from "../src/flows/home/hello.flow.js";
4
+ import { homeCartFlow } from "../src/flows/home/cart.flow.js";
5
5
 
6
6
  test("example flow reaches Docs (home -> assert -> sibling docs/ page)", async ({ context }) => {
7
- const result = await exampleFlow.run(context, new MemPage());
7
+ const result = await homeHelloFlow.run(context, new MemPage());
8
8
  expect(result).toEqual(checkpoint("Docs"));
9
9
  });
10
10
 
11
11
  test("shop flow adds then clears (Effect + Method)", async ({ context }) => {
12
12
  const mem = new MemPage();
13
- seedMemStub(mem, shopFlow);
14
- const result = await shopFlow.run(context, mem);
13
+ seedMemStub(mem, homeCartFlow);
14
+ const result = await homeCartFlow.run(context, mem);
15
15
  expect(result).toEqual(checkpoint("CartEmpty"));
16
16
  });
@@ -2,9 +2,9 @@ import { test, expect } from "@playwright/test";
2
2
  import { createServer, type Server } from "node:http";
3
3
  import { MemPage, checkpoint } from "waygraph";
4
4
  import config from "../config/waygraph.config.js";
5
- import { hoppscotchGetFlow } from "../src/flows/hoppscotch-get.flow.js";
6
- import { hoppscotchAuthFlow } from "../src/flows/hoppscotch-auth.flow.js";
7
- import { hoppscotchPostFlow } from "../src/flows/hoppscotch-post.flow.js";
5
+ import { hoppscotchGetFlow } from "../src/flows/(external)/hoppscotch/get.flow.js";
6
+ import { hoppscotchAuthFlow } from "../src/flows/(external)/hoppscotch/auth.flow.js";
7
+ import { hoppscotchPostFlow } from "../src/flows/(external)/hoppscotch/post.flow.js";
8
8
  import {
9
9
  ExpectedResponseHeader,
10
10
  ExpectedResponseText,
@@ -2,12 +2,12 @@ import { test, expect } from "@playwright/test";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import net from "node:net";
4
4
  import { MemPage, checkpoint } from "waygraph";
5
- import { mailVerifyFlow } from "../src/flows/mail-verify.flow.js";
5
+ import { emailVerificationFlow } from "../src/flows/(user-stories)/email-verification.flow.js";
6
6
  import { ExpectedRecipient, ExpectedLinkPattern, ExpectedEmailContent } from "../src/states/demo.mem-keys.js";
7
7
  import { HOME_URL } from "../src/map/(app_base)/home/_sel.js";
8
8
 
9
9
  // Plays "the app sent a verification email": starts a throwaway Mailpit container, sends a real SMTP message,
10
- // then runs mailVerifyFlow against it, including the no-email and wrong-content failure paths.
10
+ // then runs emailVerificationFlow against it, including the no-email and wrong-content failure paths.
11
11
 
12
12
  function runMailpit(): { containerId: string; smtpPort: number; httpPort: number } {
13
13
  const run = spawnSync("docker", ["run", "--rm", "-d", "-p", "0:1025", "-p", "0:8025", "axllent/mailpit"], {
@@ -80,7 +80,7 @@ async function withMailpit<T>(fn: (opts: { httpPort: number; smtpPort: number })
80
80
 
81
81
  const VERIFICATION_CONTENT = "Please confirm your email address to finish setting up your account.";
82
82
 
83
- test("mailVerifyFlow: confirms arrival, confirms content, reads the real link, navigates to it", async ({
83
+ test("emailVerificationFlow: confirms arrival, confirms content, reads the real link, navigates to it", async ({
84
84
  context,
85
85
  }) => {
86
86
  test.skip(!dockerAvailable(), "Docker not reachable in this environment");
@@ -100,7 +100,7 @@ test("mailVerifyFlow: confirms arrival, confirms content, reads the real link, n
100
100
  mem.set(ExpectedRecipient({ email: recipient }));
101
101
  mem.set(ExpectedLinkPattern, "verified=1");
102
102
  mem.set(ExpectedEmailContent, VERIFICATION_CONTENT);
103
- const result = await mailVerifyFlow.run(context, mem);
103
+ const result = await emailVerificationFlow.run(context, mem);
104
104
  expect(result).toEqual(checkpoint("HomeVerified"));
105
105
  });
106
106
  });
@@ -130,7 +130,7 @@ test("the same four mail-reading Blocks handle a second, unrelated email scenari
130
130
  mem.set(ExpectedRecipient({ email: recipient }));
131
131
  mem.set(ExpectedLinkPattern, "verified=1");
132
132
  mem.set(ExpectedEmailContent, resetContent);
133
- const result = await mailVerifyFlow.run(context, mem);
133
+ const result = await emailVerificationFlow.run(context, mem);
134
134
  expect(result).toEqual(checkpoint("HomeVerified"));
135
135
  });
136
136
  });
@@ -149,7 +149,7 @@ test("assert-email-received fails loud, naming itself, when no email ever arrive
149
149
  // not just the leaf Block - real, useful QA context, not a bug - so this
150
150
  // matches the accumulated name ending at assert-email-received, not an
151
151
  // exact "failed after \"assert-email-received\"" alone.
152
- await expect(mailVerifyFlow.run(context, mem)).rejects.toThrow(
152
+ await expect(emailVerificationFlow.run(context, mem)).rejects.toThrow(
153
153
  /trait "email-received" failed after ".*assert-email-received"/,
154
154
  );
155
155
  });
@@ -175,7 +175,7 @@ test("assert-email-content fails loud, naming itself, when the email's copy does
175
175
  mem.set(ExpectedRecipient({ email: recipient }));
176
176
  mem.set(ExpectedLinkPattern, "verified=1");
177
177
  mem.set(ExpectedEmailContent, VERIFICATION_CONTENT);
178
- await expect(mailVerifyFlow.run(context, mem)).rejects.toThrow(
178
+ await expect(emailVerificationFlow.run(context, mem)).rejects.toThrow(
179
179
  /trait "body-contains-expected-text" failed after ".*assert-email-content"/,
180
180
  );
181
181
  });
@@ -68,10 +68,14 @@ against a live site, use `--skill-pilot-blind`.
68
68
 
69
69
  ## Flows, stubs and config (the scaffold's shape)
70
70
 
71
- - **Build flows with `map()`** (`.start().gotoPage(Nav).method(B).assert(A).end({ ... })`; `.gotoExternal` for another origin),
72
- not a raw `defineFlow([start, ...])` array. Flow settings go in `.end`'s options:
73
- `.end({ title: "Shop", memStub: true, sessionReset: true })`. Never `withMemStub(withTitle(withSessionReset(...)))`.
74
- (`defineFlow(blocks, options)` takes the same options.)
71
+ - **Build flows with `map()`** (`engine.map({ memStub }).start().gotoPage(Nav).method(B, { pace }).assert(A).end()`;
72
+ `.gotoExternal` for another origin), never a raw `defineFlow([start, ...])`. Flow settings (`memStub`, `expectedFailure`, `demoPace`, `sessionReset`) go in `map({ … })` before the chain
73
+ and `.end()` takes nothing. The flow's title is `ctx.title(...)` in a stub. **Nest flows with `.flow(sub, { reset: true })`**,
74
+ not `chainFlow` / `withSessionReset`. No `withMemStub(withTitle(...))` wrappers.
75
+ - **`src/flows/` holds use cases and mirrors the front end's endpoints**: a folder per page (`flows/auth/login/`) with one
76
+ file per scenario named for its outcome (`success.flow.ts`, `wrong-password.flow.ts`, `empty-fields.flow.ts`), and
77
+ `(user-stories)/` (any `(group)`) for flows that cross pages. Export names are `<page><Outcome>Flow`. Not one file per
78
+ Block chain, not a flat folder of unrelated flows.
75
79
  - **Blocks carry no stubs; the flow does.** Demo highlights are ctx stubs chained on the Block where the flow uses it:
76
80
  `AddItemBlock.stubAfter((ctx) => { ctx.ring("cart", { selector: DemoSel.cartCount, label: "AC · item in cart" }); })`.
77
81
  Not a static `withHighlightFixtures({ "add-item": { stubAfter: {...} } })` map, and not an inline
@@ -79,19 +83,23 @@ against a live site, use `--skill-pilot-blind`.
79
83
  - **Reading a value into `mem` happens in `act`** (`mem.set(Key, value)`), like Mailpit's `extract-email-link`.
80
84
  `observe` is only for a Block that branches (it feeds `resolve`); an empty `act(){}` plus an `observe` that
81
85
  writes `mem` is a misuse.
86
+ - **Demo pace is a map option or config, never a wrapper.** `.method(SetBearerTokenBlock, { pace: "blitz" })` speeds one step,
87
+ `map({ demoPace: "fast" })` a flow, `demo: { pace, typingMs, slowMo }` in the config the whole project. Not `withBlockPace(...)`.
82
88
  - **Per-environment values** (a different `baseUrl` for staging) come from `.env` files read in the config function:
83
89
  `defineConfig(({ env }) => ({ baseUrl: env.BASE_URL }))`; `--env staging` loads `.env.staging`. Never edit the config to switch.
90
+ - **More than one config** (staging, another app) is another file, `config/waygraph.<name>.config.ts`, chosen with `--config <name>`.
91
+ The engine a config exports, and every engine in a flow, follows the selected config.
84
92
  - **A browser dialog (`confirm`, `beforeunload`) is answered with `withDialog(block, { accept, into })`**, not inside the
85
93
  Block's `act`; the click Block stays a plain click.
86
94
  - **Project settings live in `config/waygraph.config.ts`** (`defineConfig({ baseUrl, testDir, webServer,
87
- extensions, playwright: {...} })`). Flows import it and build the Engine from it (`new Engine(config)`); a project needs no
95
+ extensions, playwright: {...} })`). The config file also exports `engine = new Engine(config)`; flows `import { engine } from "../../config/waygraph.config.js"`; a project needs no
88
96
  `playwright.config.ts` - `waygraph test` builds one from it. A leftover `playwright.config.*` is deprecated.
89
97
  - **Another origin the test needs is a Map external** under `src/map/(external)/<tool>/`. For REST API
90
98
  tests, `(external)/hoppscotch` (list `extensions: ["hoppscotch"]` in the config) drives hoppscotch.io's client
91
99
  with the extension, so requests skip CORS: `set-method`, `set-url`, `set-headers`, `set-json-body`,
92
100
  `send-request`, `assert-status`, `read-response-body`, `assert-response-body`.
93
101
  - **Updating an older project:** `npm install waygraph@latest`, then `waygraph upgrade` (dry run) to see new
94
- and differing scaffold files; copy the paths you want with `--apply`.
102
+ and differing scaffold files; copy the paths you want with `--apply`. `waygraph upgrade --skills` refreshes these skills.
95
103
 
96
104
  ## Block kinds (file convention)
97
105
 
@@ -1,19 +0,0 @@
1
- import { defineConfig } from "@playwright/test";
2
- import { chromiumExecutablePath } from "waygraph";
3
-
4
- export default defineConfig({
5
- testDir: "./tests",
6
- fullyParallel: false,
7
- workers: 1,
8
- timeout: 30_000,
9
- reporter: [["list"], ["html", { open: "never" }]],
10
- use: {
11
- headless: true,
12
- baseURL: "https://www.saucedemo.com",
13
- trace: "retain-on-failure",
14
- launchOptions: {
15
- executablePath: chromiumExecutablePath(),
16
- args: ["--no-sandbox", "--disable-dev-shm-usage"],
17
- },
18
- },
19
- });
@@ -1,93 +0,0 @@
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();
@@ -1,126 +0,0 @@
1
- // Proves the dynamic add-to-cart menu for real: buildExploreMenu must offer
2
- // one row PER item still on the page (not one generic "add-to-cart" row tied
3
- // to the backpack), picking a non-default item must add THAT item (not the
4
- // backpack), and the menu must stop offering an item once it is in the cart.
5
- import { chromium } from "@playwright/test";
6
- import { join, dirname } from "node:path";
7
- import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { createRequire } from "node:module";
9
- import { chromiumExecutablePath } from "waygraph";
10
-
11
- const require = createRequire(import.meta.url);
12
- await import(require.resolve("tsx/esm"));
13
-
14
- const root = join(dirname(fileURLToPath(import.meta.url)), "..");
15
- const waygraphDist = join(root, "../../dist");
16
-
17
- const { buildExploreContext, buildExploreMenu } = await import(
18
- pathToFileURL(join(waygraphDist, "auto-explore.js")).href
19
- );
20
- const { MemPage, Engine, start, end } = await import(pathToFileURL(join(waygraphDist, "index.js")).href);
21
-
22
- const exe = chromiumExecutablePath();
23
-
24
- async function runBlock(engine, entry, context, page, mem) {
25
- const flow = engine.defineFlow([start, entry.block, end]);
26
- await flow.run(context, mem, { page, closeOnFinish: false });
27
- }
28
-
29
- const { graph, library } = await buildExploreContext(root);
30
- const mem = new MemPage();
31
- mem.set(library.byName.get("submit-login").block.requires[0], {
32
- username: "standard_user",
33
- password: "secret_sauce",
34
- });
35
-
36
- const browser = await chromium.launch({
37
- headless: true,
38
- ...(exe ? { executablePath: exe, args: ["--no-sandbox"] } : {}),
39
- });
40
- const context = await browser.newContext({
41
- baseURL: "https://www.saucedemo.com",
42
- viewport: { width: 1280, height: 900 },
43
- });
44
- const page = await context.newPage();
45
- const engine = new Engine({ headless: true });
46
-
47
- await page.goto("/", { waitUntil: "load" });
48
- await runBlock(engine, library.byName.get("submit-login"), context, page, mem);
49
- await page.waitForURL(/inventory\.html/);
50
-
51
- // 1. Menu on LoggedIn must offer one distinct add-to-cart row per product on
52
- // the inventory page - not a single generic row.
53
- const menu1 = await buildExploreMenu(page, graph, library, "LoggedIn");
54
- const addRows1 = menu1.flat.filter((e) => e.block === "add-to-cart");
55
- console.log(`add-to-cart rows on LoggedIn: ${addRows1.length}`);
56
- for (const row of addRows1) console.log(` - ${row.label}`);
57
- if (addRows1.length < 2) {
58
- throw new Error(`expected several distinct add-to-cart rows, got ${addRows1.length}`);
59
- }
60
- if (addRows1.some((r) => !r.instanceOption)) {
61
- throw new Error("every add-to-cart row must carry an instanceOption");
62
- }
63
- const backpackRow = addRows1.find((r) => r.instanceOption.value.id === "sauce-labs-backpack");
64
- const bikeLightRow = addRows1.find((r) => r.instanceOption.value.id === "sauce-labs-bike-light");
65
- if (!backpackRow || !bikeLightRow) {
66
- throw new Error("expected both the backpack and the bike light as separate rows");
67
- }
68
- if (backpackRow.label === bikeLightRow.label) {
69
- throw new Error("rows must have distinct, item-specific labels");
70
- }
71
-
72
- // 2. Picking the NON-default item (bike light, not the backpack) must add
73
- // exactly that item - proving mem really drives which product act() clicks.
74
- mem.set(bikeLightRow.instanceOption.key, bikeLightRow.instanceOption.value);
75
- await runBlock(engine, library.byName.get("add-to-cart"), context, page, mem);
76
-
77
- const bikeLightInCart = await page.locator('[data-test="remove-sauce-labs-bike-light"]').first().isVisible();
78
- const backpackStillInCart = await page.locator('[data-test="remove-sauce-labs-backpack"]').first().isVisible();
79
- console.log(`bike light in cart: ${bikeLightInCart}, backpack in cart (should be false): ${backpackStillInCart}`);
80
- if (!bikeLightInCart) throw new Error("expected the picked item (bike light) to be in the cart");
81
- if (backpackStillInCart) throw new Error("backpack should NOT be in the cart - it was never picked");
82
-
83
- // 3. Rebuilding the menu must now offer bike light's "Remove" row instead,
84
- // and drop it from the add-to-cart options - the menu reflects the live page.
85
- const menu2 = await buildExploreMenu(page, graph, library, "LoggedIn");
86
- const addRows2 = menu2.flat.filter((e) => e.block === "add-to-cart");
87
- if (addRows2.some((r) => r.instanceOption.value.id === "sauce-labs-bike-light")) {
88
- throw new Error("bike light should no longer be offered - it is already in the cart");
89
- }
90
- if (!addRows2.some((r) => r.instanceOption.value.id === "sauce-labs-backpack")) {
91
- throw new Error("backpack should still be offered - it was never added");
92
- }
93
- console.log(`add-to-cart rows after adding bike light: ${addRows2.length} (one fewer than before)`);
94
- if (addRows2.length !== addRows1.length - 1) {
95
- throw new Error(`expected exactly one fewer row (${addRows1.length - 1}), got ${addRows2.length}`);
96
- }
97
-
98
- // 4. Same rebuild must now offer Remove for the bike light (mem branch for
99
- // items already in the cart) - not only shrink the Add list.
100
- const removeRows = menu2.flat.filter((e) => e.block === "remove-from-cart");
101
- console.log(`remove-from-cart rows after add: ${removeRows.length}`);
102
- for (const row of removeRows) console.log(` - ${row.label}`);
103
- if (removeRows.length !== 1) {
104
- throw new Error(`expected exactly 1 Remove row (bike light), got ${removeRows.length}`);
105
- }
106
- if (removeRows[0].instanceOption.value.id !== "sauce-labs-bike-light") {
107
- throw new Error(`expected Remove for bike light, got ${removeRows[0].label}`);
108
- }
109
- if (!/Remove "Sauce Labs Bike Light" from cart/.test(removeRows[0].label)) {
110
- throw new Error(`unexpected Remove label: ${removeRows[0].label}`);
111
- }
112
-
113
- // 5. MemNav: Open "…" details for every inventory card (including ones already
114
- // in the cart) - separate from Add/Remove effect rows.
115
- const openRows = menu2.flat.filter((e) => e.block === "nav-item-detail");
116
- console.log(`nav-item-detail (MemNav) rows: ${openRows.length}`);
117
- for (const row of openRows) console.log(` - ${row.label}`);
118
- if (openRows.length < 6) {
119
- throw new Error(`expected 6 Open details rows, got ${openRows.length}`);
120
- }
121
- if (!openRows.some((r) => /Open "Test\.allTheThings\(\) T-Shirt \(Red\)" details/.test(r.label))) {
122
- throw new Error("missing Open details row for Test.allTheThings() T-Shirt (Red)");
123
- }
124
-
125
- console.log("\nPASS: add/remove Effect + open-details MemNav reflect the live page");
126
- await browser.close();
@@ -1,92 +0,0 @@
1
- import { chromium } from "@playwright/test";
2
- import { join, dirname } from "node:path";
3
- import { fileURLToPath, pathToFileURL } from "node:url";
4
- import { createRequire } from "node:module";
5
- import { chromiumExecutablePath } from "waygraph";
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 outDir = join(root, "../../.tmp/waygraph-auto");
12
- const waygraphDist = join(root, "../../dist");
13
-
14
- const { buildExploreContext, buildExploreMenu } = await import(
15
- pathToFileURL(join(waygraphDist, "auto-explore.js")).href
16
- );
17
- const { locate, MemPage, Engine, start, end } = await import(pathToFileURL(join(waygraphDist, "index.js")).href);
18
-
19
- const exe = chromiumExecutablePath();
20
-
21
- async function snap(page, name) {
22
- const path = join(outDir, `graph-proof-${name}.png`);
23
- await page.screenshot({ path, fullPage: false });
24
- console.log("screenshot", path);
25
- }
26
-
27
- async function resolveHere(page, navTags) {
28
- const path = new URL(page.url()).pathname;
29
- if (path.includes("inventory-item.html")) return "ItemDetailPage";
30
- if (path.includes("inventory.html")) return "LoggedIn";
31
- if (path.includes("cart.html")) return "CartPage";
32
- if (path.includes("checkout-complete.html")) return "OrderComplete";
33
- if (path === "/" || path === "") {
34
- if (await page.locator("#login-button").isVisible({ timeout: 500 }).catch(() => false)) return "LoginPage";
35
- }
36
- return locate(page, navTags, { timeoutMs: 1500 });
37
- }
38
-
39
- async function menuSummary(page, graph, library, label) {
40
- const navTags = library.navBlocks.map((n) => n.block);
41
- const resolved = await resolveHere(page, navTags);
42
- const menu = await buildExploreMenu(page, graph, library, resolved);
43
- console.log(`\n=== ${label} | here=${resolved} ===`);
44
- for (const s of menu.sections) {
45
- console.log(` ${s.title}:`);
46
- for (const e of s.edges) console.log(` ${e.block} (${e.kind}) → ${e.to}`);
47
- }
48
- return resolved;
49
- }
50
-
51
- async function runBlock(engine, entry, context, page, mem) {
52
- const flow = engine.defineFlow([start, entry.block, end]);
53
- await flow.run(context, mem, { page, closeOnFinish: false });
54
- }
55
-
56
- const { graph, library } = await buildExploreContext(root);
57
- const mem = new MemPage();
58
- mem.set(
59
- library.byName.get("submit-login").block.requires[0],
60
- { username: "standard_user", password: "secret_sauce" },
61
- );
62
-
63
- const browser = await chromium.launch({
64
- headless: true,
65
- ...(exe ? { executablePath: exe, args: ["--no-sandbox"] } : {}),
66
- });
67
- const context = await browser.newContext({ baseURL: "https://www.saucedemo.com", viewport: { width: 1280, height: 900 } });
68
- const page = await context.newPage();
69
- const engine = new Engine({ headless: true });
70
-
71
- await page.goto("/", { waitUntil: "load" });
72
- await menuSummary(page, graph, library, "1. Login page");
73
- await snap(page, "1-login-menu");
74
-
75
- await runBlock(engine, library.byName.get("submit-login"), context, page, mem);
76
- await page.waitForURL(/inventory\.html/);
77
- const here2 = await menuSummary(page, graph, library, "2. After submit-login → LoggedIn");
78
- if (here2 !== "LoggedIn") throw new Error(`expected LoggedIn got ${here2}`);
79
- await snap(page, "2-logged-in-menu");
80
-
81
- const logout = library.byName.get("submit-logout");
82
- const hasLogout = (await buildExploreMenu(page, graph, library, "LoggedIn")).flat.some((e) => e.block === "submit-logout");
83
- if (!hasLogout) throw new Error("graph menu missing submit-logout on LoggedIn");
84
-
85
- await runBlock(engine, logout, context, page, mem);
86
- await page.waitForSelector("#login-button");
87
- const here3 = await menuSummary(page, graph, library, "3. After submit-logout → LoginPage");
88
- if (here3 !== "LoginPage") throw new Error(`expected LoginPage got ${here3}`);
89
- await snap(page, "3-back-to-login-menu");
90
-
91
- console.log("\nPASS: graph cycle LoginPage → LoggedIn → LoginPage");
92
- await browser.close();
@@ -1,63 +0,0 @@
1
- /**
2
- * Headless runtime proof: wrong password -> LoginPage + error banner.
3
- * Prints PASS/FAIL to stdout only (no screenshots, no visible browser).
4
- */
5
- import { chromium } from "@playwright/test";
6
- import { join, dirname } from "node:path";
7
- import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { createRequire } from "node:module";
9
- import { chromiumExecutablePath } from "waygraph";
10
-
11
- const require = createRequire(import.meta.url);
12
- await import(require.resolve("tsx/esm"));
13
-
14
- const root = join(dirname(fileURLToPath(import.meta.url)), "..");
15
- const waygraphDist = join(root, "../../dist");
16
- const { Engine, start, end, MemPage } = await import(pathToFileURL(join(waygraphDist, "index.js")).href);
17
- const { NavLoginBlock } = await import(pathToFileURL(join(root, "src/blocks/saucedemo-web/nav-login.block.js")).href);
18
- const { FillUsernameBlock } = await import(
19
- pathToFileURL(join(root, "src/blocks/saucedemo-web/methods/fill-username.method.block.js")).href
20
- );
21
- const { FillPasswordBlock } = await import(
22
- pathToFileURL(join(root, "src/blocks/saucedemo-web/methods/fill-password.method.block.js")).href
23
- );
24
- const { SubmitLoginBlock } = await import(
25
- pathToFileURL(join(root, "src/blocks/saucedemo-web/methods/submit-login.method.block.js")).href
26
- );
27
- const { LoginCreds } = await import(pathToFileURL(join(root, "src/states/checkout.mem-keys.js")).href);
28
-
29
- const exe = chromiumExecutablePath();
30
- const baseURL = "https://www.saucedemo.com";
31
-
32
- console.log("RUNTIME: headless browser (wrong password test)...");
33
- console.log("RUNTIME: baseURL =", baseURL);
34
-
35
- const browser = await chromium.launch({
36
- headless: true,
37
- ...(exe ? { executablePath: exe, args: ["--no-sandbox", "--disable-dev-shm-usage"] } : {}),
38
- });
39
- const context = await browser.newContext({ baseURL, viewport: { width: 1280, height: 900 } });
40
- const page = await context.newPage();
41
- const engine = new Engine({ headless: true });
42
- const mem = new MemPage();
43
- mem.set(LoginCreds({ username: "standard_user", password: "wrong-password" }));
44
-
45
- const flow = engine.defineFlow([start, NavLoginBlock, FillUsernameBlock, FillPasswordBlock, SubmitLoginBlock, end]);
46
- const runOut = await flow.run(context, mem, { page, closeOnFinish: false });
47
- const result = runOut && typeof runOut === "object" && "result" in runOut ? runOut.result : runOut;
48
-
49
- const errorVisible = await page.locator('[data-test="error"]').isVisible();
50
- const url = page.url();
51
-
52
- console.log("RUNTIME: checkpoint =", result.__state);
53
- console.log("RUNTIME: url =", url);
54
- console.log("RUNTIME: error banner visible =", errorVisible);
55
-
56
- if (result.__state !== "LoginPage" || !errorVisible) {
57
- console.error("RUNTIME: FAIL");
58
- await browser.close();
59
- process.exit(1);
60
- }
61
-
62
- console.log("RUNTIME: PASS — stayed on LoginPage with error banner.");
63
- await browser.close();
@@ -1,14 +0,0 @@
1
- import { defineConfig } from "@playwright/test";
2
-
3
- export default defineConfig({
4
- testDir: "./tests",
5
- fullyParallel: false,
6
- workers: 1,
7
- timeout: 30_000,
8
- reporter: [["list"], ["html", { open: "never" }]],
9
- use: {
10
- headless: true,
11
- baseURL: "https://www.saucedemo.com",
12
- trace: "retain-on-failure",
13
- },
14
- });