waygraph 0.7.16 → 0.10.1

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 (83) 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 +53 -11
  7. package/dist/engine.js +81 -8
  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 +152 -8
  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/checkout-step-one/NAV.md +1 -1
  25. 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
  26. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/NAV.md +1 -1
  27. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/{actions/finish-order.action.block.ts → methods/finish-order.method.block.ts} +5 -5
  28. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/NAV.md +17 -5
  29. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +37 -0
  30. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts +55 -0
  31. 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
  32. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts +40 -20
  33. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts +55 -0
  34. 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
  35. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +4 -4
  36. package/examples/saucedemo/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts +2 -2
  37. package/examples/saucedemo/src/blocks/saucedemo-web/{actions/submit-login.action.block.ts → methods/submit-login.method.block.ts} +5 -5
  38. 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
  39. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +19 -0
  40. package/examples/saucedemo/src/flows/cart-mem.flow.ts +1 -1
  41. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
  42. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +4 -4
  43. package/examples/saucedemo/src/flows/checkout.flow.ts +4 -4
  44. package/examples/saucedemo/src/flows/login.flow.ts +1 -1
  45. package/examples/saucedemo/src/flows/shop.flow.ts +3 -3
  46. package/examples/saucedemo/src/flows/smoke.flow.ts +2 -2
  47. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
  48. package/examples/saucedemo/tests/flow-block-verify.spec.ts +1 -1
  49. package/examples/saucedemo/tests/submit-login-branch.spec.ts +1 -1
  50. package/package.json +1 -1
  51. package/templates/quickstart/README-saucedemo-commands.md +72 -0
  52. package/templates/quickstart/docs/CONVENTION.md +5 -5
  53. package/templates/quickstart/docs/HELPERS.md +152 -8
  54. package/templates/quickstart/package.json +9 -5
  55. package/templates/quickstart/scripts/drive-auto-cli.mjs +93 -0
  56. package/templates/quickstart/src/blocks/SITE-MAP.md +11 -8
  57. package/templates/quickstart/src/blocks/saucedemo-web/NAV.md +3 -3
  58. package/templates/quickstart/src/blocks/saucedemo-web/cart/NAV.md +1 -1
  59. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/NAV.md +1 -1
  60. 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
  61. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/NAV.md +1 -1
  62. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/{actions/finish-order.action.block.ts → methods/finish-order.method.block.ts} +5 -5
  63. package/templates/quickstart/src/blocks/saucedemo-web/inventory/NAV.md +17 -5
  64. package/templates/quickstart/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +37 -0
  65. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts +55 -0
  66. 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
  67. package/templates/quickstart/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts +40 -20
  68. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts +55 -0
  69. 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
  70. package/templates/quickstart/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +1 -1
  71. package/templates/quickstart/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts +2 -2
  72. package/templates/quickstart/src/blocks/saucedemo-web/{actions/submit-login.action.block.ts → methods/submit-login.method.block.ts} +5 -5
  73. 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
  74. package/templates/quickstart/src/flows/cart-bulk.flow.ts +19 -0
  75. package/templates/quickstart/src/flows/cart-mem.flow.ts +1 -1
  76. package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
  77. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +4 -4
  78. package/templates/quickstart/src/flows/checkout.flow.ts +4 -4
  79. package/templates/quickstart/src/flows/login.flow.ts +1 -1
  80. package/templates/quickstart/src/flows/shop.flow.ts +3 -3
  81. package/templates/quickstart/src/flows/smoke.flow.ts +2 -2
  82. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
  83. package/templates/scaffold/package.json +1 -1
@@ -8,18 +8,22 @@
8
8
  "typecheck": "tsc --noEmit",
9
9
  "check": "waygraph check",
10
10
  "auto": "waygraph auto",
11
- "auto:cli": "waygraph auto --cli",
11
+ "auto:cli": "waygraph auto --cli --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
12
12
  "graph": "waygraph graph",
13
13
  "graph:mermaid": "waygraph graph --mermaid",
14
- "demo": "waygraph chain \"loginFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}) then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}) then viewerBlockedFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}})\" --step --no-autoplay",
15
- "demo:autoplay": "waygraph chain \"loginFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}) then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}) then viewerBlockedFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}})\" --step --autoplay",
16
- "record": "waygraph chain \"loginFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}) then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}) then viewerBlockedFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}})\" --video --no-step"
14
+ "demo": "waygraph demo --blocks \"loginFlow then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}) then viewerBlockedFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}})\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
15
+ "demo:auto-next": "waygraph demo --blocks \"loginFlow then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}) then viewerBlockedFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}})\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-next",
16
+ "record": "waygraph run --blocks \"loginFlow then shopFlow({\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}) then viewerBlockedFlow({\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}})\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --video --no-step",
17
+ "demo:bulk": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
18
+ "demo:bulk:auto-next": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-next --title \"Cart bulk demo\"",
19
+ "demo:bulk:video": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-play-video --title \"Cart bulk demo\"",
20
+ "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\\\"}}\""
17
21
  },
18
22
  "waygraph": {
19
23
  "baseUrl": "https://www.saucedemo.com"
20
24
  },
21
25
  "dependencies": {
22
- "waygraph": "^0.7.16"
26
+ "waygraph": "^0.10.1"
23
27
  },
24
28
  "devDependencies": {
25
29
  "@playwright/test": "^1.55.0",
@@ -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();
@@ -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
 
@@ -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 { defineActionBlock, 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: defineActionBlock
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 = defineActionBlock<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 { defineActionBlock, 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: defineActionBlock
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 = defineActionBlock<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
+ });
@@ -0,0 +1,55 @@
1
+ import { defineMethodBlock, checkpoint, type Checkpoint } from "waygraph";
2
+ import type { RemoveFromCartOutcome } from "../../../../states/checkout.states.js";
3
+ import { collectAddableItems, InventorySel } from "./inventory-items.js";
4
+
5
+ type BulkObserve = "added" | "nothing";
6
+
7
+ /**
8
+ * Kind: Method
9
+ * Helper: defineMethodBlock
10
+ * Route: saucedemo-web/inventory/methods/ (= /inventory.html)
11
+ *
12
+ * Bulk: click every live "Add to cart" once. No instanceOptions - one auto
13
+ * menu row. In is Checkpoint<string> (from: "*") so back-and-forth works:
14
+ * leave inventory and return, collectors re-read Add buttons from the DOM.
15
+ * Out: ItemInCart if the cart has items after; LoggedIn if still empty.
16
+ */
17
+ export const AddAllToCartBlock = defineMethodBlock<
18
+ Checkpoint<string>,
19
+ RemoveFromCartOutcome
20
+ >({
21
+ name: "add-all-to-cart",
22
+ description: "Adds every product still showing Add to cart on this page.",
23
+ instruction: {
24
+ async act(page) {
25
+ const items = await collectAddableItems(page);
26
+ for (const item of items) {
27
+ await page.locator(InventorySel.addBtn(item.id)).click();
28
+ }
29
+ },
30
+ async observe(page): Promise<BulkObserve> {
31
+ const badge = await page.locator(InventorySel.cartBadge).count();
32
+ return badge > 0 ? "added" : "nothing";
33
+ },
34
+ resolve: (observed) =>
35
+ checkpoint(observed === "added" ? "ItemInCart" : "LoggedIn"),
36
+ verify: (out) =>
37
+ out.__state === "ItemInCart"
38
+ ? [
39
+ {
40
+ name: "cart-has-items",
41
+ async check(page) {
42
+ return (await page.locator(InventorySel.cartBadge).count()) > 0;
43
+ },
44
+ },
45
+ ]
46
+ : [
47
+ {
48
+ name: "cart-still-empty",
49
+ async check(page) {
50
+ return (await page.locator(InventorySel.cartBadge).count()) === 0;
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ });
@@ -1,12 +1,12 @@
1
1
  import { defineEffectBlock, checkpoint, type Checkpoint, type WaygraphInstanceOption } from "waygraph";
2
2
  import type { ItemInCart } from "../../../../states/checkout.states.js";
3
3
  import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
4
- import { collectAddableItems } from "./inventory-items.js";
4
+ import { collectAddableItems, InventorySel } from "./inventory-items.js";
5
5
 
6
6
  /**
7
7
  * Kind: Effect
8
8
  * Helper: defineEffectBlock
9
- * Route: saucedemo-web/inventory/actions/ (= /inventory.html)
9
+ * Route: saucedemo-web/inventory/methods/ (= /inventory.html)
10
10
  *
11
11
  * Mutates cart for the mem-picked product. waygraph auto lists one
12
12
  * "Add ..." row per live Add button via instanceOptions.
@@ -18,7 +18,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
18
18
  instruction: {
19
19
  async act(page, _input, mem) {
20
20
  const { id } = mem.get(SelectedItem.key);
21
- await page.locator(`[data-test="add-to-cart-${id}"]`).click();
21
+ await page.locator(InventorySel.addBtn(id)).click();
22
22
  },
23
23
  resolve: () => checkpoint("ItemInCart"),
24
24
  verify: [
@@ -26,11 +26,11 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
26
26
  name: "item-added-to-cart",
27
27
  async check(page, mem) {
28
28
  const { id } = mem.get(SelectedItem.key);
29
- return await page.locator(`[data-test="remove-${id}"]`).first().isVisible();
29
+ return await page.locator(InventorySel.removeBtn(id)).first().isVisible();
30
30
  },
31
31
  },
32
32
  ],
33
- highlights: () => [{ selector: ".shopping_cart_badge", label: "remember this: cart now has the item" }],
33
+ highlights: () => [{ selector: InventorySel.cartBadge, label: "remember this: cart now has the item" }],
34
34
  },
35
35
  async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
36
36
  const items = await collectAddableItems(page);
@@ -39,7 +39,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
39
39
  label: `Add "${item.name}" to cart`,
40
40
  key: SelectedItem.key,
41
41
  value: item,
42
- highlight: `[data-test="add-to-cart-${item.id}"]`,
42
+ highlight: InventorySel.addBtn(item.id),
43
43
  }));
44
44
  },
45
45
  });
@@ -2,27 +2,45 @@ import type { Page, Locator } from "@playwright/test";
2
2
 
3
3
  /**
4
4
  * Kind: helper (not a Block)
5
- * Route: saucedemo-web/inventory/actions/ (= /inventory.html)
5
+ * Route: saucedemo-web/inventory/methods/ (= /inventory.html)
6
6
  *
7
- * DOM collectors for Effect / MemNav instanceOptions. Discovery ignores this
8
- * file (no *.block.ts suffix). Keep selectors here so add/remove/detail stay thin.
7
+ * DOM selectors + collectors for Effect / MemNav instanceOptions. Discovery
8
+ * ignores this file (no *.block.ts suffix). InventorySel = strings only;
9
+ * collectors use Sel to spawn auto menu rows.
9
10
  */
10
11
 
11
12
  /** Product slug + display name (saucedemo inventory / detail / cart). */
12
13
  export type InventoryItem = { id: string; name: string };
13
14
 
15
+ /** DOM selectors for this screen - static strings + per-id factories. */
16
+ export const InventorySel = {
17
+ list: ".inventory_list",
18
+ card: ".inventory_item",
19
+ name: ".inventory_item_name",
20
+ detailName: ".inventory_details_name",
21
+ cartItem: ".cart_item",
22
+ cartBadge: ".shopping_cart_badge",
23
+ addBtn: (id: string) => `[data-test="add-to-cart-${id}"]`,
24
+ removeBtn: (id: string) => `[data-test="remove-${id}"]`,
25
+ addBtnPrefix: 'button[data-test^="add-to-cart-"]',
26
+ removeBtnPrefix: 'button[data-test^="remove-"]',
27
+ title: (id: string) =>
28
+ `div.inventory_item:has(button[data-test="add-to-cart-${id}"], button[data-test="remove-${id}"]) ` +
29
+ `.inventory_item_name`,
30
+ };
31
+
14
32
  async function readItemName(page: Page, button: Locator, id: string): Promise<string> {
15
33
  const card = button.locator("xpath=ancestor::div[contains(@class,'inventory_item')][1]");
16
34
  if ((await card.count()) > 0) {
17
- const text = await card.locator(".inventory_item_name").first().textContent().catch(() => null);
35
+ const text = await card.locator(InventorySel.name).first().textContent({ timeout: 500 }).catch(() => null);
18
36
  if (text?.trim()) return text.trim();
19
37
  }
20
38
  const cartRow = button.locator("xpath=ancestor::div[contains(@class,'cart_item')][1]");
21
39
  if ((await cartRow.count()) > 0) {
22
- const text = await cartRow.locator(".inventory_item_name").first().textContent().catch(() => null);
40
+ const text = await cartRow.locator(InventorySel.name).first().textContent({ timeout: 500 }).catch(() => null);
23
41
  if (text?.trim()) return text.trim();
24
42
  }
25
- const detailName = await page.locator(".inventory_details_name").first().textContent().catch(() => null);
43
+ const detailName = await page.locator(InventorySel.detailName).first().textContent({ timeout: 500 }).catch(() => null);
26
44
  if (detailName?.trim()) return detailName.trim();
27
45
  return id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
28
46
  }
@@ -33,7 +51,7 @@ async function collectByPrefix(page: Page, prefix: "add-to-cart-" | "remove-"):
33
51
  const items: InventoryItem[] = [];
34
52
  for (let i = 0; i < count; i++) {
35
53
  const button = buttons.nth(i);
36
- const testId = await button.getAttribute("data-test");
54
+ const testId = await button.getAttribute("data-test", { timeout: 500 }).catch(() => null);
37
55
  const id = testId?.replace(new RegExp(`^${prefix}`), "");
38
56
  if (!id) continue;
39
57
  items.push({ id, name: await readItemName(page, button, id) });
@@ -56,32 +74,34 @@ export function collectRemovableItems(page: Page): Promise<InventoryItem[]> {
56
74
  * Used by MemNavBlock (open item details) - not filtered by cart state.
57
75
  */
58
76
  export async function collectInventoryItems(page: Page): Promise<InventoryItem[]> {
59
- const cards = page.locator(".inventory_item");
77
+ const cards = page.locator(InventorySel.card);
60
78
  const count = await cards.count();
61
79
  const items: InventoryItem[] = [];
62
80
  for (let i = 0; i < count; i++) {
63
81
  const card = cards.nth(i);
64
- const addTest = await card.locator('button[data-test^="add-to-cart-"]').first().getAttribute("data-test").catch(() => null);
65
- const removeTest = await card
66
- .locator('button[data-test^="remove-"]')
67
- .first()
68
- .getAttribute("data-test")
69
- .catch(() => null);
82
+ // count() first - getAttribute on a missing locator waits the full default timeout.
83
+ const addLoc = card.locator(InventorySel.addBtnPrefix);
84
+ const removeLoc = card.locator(InventorySel.removeBtnPrefix);
85
+ const addTest =
86
+ (await addLoc.count()) > 0
87
+ ? await addLoc.first().getAttribute("data-test", { timeout: 500 }).catch(() => null)
88
+ : null;
89
+ const removeTest =
90
+ (await removeLoc.count()) > 0
91
+ ? await removeLoc.first().getAttribute("data-test", { timeout: 500 }).catch(() => null)
92
+ : null;
70
93
  const id =
71
94
  addTest?.replace(/^add-to-cart-/, "") ?? removeTest?.replace(/^remove-/, "") ?? null;
72
95
  if (!id) continue;
73
96
  const name =
74
- (await card.locator(".inventory_item_name").first().textContent().catch(() => null))?.trim() ??
97
+ (await card.locator(InventorySel.name).first().textContent({ timeout: 500 }).catch(() => null))?.trim() ??
75
98
  id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
76
99
  items.push({ id, name });
77
100
  }
78
101
  return items;
79
102
  }
80
103
 
81
- /** Click the product title link inside the card that owns this item id. */
104
+ /** @deprecated Prefer {@link InventorySel.title} */
82
105
  export function inventoryItemTitleSelector(id: string): string {
83
- return (
84
- `div.inventory_item:has(button[data-test="add-to-cart-${id}"], button[data-test="remove-${id}"]) ` +
85
- `.inventory_item_name`
86
- );
106
+ return InventorySel.title(id);
87
107
  }
@@ -0,0 +1,55 @@
1
+ import { defineMethodBlock, checkpoint, type Checkpoint } from "waygraph";
2
+ import type { RemoveFromCartOutcome } from "../../../../states/checkout.states.js";
3
+ import { collectRemovableItems, InventorySel } from "./inventory-items.js";
4
+
5
+ type BulkObserve = "still-has-items" | "cart-empty";
6
+
7
+ /**
8
+ * Kind: Method
9
+ * Helper: defineMethodBlock
10
+ * Route: saucedemo-web/inventory/methods/ (= /inventory.html)
11
+ *
12
+ * Bulk: click every live "Remove" once. No instanceOptions - one auto menu
13
+ * row. In is Checkpoint<string> (from: "*") so round-trips work the same as
14
+ * single-item remove. Out: LoggedIn when cart empty; ItemInCart if anything
15
+ * remains (partial failure / race).
16
+ */
17
+ export const RemoveAllFromCartBlock = defineMethodBlock<
18
+ Checkpoint<string>,
19
+ RemoveFromCartOutcome
20
+ >({
21
+ name: "remove-all-from-cart",
22
+ description: "Removes every product still showing Remove on this page.",
23
+ instruction: {
24
+ async act(page) {
25
+ const items = await collectRemovableItems(page);
26
+ for (const item of items) {
27
+ await page.locator(InventorySel.removeBtn(item.id)).click();
28
+ }
29
+ },
30
+ async observe(page): Promise<BulkObserve> {
31
+ const left = await collectRemovableItems(page);
32
+ return left.length > 0 ? "still-has-items" : "cart-empty";
33
+ },
34
+ resolve: (observed) =>
35
+ checkpoint(observed === "still-has-items" ? "ItemInCart" : "LoggedIn"),
36
+ verify: (out) =>
37
+ out.__state === "ItemInCart"
38
+ ? [
39
+ {
40
+ name: "cart-still-has-items",
41
+ async check(page) {
42
+ return (await collectRemovableItems(page)).length > 0;
43
+ },
44
+ },
45
+ ]
46
+ : [
47
+ {
48
+ name: "cart-empty",
49
+ async check(page) {
50
+ return (await page.locator(InventorySel.cartBadge).count()) === 0;
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ });
@@ -1,14 +1,14 @@
1
1
  import { defineEffectBlock, checkpoint, type Checkpoint, type WaygraphInstanceOption } from "waygraph";
2
2
  import type { RemoveFromCartOutcome } from "../../../../states/checkout.states.js";
3
3
  import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
4
- import { collectRemovableItems } from "./inventory-items.js";
4
+ import { collectRemovableItems, InventorySel } from "./inventory-items.js";
5
5
 
6
6
  type RemoveObserve = "still-has-items" | "cart-empty";
7
7
 
8
8
  /**
9
9
  * Kind: Effect
10
10
  * Helper: defineEffectBlock
11
- * Route: saucedemo-web/inventory/actions/ (= /inventory.html)
11
+ * Route: saucedemo-web/inventory/methods/ (= /inventory.html)
12
12
  *
13
13
  * Removes the mem-picked product. After Add, waygraph auto shows matching
14
14
  * "Remove ..." rows from live Remove buttons.
@@ -20,7 +20,7 @@ export const RemoveFromCartBlock = defineEffectBlock<Checkpoint<string>, RemoveF
20
20
  instruction: {
21
21
  async act(page, _input, mem) {
22
22
  const { id } = mem.get(SelectedItem.key);
23
- await page.locator(`[data-test="remove-${id}"]`).click();
23
+ await page.locator(InventorySel.removeBtn(id)).click();
24
24
  },
25
25
  async observe(page): Promise<RemoveObserve> {
26
26
  const left = await collectRemovableItems(page);
@@ -42,7 +42,7 @@ export const RemoveFromCartBlock = defineEffectBlock<Checkpoint<string>, RemoveF
42
42
  {
43
43
  name: "cart-empty",
44
44
  async check(page) {
45
- return (await page.locator(".shopping_cart_badge").count()) === 0;
45
+ return (await page.locator(InventorySel.cartBadge).count()) === 0;
46
46
  },
47
47
  },
48
48
  ],
@@ -54,7 +54,7 @@ export const RemoveFromCartBlock = defineEffectBlock<Checkpoint<string>, RemoveF
54
54
  label: `Remove "${item.name}" from cart`,
55
55
  key: SelectedItem.key,
56
56
  value: item,
57
- highlight: `[data-test="remove-${item.id}"]`,
57
+ highlight: InventorySel.removeBtn(item.id),
58
58
  }));
59
59
  },
60
60
  });