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
@@ -4,17 +4,7 @@ import type { MailpitInbox, MailpitMessageOpen } from "../../../../states/demo.s
4
4
  import { ExpectedRecipient } from "../../../../states/demo.mem-keys.js";
5
5
  import { MailpitSel } from "../_sel.js";
6
6
 
7
- /**
8
- * Kind: Method
9
- * Helper: defineMethodBlock
10
- * Route: demo-external/mailpit/_methods/
11
- *
12
- * Opens the newest inbox row addressed to the mem-set recipient (not bare
13
- * inbox position - a prior run's leftover mail for a different recipient
14
- * would otherwise pick the wrong row). UI click only, no mail-catcher REST
15
- * API - the row itself is a real link (`page.waitForURL` sees the client-side
16
- * route change the same as any other in-page navigation).
17
- */
7
+ /** Opens the newest message for the expected recipient (matched by email, not position). */
18
8
  export const OpenMessageBlock = defineMethodBlock<MailpitInbox, MailpitMessageOpen>({
19
9
  name: "open-message",
20
10
  description: "Opens the newest message addressed to the expected recipient.",
@@ -1,15 +1,7 @@
1
1
  import { defineNavBlock, Trait } from "waygraph";
2
2
  import type { MailpitInbox } from "../../../states/demo.states.js";
3
3
 
4
- /**
5
- * Kind: Nav
6
- * Helper: defineNavBlock
7
- * Route: (external)/mailpit/ (cross-origin - a real Mailpit web UI, not this app)
8
- *
9
- * Cross-origin tooling lives under (external)/, never (app_base)/ - this is
10
- * a dev-inbox screen, not part of the app under test. `WAYGRAPH_MAIL_URL`
11
- * defaults to Mailpit's own usual local port.
12
- */
4
+ // Cross-origin, so it lives under (external)/ and the URL is a function.
13
5
  const mailpitUrl = (): string => process.env.WAYGRAPH_MAIL_URL ?? "http://127.0.0.1:8025";
14
6
 
15
7
  export const NavMailpitInboxBlock = defineNavBlock<MailpitInbox>({
@@ -5,17 +5,6 @@ import { OpenMessageBlock } from "./_methods/open-message.method.block.js";
5
5
  import { AssertEmailContentBlock } from "./_methods/assert-email-content.method.block.js";
6
6
  import { ExtractEmailLinkBlock } from "./_methods/extract-email-link.method.block.js";
7
7
 
8
- /**
9
- * Kind: Page
10
- * Helper: definePageBlock
11
- * Route: (external)/mailpit/ (= http://127.0.0.1:8025/ by default)
12
- *
13
- * Hub for the Mailpit dev-inbox screen. Arrival-only (no url/click) -
14
- * nav-mailpit-inbox owns the goto. Methods: confirm the email arrived, open
15
- * it, confirm its content, then read its link - all mem-driven (recipient,
16
- * link pattern, expected content), so this one page's methods serve any
17
- * number of different email scenarios, not just one.
18
- */
19
8
  export const MailpitInboxPageBlock = definePageBlock<MailpitInbox>({
20
9
  name: "page-mailpit-inbox",
21
10
  description: "Mailpit dev inbox hub - confirm arrival/content, open the expected message, read its link.",
@@ -1,31 +1,32 @@
1
1
  import { key, keyGroup } from "waygraph";
2
2
 
3
- /**
4
- * Which catalog row `add-item` / `remove-item` act on.
5
- * `waygraph auto` fills this via Effect `instanceOptions` (one menu row per live button).
6
- */
3
+ /** The catalog row `add-item` / `remove-item` act on; `waygraph auto` fills it from `instanceOptions`. */
7
4
  export const SelectedItem = keyGroup<{ id: string; name: string }>("demo.selectedItem");
8
5
 
9
6
  /** The link read out of the opened message - set by extract-email-link. */
10
7
  export const EmailLink = keyGroup<{ url: string }>("demo.emailLink");
11
8
 
12
- /**
13
- * The recipient open-message looks for in the inbox. Mailpit lists newest
14
- * first, but `.first()` alone picks the wrong row once more than one
15
- * recipient's mail is sitting in the same inbox - match by mem, not position.
16
- */
9
+ /** Matched by email, not inbox position, so one inbox can hold several recipients' mail. */
17
10
  export const ExpectedRecipient = keyGroup<{ email: string }>("demo.expectedRecipient");
18
11
 
19
- /**
20
- * Substring the link's `href` must contain - lets the same extract-email-link
21
- * Block serve any number of different email templates/scenarios in one
22
- * project without a new Block per scenario. Only the mem values change per
23
- * run, not the Blocks.
24
- */
12
+ /** Substring the link's `href` must contain; mem varies per scenario, the Blocks do not. */
25
13
  export const ExpectedLinkPattern = key<string>("demo.expectedLinkPattern");
26
14
 
27
- /**
28
- * The substring the open message's body must contain - same reuse reasoning
29
- * as ExpectedLinkPattern: one Block, many scenarios, driven by mem.
30
- */
15
+ /** Substring the open message's body must contain. */
31
16
  export const ExpectedEmailContent = key<string>("demo.expectedEmailContent");
17
+
18
+ export const RequestMethod = key<string>("demo.requestMethod");
19
+ export const RequestUrl = key<string>("demo.requestUrl");
20
+ export const RequestHeaders = key<Record<string, string>>("demo.requestHeaders");
21
+ export const RequestParams = key<Record<string, string>>("demo.requestParams");
22
+ export const RequestBearerToken = key<string>("demo.requestBearerToken");
23
+ /** Raw JSON text typed into the request body editor. */
24
+ export const RequestBody = key<string>("demo.requestBody");
25
+ export const ExpectedStatus = key<number>("demo.expectedStatus");
26
+ /** Substring the response body must contain. */
27
+ export const ExpectedResponseText = key<string>("demo.expectedResponseText");
28
+ /** The raw response body, set by read-response-body. */
29
+ export const ResponseBody = key<string>("demo.responseBody");
30
+ /** Response headers (lower-cased names), set by read-response-headers. */
31
+ export const ResponseHeaders = key<Record<string, string>>("demo.responseHeaders");
32
+ export const ExpectedResponseHeader = keyGroup<{ name: string; value: string }>("demo.expectedResponseHeader");
@@ -0,0 +1,4 @@
1
+ import { registerMemStub } from "waygraph";
2
+ import { SelectedItem } from "./demo.mem-keys.js";
3
+
4
+ registerMemStub(SelectedItem.key, () => ({ id: "alpha", name: "Alpha" }));
@@ -15,5 +15,9 @@ export type MailpitInbox = Checkpoint<"MailpitInbox">;
15
15
  /** The message addressed to the expected recipient is open; preview iframe visible. */
16
16
  export type MailpitMessageOpen = Checkpoint<"MailpitMessageOpen">;
17
17
 
18
- /** Reached via src/map/(app_base)/docs/ - see openspec/changes/waygraph-map. */
18
+ // --- Hoppscotch (external API client, src/map/(external)/hoppscotch/) ---
19
+
20
+ export type HoppscotchSettings = Checkpoint<"HoppscotchSettings">;
21
+ export type HoppscotchRest = Checkpoint<"HoppscotchRest">;
22
+
19
23
  export type Docs = Checkpoint<"Docs">;
@@ -1,17 +1,16 @@
1
1
  import { test, expect } from "@playwright/test";
2
- import { MemPage, checkpoint } from "waygraph";
3
- import { exampleFlow } from "../src/flows/example.flow.js";
4
- import { shopFlow } from "../src/flows/shop.flow.js";
5
- import { SelectedItem } from "../src/states/demo.mem-keys.js";
2
+ import { MemPage, checkpoint, seedMemStub } from "waygraph";
3
+ import { homeHelloFlow } from "../src/flows/home/hello.flow.js";
4
+ import { homeCartFlow } from "../src/flows/home/cart.flow.js";
6
5
 
7
6
  test("example flow reaches Docs (home -> assert -> sibling docs/ page)", async ({ context }) => {
8
- const result = await exampleFlow.run(context, new MemPage());
7
+ const result = await homeHelloFlow.run(context, new MemPage());
9
8
  expect(result).toEqual(checkpoint("Docs"));
10
9
  });
11
10
 
12
11
  test("shop flow adds then clears (Effect + Method)", async ({ context }) => {
13
12
  const mem = new MemPage();
14
- mem.set(SelectedItem.key, { id: "alpha", name: "Alpha" });
15
- const result = await shopFlow.run(context, mem);
13
+ seedMemStub(mem, homeCartFlow);
14
+ const result = await homeCartFlow.run(context, mem);
16
15
  expect(result).toEqual(checkpoint("CartEmpty"));
17
16
  });
@@ -0,0 +1,97 @@
1
+ import { test, expect } from "@playwright/test";
2
+ import { createServer, type Server } from "node:http";
3
+ import { MemPage, checkpoint } from "waygraph";
4
+ import config from "../config/waygraph.config.js";
5
+ import { hoppscotchGetFlow } from "../src/flows/(external)/hoppscotch/get.flow.js";
6
+ import { hoppscotchAuthFlow } from "../src/flows/(external)/hoppscotch/auth.flow.js";
7
+ import { hoppscotchPostFlow } from "../src/flows/(external)/hoppscotch/post.flow.js";
8
+ import {
9
+ ExpectedResponseHeader,
10
+ ExpectedResponseText,
11
+ ExpectedStatus,
12
+ RequestBearerToken,
13
+ RequestBody,
14
+ RequestHeaders,
15
+ RequestMethod,
16
+ RequestParams,
17
+ RequestUrl,
18
+ } from "../src/states/demo.mem-keys.js";
19
+
20
+ // The API under test: no CORS headers, so only the extension can reach it from hoppscotch.io.
21
+ let server: Server;
22
+ let apiUrl: string;
23
+
24
+ test.beforeAll(async () => {
25
+ server = createServer((req, res) => {
26
+ let body = "";
27
+ req.on("data", (chunk) => (body += chunk));
28
+ req.on("end", () => {
29
+ res.statusCode = req.method === "POST" ? 201 : 200;
30
+ res.setHeader("content-type", "application/json");
31
+ res.setHeader("x-request-id", "req-42");
32
+ res.end(JSON.stringify({ method: req.method, url: req.url, token: req.headers["x-token"], auth: req.headers.authorization, body }));
33
+ });
34
+ });
35
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
36
+ apiUrl = `http://127.0.0.1:${(server.address() as { port: number }).port}/items`;
37
+ });
38
+
39
+ test.afterAll(() => server.close());
40
+
41
+ test.skip(!config.extensions?.includes("hoppscotch"), 'add extensions: ["hoppscotch"] to config/waygraph.config.ts to run these');
42
+ test.setTimeout(120_000);
43
+
44
+ test("GET returns 200", async () => {
45
+ const mem = new MemPage();
46
+ mem.set(RequestUrl, apiUrl);
47
+ mem.set(ExpectedStatus, 200);
48
+ expect(await hoppscotchGetFlow.run(mem, {})).toEqual(checkpoint("HoppscotchRest"));
49
+ });
50
+
51
+ test("POST sends the header and JSON body, returns 201", async () => {
52
+ const mem = new MemPage();
53
+ mem.set(RequestMethod, "POST");
54
+ mem.set(RequestUrl, apiUrl);
55
+ mem.set(RequestHeaders, { "x-token": "abc123" });
56
+ mem.set(RequestBody, '{"name":"waygraph"}');
57
+ mem.set(ExpectedStatus, 201);
58
+ mem.set(ExpectedResponseText, '"token":"abc123"');
59
+ expect(await hoppscotchPostFlow.run(mem, {})).toEqual(checkpoint("HoppscotchRest"));
60
+ });
61
+
62
+ test("a wrong expected status fails the flow, naming assert-status", async () => {
63
+ const mem = new MemPage();
64
+ mem.set(RequestUrl, apiUrl);
65
+ mem.set(ExpectedStatus, 404);
66
+ await expect(hoppscotchGetFlow.run(mem, {})).rejects.toThrow(/assert-status/);
67
+ });
68
+
69
+ test("a response body without the expected text fails the flow, naming assert-response-body", async () => {
70
+ const mem = new MemPage();
71
+ mem.set(RequestMethod, "POST");
72
+ mem.set(RequestUrl, apiUrl);
73
+ mem.set(RequestHeaders, { "x-token": "abc123" });
74
+ mem.set(RequestBody, '{"name":"waygraph"}');
75
+ mem.set(ExpectedStatus, 201);
76
+ mem.set(ExpectedResponseText, '"token":"nope"');
77
+ await expect(hoppscotchPostFlow.run(mem, {})).rejects.toThrow(/assert-response-body/);
78
+ });
79
+
80
+ const authMem = (headerValue: string): MemPage => {
81
+ const mem = new MemPage();
82
+ mem.set(RequestUrl, apiUrl);
83
+ mem.set(RequestParams, { q: "waygraph" });
84
+ mem.set(RequestBearerToken, "tok-9");
85
+ mem.set(ExpectedStatus, 200);
86
+ mem.set(ExpectedResponseText, '"url":"/items?q=waygraph","auth":"Bearer tok-9"');
87
+ mem.set(ExpectedResponseHeader({ name: "X-Request-Id", value: headerValue }));
88
+ return mem;
89
+ };
90
+
91
+ test("query params and a bearer token reach the API; a response header is checked", async () => {
92
+ expect(await hoppscotchAuthFlow.run(authMem("req-42"), {})).toEqual(checkpoint("HoppscotchRest"));
93
+ });
94
+
95
+ test("a wrong response header value fails the flow, naming assert-response-header", async () => {
96
+ await expect(hoppscotchAuthFlow.run(authMem("other"), {})).rejects.toThrow(/assert-response-header/);
97
+ });
@@ -2,21 +2,12 @@ import { test, expect } from "@playwright/test";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import net from "node:net";
4
4
  import { MemPage, checkpoint } from "waygraph";
5
- import { mailVerifyFlow } from "../src/flows/mail-verify.flow.js";
5
+ import { emailVerificationFlow } from "../src/flows/(user-stories)/email-verification.flow.js";
6
6
  import { ExpectedRecipient, ExpectedLinkPattern, ExpectedEmailContent } from "../src/states/demo.mem-keys.js";
7
7
  import { HOME_URL } from "../src/map/(app_base)/home/_sel.js";
8
8
 
9
- /**
10
- * This scaffold has no real backend, so these tests play the role of "the
11
- * app sent a verification email" themselves: spin up a real, throwaway
12
- * Mailpit container, send a real SMTP message, then run mailVerifyFlow for
13
- * real - a real cross-origin browser hop into Mailpit's own web UI and
14
- * back, no mail-catcher REST API anywhere. Covers the happy path, both
15
- * failure paths its QA-facing asserts exist for (no email arrived, wrong
16
- * content), and - the actual point of making the mail-reading Blocks
17
- * mem-driven - a second, entirely different email scenario running through
18
- * the exact same four Blocks with zero new code.
19
- */
9
+ // Plays "the app sent a verification email": starts a throwaway Mailpit container, sends a real SMTP message,
10
+ // then runs emailVerificationFlow against it, including the no-email and wrong-content failure paths.
20
11
 
21
12
  function runMailpit(): { containerId: string; smtpPort: number; httpPort: number } {
22
13
  const run = spawnSync("docker", ["run", "--rm", "-d", "-p", "0:1025", "-p", "0:8025", "axllent/mailpit"], {
@@ -89,7 +80,7 @@ async function withMailpit<T>(fn: (opts: { httpPort: number; smtpPort: number })
89
80
 
90
81
  const VERIFICATION_CONTENT = "Please confirm your email address to finish setting up your account.";
91
82
 
92
- test("mailVerifyFlow: confirms arrival, confirms content, reads the real link, navigates to it", async ({
83
+ test("emailVerificationFlow: confirms arrival, confirms content, reads the real link, navigates to it", async ({
93
84
  context,
94
85
  }) => {
95
86
  test.skip(!dockerAvailable(), "Docker not reachable in this environment");
@@ -109,7 +100,7 @@ test("mailVerifyFlow: confirms arrival, confirms content, reads the real link, n
109
100
  mem.set(ExpectedRecipient({ email: recipient }));
110
101
  mem.set(ExpectedLinkPattern, "verified=1");
111
102
  mem.set(ExpectedEmailContent, VERIFICATION_CONTENT);
112
- const result = await mailVerifyFlow.run(context, mem);
103
+ const result = await emailVerificationFlow.run(context, mem);
113
104
  expect(result).toEqual(checkpoint("HomeVerified"));
114
105
  });
115
106
  });
@@ -139,7 +130,7 @@ test("the same four mail-reading Blocks handle a second, unrelated email scenari
139
130
  mem.set(ExpectedRecipient({ email: recipient }));
140
131
  mem.set(ExpectedLinkPattern, "verified=1");
141
132
  mem.set(ExpectedEmailContent, resetContent);
142
- const result = await mailVerifyFlow.run(context, mem);
133
+ const result = await emailVerificationFlow.run(context, mem);
143
134
  expect(result).toEqual(checkpoint("HomeVerified"));
144
135
  });
145
136
  });
@@ -158,7 +149,7 @@ test("assert-email-received fails loud, naming itself, when no email ever arrive
158
149
  // not just the leaf Block - real, useful QA context, not a bug - so this
159
150
  // matches the accumulated name ending at assert-email-received, not an
160
151
  // exact "failed after \"assert-email-received\"" alone.
161
- await expect(mailVerifyFlow.run(context, mem)).rejects.toThrow(
152
+ await expect(emailVerificationFlow.run(context, mem)).rejects.toThrow(
162
153
  /trait "email-received" failed after ".*assert-email-received"/,
163
154
  );
164
155
  });
@@ -184,7 +175,7 @@ test("assert-email-content fails loud, naming itself, when the email's copy does
184
175
  mem.set(ExpectedRecipient({ email: recipient }));
185
176
  mem.set(ExpectedLinkPattern, "verified=1");
186
177
  mem.set(ExpectedEmailContent, VERIFICATION_CONTENT);
187
- await expect(mailVerifyFlow.run(context, mem)).rejects.toThrow(
178
+ await expect(emailVerificationFlow.run(context, mem)).rejects.toThrow(
188
179
  /trait "body-contains-expected-text" failed after ".*assert-email-content"/,
189
180
  );
190
181
  });
@@ -10,5 +10,5 @@
10
10
  "skipLibCheck": true,
11
11
  "forceConsistentCasingInFileNames": true
12
12
  },
13
- "include": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.ts"]
13
+ "include": ["config/**/*.ts", "src/**/*.ts", "tests/**/*.ts", "scripts/**/*.ts"]
14
14
  }
@@ -17,6 +17,9 @@ against a live site, use `--skill-pilot-blind`.
17
17
  2. **Navigation only inside Nav.** `page.goto` / NavBlock `click` live in
18
18
  `defineNavBlock` / `defineNavClickBlock` only. Method/Effect `act` uses ActionPage
19
19
  (no goto) even when the app changes URL as a side effect of a button click.
20
+ A Nav for a page that only exists after something else (every in-app page after sign-in)
21
+ declares `from: [...]` with the Checkpoints it is valid from (a `SIGNED_IN` list in
22
+ `src/states/`); without it, routing jumps there from anywhere, skipping sign-in.
20
23
  3. **Selectors in `*Sel`, never inline.** DOM strings live in a `*Sel` next to the page - `_sel.ts`
21
24
  in the Map layout (one per page folder, next to `_nav`/`_page`/`_methods`), `<name>.sel.ts` in a
22
25
  freeform layout (one per concern) - never a fresh file-naming convention per project (`_sel.ts`,
@@ -39,7 +42,7 @@ against a live site, use `--skill-pilot-blind`.
39
42
  5. **`requires` = externally supplied.** A key an earlier Block in the same chain produces
40
43
  is never listed under `requires` - preflight checks the whole chain up front. For a
41
44
  genuinely arbitrary key (a username, an email) that a demo/dev run shouldn't need
42
- hand-supplied every time, `registerMemStub(key, fake)` + `withMemStub(flow)` (or
45
+ hand-supplied every time, `registerMemStub(key, fake)` + `memStub: true` on the flow (or
43
46
  `--mem-stub`) fills it from a registry instead - a key with no registration still fails
44
47
  preflight exactly as before, so this narrows what needs real input, never removes the guard.
45
48
  6. **Keep Checkpoints typed.** Block helpers take concrete Checkpoint types from `src/states/`
@@ -63,6 +66,41 @@ against a live site, use `--skill-pilot-blind`.
63
66
  when a *different* Block right after it in the `.flow.ts` is what actually confirms the
64
67
  transition (e.g. an `AssertXBlock`) - `// waygraph-ignore: empty-verify`.
65
68
 
69
+ ## Flows, stubs and config (the scaffold's shape)
70
+
71
+ - **Build flows with `map()`** (`engine.map({ memStub }).start().gotoPage(Nav).method(B, { pace }).assert(A).end()`;
72
+ `.gotoExternal` for another origin), never a raw `defineFlow([start, ...])`. Flow settings go in `map({ … })` before the chain
73
+ and `.end()` takes nothing. The flow's title is `ctx.title(...)` in a stub. **Nest flows with `.flow(sub, { reset: true })`**,
74
+ not `chainFlow` / `withSessionReset`. No `withMemStub(withTitle(...))` wrappers.
75
+ - **`src/flows/` holds use cases and mirrors the front end's endpoints**: a folder per page (`flows/auth/login/`) with one
76
+ file per scenario named for its outcome (`success.flow.ts`, `wrong-password.flow.ts`, `empty-fields.flow.ts`), and
77
+ `(user-stories)/` (any `(group)`) for flows that cross pages. Export names are `<page><Outcome>Flow`. Not one file per
78
+ Block chain, not a flat folder of unrelated flows.
79
+ - **Blocks carry no stubs; the flow does.** Demo highlights are ctx stubs chained on the Block where the flow uses it:
80
+ `AddItemBlock.stubAfter((ctx) => { ctx.ring("cart", { selector: DemoSel.cartCount, label: "AC · item in cart" }); })`.
81
+ Not a static `withHighlightFixtures({ "add-item": { stubAfter: {...} } })` map, and not an inline
82
+ `instruction: { stubAfter() {} }`. A Block used twice gets one `const AddShown = AddItemBlock.stubAfter(...)`.
83
+ - **Reading a value into `mem` happens in `act`** (`mem.set(Key, value)`), like Mailpit's `extract-email-link`.
84
+ `observe` is only for a Block that branches (it feeds `resolve`); an empty `act(){}` plus an `observe` that
85
+ writes `mem` is a misuse.
86
+ - **Demo pace is a map option or config, never a wrapper.** `.method(SetBearerTokenBlock, { pace: "blitz" })` speeds one step,
87
+ `map({ demoPace: "fast" })` a flow, `demo: { pace, typingMs, slowMo }` in the config the whole project. Not `withBlockPace(...)`.
88
+ - **Per-environment values** (a different `baseUrl` for staging) come from `.env` files read in the config function:
89
+ `defineConfig(({ env }) => ({ baseUrl: env.BASE_URL }))`; `--env staging` loads `.env.staging`. Never edit the config to switch.
90
+ - **More than one config** (staging, another app) is another file, `config/waygraph.<name>.config.ts`, chosen with `--config <name>`.
91
+ The engine a config exports, and every engine in a flow, follows the selected config.
92
+ - **A browser dialog (`confirm`, `beforeunload`) is answered with `withDialog(block, { accept, into })`**, not inside the
93
+ Block's `act`; the click Block stays a plain click.
94
+ - **Project settings live in `config/waygraph.config.ts`** (`defineConfig({ baseUrl, testDir, webServer,
95
+ extensions, playwright: {...} })`). The config file also exports `engine = new Engine(config)`; flows `import { engine } from "../../config/waygraph.config.js"`; a project needs no
96
+ `playwright.config.ts` - `waygraph test` builds one from it. A leftover `playwright.config.*` is deprecated.
97
+ - **Another origin the test needs is a Map external** under `src/map/(external)/<tool>/`. For REST API
98
+ tests, `(external)/hoppscotch` (list `extensions: ["hoppscotch"]` in the config) drives hoppscotch.io's client
99
+ with the extension, so requests skip CORS: `set-method`, `set-url`, `set-headers`, `set-json-body`,
100
+ `send-request`, `assert-status`, `read-response-body`, `assert-response-body`.
101
+ - **Updating an older project:** `npm install waygraph@latest`, then `waygraph upgrade` (dry run) to see new
102
+ and differing scaffold files; copy the paths you want with `--apply`. `waygraph upgrade --skills` refreshes these skills.
103
+
66
104
  ## Block kinds (file convention)
67
105
 
68
106
  | Kind | File | Helper | `page.goto`? |
@@ -113,8 +151,7 @@ src/map/
113
151
  - `(group)` parentheses and leading-underscore segments (`_methods/`, `_page…`) are
114
152
  **not** URL segments. `waygraph map` excludes them when checking folder-vs-URL.
115
153
  - A bare non-underscore folder always means a real child Checkpoint/URL segment.
116
- - Prefer `map()` over a hand-assembled `defineFlow([start, ...])` when wiring Map Blocks -
117
- it rejects Blocks that didn't come from a real factory (no silent teleporting).
154
+ - `map()` rejects Blocks that didn't come from a real factory (no silent teleporting).
118
155
 
119
156
  Mail / external tools: under `*-external/<tool>/` or `map/(external)/<tool>/`, never under
120
157
  the site's `*-web/` tree. Browser-driven (DOM / iframe Traits), not the catcher's REST API.
@@ -104,13 +104,12 @@ thing that differs from demo is *who* triggers the paint:
104
104
 
105
105
  | Mode | Who paints | How |
106
106
  |------|------------|-----|
107
- | `waygraph demo` | Engine, automatically | Block `stubBefore` / `stubAfter` / `withHighlightFixtures` run each step |
107
+ | `waygraph demo` | Engine, automatically | ctx stubs chained in the flow run each step |
108
108
  | Pilot / `auto --detach` | **You (the agent)** | `waygraph auto highlight <sessionId> '<json>'` before (or between) sends |
109
109
 
110
- Block-authored `stubBefore` / `withHighlightFixtures` still exist on Blocks in Pilot —
111
- they show up in `auto trace` as metadata — but they do **not** auto-draw. You re-express
112
- the same intent with `auto highlight` (copy selectors/labels from the Block stubs or from
113
- `auto dom`). Paint, then `auto send` the real Block. Clear when done.
110
+ The ctx stubs a flow chains onto Blocks show up in Pilot's `auto trace` as metadata, but they do **not**
111
+ auto-draw. You re-express the same intent with `auto highlight` (copy selectors/labels from the flow's stubs or
112
+ from `auto dom`). Paint, then `auto send` the real Block. Clear when done.
114
113
 
115
114
  ```
116
115
  # Headful watch session (rings visible to a human audience):
@@ -1,5 +0,0 @@
1
- /**
2
- * CHROME_PATH / CHROMIUM_PATH, if set and the file exists. A stale path (browser uninstalled)
3
- * falls back to Playwright's bundled Chromium with a one-time warning instead of failing the run.
4
- */
5
- export declare function chromiumExecutablePath(): string | undefined;
@@ -1,18 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- let warned = false;
3
- /**
4
- * CHROME_PATH / CHROMIUM_PATH, if set and the file exists. A stale path (browser uninstalled)
5
- * falls back to Playwright's bundled Chromium with a one-time warning instead of failing the run.
6
- */
7
- export function chromiumExecutablePath() {
8
- const path = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
9
- if (!path)
10
- return undefined;
11
- if (existsSync(path))
12
- return path;
13
- if (!warned) {
14
- warned = true;
15
- console.error(`waygraph: CHROME_PATH/CHROMIUM_PATH "${path}" does not exist - using Playwright's bundled Chromium`);
16
- }
17
- return undefined;
18
- }
@@ -1,8 +0,0 @@
1
- import type { Page } from "@playwright/test";
2
- import type { StubPhaseResult } from "../highlights/types.js";
3
-
4
- export interface SessionOverlay {
5
- present(tag: string, phase: Partial<StubPhaseResult>): Promise<void>;
6
- restore(): Promise<void>;
7
- }
8
- export function createSessionOverlay(page: Page, surface: string): SessionOverlay;
@@ -1,57 +0,0 @@
1
- // Presents a resolved stub phase ("waygraph overlay fixtures": ctx.todos / device / title / banner ...) on a LIVE page
2
- // for the session surfaces (auto / browser / pilot). Same OverlayStage + same in-page installers + same verbose log
3
- // lines as `waygraph demo`, so an overlay fixture behaves identically on every surface. Rings are painted separately
4
- // (pilot holds them; demo cycles them) - that is the only intended difference.
5
- import { installOverlay } from "./overlay-install.js";
6
- import { createOverlayStage } from "./overlay-stage.js";
7
- import { demoLog, logStubPhaseFixtures, logTodoDockFull, setDemoLogSurface, summarizeDevice } from "./demo-log.js";
8
- import { logBannerDom, probeBanner } from "./banner-log.js";
9
- import { probeTodoDocksOnPage } from "./todo-dock.js";
10
-
11
- export function createSessionOverlay(page, surface) {
12
- setDemoLogSurface(surface);
13
- const stage = createOverlayStage(page, { todoDockRef: { current: undefined }, deviceRef: { current: undefined } });
14
- let title = undefined;
15
- let painted = false;
16
-
17
- const paintChrome = async (phaseParallel) => {
18
- await installOverlay(page, title, stage.state.lastBannerUi);
19
- await stage.syncAllTodoDocks({ parallel: !!phaseParallel });
20
- };
21
-
22
- return {
23
- stage,
24
- /** Apply one phase (tag = before | after | error | highlight) and log the resulting overlay state. */
25
- async present(tag, phase) {
26
- setDemoLogSurface(surface);
27
- logStubPhaseFixtures(tag, phase);
28
- await stage.pushTodoDockUi(phase.todoDockUi);
29
- const { appliedTodo, appliedDevice } = await stage.applyPhase(phase, { reapplyDeviceOnKeep: true });
30
- if (phase.title && String(phase.title).trim()) title = String(phase.title).trim();
31
- const wantsChrome = !!(title || stage.state.lastTodoDock || phase.todoSync === "set");
32
- if (wantsChrome) {
33
- painted = true;
34
- await paintChrome(phase.todoParallel);
35
- }
36
- logTodoDockFull(stage.state.lastTodoDock, " " + tag);
37
- demoLog(" " + tag + " device=" + summarizeDevice(stage.state.lastDevice) + " deviceSync=" + appliedDevice.sync);
38
- demoLog(" " + tag + " todoSync=" + appliedTodo.sync);
39
- const docks = await probeTodoDocksOnPage(page);
40
- if (!docks.length && stage.state.lastTodoDock && appliedTodo.sync !== "clear") {
41
- demoLog(" WARN " + tag + ": todos authored but dock-dom empty (not painted?)");
42
- }
43
- for (const d of docks) {
44
- demoLog(
45
- " " + tag + " dock-dom " + d.key + "@" + d.pos + " " + d.left + "," + d.top + " " + d.w + "x" + d.h +
46
- (d.onScreen ? "" : " OFFSCREEN") + " n=" + d.n + (d.current ? ' cur="' + d.current + '"' : ""),
47
- );
48
- }
49
- if (title || painted) logBannerDom(tag, await probeBanner(page));
50
- },
51
- /** A navigation wipes the overlay DOM: rebuild what was authored (same job as the demo's post-navigation restore). */
52
- async restore() {
53
- if (!painted) return;
54
- await paintChrome(false);
55
- },
56
- };
57
- }
@@ -1,2 +0,0 @@
1
- /** Top-left demo banner (`#wg-banner`). */
2
- export declare function bannerCss(): string;
@@ -1,10 +0,0 @@
1
- import { ACCENT_SOFT, SURFACE } from "../tokens.js";
2
- /** Top-left demo banner (`#wg-banner`). */
3
- export function bannerCss() {
4
- return ("#wg-banner{position:fixed;z-index:2147483647;top:14px;left:14px;" +
5
- `background:${SURFACE};color:#fff;border-radius:12px;padding:10px 16px;` +
6
- "font:14px/1.4 system-ui,sans-serif;box-shadow:0 8px 20px rgba(0,0,0,.3);" +
7
- "border:1px solid rgba(124,58,237,.4);}" +
8
- `#wg-banner .wg-banner-tag{display:block;font-size:10px;font-weight:700;color:${ACCENT_SOFT};` +
9
- "letter-spacing:.05em;text-transform:uppercase;margin-bottom:2px;}");
10
- }
@@ -1,2 +0,0 @@
1
- /** Travelling mouse cursor + click ripple (`#wg-cursor`, `#wg-click-pulse`). */
2
- export declare function cursorCss(): string;
@@ -1,19 +0,0 @@
1
- import { TONES } from "../tokens.js";
2
- /** Travelling mouse cursor + click ripple (`#wg-cursor`, `#wg-click-pulse`). */
3
- export function cursorCss() {
4
- const p = TONES.planned;
5
- const pulseTones = ["auto", "info", "warning", "danger", "success"]
6
- .map((n) => `#wg-click-pulse[data-tone=${n}]{border-color:${TONES[n].ring};background:${TONES[n].pulse};}`)
7
- .join("");
8
- return ("#wg-cursor{position:fixed;z-index:2147483647;width:24px;height:24px;pointer-events:none;" +
9
- "left:0;top:0;opacity:0;margin:0;" +
10
- // Travel time is JS-driven via --wg-cursor-ms (a hardcoded duration would make speed a no-op).
11
- "transition:transform var(--wg-cursor-ms,600ms) cubic-bezier(.22,1,.36,1),opacity .2s ease;" +
12
- "filter:drop-shadow(0 2px 4px rgba(12,12,26,.4));}" +
13
- "#wg-click-pulse{position:fixed;z-index:2147483647;width:14px;height:14px;" +
14
- "margin-left:-7px;margin-top:-7px;border-radius:50%;pointer-events:none;opacity:0;" +
15
- `border:2px solid ${p.ring};background:${p.pulse};}` +
16
- pulseTones +
17
- "#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
18
- "@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}");
19
- }
@@ -1,12 +0,0 @@
1
- import { defineConfig } from "@playwright/test";
2
-
3
- export default defineConfig({
4
- testDir: "./tests",
5
- fullyParallel: true,
6
- reporter: [["list"], ["html", { open: "never" }]],
7
- use: {
8
- // Demo pages open via file:// - no server. Filled in by `waygraph init`.
9
- baseURL: "__APP_BASE_URL__",
10
- trace: "retain-on-failure",
11
- },
12
- });
@@ -1,29 +0,0 @@
1
- import { Engine, start, end, withTitle, withHighlightFixtures } from "waygraph";
2
- import { NavHomeBlock } from "../map/(app_base)/home/_nav.block.js";
3
- import { AssertHelloBlock } from "../map/(app_base)/home/_methods/assert-hello.method.block.js";
4
- import { NavDocsBlock } from "../map/(app_base)/docs/_nav.block.js";
5
- // Page hubs registered for auto/docs grouping (arrival-only after nav).
6
- import "../map/(app_base)/home/_page.block.js";
7
- import "../map/(app_base)/docs/_page.block.js";
8
-
9
- const engine = new Engine();
10
-
11
- /** Smoke: nav + assert (no mem), then nav to the sibling docs/ page. Green offline. */
12
- export const exampleFlow = withHighlightFixtures(
13
- withTitle(
14
- engine.defineFlow([start, NavHomeBlock, AssertHelloBlock, NavDocsBlock, end]),
15
- "Scaffold: Hello Waygraph",
16
- ),
17
- {
18
- "assert-hello": {
19
- stubAfter: {
20
- title: {
21
- label: "AC · Hello visible",
22
- detail: "Replace this fixture copy with your ticket lines.",
23
- tag: "AC",
24
- duration: true,
25
- },
26
- },
27
- },
28
- },
29
- );