waygraph 0.15.51 → 0.15.53

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 (153) hide show
  1. package/README.md +8 -1
  2. package/dist/analysis/graph.d.ts +1 -1
  3. package/dist/analysis/graph.js +10 -15
  4. package/dist/analysis/map-check.d.ts +3 -2
  5. package/dist/analysis/practices-check.d.ts +0 -1
  6. package/dist/analysis/practices-check.js +0 -13
  7. package/dist/auto-session/session.d.ts +1 -1
  8. package/dist/auto-session/session.js +13 -8
  9. package/dist/blocks-select.d.ts +7 -4
  10. package/dist/blocks-select.js +0 -14
  11. package/dist/browser.d.ts +27 -0
  12. package/dist/browser.js +62 -0
  13. package/dist/cli/flags.js +3 -1
  14. package/dist/cli/run-chain.js +4 -0
  15. package/dist/cli/usage.js +17 -1
  16. package/dist/cli.js +13 -1
  17. package/dist/commands/agent-dive-scaffold.d.ts +5 -20
  18. package/dist/commands/agent-dive-scaffold.js +10 -3
  19. package/dist/commands/extensions.d.ts +1 -0
  20. package/dist/commands/extensions.js +22 -0
  21. package/dist/commands/init.js +43 -5
  22. package/dist/commands/test.js +16 -2
  23. package/dist/commands/try.js +2 -1
  24. package/dist/commands/upgrade.d.ts +6 -0
  25. package/dist/commands/upgrade.js +113 -0
  26. package/dist/config.d.ts +81 -3
  27. package/dist/config.js +91 -16
  28. package/dist/core/block.d.ts +19 -2
  29. package/dist/core/block.js +43 -3
  30. package/dist/core/blocks/nav.d.ts +11 -0
  31. package/dist/core/blocks/nav.js +8 -0
  32. package/dist/core/config.d.ts +5 -0
  33. package/dist/core/engine.d.ts +19 -16
  34. package/dist/core/engine.js +6 -4
  35. package/dist/core/flow.d.ts +27 -0
  36. package/dist/core/flow.js +75 -28
  37. package/dist/core/map-builder.d.ts +30 -4
  38. package/dist/core/map-builder.js +45 -7
  39. package/dist/env-file.d.ts +7 -7
  40. package/dist/env-file.js +29 -16
  41. package/dist/extensions.d.ts +21 -0
  42. package/dist/extensions.js +178 -0
  43. package/dist/highlights/demo-pace.d.ts +0 -1
  44. package/dist/highlights/demo-pace.js +24 -3
  45. package/dist/index.d.ts +7 -5
  46. package/dist/index.js +3 -2
  47. package/dist/pilot-overlay/action-ring.d.ts +0 -9
  48. package/dist/pilot-overlay/action-ring.js +1 -1
  49. package/dist/pilot-overlay/overlay-fixtures.d.ts +0 -7
  50. package/dist/pilot-overlay/overlay-fixtures.js +0 -45
  51. package/dist/pilot-overlay/panel.d.ts +2 -1
  52. package/dist/playwright-config.d.ts +4 -0
  53. package/dist/playwright-config.js +15 -0
  54. package/dist/playwright.config.d.ts +2 -0
  55. package/dist/playwright.config.js +6 -0
  56. package/dist/runner/demo-log.js +1 -7
  57. package/dist/runner/inpage/core.d.ts +1 -1
  58. package/dist/runner/inpage/core.js +8 -7
  59. package/dist/runner/interaction-patch.js +32 -1
  60. package/dist/runner/main.js +22 -11
  61. package/dist/runner/rings.js +0 -49
  62. package/dist/runner/step-mode.js +10 -4
  63. package/dist/session/auto-explore-run.js +9 -5
  64. package/dist/session/auto-explore.d.ts +2 -1
  65. package/dist/session/auto-explore.js +6 -4
  66. package/dist/session/auto-session-ipc.d.ts +1 -2
  67. package/dist/session/auto-session-ipc.js +1 -1
  68. package/dist/session/block-inject.d.ts +0 -13
  69. package/dist/session/block-inject.js +3 -3
  70. package/dist/session/pilot.d.ts +0 -2
  71. package/dist/session/pilot.js +1 -2
  72. package/dist/session/step-overlay.js +3 -1
  73. package/dist/traverse/coverage.d.ts +0 -4
  74. package/dist/traverse/coverage.js +1 -1
  75. package/dist/traverse/run.js +3 -1
  76. package/dist/types.d.ts +1 -1
  77. package/dist/ui/components/ring.js +1 -0
  78. package/dist/ui/overlay.css +1 -0
  79. package/dist/ui/shadow.d.ts +1 -3
  80. package/dist/ui/shadow.js +0 -4
  81. package/dist/ui/tokens.d.ts +0 -11
  82. package/dist/ui/tokens.js +0 -5
  83. package/dist/video-dir.d.ts +8 -0
  84. package/dist/video-dir.js +60 -0
  85. package/dist/webserver.js +5 -1
  86. package/package.json +11 -5
  87. package/templates/scaffold/README.md +5 -3
  88. package/templates/scaffold/STRUCTURE.md +10 -4
  89. package/templates/scaffold/config/waygraph.config.ts +45 -0
  90. package/templates/scaffold/package.json +7 -7
  91. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +36 -0
  92. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +24 -0
  93. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +34 -0
  94. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +47 -0
  95. package/templates/scaffold/src/flows/README.md +35 -14
  96. package/templates/scaffold/src/flows/home/cart.flow.ts +51 -0
  97. package/templates/scaffold/src/flows/home/hello.flow.ts +42 -0
  98. package/templates/scaffold/src/map/(app_base)/docs/_nav.block.ts +0 -9
  99. package/templates/scaffold/src/map/(app_base)/docs/_page.block.ts +0 -5
  100. package/templates/scaffold/src/map/(app_base)/docs/_sel.ts +1 -1
  101. package/templates/scaffold/src/map/(app_base)/home/_methods/add-item.effect.block.ts +3 -19
  102. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +2 -25
  103. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts +2 -19
  104. package/templates/scaffold/src/map/(app_base)/home/_methods/clear-cart.method.block.ts +1 -17
  105. package/templates/scaffold/src/map/(app_base)/home/_methods/remove-item.effect.block.ts +1 -10
  106. package/templates/scaffold/src/map/(app_base)/home/_nav.block.ts +1 -12
  107. package/templates/scaffold/src/map/(app_base)/home/_page.block.ts +0 -9
  108. package/templates/scaffold/src/map/(app_base)/home/_sel.ts +3 -1
  109. package/templates/scaffold/src/map/(app_base)/home/nav-verification-link.block.ts +1 -23
  110. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-response-body.method.block.ts +21 -0
  111. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-response-header.method.block.ts +19 -0
  112. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/assert-status.method.block.ts +20 -0
  113. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/pick-extension-interceptor.method.block.ts +18 -0
  114. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/read-response-body.method.block.ts +21 -0
  115. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/read-response-headers.method.block.ts +21 -0
  116. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/send-request.method.block.ts +15 -0
  117. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-bearer-token.method.block.ts +20 -0
  118. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-headers.method.block.ts +23 -0
  119. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-json-body.method.block.ts +36 -0
  120. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-method.method.block.ts +17 -0
  121. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-query-params.method.block.ts +22 -0
  122. package/templates/scaffold/src/map/(external)/hoppscotch/_methods/set-url.method.block.ts +18 -0
  123. package/templates/scaffold/src/map/(external)/hoppscotch/_nav.block.ts +11 -0
  124. package/templates/scaffold/src/map/(external)/hoppscotch/_page.block.ts +36 -0
  125. package/templates/scaffold/src/map/(external)/hoppscotch/_sel.ts +27 -0
  126. package/templates/scaffold/src/map/(external)/hoppscotch/settings/_nav.block.ts +10 -0
  127. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-content.method.block.ts +2 -27
  128. package/templates/scaffold/src/map/(external)/mailpit/_methods/assert-email-received.method.block.ts +2 -21
  129. package/templates/scaffold/src/map/(external)/mailpit/_methods/extract-email-link.method.block.ts +1 -19
  130. package/templates/scaffold/src/map/(external)/mailpit/_methods/open-message.method.block.ts +1 -11
  131. package/templates/scaffold/src/map/(external)/mailpit/_nav.block.ts +1 -9
  132. package/templates/scaffold/src/map/(external)/mailpit/_page.block.ts +0 -11
  133. package/templates/scaffold/src/states/demo.mem-keys.ts +20 -19
  134. package/templates/scaffold/src/states/demo.mem-stubs.ts +4 -0
  135. package/templates/scaffold/src/states/demo.states.ts +5 -1
  136. package/templates/scaffold/tests/example.spec.ts +6 -7
  137. package/templates/scaffold/tests/hoppscotch.spec.ts +97 -0
  138. package/templates/scaffold/tests/mail-verify.spec.ts +8 -17
  139. package/templates/scaffold/tsconfig.json +1 -1
  140. package/templates/skills/waygraph-convention.skill.md +40 -3
  141. package/templates/skills/waygraph-pilot.skill.md +4 -5
  142. package/dist/browser-path.d.ts +0 -5
  143. package/dist/browser-path.js +0 -18
  144. package/dist/runner/overlay-present.d.ts +0 -8
  145. package/dist/runner/overlay-present.js +0 -57
  146. package/dist/ui/components/banner.d.ts +0 -2
  147. package/dist/ui/components/banner.js +0 -10
  148. package/dist/ui/components/cursor.d.ts +0 -2
  149. package/dist/ui/components/cursor.js +0 -19
  150. package/templates/scaffold/playwright.config.ts +0 -12
  151. package/templates/scaffold/src/flows/example.flow.ts +0 -29
  152. package/templates/scaffold/src/flows/mail-verify.flow.ts +0 -85
  153. package/templates/scaffold/src/flows/shop.flow.ts +0 -74
@@ -2,16 +2,6 @@ import { defineMethodBlock, checkpoint, Trait } from "waygraph";
2
2
  import type { ItemInCart, CartEmpty } from "../../../../states/demo.states.js";
3
3
  import { DemoSel } from "../_sel.js";
4
4
 
5
- /**
6
- * Kind: Method
7
- * Helper: defineMethodBlock
8
- * Route: (app_base)/home/_methods/
9
- *
10
- * Real, pre-existing bug found live while verifying the Waygraph Map
11
- * rename: this Block had no explicit <In, Out> generic at all, so static
12
- * discovery (`waygraph graph`) always skipped it silently ("no ...generic
13
- * call found") - present before this move, not caused by it.
14
- */
15
5
  export const ClearCartBlock = defineMethodBlock<ItemInCart, CartEmpty>({
16
6
  name: "clear-cart",
17
7
  description: "Clears every item via the Clear cart control.",
@@ -22,10 +12,4 @@ export const ClearCartBlock = defineMethodBlock<ItemInCart, CartEmpty>({
22
12
  resolve: () => checkpoint("CartEmpty"),
23
13
  verify: [Trait.text(DemoSel.cartCount, "0")],
24
14
  },
25
- })
26
- .stubBefore((ctx) => {
27
- ctx.ring("clear", { selector: DemoSel.clear, label: "Clear cart" });
28
- })
29
- .stubAfter((ctx) => {
30
- ctx.ring("cart", { selector: DemoSel.cartCount, label: "Empty cart", duration: true });
31
- });
15
+ });
@@ -7,13 +7,6 @@ import type { CartEmpty, ItemInCart } from "../../../../states/demo.states.js";
7
7
  import { SelectedItem } from "../../../../states/demo.mem-keys.js";
8
8
  import { DemoSel } from "../_sel.js";
9
9
 
10
- /**
11
- * Kind: Effect
12
- * Helper: defineEffectBlock
13
- * Route: demo-web/methods/
14
- *
15
- * Removes one in-cart row. Auto menus one row per visible Remove button.
16
- */
17
10
  export const RemoveItemBlock = defineEffectBlock<ItemInCart, CartEmpty>({
18
11
  name: "remove-item",
19
12
  description: "Removes the mem-selected catalog item from the cart.",
@@ -35,7 +28,7 @@ export const RemoveItemBlock = defineEffectBlock<ItemInCart, CartEmpty>({
35
28
  ],
36
29
  },
37
30
  async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
38
- const buttons = page.locator("button.remove:not([hidden])");
31
+ const buttons = page.locator(DemoSel.removeButtons);
39
32
  const n = await buttons.count();
40
33
  const out: WaygraphInstanceOption[] = [];
41
34
  for (let i = 0; i < n; i++) {
@@ -54,6 +47,4 @@ export const RemoveItemBlock = defineEffectBlock<ItemInCart, CartEmpty>({
54
47
  }
55
48
  return out;
56
49
  },
57
- }).stubAfter((ctx) => {
58
- ctx.ring("cart", { selector: DemoSel.cartCount, label: "Cart count", duration: true });
59
50
  });
@@ -2,21 +2,10 @@ import { defineNavBlock, Trait } from "waygraph";
2
2
  import type { Home } from "../../../states/demo.states.js";
3
3
  import { DemoSel, HOME_URL } from "./_sel.js";
4
4
 
5
- /**
6
- * Kind: Nav
7
- * Helper: defineNavBlock
8
- * Route: (app_base)/home/ (synthetic "/")
9
- */
10
5
  export const NavHomeBlock = defineNavBlock<Home>({
11
6
  name: "nav-home",
12
7
  description: "Opens the offline catalog page (file://, no server).",
13
8
  checkpoint: "Home",
14
9
  url: HOME_URL,
15
10
  verify: [Trait.text(DemoSel.title, "Hello Waygraph"), Trait.visible(DemoSel.catalog)],
16
- })
17
- .stubAfter((ctx) => {
18
- ctx.ring("title", { selector: DemoSel.title, label: "Home heading", duration: true });
19
- })
20
- .stubOnError((ctx) => {
21
- ctx.ring("title", { selector: DemoSel.title, label: "Home heading at failure" });
22
- });
11
+ });
@@ -6,15 +6,6 @@ import { RemoveItemBlock } from "./_methods/remove-item.effect.block.js";
6
6
  import { ClearCartBlock } from "./_methods/clear-cart.method.block.js";
7
7
  import { AssertHelloBlock } from "./_methods/assert-hello.method.block.js";
8
8
 
9
- /**
10
- * Kind: Page
11
- * Helper: definePageBlock
12
- * Route: (app_base)/home/
13
- *
14
- * Hub for the offline home screen. Methods hang here for docs/auto grouping;
15
- * edges in flows still reference the method Blocks directly.
16
- * Arrival-only (nav-home already landed on Home).
17
- */
18
9
  export const HomePageBlock = definePageBlock<Home>({
19
10
  name: "page-home",
20
11
  description: "Offline home hub - add/remove effects, clear, assert-hello.",
@@ -1,4 +1,4 @@
1
- // Relative to playwright.config.ts's baseURL (the (app_base) folder, set by `waygraph init`).
1
+ // Relative to baseUrl in config/waygraph.config.ts (the (app_base) folder, set by `waygraph init`).
2
2
  export const HOME_URL = "home/index.html";
3
3
 
4
4
  export const DemoSel = {
@@ -7,6 +7,8 @@ export const DemoSel = {
7
7
  cartCount: "[data-test=cart-count]",
8
8
  catalog: "#catalog",
9
9
  clear: "[data-test=clear-cart]",
10
+ addButtons: "button.add:not([hidden])",
11
+ removeButtons: "button.remove:not([hidden])",
10
12
  addBtn: (id: string) => `[data-test=add-${id}]`,
11
13
  removeBtn: (id: string) => `[data-test=remove-${id}]`,
12
14
  verifiedBanner: "#verified-banner",
@@ -3,29 +3,7 @@ import type { HomeVerified } from "../../../states/demo.states.js";
3
3
  import { EmailLink } from "../../../states/demo.mem-keys.js";
4
4
  import { DemoSel } from "./_sel.js";
5
5
 
6
- /**
7
- * Kind: Nav
8
- * Helper: defineNavBlock
9
- * Route: (app_base)/home/
10
- *
11
- * Navigates back into the app to the link extract-email-link read out of
12
- * the real Mailpit message, then confirms the page actually reflects it
13
- * (the verified banner only appears when the exact `verified=1` link was
14
- * followed, not just any URL). This particular check is app-specific (this
15
- * scaffold's own verification landing route) even though the mail-reading
16
- * Blocks feeding it are scenario-agnostic - a real project would have one
17
- * such Nav per distinct landing route, same as any other app screen.
18
- *
19
- * Deliberately no `requires: [EmailLink.key]` here: `requires` means "the
20
- * caller must seed this externally before the flow starts" (preflight
21
- * checks the whole chain's requires up front) - `EmailLink` is instead
22
- * produced by an earlier Block in this very chain, so declaring it as a
23
- * requirement would make preflight fail before the flow ever gets the
24
- * chance to produce it. Engine support for declaring a mem key as "produced
25
- * by an earlier Block in this chain" (vs. externally supplied) is a
26
- * separate, not-yet-built roadmap item - see ROADMAP.md's "Split requires"
27
- * row.
28
- */
6
+ /** Follows the link `extract-email-link` put in mem. No `requires`: an earlier Block in the chain produces it. */
29
7
  export const NavVerificationLinkBlock = defineNavBlock<HomeVerified>({
30
8
  name: "nav-verification-link",
31
9
  checkpoint: "HomeVerified",
@@ -0,0 +1,21 @@
1
+ import { defineAssertBlock, type Trait } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { ExpectedResponseText, ResponseBody } from "../../../../states/demo.mem-keys.js";
4
+
5
+ // Hoppscotch pretty-prints JSON, so compare with whitespace removed from both sides.
6
+ const squash = (text: string): string => text.replace(/\s+/g, "");
7
+
8
+ const bodyContainsExpected: Trait = {
9
+ name: "response-body-contains-expected",
10
+ async check(_page, mem) {
11
+ return squash(mem.get(ResponseBody)).includes(squash(mem.get(ExpectedResponseText)));
12
+ },
13
+ };
14
+
15
+ export const AssertResponseBodyBlock = defineAssertBlock<HoppscotchRest>({
16
+ name: "assert-response-body",
17
+ description: "Confirms the response body read earlier contains the mem-supplied text (whitespace ignored).",
18
+ checkpoint: "HoppscotchRest",
19
+ requires: [ExpectedResponseText],
20
+ verify: [bodyContainsExpected],
21
+ });
@@ -0,0 +1,19 @@
1
+ import { defineAssertBlock, type Trait } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { ExpectedResponseHeader, ResponseHeaders } from "../../../../states/demo.mem-keys.js";
4
+
5
+ const headerMatches: Trait = {
6
+ name: "response-header-matches",
7
+ async check(_page, mem) {
8
+ const { name, value } = mem.get(ExpectedResponseHeader.key);
9
+ return mem.get(ResponseHeaders)[name.toLowerCase()] === value;
10
+ },
11
+ };
12
+
13
+ export const AssertResponseHeaderBlock = defineAssertBlock<HoppscotchRest>({
14
+ name: "assert-response-header",
15
+ description: "Confirms a response header read earlier has the mem-supplied value.",
16
+ checkpoint: "HoppscotchRest",
17
+ requires: [ExpectedResponseHeader.key],
18
+ verify: [headerMatches],
19
+ });
@@ -0,0 +1,20 @@
1
+ import { defineAssertBlock, type Trait } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { ExpectedStatus } from "../../../../states/demo.mem-keys.js";
4
+ import { HoppscotchSel } from "../_sel.js";
5
+
6
+ const statusMatches: Trait = {
7
+ name: "response-status-matches",
8
+ async check(page, mem) {
9
+ const text = (await page.locator(HoppscotchSel.status).first().textContent()) ?? "";
10
+ return Number(text.match(/Status:\s*(\d+)/)?.[1]) === mem.get(ExpectedStatus);
11
+ },
12
+ };
13
+
14
+ export const AssertStatusBlock = defineAssertBlock<HoppscotchRest>({
15
+ name: "assert-status",
16
+ description: "Confirms the response status code equals the mem-supplied one.",
17
+ checkpoint: "HoppscotchRest",
18
+ requires: [ExpectedStatus],
19
+ verify: [statusMatches],
20
+ });
@@ -0,0 +1,18 @@
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
+ import type { HoppscotchSettings } from "../../../../states/demo.states.js";
3
+ import { HoppscotchSel } from "../_sel.js";
4
+
5
+ export const PickExtensionInterceptorBlock = defineMethodBlock<HoppscotchSettings, HoppscotchSettings>({
6
+ name: "pick-extension-interceptor",
7
+ description: "Routes Hoppscotch's requests through the browser extension, which skips CORS.",
8
+ instruction: {
9
+ async act(page) {
10
+ const radio = page.locator(HoppscotchSel.extensionInterceptor);
11
+ await radio.waitFor({ timeout: 10_000 }).catch(() => {
12
+ throw new Error('Hoppscotch extension not loaded - add extensions: ["hoppscotch"] to waygraph.config.ts');
13
+ });
14
+ await radio.click();
15
+ },
16
+ resolve: () => checkpoint("HoppscotchSettings"),
17
+ },
18
+ });
@@ -0,0 +1,21 @@
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { ResponseBody } from "../../../../states/demo.mem-keys.js";
4
+ import { HoppscotchSel } from "../_sel.js";
5
+
6
+ export const ReadResponseBodyBlock = defineMethodBlock<HoppscotchRest, HoppscotchRest>({
7
+ name: "read-response-body",
8
+ description: "Reads the raw response body into mem.",
9
+ instruction: {
10
+ async act(page, _in, mem) {
11
+ await page.locator(HoppscotchSel.rawTab).click();
12
+ const body = page.locator(HoppscotchSel.responseEditor);
13
+ await body.waitFor();
14
+ mem.set(ResponseBody, (await body.innerText()).trim());
15
+ // Raw is the reliable place to read from; go back to the formatted JSON view so a recording ends on it.
16
+ const json = page.locator(HoppscotchSel.responseJsonTab);
17
+ if (await json.count()) await json.click();
18
+ },
19
+ resolve: () => checkpoint("HoppscotchRest"),
20
+ },
21
+ });
@@ -0,0 +1,21 @@
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { ResponseHeaders } from "../../../../states/demo.mem-keys.js";
4
+ import { HoppscotchSel } from "../_sel.js";
5
+
6
+ export const ReadResponseHeadersBlock = defineMethodBlock<HoppscotchRest, HoppscotchRest>({
7
+ name: "read-response-headers",
8
+ description: "Reads the response headers (lower-cased names) into mem.",
9
+ instruction: {
10
+ async act(page, _in, mem) {
11
+ await page.locator(HoppscotchSel.responseHeadersTab).click();
12
+ const cells = page.locator(HoppscotchSel.responseHeaderCell);
13
+ await cells.first().waitFor();
14
+ const texts = (await cells.allInnerTexts()).map((t) => t.trim());
15
+ const headers: Record<string, string> = {};
16
+ for (let i = 0; i + 1 < texts.length; i += 2) headers[texts[i]!.toLowerCase()] = texts[i + 1]!;
17
+ mem.set(ResponseHeaders, headers);
18
+ },
19
+ resolve: () => checkpoint("HoppscotchRest"),
20
+ },
21
+ });
@@ -0,0 +1,15 @@
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { HoppscotchSel } from "../_sel.js";
4
+
5
+ export const SendRequestBlock = defineMethodBlock<HoppscotchRest, HoppscotchRest>({
6
+ name: "send-request",
7
+ description: "Sends the request and waits for the response status to appear.",
8
+ instruction: {
9
+ async act(page) {
10
+ await page.locator(HoppscotchSel.send).click();
11
+ await page.locator(HoppscotchSel.status).waitFor({ timeout: 20_000 });
12
+ },
13
+ resolve: () => checkpoint("HoppscotchRest"),
14
+ },
15
+ });
@@ -0,0 +1,20 @@
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { RequestBearerToken } from "../../../../states/demo.mem-keys.js";
4
+ import { HoppscotchSel } from "../_sel.js";
5
+
6
+ export const SetBearerTokenBlock = defineMethodBlock<HoppscotchRest, HoppscotchRest>({
7
+ name: "set-bearer-token",
8
+ description: "Sets the Authorization tab to Bearer and types the mem-supplied token.",
9
+ requires: [RequestBearerToken],
10
+ instruction: {
11
+ async act(page, _in, mem) {
12
+ await page.locator(HoppscotchSel.tab("Authorization")).click();
13
+ await page.locator(HoppscotchSel.authTypeMenu).click();
14
+ await page.locator(HoppscotchSel.menuItem("Bearer")).click();
15
+ await page.locator(HoppscotchSel.bearerToken).locator(HoppscotchSel.editor).click();
16
+ await page.keyboard.type(mem.get(RequestBearerToken));
17
+ },
18
+ resolve: () => checkpoint("HoppscotchRest"),
19
+ },
20
+ });
@@ -0,0 +1,23 @@
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { RequestHeaders } from "../../../../states/demo.mem-keys.js";
4
+ import { HoppscotchSel } from "../_sel.js";
5
+
6
+ export const SetHeadersBlock = defineMethodBlock<HoppscotchRest, HoppscotchRest>({
7
+ name: "set-headers",
8
+ description: "Adds each mem-supplied header (name -> value) on the Headers tab.",
9
+ requires: [RequestHeaders],
10
+ instruction: {
11
+ async act(page, _in, mem) {
12
+ await page.locator(HoppscotchSel.tab("Headers")).click();
13
+ const headers = Object.entries(mem.get(RequestHeaders));
14
+ for (const [i, [name, value]] of headers.entries()) {
15
+ await page.locator(HoppscotchSel.keyCell).nth(i).locator(HoppscotchSel.editor).click();
16
+ await page.keyboard.type(name);
17
+ await page.locator(HoppscotchSel.valueCell).nth(i).locator(HoppscotchSel.editor).click();
18
+ await page.keyboard.type(value);
19
+ }
20
+ },
21
+ resolve: () => checkpoint("HoppscotchRest"),
22
+ },
23
+ });
@@ -0,0 +1,36 @@
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { RequestBody } from "../../../../states/demo.mem-keys.js";
4
+ import { HoppscotchSel } from "../_sel.js";
5
+
6
+ const spansLines = (text: string): boolean => {
7
+ try {
8
+ return JSON.stringify(JSON.parse(text), null, 2).includes("\n");
9
+ } catch {
10
+ return false;
11
+ }
12
+ };
13
+
14
+ export const SetJsonBodyBlock = defineMethodBlock<HoppscotchRest, HoppscotchRest>({
15
+ name: "set-json-body",
16
+ description: "Sets the content type to application/json and types the mem-supplied body, prettified.",
17
+ requires: [RequestBody],
18
+ instruction: {
19
+ async act(page, _in, mem) {
20
+ await page.locator(HoppscotchSel.tab("Body")).click();
21
+ await page.locator(HoppscotchSel.contentTypeMenu).click();
22
+ await page.locator(HoppscotchSel.menuItem("application/json")).click();
23
+ const editor = page.locator(HoppscotchSel.visibleEditor).last();
24
+ await editor.click();
25
+ const body = mem.get(RequestBody);
26
+ await page.keyboard.type(body);
27
+ if (!spansLines(body)) return;
28
+ // Prettify is the third toolbar button (Clear, Wrap lines, Prettify); typed JSON stays on one line otherwise.
29
+ await page.locator(HoppscotchSel.bodyToolbarButton).nth(2).click();
30
+ if ((await editor.locator(HoppscotchSel.bodyLine).count()) < 2) {
31
+ throw new Error("set-json-body: Hoppscotch's Prettify button was not found, so the body stayed on one line");
32
+ }
33
+ },
34
+ resolve: () => checkpoint("HoppscotchRest"),
35
+ },
36
+ });
@@ -0,0 +1,17 @@
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { RequestMethod } from "../../../../states/demo.mem-keys.js";
4
+ import { HoppscotchSel } from "../_sel.js";
5
+
6
+ export const SetMethodBlock = defineMethodBlock<HoppscotchRest, HoppscotchRest>({
7
+ name: "set-method",
8
+ description: "Picks the HTTP method (GET, POST, PUT, PATCH, DELETE, ...) from the request bar.",
9
+ requires: [RequestMethod],
10
+ instruction: {
11
+ async act(page, _in, mem) {
12
+ await page.locator(HoppscotchSel.methodInput).click();
13
+ await page.locator(HoppscotchSel.menuItem(mem.get(RequestMethod))).click();
14
+ },
15
+ resolve: () => checkpoint("HoppscotchRest"),
16
+ },
17
+ });
@@ -0,0 +1,22 @@
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { RequestParams } from "../../../../states/demo.mem-keys.js";
4
+ import { HoppscotchSel } from "../_sel.js";
5
+
6
+ export const SetQueryParamsBlock = defineMethodBlock<HoppscotchRest, HoppscotchRest>({
7
+ name: "set-query-params",
8
+ description: "Adds each mem-supplied query parameter (name -> value) on the Parameters tab.",
9
+ requires: [RequestParams],
10
+ instruction: {
11
+ async act(page, _in, mem) {
12
+ await page.locator(HoppscotchSel.tab("Parameters")).click();
13
+ for (const [i, [name, value]] of Object.entries(mem.get(RequestParams)).entries()) {
14
+ await page.locator(HoppscotchSel.keyCell).nth(i).locator(HoppscotchSel.editor).click();
15
+ await page.keyboard.type(name);
16
+ await page.locator(HoppscotchSel.valueCell).nth(i).locator(HoppscotchSel.editor).click();
17
+ await page.keyboard.type(value);
18
+ }
19
+ },
20
+ resolve: () => checkpoint("HoppscotchRest"),
21
+ },
22
+ });
@@ -0,0 +1,18 @@
1
+ import { defineMethodBlock, checkpoint } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../../states/demo.states.js";
3
+ import { RequestUrl } from "../../../../states/demo.mem-keys.js";
4
+ import { HoppscotchSel } from "../_sel.js";
5
+
6
+ export const SetUrlBlock = defineMethodBlock<HoppscotchRest, HoppscotchRest>({
7
+ name: "set-url",
8
+ description: "Types the request URL into the request bar.",
9
+ requires: [RequestUrl],
10
+ instruction: {
11
+ async act(page, _in, mem) {
12
+ await page.locator(HoppscotchSel.urlInput).first().click();
13
+ await page.keyboard.press("Control+A");
14
+ await page.keyboard.type(mem.get(RequestUrl));
15
+ },
16
+ resolve: () => checkpoint("HoppscotchRest"),
17
+ },
18
+ });
@@ -0,0 +1,11 @@
1
+ import { defineNavBlock, Trait } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../states/demo.states.js";
3
+ import { HoppscotchSel } from "./_sel.js";
4
+
5
+ export const NavHoppscotchRestBlock = defineNavBlock<HoppscotchRest>({
6
+ name: "nav-hoppscotch-rest",
7
+ checkpoint: "HoppscotchRest",
8
+ // Cross-origin, so a function: the Map folder rule only checks static URLs.
9
+ url: () => "https://hoppscotch.io/",
10
+ verify: [Trait.visible(HoppscotchSel.send)],
11
+ });
@@ -0,0 +1,36 @@
1
+ import { definePageBlock, Trait } from "waygraph";
2
+ import type { HoppscotchRest } from "../../../states/demo.states.js";
3
+ import { SetMethodBlock } from "./_methods/set-method.method.block.js";
4
+ import { SetUrlBlock } from "./_methods/set-url.method.block.js";
5
+ import { SetHeadersBlock } from "./_methods/set-headers.method.block.js";
6
+ import { SetJsonBodyBlock } from "./_methods/set-json-body.method.block.js";
7
+ import { SendRequestBlock } from "./_methods/send-request.method.block.js";
8
+ import { AssertStatusBlock } from "./_methods/assert-status.method.block.js";
9
+ import { ReadResponseBodyBlock } from "./_methods/read-response-body.method.block.js";
10
+ import { AssertResponseBodyBlock } from "./_methods/assert-response-body.method.block.js";
11
+ import { SetQueryParamsBlock } from "./_methods/set-query-params.method.block.js";
12
+ import { SetBearerTokenBlock } from "./_methods/set-bearer-token.method.block.js";
13
+ import { ReadResponseHeadersBlock } from "./_methods/read-response-headers.method.block.js";
14
+ import { AssertResponseHeaderBlock } from "./_methods/assert-response-header.method.block.js";
15
+ import { HoppscotchSel } from "./_sel.js";
16
+
17
+ export const HoppscotchRestPageBlock = definePageBlock<HoppscotchRest>({
18
+ name: "page-hoppscotch-rest",
19
+ description: "Hoppscotch REST client - build a request, send it, check the response.",
20
+ checkpoint: "HoppscotchRest",
21
+ verify: [Trait.visible(HoppscotchSel.send)],
22
+ methods: {
23
+ setMethod: () => SetMethodBlock,
24
+ setUrl: () => SetUrlBlock,
25
+ setHeaders: () => SetHeadersBlock,
26
+ setQueryParams: () => SetQueryParamsBlock,
27
+ setBearerToken: () => SetBearerTokenBlock,
28
+ setJsonBody: () => SetJsonBodyBlock,
29
+ sendRequest: () => SendRequestBlock,
30
+ assertStatus: () => AssertStatusBlock,
31
+ readResponseHeaders: () => ReadResponseHeadersBlock,
32
+ assertResponseHeader: () => AssertResponseHeaderBlock,
33
+ readResponseBody: () => ReadResponseBodyBlock,
34
+ assertResponseBody: () => AssertResponseBodyBlock,
35
+ },
36
+ });
@@ -0,0 +1,27 @@
1
+ // Structural selectors (roles, aria-labels), not bare text: waygraph's own demo overlay repeats page text.
2
+ export const HoppscotchSel = {
3
+ urlInput: ".cm-content",
4
+ send: "#send",
5
+ methodInput: "input#method",
6
+ /** The request's tab; the response has its own Headers tab, so take the first match. */
7
+ tab: (name: string) => `button.tab[aria-label='${name}'] >> nth=0`,
8
+ menuItem: (name: string) => `button[role=menuitem]:has(:text-is("${name}"))`,
9
+ keyCell: '[placeholder^="Key"]:visible',
10
+ valueCell: '[placeholder^="Value"]:visible',
11
+ contentTypeMenu: "label:text-is('Content Type') + span button",
12
+ bearerToken: '[placeholder^="Your Bearer Token"]',
13
+ authTypeMenu: "label:text-is('Authorization Type') + span button",
14
+ /** The element holding "Status: 201 • Created". */
15
+ status: "*:has(> span.text-secondary:text-is('Status:'))",
16
+ responseHeadersTab: "button.tab[aria-label='Headers'] >> nth=-1",
17
+ responseHeaderCell: "*:has(> span.text-secondary:text-is('Status:')) >> xpath=following::span[contains(@class,'select-all')]",
18
+ responseJsonTab: "button.tab[aria-label='JSON']",
19
+ rawTab: "button.tab[aria-label='Raw']",
20
+ responseEditor: "div.flex-col:has(> div.sticky label:text-is('Response Body')) .cm-content",
21
+ bodyToolbarButton: "div.sticky:has(label:text-is('Raw Request Body')) button",
22
+ bodyLine: ".cm-line",
23
+ editor: ".cm-content",
24
+ visibleEditor: ".cm-content:visible",
25
+ interceptorHeading: "h4:text('Interceptor')",
26
+ extensionInterceptor: 'button:has-text("Browser extension: v")',
27
+ } as const;
@@ -0,0 +1,10 @@
1
+ import { defineNavBlock, Trait } from "waygraph";
2
+ import type { HoppscotchSettings } from "../../../../states/demo.states.js";
3
+ import { HoppscotchSel } from "../_sel.js";
4
+
5
+ export const NavHoppscotchSettingsBlock = defineNavBlock<HoppscotchSettings>({
6
+ name: "nav-hoppscotch-settings",
7
+ checkpoint: "HoppscotchSettings",
8
+ url: () => "https://hoppscotch.io/settings",
9
+ verify: [Trait.visible(HoppscotchSel.interceptorHeading)],
10
+ });
@@ -3,13 +3,7 @@ import type { MailpitMessageOpen } from "../../../../states/demo.states.js";
3
3
  import { ExpectedEmailContent } from "../../../../states/demo.mem-keys.js";
4
4
  import { MailpitSel } from "../_sel.js";
5
5
 
6
- /**
7
- * Mem-aware Trait: the expected copy varies per email scenario, so it can't
8
- * be a static `Trait.frameContains(...)` call (which bakes its expected
9
- * string in at Block-definition time) - a Trait's own `check(page, mem)`
10
- * already receives mem, so a bespoke Trait reads it at check time instead.
11
- * This is what lets one Block serve any number of different email templates.
12
- */
6
+ // Reads the expected copy from mem, so one Block serves any email template.
13
7
  const bodyContainsExpectedText: Trait = {
14
8
  name: "body-contains-expected-text",
15
9
  async check(page, mem) {
@@ -19,26 +13,7 @@ const bodyContainsExpectedText: Trait = {
19
13
  },
20
14
  };
21
15
 
22
- /**
23
- * Kind: Assert
24
- * Helper: defineAssertBlock
25
- * Route: demo-external/mailpit/_methods/
26
- *
27
- * "Does the email actually say what it's supposed to" - not just "does a
28
- * link exist somewhere in it." Reads the message body straight out of the
29
- * preview iframe (the top-level page's own `Trait.text`/`Trait.visible`
30
- * can't see inside an iframe at all - see docs/REFERENCE.md "Mail adapters" section).
31
- * Self-loop on MailpitMessageOpen - no state change, verify only.
32
- *
33
- * Deliberately named for what it does, not for one scenario: the expected
34
- * copy comes from mem (`ExpectedEmailContent`), not a hardcoded string in
35
- * this file, so this one Block serves any number of different email
36
- * scenarios - a Block named "assert-verification-email-content" would
37
- * wrongly imply it only ever checks verification-email copy.
38
- *
39
- * Explicit `<MailpitMessageOpen>` type argument - see the same note on
40
- * `assert-email-received.method.block.ts` for why this can't be inferred.
41
- */
16
+ /** The open message says what the scenario expects (copy comes from mem). */
42
17
  export const AssertEmailContentBlock = defineAssertBlock<MailpitMessageOpen>({
43
18
  name: "assert-email-content",
44
19
  description: "Confirms the open message's body contains the mem-supplied expected copy.",
@@ -3,12 +3,7 @@ import type { MailpitInbox } from "../../../../states/demo.states.js";
3
3
  import { ExpectedRecipient } from "../../../../states/demo.mem-keys.js";
4
4
  import { MailpitSel } from "../_sel.js";
5
5
 
6
- /**
7
- * Mem-aware Trait: the selector depends on which recipient this run expects,
8
- * so it can't be a static `Trait.visible(...)` string - a Trait's own
9
- * `check(page, mem)` already receives mem, so a bespoke Trait object reads it
10
- * directly instead of needing a new engine primitive.
11
- */
6
+ // Mem-aware: which recipient to look for is only known at run time.
12
7
  const emailReceived: Trait = {
13
8
  name: "email-received",
14
9
  async check(page, mem) {
@@ -25,21 +20,7 @@ const emailReceived: Trait = {
25
20
  },
26
21
  };
27
22
 
28
- /**
29
- * Kind: Assert
30
- * Helper: defineAssertBlock
31
- * Route: demo-external/mailpit/_methods/
32
- *
33
- * "Did the email even arrive" - a lightweight existence check that does not
34
- * open or consume the message, so it's safe to run before deciding to open
35
- * anything. Self-loop on MailpitInbox - no state change, verify only.
36
- *
37
- * Explicit `<MailpitInbox>` type argument: `defineAssertBlock` defaults to
38
- * wildcard `Checkpoint<string>` on both sides when uninferred, which only
39
- * "works" when the assert sits last in a chain (nothing downstream narrows
40
- * it) - this one sits between two specifically-typed Blocks, so it must be
41
- * parameterized explicitly or `defineFlow`'s tuple typing breaks.
42
- */
23
+ /** A message for the expected recipient has arrived; nothing is opened or consumed. */
43
24
  export const AssertEmailReceivedBlock = defineAssertBlock<MailpitInbox>({
44
25
  name: "assert-email-received",
45
26
  description: "Confirms a message addressed to the expected recipient has arrived.",
@@ -3,25 +3,7 @@ import type { MailpitMessageOpen } from "../../../../states/demo.states.js";
3
3
  import { EmailLink, ExpectedLinkPattern } from "../../../../states/demo.mem-keys.js";
4
4
  import { MailpitSel } from "../_sel.js";
5
5
 
6
- /**
7
- * Kind: Method
8
- * Helper: defineMethodBlock
9
- * Route: demo-external/mailpit/_methods/
10
- *
11
- * Reads a link straight out of the open message's HTML preview iframe via a
12
- * real DOM read (getAttribute) - no mail-catcher REST API - and stores it in
13
- * mem. A separate NavBlock does the actual navigation: folding "read the
14
- * link" and "go there" into one Block would repeat the exact compound-action
15
- * anti-pattern `waygraph check` flags elsewhere.
16
- *
17
- * Deliberately named for what it does, not for one scenario: the
18
- * link-matching substring comes from mem (`ExpectedLinkPattern`), not a
19
- * hardcoded string, so this one Block serves any number of different email
20
- * scenarios in a project (a signup confirmation, a password reset, a magic
21
- * link, ...) by varying mem per run, not by writing a new Block per
22
- * scenario - a Block named "extract-verification-link" would wrongly imply
23
- * it only ever handles verification emails.
24
- */
6
+ /** Reads the link matching `ExpectedLinkPattern` out of the preview into mem; going there is a separate Nav. */
25
7
  export const ExtractEmailLinkBlock = defineMethodBlock<MailpitMessageOpen, MailpitMessageOpen>({
26
8
  name: "extract-email-link",
27
9
  description: "Reads the link matching the mem-supplied pattern out of the open message's preview.",