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
@@ -1,4 +1,5 @@
1
- import { Engine, withTitle } from "waygraph";
1
+ import { engine } from "../../config/waygraph.config.js";
2
+ import "../states/checkout.mem-stubs.js";
2
3
  import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.js";
3
4
  import { NavCartBlock } from "../blocks/saucedemo-web/cart/nav-cart.block.js";
4
5
  import { NavCheckoutInfoBlock } from "../blocks/saucedemo-web/cart/nav-checkout-info.block.js";
@@ -8,22 +9,16 @@ import { FillPostalCodeBlock } from "../blocks/saucedemo-web/checkout-step-one/m
8
9
  import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.js";
9
10
  import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.js";
10
11
 
11
- const engine = new Engine();
12
-
13
- // Episode-2 half of chainFlow(loginFlow, shopFlow) - assumes already LoggedIn
14
- // (AddToCartBlock's In is the Checkpoint<string> wildcard).
15
- export const shopFlow = withTitle(
16
- engine
17
- .map()
18
- .start()
19
- .method(AddToCartBlock)
20
- .gotoPage(NavCartBlock)
21
- .gotoPage(NavCheckoutInfoBlock)
22
- .method(FillFirstNameBlock)
23
- .method(FillLastNameBlock)
24
- .method(FillPostalCodeBlock)
25
- .method(SubmitCheckoutInfoBlock)
26
- .method(FinishOrderBlock)
27
- .end(),
28
- "Shop & Checkout",
29
- );
12
+ /** Add the selected item, check out and place the order. Starts signed in (run it after loginFlow). */
13
+ export const shopFlow = engine
14
+ .map({ memStub: true })
15
+ .start()
16
+ .method(AddToCartBlock.stubBefore((ctx) => ctx.title("Shop & Checkout")))
17
+ .gotoPage(NavCartBlock)
18
+ .gotoPage(NavCheckoutInfoBlock)
19
+ .method(FillFirstNameBlock)
20
+ .method(FillLastNameBlock)
21
+ .method(FillPostalCodeBlock)
22
+ .method(SubmitCheckoutInfoBlock)
23
+ .method(FinishOrderBlock)
24
+ .end();
@@ -1,33 +1,18 @@
1
- import { Engine, start, end } from "waygraph";
2
- import type { LoginPage, LoggedIn, ItemInCart } from "../states/checkout.states.js";
1
+ import { engine } from "../../config/waygraph.config.js";
2
+ import "../states/checkout.mem-stubs.js";
3
3
  import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
4
4
  import { FillUsernameBlock } from "../blocks/saucedemo-web/methods/fill-username.method.block.js";
5
5
  import { FillPasswordBlock } from "../blocks/saucedemo-web/methods/fill-password.method.block.js";
6
6
  import { SubmitLoginForFlow } from "../blocks/saucedemo-web/methods/submit-login-for-flow.js";
7
7
  import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.js";
8
8
 
9
- const engine = new Engine();
10
-
11
- // The QA case: a smoke test only cares that it was able to navigate this far -
12
- // it doesn't need the same DOM confirmation the real checkout.flow.ts wants.
13
- // Same Blocks, same act/resolve, zero duplication - only verify differs.
14
- // .withVerify([]) reads from the Block itself, discoverable by typing
15
- // "NavLoginBlock."/"SubmitLoginForFlow." in an editor - not a free function
16
- // you have to already know exists.
17
- //
18
- // Explicit <LoginPage, LoginPage, LoginPage, LoggedIn, ItemInCart> here -
19
- // AddToCartBlock's In is now the wildcard Checkpoint<string> (it's reused as
20
- // shopFlow's own entry block too), and inferring the chain purely from
21
- // several chained .withVerify([]) calls in one array trips TS's overload
22
- // resolution into unifying the wrong Out type partway through; pinning the
23
- // type args sidesteps it rather than fighting it. The two middle `LoginPage`
24
- // entries are fill-username / fill-password (self-loop Methods).
25
- export const smokeFlow = engine.defineFlow<LoginPage, LoginPage, LoginPage, LoggedIn, ItemInCart>([
26
- start,
27
- NavLoginBlock.withVerify([]),
28
- FillUsernameBlock,
29
- FillPasswordBlock,
30
- SubmitLoginForFlow.withVerify([]),
31
- AddToCartBlock.withVerify([]),
32
- end,
33
- ]);
9
+ /** Smoke: the happy path with the Blocks' own verify lists switched off where a later step already proves the outcome. */
10
+ export const smokeFlow = engine
11
+ .map()
12
+ .start()
13
+ .gotoPage(NavLoginBlock.withVerify([]))
14
+ .method(FillUsernameBlock)
15
+ .method(FillPasswordBlock)
16
+ .method(SubmitLoginForFlow.withVerify([]))
17
+ .method(AddToCartBlock.withVerify([]))
18
+ .end();
@@ -1,50 +1,31 @@
1
- import {
2
- Engine,
3
- start,
4
- end,
5
- withTitle,
6
- withSessionReset,
7
- withExpectedFailure,
8
- withHighlightFixtures,
9
- Trait,
10
- } from "waygraph";
1
+ import { engine } from "../../config/waygraph.config.js";
2
+ import "../states/checkout.mem-stubs.js";
3
+ import { Trait } from "waygraph";
11
4
  import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
12
5
  import { FillUsernameBlock } from "../blocks/saucedemo-web/methods/fill-username.method.block.js";
13
6
  import { FillPasswordBlock } from "../blocks/saucedemo-web/methods/fill-password.method.block.js";
14
7
  import { SubmitLoginBlock } from "../blocks/saucedemo-web/methods/submit-login.method.block.js";
15
-
16
- const engine = new Engine();
8
+ import { loginFailed } from "../stubs/login.js";
17
9
 
18
10
  /**
19
- * Prove stubOnError (waygraph 0.12.4+): force verify to demand /inventory.html
20
- * while mem is locked_out_user — step throws, demo rings the error banner
21
- * before the error panel, then expected-failure styling.
22
- *
23
- * waygraph demo --blocks stubOnErrorFlow --auto-next --fast \\
24
- * --data '{"saucedemo.credentials":{"username":"locked_out_user","password":"secret_sauce"}}'
11
+ * Forces a verify failure (locked_out_user is refused, but the verify demands the inventory page) to show `stubOnError` ringing
12
+ * the error banner before the failure panel. Needs locked_out_user credentials: `--data` or mem.
25
13
  */
26
- const forcedInventoryVerify = withSessionReset(
27
- engine
28
- .defineFlow([start, NavLoginBlock, FillUsernameBlock, FillPasswordBlock, SubmitLoginBlock, end])
29
- .withBlockVerify("submit-login", [Trait.url({ pathname: "/inventory.html" })]),
30
- );
31
-
32
- export const stubOnErrorFlow = withHighlightFixtures(
33
- withExpectedFailure(
34
- withTitle(forcedInventoryVerify, "stubOnError: locked_out fail rings"),
35
- "Intentional verify fail - stubOnError rings the error banner before the panel",
36
- ),
37
- {
38
- "submit-login": {
39
- stubOnError: {
40
- error: {
41
- label: "BUG · locked out still on login",
42
- detail: "Verify demanded inventory; banner is the proof point.",
43
- tag: "FAIL",
44
- duration: true,
45
- fastMode: 500,
46
- },
47
- },
48
- },
49
- },
50
- );
14
+ export const stubOnErrorFlow = engine
15
+ .map({
16
+ expectedFailure:
17
+ "Intentional verify fail - stubOnError rings the error banner before the panel",
18
+ })
19
+ .start()
20
+ .gotoPage(
21
+ NavLoginBlock.stubBefore((ctx) =>
22
+ ctx.title("stubOnError: locked_out fail rings"),
23
+ ),
24
+ )
25
+ .method(FillUsernameBlock)
26
+ .method(FillPasswordBlock)
27
+ .method(SubmitLoginBlock.stubOnError(loginFailed))
28
+ .end()
29
+ .withBlockVerify("submit-login", [
30
+ Trait.url({ pathname: "/inventory.html" }),
31
+ ]);
@@ -1,40 +1,32 @@
1
- import { Engine, withTitle, withSessionReset, withExpectedFailure } from "waygraph";
1
+ import { engine } from "../../config/waygraph.config.js";
2
+ import "../states/checkout.mem-stubs.js";
2
3
  import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
3
4
  import { FillUsernameBlock } from "../blocks/saucedemo-web/methods/fill-username.method.block.js";
4
5
  import { FillPasswordBlock } from "../blocks/saucedemo-web/methods/fill-password.method.block.js";
5
6
  import { SubmitLoginBlock } from "../blocks/saucedemo-web/methods/submit-login.method.block.js";
7
+ import { loginRefused } from "../stubs/login.js";
6
8
 
7
- const engine = new Engine();
8
-
9
- // A genuinely DIFFERENT scenario from checkoutFlow - not the same steps
10
- // split in half. Same login blocks, different user, different outcome:
11
- // locked_out_user attempts the exact same login an Owner (standard_user,
12
- // checkoutFlow) just completed successfully, and is blocked -
13
- // saucedemo.com's real response, verified live: stays on "/", shows
14
- // "Epic sadface: Sorry, this user has been locked out."
15
- //
16
- // withSessionReset matters here for real, not cosmetically - entered right
17
- // after checkoutFlow's already-authenticated session, this needs its own
18
- // genuinely fresh cookies/storage, or the previous user's session would
19
- // still be live and this login attempt wouldn't mean anything.
20
- //
21
- // submit-login branches: observe sees no /inventory.html navigation,
22
- // resolve -> LoginPage, verify confirms the error banner. Demo still wraps
23
- // withExpectedFailure so step 9/9 shows stubOnError rings + amber
24
- // "expected outcome" panel (branching no longer throws).
25
- export const viewerBlockedFlow = withExpectedFailure(
26
- withTitle(
27
- withSessionReset(
28
- engine
29
- .map()
30
- .start()
31
- .gotoPage(NavLoginBlock)
32
- .method(FillUsernameBlock)
33
- .method(FillPasswordBlock)
34
- .method(SubmitLoginBlock)
35
- .end(),
9
+ /**
10
+ * locked_out_user attempts the login a standard user just completed and is refused: the app stays on the login page and
11
+ * shows the error banner. Run it after another flow to see it needs a reset: `.flow(viewerBlockedFlow, { reset: true })`.
12
+ */
13
+ export const viewerBlockedFlow = engine
14
+ .map({
15
+ sessionReset: true,
16
+ expectedFailure:
17
+ "locked_out_user stays on LoginPage with the error banner - this is the product working as intended.",
18
+ })
19
+ .start()
20
+ .gotoPage(
21
+ NavLoginBlock.stubBefore((ctx) =>
22
+ ctx.title("Viewer: Blocked Login Attempt"),
36
23
  ),
37
- "Viewer: Blocked Login Attempt",
38
- ),
39
- "locked_out_user stays on LoginPage with the error banner - this is the product working as intended.",
40
- );
24
+ )
25
+ .method(FillUsernameBlock)
26
+ .method(FillPasswordBlock)
27
+ .method(
28
+ SubmitLoginBlock.stubAfter((ctx) => {
29
+ if (ctx.out?.__state === "LoginPage") loginRefused(ctx);
30
+ }),
31
+ )
32
+ .end();
@@ -0,0 +1,11 @@
1
+ import { registerMemStub } from "waygraph";
2
+ import { LoginCreds, SelectedItem } from "./checkout.mem-keys.js";
3
+
4
+ registerMemStub(LoginCreds.key, () => ({
5
+ username: "standard_user",
6
+ password: "secret_sauce",
7
+ }));
8
+ registerMemStub(SelectedItem.key, () => ({
9
+ id: "sauce-labs-backpack",
10
+ name: "Sauce Labs Backpack",
11
+ }));
@@ -0,0 +1,193 @@
1
+ import type { StubCtx } from "waygraph";
2
+ import { LoginCreds } from "../states/checkout.mem-keys.js";
3
+ import { LoginSel } from "../blocks/saucedemo-web/methods/login.sel.js";
4
+ import { CartSel } from "../blocks/saucedemo-web/cart/cart.sel.js";
5
+ import { InventorySel } from "../blocks/saucedemo-web/inventory/methods/inventory-items.js";
6
+
7
+ const PRODUCT_ID = "sauce-labs-backpack";
8
+
9
+ /** The six-step checklist of the device showcase; `done` is how many are ticked, `index` the highlighted one. */
10
+ function checklist(done: number, index: number) {
11
+ const steps = [
12
+ "Enter username",
13
+ "Enter password",
14
+ "Tap Login",
15
+ "Find the product",
16
+ "Add to cart",
17
+ "Open cart",
18
+ ];
19
+ const ids = [
20
+ "login-user",
21
+ "login-pass",
22
+ "login-submit",
23
+ "cart-find",
24
+ "cart-add",
25
+ "cart-open",
26
+ ];
27
+ return [
28
+ ids.map((id, i) => ({
29
+ id,
30
+ text: steps[i]!,
31
+ ...(i < done ? { done: true } : {}),
32
+ })),
33
+ { title: "Device showcase", index },
34
+ ] as const;
35
+ }
36
+
37
+ /** Narration for the device showcase flow: phone login, tablet shelf, landscape cart. Each is a stub to chain on its Block. */
38
+ export const deviceShowcase = {
39
+ username: {
40
+ before: (ctx: StubCtx) => {
41
+ ctx.title("Signing in · mobile");
42
+ ctx.device("mobile");
43
+ ctx.portrait();
44
+ ctx.todos("saucedemo", ...checklist(0, 0));
45
+ ctx.zoom(1.35);
46
+ const fromMem = ctx.mem?.get(LoginCreds.key)?.username;
47
+ ctx.ring("username", {
48
+ selector: LoginSel.username,
49
+ label: "Username",
50
+ detail: fromMem
51
+ ? `tone: info (blue) - mem ${fromMem}`
52
+ : "tone: info (blue)",
53
+ tone: "info",
54
+ todo: "login-user",
55
+ focus: true,
56
+ zoom: 1.4,
57
+ });
58
+ },
59
+ after: (ctx: StubCtx) => {
60
+ ctx.todos("saucedemo", ...checklist(1, 1));
61
+ },
62
+ },
63
+ password: {
64
+ before: (ctx: StubCtx) => {
65
+ ctx.ring("password", {
66
+ selector: LoginSel.password,
67
+ label: "Password",
68
+ detail: "tone: planned (purple)",
69
+ todo: "login-pass",
70
+ tone: "planned",
71
+ focus: true,
72
+ zoom: 1.4,
73
+ });
74
+ },
75
+ after: (ctx: StubCtx) => {
76
+ ctx.todos("saucedemo", ...checklist(2, 2));
77
+ },
78
+ },
79
+ submit: {
80
+ before: (ctx: StubCtx) => {
81
+ ctx.highlights({
82
+ submit: {
83
+ selector: LoginSel.loginButton,
84
+ label: "Login",
85
+ detail: "tone: warning (yellow)",
86
+ todo: "login-submit",
87
+ tone: "warning",
88
+ focus: true,
89
+ zoom: 1.55,
90
+ weight: "bold",
91
+ gesture: "tap",
92
+ },
93
+ });
94
+ },
95
+ after: (ctx: StubCtx) => {
96
+ ctx.banner("Inventory · landscape");
97
+ ctx.landscape();
98
+ ctx.todos("saucedemo", ...checklist(3, 3));
99
+ ctx.ring("shelf", {
100
+ selector: ".inventory_list",
101
+ label: "Product shelf",
102
+ detail: "tone: success (green) - ready to shop",
103
+ tone: "success",
104
+ focus: true,
105
+ zoom: 1.25,
106
+ duration: true,
107
+ });
108
+ },
109
+ },
110
+ addToCart: {
111
+ before: (ctx: StubCtx) => {
112
+ ctx.title("Pick a product · tablet");
113
+ ctx.device("tablet");
114
+ ctx.portrait();
115
+ ctx.todos("saucedemo", ...checklist(3, 3));
116
+ ctx.zoom(1.35);
117
+ ctx.zoomOut(false);
118
+ ctx.highlights({
119
+ product: {
120
+ selector: InventorySel.title(PRODUCT_ID),
121
+ label: "Sauce Labs Backpack",
122
+ detail: "tone: info (blue)",
123
+ tone: "info",
124
+ focus: true,
125
+ zoom: 1.45,
126
+ zoomOut: false,
127
+ },
128
+ add: {
129
+ selector: InventorySel.addBtn(PRODUCT_ID),
130
+ label: "Add to cart",
131
+ detail: "tone: success (green)",
132
+ tone: "success",
133
+ focus: true,
134
+ zoom: 1.5,
135
+ zoomOut: false,
136
+ weight: "bold",
137
+ },
138
+ });
139
+ },
140
+ after: (ctx: StubCtx) => {
141
+ ctx.banner("In the cart");
142
+ ctx.todos("saucedemo", ...checklist(4, 4));
143
+ ctx.zoomOut(false);
144
+ ctx.ring("badge", {
145
+ selector: InventorySel.cartBadge,
146
+ label: "Cart badge",
147
+ detail: "tone: success (green) - count updated",
148
+ tone: "success",
149
+ focus: true,
150
+ zoomOut: false,
151
+ duration: true,
152
+ });
153
+ },
154
+ },
155
+ openCart: {
156
+ before: (ctx: StubCtx) => {
157
+ ctx.title("Open cart · landscape");
158
+ ctx.landscape();
159
+ ctx.todos("saucedemo", ...checklist(5, 5));
160
+ ctx.zoom(1.4);
161
+ ctx.zoomOut(false);
162
+ ctx.highlights({
163
+ cart: {
164
+ selector: CartSel.cartLink,
165
+ label: "Cart",
166
+ detail: "tone: planned (purple) - open cart",
167
+ tone: "planned",
168
+ focus: true,
169
+ zoomOut: false,
170
+ followMouse: false,
171
+ weight: "bold",
172
+ gesture: "tap",
173
+ },
174
+ });
175
+ },
176
+ after: (ctx: StubCtx) => {
177
+ ctx.banner("Your cart · desktop");
178
+ ctx.clearDevice();
179
+ ctx.hideTodos();
180
+ ctx.zoomOut(true);
181
+ ctx.ring("items", {
182
+ selector: InventorySel.cartItem,
183
+ label: "Cart items",
184
+ detail: "tone: success (green) - product in cart",
185
+ tone: "success",
186
+ focus: true,
187
+ zoomOut: true,
188
+ followMouse: false,
189
+ duration: true,
190
+ });
191
+ },
192
+ },
193
+ };
@@ -0,0 +1,37 @@
1
+ import type { StubCtx } from "waygraph";
2
+ import { LoginSel } from "../blocks/saucedemo-web/methods/login.sel.js";
3
+
4
+ /** What a refused login looks like on screen: the title, a finished checklist and the error banner. */
5
+ export function loginRefused(ctx: StubCtx): void {
6
+ ctx.title("Login blocked");
7
+ ctx.todos(
8
+ "saucedemo",
9
+ [
10
+ { id: "login-user", text: "Enter username", done: true },
11
+ { id: "login-pass", text: "Enter password", done: true },
12
+ { id: "login-submit", text: "Click Login", done: true },
13
+ ],
14
+ { title: "Sign in", style: "checklist" },
15
+ );
16
+ ctx.ring("error", {
17
+ selector: LoginSel.errorBanner,
18
+ label: "Login error banner",
19
+ detail: "tone: danger (red)",
20
+ tone: "danger",
21
+ focus: true,
22
+ duration: true,
23
+ });
24
+ }
25
+
26
+ /** Rings the error banner when the login step itself throws. */
27
+ export function loginFailed(ctx: StubCtx): void {
28
+ ctx.title("Login failed");
29
+ ctx.ring("error", {
30
+ selector: LoginSel.errorBanner,
31
+ label: "Login error banner",
32
+ detail: "tone: danger (red)",
33
+ tone: "danger",
34
+ focus: true,
35
+ duration: true,
36
+ });
37
+ }
@@ -1,30 +1,28 @@
1
1
  import { test, expect } from "@playwright/test";
2
- import { MemPage, checkpoint, chainFlow } from "waygraph";
2
+ import { MemPage, checkpoint } from "waygraph";
3
+ import { engine } from "../config/waygraph.config.js";
3
4
  import { loginFlow } from "../src/flows/login.flow.js";
4
5
  import { shopFlow } from "../src/flows/shop.flow.js";
5
6
  import { LoginCreds, SelectedItem } from "../src/states/checkout.mem-keys.js";
6
7
 
7
- // Proves the 2-episode chainFlow scenario the step-mode CLI demo shows
8
+ // Proves the 2-episode nested-flow scenario the step-mode CLI demo shows
8
9
  // interactively ("Episode 1: Sign In" -> "Episode 2: Shop & Checkout") also
9
10
  // works as a real, automated, headless test - not just something you have
10
11
  // to click through by hand. loginFlow/shopFlow are the exact same Flow
11
12
  // objects `waygraph chain "loginFlow(...) then shopFlow"` drives.
12
- test("chainFlow sequences 2 titled episodes (loginFlow then shopFlow) against the real live site", async () => {
13
+ test("nested flows run 2 episodes (loginFlow then shopFlow) against the real live site", async () => {
13
14
  const mem = new MemPage();
14
15
  mem.set(LoginCreds({ username: "standard_user", password: "secret_sauce" }));
15
16
  mem.set(SelectedItem({ id: "sauce-labs-backpack", name: "Sauce Labs Backpack" }));
16
17
 
17
- const combined = chainFlow(loginFlow, shopFlow);
18
+ const combined = engine.map().start().flow(loginFlow).flow(shopFlow).end();
18
19
  const result = await combined.run(mem);
19
20
 
20
21
  expect(result).toEqual(checkpoint("OrderComplete"));
21
22
  });
22
23
 
23
- test("each episode's title survives chaining, and blocks() reports the flattened chain in order", () => {
24
- expect(loginFlow.title).toBe("Sign In");
25
- expect(shopFlow.title).toBe("Shop & Checkout");
26
-
27
- const combined = chainFlow(loginFlow, shopFlow);
24
+ test("blocks() reports the flattened chain in order", () => {
25
+ const combined = engine.map().start().flow(loginFlow).flow(shopFlow).end();
28
26
  const blocks = combined.blocks();
29
27
 
30
28
  expect(blocks.map((b) => b.name)).toEqual([
@@ -1,5 +1,6 @@
1
1
  import { test, expect } from "@playwright/test";
2
- import { MemPage, Engine, start, end } from "waygraph";
2
+ import { MemPage } from "waygraph";
3
+ import { engine } from "../config/waygraph.config.js";
3
4
  import { NavLoginBlock } from "../src/blocks/saucedemo-web/nav-login.block.js";
4
5
  import { FillUsernameBlock } from "../src/blocks/saucedemo-web/methods/fill-username.method.block.js";
5
6
  import { FillPasswordBlock } from "../src/blocks/saucedemo-web/methods/fill-password.method.block.js";
@@ -10,8 +11,14 @@ test("submit-login: wrong password branches to LoginPage with error banner", asy
10
11
  const mem = new MemPage();
11
12
  mem.set(LoginCreds({ username: "standard_user", password: "wrong-password" }));
12
13
 
13
- const engine = new Engine();
14
- const flow = engine.defineFlow([start, NavLoginBlock, FillUsernameBlock, FillPasswordBlock, SubmitLoginBlock, end]);
14
+ const flow = engine
15
+ .map()
16
+ .start()
17
+ .gotoPage(NavLoginBlock)
18
+ .method(FillUsernameBlock)
19
+ .method(FillPasswordBlock)
20
+ .method(SubmitLoginBlock)
21
+ .end();
15
22
  const result = await flow.run(mem);
16
23
 
17
24
  expect(result.__state).toBe("LoginPage");
@@ -21,8 +28,14 @@ test("submit-login: good password branches to LoggedIn", async () => {
21
28
  const mem = new MemPage();
22
29
  mem.set(LoginCreds({ username: "standard_user", password: "secret_sauce" }));
23
30
 
24
- const engine = new Engine();
25
- const flow = engine.defineFlow([start, NavLoginBlock, FillUsernameBlock, FillPasswordBlock, SubmitLoginBlock, end]);
31
+ const flow = engine
32
+ .map()
33
+ .start()
34
+ .gotoPage(NavLoginBlock)
35
+ .method(FillUsernameBlock)
36
+ .method(FillPasswordBlock)
37
+ .method(SubmitLoginBlock)
38
+ .end();
26
39
  const result = await flow.run(mem);
27
40
 
28
41
  expect(result.__state).toBe("LoggedIn");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.53",
3
+ "version": "0.15.54",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -59,7 +59,8 @@
59
59
  "docs:build": "npm run docs:api && vitepress build docs",
60
60
  "docs:preview": "vitepress preview docs",
61
61
  "docs:api": "typedoc && node scripts/gen-deprecated.mjs",
62
- "knip": "knip"
62
+ "knip": "knip",
63
+ "sync:quickstart": "node scripts/sync-quickstart.mjs"
63
64
  },
64
65
  "peerDependencies": {
65
66
  "@playwright/test": "^1.40.0"
@@ -1,10 +1,7 @@
1
1
  # waygraph quickstart (Sauce Demo)
2
2
 
3
- Temp-dir template for `npx waygraph try demo`. Same Blocks as
4
- `examples/saucedemo` (route-folder convention under `src/blocks/saucedemo-web/`).
3
+ Temp-dir template for `npx waygraph try demo`. A copy of `examples/saucedemo` (freeform route-folder layout under
4
+ `src/blocks/saucedemo-web/`, flows in `src/flows/`, narration in `src/stubs/`, settings in `config/waygraph.config.ts`).
5
5
 
6
- Convention walkthrough on Pages:
7
- https://deviate-dv8.github.io/waygraph/saucedemo/
8
-
9
- Do not edit this tree as the long-lived showcase - edit `examples/saucedemo`,
10
- then re-copy Blocks here when shipping a release that bumps `try demo`.
6
+ Do not edit this tree as the long-lived showcase: edit `examples/saucedemo`, then run `npm run sync:quickstart` in the
7
+ waygraph repo before a release.
@@ -0,0 +1,17 @@
1
+ import { Engine, defineConfig } from "waygraph";
2
+
3
+ const config = defineConfig({
4
+ baseUrl: "https://www.saucedemo.com",
5
+ playwright: {
6
+ workers: 1,
7
+ fullyParallel: false,
8
+ timeout: 30_000,
9
+ use: {
10
+ headless: true,
11
+ launchOptions: { args: ["--no-sandbox", "--disable-dev-shm-usage"] },
12
+ },
13
+ },
14
+ });
15
+
16
+ export default config;
17
+ export const engine = new Engine(config);
@@ -82,5 +82,4 @@ waygraph chain "loginFlow" --step --no-autoplay
82
82
 
83
83
  Flags beat env: `--step` / `--no-step`, `--autoplay` / `--no-autoplay`,
84
84
  `--base-url <url>`, `--title <text>`. Default BASE_URL resolution order for
85
- `demo`: `--base-url` > `WAYGRAPH_BASE_URL` > `package.json` `waygraph.baseUrl`
86
- > `playwright.config` `baseURL`.
85
+ `demo`: `--base-url` > `WAYGRAPH_BASE_URL` > `baseUrl` in `config/waygraph.config.ts`.
@@ -99,7 +99,7 @@ branch(SubmitLoginActionBlock, {
99
99
  });
100
100
  ```
101
101
 
102
- Linear `defineFlow([start, …, end])` usually skips `branch()` (one happy path).
102
+ A linear `map()` chain usually skips `branch()` (one happy path).
103
103
 
104
104
  ---
105
105