waygraph 0.15.53 → 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 (108) hide show
  1. package/dist/analysis/graph.d.ts +3 -0
  2. package/dist/auto-session/helpers.d.ts +10 -0
  3. package/dist/blocks-select.d.ts +3 -0
  4. package/dist/blocks-select.js +2 -0
  5. package/dist/config.d.ts +2 -0
  6. package/dist/core/blocks/api.d.ts +1 -0
  7. package/dist/core/blocks/method.d.ts +2 -0
  8. package/dist/core/blocks/method.js +1 -0
  9. package/dist/core/blocks/nav.d.ts +1 -0
  10. package/dist/core/blocks/page.d.ts +1 -0
  11. package/dist/core/branch-regression.d.ts +1 -0
  12. package/dist/core/compose.d.ts +1 -0
  13. package/dist/core/flow.d.ts +1 -0
  14. package/dist/core/map-builder.d.ts +8 -1
  15. package/dist/core/map-builder.js +1 -1
  16. package/dist/core/run-graph.d.ts +2 -0
  17. package/dist/core/run-graph.js +1 -0
  18. package/dist/errors.d.ts +1 -0
  19. package/dist/extensions.d.ts +1 -0
  20. package/dist/extensions.js +1 -0
  21. package/dist/highlights/demo-pace.d.ts +4 -0
  22. package/dist/highlights/demo-pace.js +4 -0
  23. package/dist/highlights/device.d.ts +2 -0
  24. package/dist/highlights/device.js +2 -0
  25. package/dist/highlights/types.d.ts +6 -0
  26. package/dist/session/auto-explore-run.js +6 -0
  27. package/dist/session/overlay-beacon.d.ts +7 -0
  28. package/dist/session/overlay-beacon.js +5 -0
  29. package/dist/session/pilot.d.ts +1 -0
  30. package/dist/traverse/coverage.d.ts +4 -0
  31. package/dist/traverse/coverage.js +3 -0
  32. package/dist/traverse/lease.d.ts +1 -0
  33. package/dist/traverse/lease.js +1 -0
  34. package/dist/traverse/run.d.ts +3 -0
  35. package/examples/routed-demo/src/flows/routed-demo-map.flow.ts +1 -1
  36. package/examples/routed-demo/src/flows/routed-demo.flow.ts +2 -2
  37. package/examples/saucedemo/README.md +11 -12
  38. package/examples/saucedemo/config/waygraph.config.ts +17 -0
  39. package/examples/saucedemo/docs/CONVENTION.md +1 -2
  40. package/examples/saucedemo/docs/HELPERS.md +1 -1
  41. package/examples/saucedemo/package-lock.json +10 -3
  42. package/examples/saucedemo/package.json +12 -29
  43. package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-cart.block.ts +0 -49
  44. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +22 -25
  45. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +0 -6
  46. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +38 -96
  47. package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +1 -26
  48. package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +1 -45
  49. package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +14 -77
  50. package/examples/saucedemo/src/flows/api-demo.flow.ts +17 -11
  51. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +19 -14
  52. package/examples/saucedemo/src/flows/cart-continue.flow.ts +10 -8
  53. package/examples/saucedemo/src/flows/cart-mem.flow.ts +7 -7
  54. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +13 -8
  55. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +26 -48
  56. package/examples/saucedemo/src/flows/checkout.flow.ts +70 -86
  57. package/examples/saucedemo/src/flows/device-showcase.flow.ts +24 -25
  58. package/examples/saucedemo/src/flows/login.flow.ts +11 -18
  59. package/examples/saucedemo/src/flows/mem-stub-demo.flow.ts +9 -20
  60. package/examples/saucedemo/src/flows/shop.flow.ts +15 -20
  61. package/examples/saucedemo/src/flows/smoke.flow.ts +12 -27
  62. package/examples/saucedemo/src/flows/stub-on-error.flow.ts +24 -43
  63. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +26 -34
  64. package/examples/saucedemo/src/states/checkout.mem-stubs.ts +11 -0
  65. package/examples/saucedemo/src/stubs/device-showcase.ts +193 -0
  66. package/examples/saucedemo/src/stubs/login.ts +37 -0
  67. package/examples/saucedemo/tests/chain-flow-episodes.spec.ts +7 -9
  68. package/examples/saucedemo/tests/submit-login-branch.spec.ts +18 -5
  69. package/package.json +3 -2
  70. package/templates/quickstart/README.md +4 -7
  71. package/templates/quickstart/config/waygraph.config.ts +17 -0
  72. package/templates/quickstart/docs/CONVENTION.md +1 -2
  73. package/templates/quickstart/docs/HELPERS.md +1 -1
  74. package/templates/quickstart/package.json +14 -14
  75. package/templates/quickstart/src/blocks/SITE-MAP.md +14 -3
  76. package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +0 -49
  77. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +22 -25
  78. package/templates/quickstart/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +0 -6
  79. package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +38 -96
  80. package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +1 -26
  81. package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +1 -45
  82. package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +14 -77
  83. package/templates/quickstart/src/flows/api-demo.flow.ts +43 -0
  84. package/templates/quickstart/src/flows/cart-bulk.flow.ts +19 -14
  85. package/templates/quickstart/src/flows/cart-continue.flow.ts +10 -8
  86. package/templates/quickstart/src/flows/cart-mem.flow.ts +7 -7
  87. package/templates/quickstart/src/flows/catalog-logout.flow.ts +13 -8
  88. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +26 -48
  89. package/templates/quickstart/src/flows/checkout.flow.ts +70 -63
  90. package/templates/quickstart/src/flows/device-showcase.flow.ts +24 -25
  91. package/templates/quickstart/src/flows/login.flow.ts +11 -18
  92. package/templates/quickstart/src/flows/mem-stub-demo.flow.ts +12 -0
  93. package/templates/quickstart/src/flows/shop.flow.ts +15 -20
  94. package/templates/quickstart/src/flows/smoke.flow.ts +12 -27
  95. package/templates/quickstart/src/flows/stub-on-error.flow.ts +31 -0
  96. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +26 -34
  97. package/templates/quickstart/src/states/checkout.mem-stubs.ts +11 -0
  98. package/templates/quickstart/src/stubs/device-showcase.ts +193 -0
  99. package/templates/quickstart/src/stubs/login.ts +37 -0
  100. package/templates/quickstart/tests/chain-flow.spec.ts +7 -10
  101. package/templates/skills/waygraph-convention.skill.md +1 -1
  102. package/examples/saucedemo/playwright.config.ts +0 -19
  103. package/examples/saucedemo/scripts/drive-auto-cli.mjs +0 -93
  104. package/examples/saucedemo/scripts/prove-add-to-cart-menu.mjs +0 -126
  105. package/examples/saucedemo/scripts/prove-auto-graph.mjs +0 -92
  106. package/examples/saucedemo/scripts/prove-wrong-login-runtime.mjs +0 -63
  107. package/templates/quickstart/playwright.config.ts +0 -14
  108. package/templates/quickstart/scripts/drive-auto-cli.mjs +0 -93
@@ -4,30 +4,30 @@
4
4
  "type": "module",
5
5
  "description": "Self-contained saucedemo quickstart (copied into a temp dir by `waygraph try demo`).",
6
6
  "scripts": {
7
+ "typecheck": "tsc --noEmit",
7
8
  "test": "waygraph test",
8
9
  "test:ui": "waygraph test ui",
9
- "typecheck": "tsc --noEmit",
10
+ "flows": "waygraph list",
10
11
  "check": "waygraph check",
11
- "auto": "waygraph auto",
12
- "auto:cli": "waygraph auto --cli --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
13
- "traverse": "waygraph traverse --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 40",
14
- "traverse:parallel": "waygraph traverse --parallel 2 --session clone --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 30",
15
- "traverse:coverage": "waygraph traverse --min-edge-coverage 10% --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 40",
16
12
  "graph": "waygraph graph",
17
13
  "graph:mermaid": "waygraph graph --mermaid",
18
- "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\\\"}}\"",
19
- "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",
20
- "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",
21
- "demo:bulk": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
22
- "demo:bulk:auto-next": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-next --title \"Cart bulk demo\"",
23
- "demo:bulk:video": "waygraph demo --blocks cartBulkFlow --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --auto-play-video --title \"Cart bulk demo\"",
24
- "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\\\"}}\""
14
+ "auto": "waygraph auto",
15
+ "auto:cli": "waygraph auto --cli",
16
+ "demo": "waygraph demo --blocks \"loginFlow then shopFlow\"",
17
+ "demo:auto-next": "waygraph demo --blocks \"loginFlow then shopFlow\" --auto-next",
18
+ "demo:checkout": "waygraph demo --blocks checkoutFlow --auto-next",
19
+ "demo:device": "waygraph demo --blocks deviceShowcaseFlow --auto-next",
20
+ "demo:bulk": "waygraph demo --blocks cartBulkFlow --auto-next",
21
+ "demo:api": "waygraph demo --blocks apiDemoFlow --auto-next",
22
+ "demo:video": "waygraph demo --blocks deviceShowcaseFlow --auto-play-video",
23
+ "record": "waygraph run --blocks \"loginFlow then shopFlow\" --video --no-step",
24
+ "traverse": "waygraph traverse --max-steps 40"
25
25
  },
26
26
  "waygraph": {
27
27
  "baseUrl": "https://www.saucedemo.com"
28
28
  },
29
29
  "dependencies": {
30
- "waygraph": "^0.12.20"
30
+ "waygraph": "latest"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@playwright/test": "^1.55.0",
@@ -8,11 +8,15 @@ src/blocks/
8
8
  SITE-MAP.md
9
9
  saucedemo-web/ # URL /
10
10
  NAV.md
11
- nav-login.block.ts # Nav defineNavBlock url:/
11
+ nav-login.block.ts # Page definePageBlock url:/
12
+ ff-owner-auth.block.ts # FFCompose (nav-login + fill-username + fill-password + submit-login)
12
13
  methods/
13
- submit-login.method.block.ts # Method defineMethodBlock
14
+ fill-username.method.block.ts # Method defineMethodBlock (self-loop on LoginPage)
15
+ fill-password.method.block.ts # Method defineMethodBlock (self-loop on LoginPage)
16
+ submit-login.method.block.ts # Method defineMethodBlock (click only; branches)
14
17
  submit-login-for-flow.ts # helper (not a discoverable *.block.ts alone)
15
18
  submit-logout.method.block.ts # Method defineMethodBlock
19
+ login.sel.ts # LoginSel (not a Block)
16
20
  inventory/ # URL /inventory.html
17
21
  inventory.page.block.ts # Page definePageBlock
18
22
  NAV.md
@@ -25,20 +29,27 @@ src/blocks/
25
29
  remove-from-cart.effect.block.ts
26
30
  inventory-item/ # URL /inventory-item.html
27
31
  NAV.md
32
+ item-detail.sel.ts # ItemDetailSel (not a Block)
28
33
  nav-continue-shopping.block.ts
29
34
  nav-back-to-products.block.ts
30
35
  nav-back-to-inventory.block.ts
31
36
  cart/ # URL /cart.html
32
37
  NAV.md
38
+ cart.sel.ts # CartSel (not a Block)
33
39
  nav-cart.block.ts # Nav click header cart
34
40
  nav-checkout-info.block.ts
35
41
  checkout-step-one/ # URL /checkout-step-one.html
36
42
  NAV.md
37
43
  methods/
38
- submit-checkout-info.method.block.ts
44
+ checkout-info.sel.ts # CheckoutInfoSel (not a Block)
45
+ fill-first-name.method.block.ts # Method defineMethodBlock (self-loop on CheckoutInfoPage)
46
+ fill-last-name.method.block.ts # Method defineMethodBlock (self-loop on CheckoutInfoPage)
47
+ fill-postal-code.method.block.ts # Method defineMethodBlock (self-loop on CheckoutInfoPage)
48
+ submit-checkout-info.method.block.ts # Method defineMethodBlock (click only)
39
49
  checkout-step-two/ # URL /checkout-step-two.html
40
50
  NAV.md
41
51
  methods/
52
+ checkout-overview.sel.ts # CheckoutOverviewSel (not a Block)
42
53
  finish-order.method.block.ts
43
54
  ```
44
55
 
@@ -1,7 +1,5 @@
1
1
  import { defineNavClickBlock, Trait } from "waygraph";
2
- import type { StubCtx } from "waygraph";
3
2
  import type { CartPage } from "../../../states/checkout.states.js";
4
- import { InventorySel } from "../inventory/methods/inventory-items.js";
5
3
  import { CartSel } from "./cart.sel.js";
6
4
 
7
5
  /**
@@ -17,51 +15,4 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
17
15
  checkpoint: "CartPage",
18
16
  click: CartSel.cartLink,
19
17
  verify: [Trait.url({ pathname: "/cart.html" })],
20
- stubBefore: (ctx: StubCtx) => {
21
- ctx.title("Open cart · landscape");
22
- ctx.landscape();
23
- ctx.todos(
24
- "saucedemo",
25
- [
26
- { id: "login-user", text: "Enter username", done: true },
27
- { id: "login-pass", text: "Enter password", done: true },
28
- { id: "login-submit", text: "Tap Login", done: true },
29
- { id: "cart-find", text: "Find the product", done: true },
30
- { id: "cart-add", text: "Add to cart", done: true },
31
- { id: "cart-open", text: "Open cart" },
32
- ],
33
- { title: "Device showcase", index: 5 },
34
- );
35
- ctx.zoom(1.4);
36
- ctx.zoomOut(false);
37
- ctx.highlights({
38
- cart: {
39
- selector: CartSel.cartLink,
40
- label: "Cart",
41
- detail: "tone: planned (purple) - open cart",
42
- tone: "planned",
43
- focus: true,
44
- zoomOut: false,
45
- followMouse: false,
46
- weight: "bold",
47
- gesture: "tap",
48
- },
49
- });
50
- },
51
- stubAfter: (ctx: StubCtx) => {
52
- ctx.banner("Your cart · desktop");
53
- ctx.clearDevice();
54
- ctx.hideTodos();
55
- ctx.zoomOut(true);
56
- ctx.ring("items", {
57
- selector: InventorySel.cartItem,
58
- label: "Cart items",
59
- detail: "tone: success (green) - product in cart",
60
- tone: "success",
61
- focus: true,
62
- zoomOut: true,
63
- followMouse: false,
64
- duration: true,
65
- });
66
- },
67
18
  });
@@ -1,5 +1,8 @@
1
- import { defineMethodBlock, checkpoint, Trait, narrate, type StubCtx } from "waygraph";
2
- import type { CheckoutOverviewPage, OrderComplete } from "../../../../states/checkout.states.js";
1
+ import { defineMethodBlock, checkpoint, Trait, narrate } from "waygraph";
2
+ import type {
3
+ CheckoutOverviewPage,
4
+ OrderComplete,
5
+ } from "../../../../states/checkout.states.js";
3
6
  import { CheckoutOverviewSel } from "./checkout-overview.sel.js";
4
7
 
5
8
  /**
@@ -10,34 +13,27 @@ import { CheckoutOverviewSel } from "./checkout-overview.sel.js";
10
13
  * Places the order on the overview page - form/finish click, not a NavBlock.
11
14
  * Stubs use open ctx lifecycles (preferred) - not static slot maps.
12
15
  */
13
- export const FinishOrderBlock = defineMethodBlock<CheckoutOverviewPage, OrderComplete>({
16
+ export const FinishOrderBlock = defineMethodBlock<
17
+ CheckoutOverviewPage,
18
+ OrderComplete
19
+ >({
14
20
  name: "finish-order",
15
- description: "Places the order and confirms the 'Thank you for your order!' completion page.",
21
+ description:
22
+ "Places the order and confirms the 'Thank you for your order!' completion page.",
16
23
  instruction: {
17
24
  async act(page) {
18
25
  const finishButton = page.locator(CheckoutOverviewSel.finishBtn);
19
- await narrate(finishButton, "Placing the order", () => finishButton.click());
26
+ await narrate(finishButton, "Placing the order", () =>
27
+ finishButton.click(),
28
+ );
20
29
  },
21
30
  resolve: () => checkpoint("OrderComplete"),
22
- verify: [Trait.text(CheckoutOverviewSel.completeHeader, "Thank you for your order!")],
23
- stubBefore: (ctx: StubCtx) => {
24
- ctx.ring("finish", {
25
- selector: CheckoutOverviewSel.finishBtn,
26
- label: "Finish",
27
- detail: "tone: warning (yellow) - place order",
28
- tone: "warning",
29
- weight: "bold",
30
- });
31
- },
32
- stubAfter: (ctx: StubCtx) => {
33
- ctx.ring("thanks", {
34
- selector: CheckoutOverviewSel.completeHeader,
35
- label: "Order confirmed",
36
- detail: "tone: success (green) - thank you!",
37
- tone: "success",
38
- duration: true,
39
- });
40
- },
31
+ verify: [
32
+ Trait.text(
33
+ CheckoutOverviewSel.completeHeader,
34
+ "Thank you for your order!",
35
+ ),
36
+ ],
41
37
  // Shown AFTER act (order already placed) - selectors must match the
42
38
  // completion page, not #finish on the overview.
43
39
  slides: [
@@ -50,7 +46,8 @@ export const FinishOrderBlock = defineMethodBlock<CheckoutOverviewPage, OrderCom
50
46
  },
51
47
  {
52
48
  caption: "Completion page is the durable proof",
53
- detail: "tone: success - the thank-you header is what verify asserts on.",
49
+ detail:
50
+ "tone: success - the thank-you header is what verify asserts on.",
54
51
  tag: "YAP",
55
52
  tone: "success",
56
53
  selector: CheckoutOverviewSel.completeHeader,
@@ -1,5 +1,4 @@
1
1
  import { definePageBlock, Trait } from "waygraph";
2
- import type { StubCtx } from "waygraph";
3
2
  import type { LoggedIn } from "../../../states/checkout.states.js";
4
3
  import { AddToCartBlock } from "./methods/add-to-cart.effect.block.js";
5
4
  import { RemoveFromCartBlock } from "./methods/remove-from-cart.effect.block.js";
@@ -28,11 +27,6 @@ export const InventoryPageBlock = definePageBlock<LoggedIn>({
28
27
  Trait.url({ pathname: "/inventory.html" }),
29
28
  Trait.visible(InventorySel.list),
30
29
  ],
31
- // Showcase responsive dashboard: tablet after mobile login (toast + lerp).
32
- stubBefore: (ctx: StubCtx) => {
33
- ctx.title("Inventory · tablet");
34
- ctx.device("tablet");
35
- },
36
30
  methods: {
37
31
  addToCart: () => AddToCartBlock,
38
32
  removeFromCart: () => RemoveFromCartBlock,
@@ -1,11 +1,13 @@
1
- import { defineEffectBlock, checkpoint, type Checkpoint, type WaygraphInstanceOption } from "waygraph";
2
- import type { StubCtx } from "waygraph";
1
+ import {
2
+ defineEffectBlock,
3
+ checkpoint,
4
+ type Checkpoint,
5
+ type WaygraphInstanceOption,
6
+ } from "waygraph";
3
7
  import type { ItemInCart } from "../../../../states/checkout.states.js";
4
8
  import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
5
9
  import { collectAddableItems, InventorySel } from "./inventory-items.js";
6
10
 
7
- /** Showcase product for headed demos (matches shopFlow --data selectedItem). */
8
- const DEMO_PRODUCT_ID = "sauce-labs-backpack";
9
11
 
10
12
  /**
11
13
  * Kind: Effect
@@ -15,100 +17,40 @@ const DEMO_PRODUCT_ID = "sauce-labs-backpack";
15
17
  * Mutates cart for the mem-picked product. waygraph auto lists one
16
18
  * "Add ..." row per live Add button via instanceOptions.
17
19
  */
18
- export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>({
19
- name: "add-to-cart",
20
- description: "Adds the item named by mem (saucedemo.selectedItem) to the cart.",
21
- requires: [SelectedItem.key],
22
- instruction: {
23
- async act(page, _input, mem) {
24
- const { id } = mem.get(SelectedItem.key);
25
- await page.locator(InventorySel.addBtn(id)).click();
26
- },
27
- resolve: () => checkpoint("ItemInCart"),
28
- verify: [
29
- {
30
- name: "item-added-to-cart",
31
- async check(page, mem) {
32
- const { id } = mem.get(SelectedItem.key);
33
- return await page.locator(InventorySel.removeBtn(id)).first().isVisible();
34
- },
20
+ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
21
+ {
22
+ name: "add-to-cart",
23
+ description:
24
+ "Adds the item named by mem (saucedemo.selectedItem) to the cart.",
25
+ requires: [SelectedItem.key],
26
+ instruction: {
27
+ async act(page, _input, mem) {
28
+ const { id } = mem.get(SelectedItem.key);
29
+ await page.locator(InventorySel.addBtn(id)).click();
35
30
  },
36
- ],
37
- // Demo: zoom product -> add (stay zoomed), then badge after act.
38
- stubBefore: (ctx: StubCtx) => {
39
- ctx.title("Pick a product · tablet");
40
- ctx.device("tablet");
41
- ctx.portrait();
42
- ctx.todos(
43
- "saucedemo",
44
- [
45
- { id: "login-user", text: "Enter username", done: true },
46
- { id: "login-pass", text: "Enter password", done: true },
47
- { id: "login-submit", text: "Tap Login", done: true },
48
- { id: "cart-find", text: "Find the product" },
49
- { id: "cart-add", text: "Add to cart" },
50
- { id: "cart-open", text: "Open cart" },
51
- ],
52
- { title: "Device showcase", index: 3 },
53
- );
54
- ctx.zoom(1.35);
55
- ctx.zoomOut(false);
56
- ctx.highlights({
57
- product: {
58
- selector: InventorySel.title(DEMO_PRODUCT_ID),
59
- label: "Sauce Labs Backpack",
60
- detail: "tone: info (blue)",
61
- tone: "info",
62
- focus: true,
63
- zoom: 1.45,
64
- zoomOut: false,
31
+ resolve: () => checkpoint("ItemInCart"),
32
+ verify: [
33
+ {
34
+ name: "item-added-to-cart",
35
+ async check(page, mem) {
36
+ const { id } = mem.get(SelectedItem.key);
37
+ return await page
38
+ .locator(InventorySel.removeBtn(id))
39
+ .first()
40
+ .isVisible();
41
+ },
65
42
  },
66
- add: {
67
- selector: InventorySel.addBtn(DEMO_PRODUCT_ID),
68
- label: "Add to cart",
69
- detail: "tone: success (green)",
70
- tone: "success",
71
- focus: true,
72
- zoom: 1.5,
73
- zoomOut: false,
74
- weight: "bold",
75
- },
76
- });
43
+ ],
77
44
  },
78
- stubAfter: (ctx: StubCtx) => {
79
- ctx.banner("In the cart");
80
- ctx.todos(
81
- "saucedemo",
82
- [
83
- { id: "login-user", text: "Enter username", done: true },
84
- { id: "login-pass", text: "Enter password", done: true },
85
- { id: "login-submit", text: "Tap Login", done: true },
86
- { id: "cart-find", text: "Find the product", done: true },
87
- { id: "cart-add", text: "Add to cart" },
88
- { id: "cart-open", text: "Open cart" },
89
- ],
90
- { title: "Device showcase", index: 4 },
91
- );
92
- ctx.zoomOut(false);
93
- ctx.ring("badge", {
94
- selector: InventorySel.cartBadge,
95
- label: "Cart badge",
96
- detail: "tone: success (green) - count updated",
97
- tone: "success",
98
- focus: true,
99
- zoomOut: false,
100
- duration: true,
101
- });
45
+ async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
46
+ const items = await collectAddableItems(page);
47
+ return items.map((item) => ({
48
+ id: item.id,
49
+ label: `Add "${item.name}" to cart`,
50
+ key: SelectedItem.key,
51
+ value: item,
52
+ highlight: InventorySel.addBtn(item.id),
53
+ }));
102
54
  },
103
55
  },
104
- async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
105
- const items = await collectAddableItems(page);
106
- return items.map((item) => ({
107
- id: item.id,
108
- label: `Add "${item.name}" to cart`,
109
- key: SelectedItem.key,
110
- value: item,
111
- highlight: InventorySel.addBtn(item.id),
112
- }));
113
- },
114
- });
56
+ );
@@ -1,4 +1,4 @@
1
- import { defineMethodBlock, checkpoint, type StubCtx } from "waygraph";
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
2
  import type { LoginPage } from "../../../states/checkout.states.js";
3
3
  import { LoginCreds } from "../../../states/checkout.mem-keys.js";
4
4
  import { LoginSel } from "./login.sel.js";
@@ -21,30 +21,5 @@ export const FillPasswordBlock = defineMethodBlock<LoginPage, LoginPage>({
21
21
  await page.locator(LoginSel.password).fill(password);
22
22
  },
23
23
  resolve: () => checkpoint("LoginPage"),
24
- stubBefore: (ctx: StubCtx) => {
25
- ctx.ring("password", {
26
- selector: LoginSel.password,
27
- label: "Password",
28
- detail: "tone: planned (purple)",
29
- todo: "login-pass",
30
- tone: "planned",
31
- focus: true,
32
- zoom: 1.4,
33
- });
34
- },
35
- stubAfter: (ctx: StubCtx) => {
36
- ctx.todos(
37
- "saucedemo",
38
- [
39
- { id: "login-user", text: "Enter username", done: true },
40
- { id: "login-pass", text: "Enter password", done: true },
41
- { id: "login-submit", text: "Tap Login" },
42
- { id: "cart-find", text: "Find the product" },
43
- { id: "cart-add", text: "Add to cart" },
44
- { id: "cart-open", text: "Open cart" },
45
- ],
46
- { title: "Device showcase", index: 2 },
47
- );
48
- },
49
24
  },
50
25
  });
@@ -1,4 +1,4 @@
1
- import { defineMethodBlock, checkpoint, type StubCtx } from "waygraph";
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
2
  import type { LoginPage } from "../../../states/checkout.states.js";
3
3
  import { LoginCreds } from "../../../states/checkout.mem-keys.js";
4
4
  import { LoginSel } from "./login.sel.js";
@@ -23,49 +23,5 @@ export const FillUsernameBlock = defineMethodBlock<LoginPage, LoginPage>({
23
23
  await page.locator(LoginSel.username).fill(username);
24
24
  },
25
25
  resolve: () => checkpoint("LoginPage"),
26
- stubBefore: (ctx: StubCtx) => {
27
- ctx.title("Signing in · mobile");
28
- ctx.device("mobile");
29
- ctx.portrait();
30
- ctx.todos(
31
- "saucedemo",
32
- [
33
- { id: "login-user", text: "Enter username" },
34
- { id: "login-pass", text: "Enter password" },
35
- { id: "login-submit", text: "Tap Login" },
36
- { id: "cart-find", text: "Find the product" },
37
- { id: "cart-add", text: "Add to cart" },
38
- { id: "cart-open", text: "Open cart" },
39
- ],
40
- { title: "Device showcase", index: 0 },
41
- );
42
- ctx.zoom(1.35);
43
- const fromMem = ctx.mem?.get(LoginCreds.key)?.username;
44
- ctx.ring("username", {
45
- selector: LoginSel.username,
46
- label: "Username",
47
- detail: fromMem
48
- ? `tone: info (blue) - mem ${fromMem}`
49
- : "tone: info (blue)",
50
- tone: "info",
51
- todo: "login-user",
52
- focus: true,
53
- zoom: 1.4,
54
- });
55
- },
56
- stubAfter: (ctx: StubCtx) => {
57
- ctx.todos(
58
- "saucedemo",
59
- [
60
- { id: "login-user", text: "Enter username", done: true },
61
- { id: "login-pass", text: "Enter password" },
62
- { id: "login-submit", text: "Tap Login" },
63
- { id: "cart-find", text: "Find the product" },
64
- { id: "cart-add", text: "Add to cart" },
65
- { id: "cart-open", text: "Open cart" },
66
- ],
67
- { title: "Device showcase", index: 1 },
68
- );
69
- },
70
26
  },
71
27
  });
@@ -1,5 +1,8 @@
1
- import { defineMethodBlock, checkpoint, Trait, type StubCtx } from "waygraph";
2
- import type { LoginPage, LoginSubmitOutcome } from "../../../states/checkout.states.js";
1
+ import { defineMethodBlock, checkpoint, Trait } from "waygraph";
2
+ import type {
3
+ LoginPage,
4
+ LoginSubmitOutcome,
5
+ } from "../../../states/checkout.states.js";
3
6
  import { LoginSel } from "./login.sel.js";
4
7
 
5
8
  type LoginObserve = "success" | "failure";
@@ -14,7 +17,10 @@ type LoginObserve = "success" | "failure";
14
17
  * already did that). One Block, one action: submit.
15
18
  * Branches: good creds -> LoggedIn; bad/locked -> LoginPage with error banner.
16
19
  */
17
- export const SubmitLoginBlock = defineMethodBlock<LoginPage, LoginSubmitOutcome>({
20
+ export const SubmitLoginBlock = defineMethodBlock<
21
+ LoginPage,
22
+ LoginSubmitOutcome
23
+ >({
18
24
  name: "submit-login",
19
25
  description: "Submits the login form; stays on LoginPage when auth fails.",
20
26
  instruction: {
@@ -26,7 +32,9 @@ export const SubmitLoginBlock = defineMethodBlock<LoginPage, LoginSubmitOutcome>
26
32
  async observe(page): Promise<LoginObserve> {
27
33
  // Race inventory nav vs error banner - locked_out / wrong password show
28
34
  // Epic sadface immediately; do not burn the full inventory timeout.
29
- const inventory = page.waitForURL(/\/inventory\.html/, { timeout: 8_000 }).then(() => "success" as const);
35
+ const inventory = page
36
+ .waitForURL(/\/inventory\.html/, { timeout: 8_000 })
37
+ .then(() => "success" as const);
30
38
  const errorBanner = page
31
39
  .locator(LoginSel.errorBanner)
32
40
  .first()
@@ -38,82 +46,11 @@ export const SubmitLoginBlock = defineMethodBlock<LoginPage, LoginSubmitOutcome>
38
46
  return "failure";
39
47
  }
40
48
  },
41
- resolve: (observed) => checkpoint(observed === "success" ? "LoggedIn" : "LoginPage"),
49
+ resolve: (observed) =>
50
+ checkpoint(observed === "success" ? "LoggedIn" : "LoginPage"),
42
51
  verify: (out) =>
43
52
  out.__state === "LoggedIn"
44
53
  ? [Trait.url({ pathname: "/inventory.html" })]
45
54
  : [Trait.visible(LoginSel.errorBanner)],
46
- stubBefore: (ctx: StubCtx) => {
47
- ctx.highlights({
48
- submit: {
49
- selector: LoginSel.loginButton,
50
- label: "Login",
51
- detail: "tone: warning (yellow)",
52
- todo: "login-submit",
53
- tone: "warning",
54
- focus: true,
55
- zoom: 1.55,
56
- weight: "bold",
57
- gesture: "tap",
58
- },
59
- });
60
- },
61
- stubAfter: (ctx: StubCtx) => {
62
- if (ctx.out && ctx.out.__state === "LoginPage") {
63
- ctx.title("Login blocked");
64
- ctx.todos(
65
- "saucedemo",
66
- [
67
- { id: "login-user", text: "Enter username", done: true },
68
- { id: "login-pass", text: "Enter password", done: true },
69
- { id: "login-submit", text: "Click Login", done: true },
70
- ],
71
- { title: "Sign in", style: "checklist" },
72
- );
73
- ctx.ring("error", {
74
- selector: LoginSel.errorBanner,
75
- label: "Login error banner",
76
- detail: "tone: danger (red)",
77
- tone: "danger",
78
- focus: true,
79
- duration: true,
80
- });
81
- return;
82
- }
83
- ctx.banner("Inventory · landscape");
84
- ctx.landscape();
85
- ctx.todos(
86
- "saucedemo",
87
- [
88
- { id: "login-user", text: "Enter username", done: true },
89
- { id: "login-pass", text: "Enter password", done: true },
90
- { id: "login-submit", text: "Tap Login", done: true },
91
- { id: "cart-find", text: "Find the product" },
92
- { id: "cart-add", text: "Add to cart" },
93
- { id: "cart-open", text: "Open cart" },
94
- ],
95
- { title: "Device showcase", index: 3 },
96
- );
97
- ctx.ring("shelf", {
98
- selector: ".inventory_list",
99
- label: "Product shelf",
100
- detail: "tone: success (green) - ready to shop",
101
- tone: "success",
102
- focus: true,
103
- zoom: 1.25,
104
- duration: true,
105
- });
106
- },
107
- stubOnError: (ctx: StubCtx) => {
108
- ctx.title("Login failed");
109
- ctx.ring("error", {
110
- selector: LoginSel.errorBanner,
111
- label: "Login error banner",
112
- detail: "tone: danger (red)",
113
- tone: "danger",
114
- focus: true,
115
- duration: true,
116
- });
117
- },
118
55
  },
119
56
  });
@@ -0,0 +1,43 @@
1
+ import { checkpoint, defineApiBlock } from "waygraph";
2
+ import type { Checkpoint, StubCtx } from "waygraph";
3
+ import { engine } from "../../config/waygraph.config.js";
4
+ import { loginFlow } from "./login.flow.js";
5
+
6
+ /**
7
+ * Proof of concept for FE+BE in one Flow: a real backend check
8
+ * (defineApiBlock, Playwright's own page.request - no browser, no curl
9
+ * shell-out) resolved into a Checkpoint exactly like a UI Block, chained
10
+ * right after the UI login. This is the QA-team ask: show a mixed
11
+ * frontend + backend journey narrated as one state machine, not two
12
+ * separate test files with no shared story.
13
+ *
14
+ * saucedemo.com is a static demo site with no backend of its own, so this
15
+ * hits a well-known public REST test API (jsonplaceholder) purely to prove
16
+ * the mechanism - a real project would point `call` at its own backend.
17
+ */
18
+ export type BackendHealthy = Checkpoint<"BackendHealthy">;
19
+ export type BackendDown = Checkpoint<"BackendDown">;
20
+
21
+ const CheckBackendHealth = defineApiBlock<any, BackendHealthy | BackendDown>({
22
+ name: "check-backend-health",
23
+ call: ({ request }) =>
24
+ request.get("https://jsonplaceholder.typicode.com/todos/1"),
25
+ resolve: (result) =>
26
+ result.ok ? checkpoint("BackendHealthy") : checkpoint("BackendDown"),
27
+ stubBefore: (ctx: StubCtx<BackendHealthy | BackendDown>) => {
28
+ ctx.title("Checking backend health · real HTTP request, no browser");
29
+ },
30
+ });
31
+
32
+ export const apiDemoEpisode = engine
33
+ .map()
34
+ .start()
35
+ .method(CheckBackendHealth)
36
+ .end();
37
+
38
+ export const apiDemoFlow = engine
39
+ .map({ memStub: true })
40
+ .start()
41
+ .flow(loginFlow)
42
+ .flow(apiDemoEpisode)
43
+ .end();