waygraph 0.16.1 → 0.16.4

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 (154) hide show
  1. package/README.md +12 -0
  2. package/dist/analysis/practices-check.d.ts +1 -1
  3. package/dist/analysis/practices-check.js +36 -0
  4. package/dist/beta.d.ts +6 -0
  5. package/dist/beta.js +5 -0
  6. package/dist/cli/commands/codemod-flows.js +2 -2
  7. package/dist/cli/commands/codemod-stubs.js +132 -2
  8. package/dist/cli/commands/pilot.js +3 -16
  9. package/dist/cli/commands/session-ops.js +26 -1
  10. package/dist/cli/flags.d.ts +1 -1
  11. package/dist/cli/flags.js +1 -1
  12. package/dist/cli/run-chain.js +2 -2
  13. package/dist/cli/usage.js +6 -0
  14. package/dist/cli/util.js +1 -1
  15. package/dist/demo/narration/demo-pace.d.ts +1 -1
  16. package/dist/demo/narration/demo-pace.js +1 -1
  17. package/dist/demo/narration/merge.d.ts +1 -1
  18. package/dist/demo/narration/merge.js +1 -1
  19. package/dist/demo/narration/narration.d.ts +2 -6
  20. package/dist/demo/narration/narration.js +5 -17
  21. package/dist/demo/narration/stub-ctx.js +126 -124
  22. package/dist/demo/overlay/cursor.js +1 -1
  23. package/dist/demo/overlay/inpage/banner.js +5 -0
  24. package/dist/demo/overlay/inpage/core.js +1 -6
  25. package/dist/demo/overlay/inpage/network.js +1 -1
  26. package/dist/demo/overlay/inpage/todos.js +0 -15
  27. package/dist/demo/overlay/network-capture.js +2 -2
  28. package/dist/demo/overlay/overlay-css.js +19 -76
  29. package/dist/demo/overlay/overlay-stage.js +1 -1
  30. package/dist/demo/overlay/overlay.js +3 -2
  31. package/dist/demo/overlay/phase-fixtures.js +5 -5
  32. package/dist/demo/overlay/pilot/shell.js +7 -5
  33. package/dist/demo/overlay/rings.d.ts +0 -8
  34. package/dist/demo/overlay/rings.js +0 -15
  35. package/dist/demo/overlay/ui/components/ring.js +2 -2
  36. package/dist/demo/overlay/ui/overlay.css +136 -33
  37. package/dist/demo/overlay/ui/shadow.js +69 -2
  38. package/dist/demo/stepper/after.js +4 -17
  39. package/dist/demo/stepper/before.js +3 -1
  40. package/dist/demo/stepper/demo-log.js +8 -8
  41. package/dist/demo/stepper/interaction-patch.js +3 -15
  42. package/dist/demo/stepper/panel-after.js +1 -1
  43. package/dist/demo/stepper/slides.d.ts +3 -0
  44. package/dist/demo/stepper/slides.js +18 -2
  45. package/dist/demo/stepper/stepper.js +1 -1
  46. package/dist/demo/stepper/teardown.js +0 -1
  47. package/dist/demo/verbose-log.js +1 -1
  48. package/dist/engine/block/block.d.ts +1 -12
  49. package/dist/engine/block/kind.d.ts +3 -0
  50. package/dist/engine/block/kind.js +11 -0
  51. package/dist/engine/block/kinds/method.d.ts +0 -2
  52. package/dist/engine/block/kinds/method.js +0 -1
  53. package/dist/engine/block/kinds/nav.js +1 -1
  54. package/dist/engine/errors.d.ts +12 -0
  55. package/dist/engine/errors.js +12 -0
  56. package/dist/engine/read/read-block.d.ts +61 -0
  57. package/dist/engine/read/read-block.js +94 -0
  58. package/dist/engine/read/read-key.d.ts +32 -0
  59. package/dist/engine/read/read-key.js +34 -0
  60. package/dist/engine/read/read-page.d.ts +19 -0
  61. package/dist/engine/read/read-page.js +50 -0
  62. package/dist/engine/read/readers.d.ts +82 -0
  63. package/dist/engine/read/readers.js +43 -0
  64. package/dist/engine/read/run-read.d.ts +17 -0
  65. package/dist/engine/read/run-read.js +156 -0
  66. package/dist/engine/run/connect.js +3 -2
  67. package/dist/engine/run/events.d.ts +28 -10
  68. package/dist/engine/run/events.js +52 -38
  69. package/dist/engine/run/run.d.ts +1 -19
  70. package/dist/engine/run/run.js +21 -97
  71. package/dist/engine/stub/ring.d.ts +2 -4
  72. package/dist/engine/stub/ring.js +1 -0
  73. package/dist/engine/stub/stub.d.ts +38 -25
  74. package/dist/index.d.ts +9 -7
  75. package/dist/index.js +11 -6
  76. package/dist/live/auto/auto.js +2 -3
  77. package/dist/live/auto/menu.js +3 -2
  78. package/dist/live/auto-session/auto-session.d.ts +9 -2
  79. package/dist/live/auto-session/auto-session.js +23 -20
  80. package/dist/live/auto-session/feed.d.ts +16 -0
  81. package/dist/live/auto-session/feed.js +71 -0
  82. package/dist/live/auto-session/helpers.d.ts +7 -0
  83. package/dist/live/auto-session/helpers.js +9 -0
  84. package/dist/live/auto-session/ipc.d.ts +15 -47
  85. package/dist/live/auto-session/ipc.js +34 -4
  86. package/dist/live/auto-session/protocol.d.ts +127 -0
  87. package/dist/live/auto-session/protocol.js +6 -0
  88. package/dist/live/auto-session/read.d.ts +25 -0
  89. package/dist/live/auto-session/read.js +50 -0
  90. package/dist/live/traverse/worker.d.ts +1 -0
  91. package/dist/live/traverse/worker.js +6 -1
  92. package/dist/manifest/manifest.d.ts +22 -2
  93. package/dist/manifest/manifest.js +27 -1
  94. package/dist/manifest/project.d.ts +10 -1
  95. package/dist/manifest/project.js +9 -12
  96. package/dist/manifest/read-blocks.d.ts +2 -0
  97. package/dist/manifest/read-blocks.js +23 -0
  98. package/dist/manifest/scan.d.ts +3 -2
  99. package/dist/manifest/scan.js +8 -4
  100. package/dist/runner/plan/find.js +2 -2
  101. package/dist/runner/plan/plan.js +1 -1
  102. package/dist/runner/plan/seed-mem.js +5 -5
  103. package/dist/runner/plan/spec.js +1 -1
  104. package/dist/runner/runner.d.ts +0 -1
  105. package/dist/runner/runner.js +6 -6
  106. package/dist/runtime/full-launch.d.ts +2 -1
  107. package/dist/runtime/full-launch.js +29 -27
  108. package/dist/runtime/run-log.d.ts +2 -0
  109. package/dist/runtime/run-log.js +13 -6
  110. package/examples/saucedemo/README.md +2 -2
  111. package/examples/saucedemo/src/blocks/SITE-MAP.md +3 -0
  112. package/examples/saucedemo/src/blocks/saucedemo-web/cart/cart.read.block.ts +26 -0
  113. package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  114. package/examples/saucedemo/src/blocks/saucedemo-web/header.read.block.ts +25 -0
  115. package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.read.block.ts +28 -0
  116. package/examples/saucedemo/src/flows/api-demo.flow.ts +1 -1
  117. package/examples/saucedemo/src/flows/cart-bulk.flow.ts +1 -1
  118. package/examples/saucedemo/src/flows/cart-continue.flow.ts +1 -1
  119. package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
  120. package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +3 -2
  121. package/examples/saucedemo/src/flows/checkout.flow.ts +13 -12
  122. package/examples/saucedemo/src/flows/login.flow.ts +1 -1
  123. package/examples/saucedemo/src/flows/shop.flow.ts +3 -2
  124. package/examples/saucedemo/src/flows/stub-on-error.flow.ts +1 -1
  125. package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
  126. package/examples/saucedemo/src/states/checkout.reads.ts +9 -0
  127. package/examples/saucedemo/src/stubs/device-showcase.ts +70 -76
  128. package/examples/saucedemo/src/stubs/finish-order.ts +38 -0
  129. package/examples/saucedemo/src/stubs/login.ts +5 -5
  130. package/package.json +5 -1
  131. package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
  132. package/templates/quickstart/src/flows/api-demo.flow.ts +1 -1
  133. package/templates/quickstart/src/flows/cart-bulk.flow.ts +1 -1
  134. package/templates/quickstart/src/flows/cart-continue.flow.ts +1 -1
  135. package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
  136. package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +3 -2
  137. package/templates/quickstart/src/flows/checkout.flow.ts +13 -12
  138. package/templates/quickstart/src/flows/login.flow.ts +1 -1
  139. package/templates/quickstart/src/flows/shop.flow.ts +3 -2
  140. package/templates/quickstart/src/flows/stub-on-error.flow.ts +1 -1
  141. package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
  142. package/templates/quickstart/src/stubs/device-showcase.ts +70 -76
  143. package/templates/quickstart/src/stubs/finish-order.ts +38 -0
  144. package/templates/quickstart/src/stubs/login.ts +5 -5
  145. package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +1 -1
  146. package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +1 -1
  147. package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +1 -1
  148. package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +4 -4
  149. package/templates/scaffold/src/flows/README.md +1 -1
  150. package/templates/scaffold/src/flows/home/cart.flow.ts +7 -7
  151. package/templates/scaffold/src/flows/home/hello.flow.ts +10 -7
  152. package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +1 -9
  153. package/templates/skills/waygraph-convention.skill.md +10 -2
  154. package/templates/skills/waygraph-pilot.skill.md +7 -0
@@ -32,11 +32,11 @@ export async function findBlock(projectDir, ref) {
32
32
  if (byRuntimeName.length === 1)
33
33
  return byRuntimeName[0];
34
34
  if (byRuntimeName.length > 1) {
35
- throw new Error("waygraph chain: \"" + ref + "\" matches " + byRuntimeName.length + " Blocks by name - " +
35
+ throw new Error("waygraph run --blocks: \"" + ref + "\" matches " + byRuntimeName.length + " Blocks by name - " +
36
36
  byRuntimeName.map((b) => b.exportName + " (" + b.file + ")").join(", ") +
37
37
  ". Use the export name to disambiguate.");
38
38
  }
39
- throw new Error("waygraph chain: no Block named \"" + ref + "\" found under " + projectDir);
39
+ throw new Error("waygraph run --blocks: no Block named \"" + ref + "\" found under " + projectDir);
40
40
  }
41
41
  /**
42
42
  * Finds an already-defined Flow by its export name (e.g. loginFlow) among
@@ -24,7 +24,7 @@ async function findBareFlow(projectDir, spec) {
24
24
  function parseSegments(spec) {
25
25
  const segments = parseChainSpec(spec);
26
26
  if (segments.length === 0)
27
- throw new Error("waygraph chain: empty spec - give at least one block name");
27
+ throw new Error("waygraph run --blocks: empty spec - give at least one block name");
28
28
  return segments;
29
29
  }
30
30
  async function resolveSegment(projectDir, seg, mem, options) {
@@ -4,7 +4,7 @@ const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v)
4
4
  function seedMemFromRequires(mem, requires, json, label, stubActive) {
5
5
  if (requires.length === 0) {
6
6
  if (json !== undefined) {
7
- throw new Error("waygraph chain: \"" + label + "\" takes no input (empty requires) but got a payload: " + json);
7
+ throw new Error("waygraph run --blocks: \"" + label + "\" takes no input (empty requires) but got a payload: " + json);
8
8
  }
9
9
  return;
10
10
  }
@@ -22,7 +22,7 @@ function seedMemFromRequires(mem, requires, json, label, stubActive) {
22
22
  haveParsed = true;
23
23
  }
24
24
  catch (err) {
25
- throw new Error("waygraph chain: \"" + label + "\" payload is not valid JSON - " + String(err));
25
+ throw new Error("waygraph run --blocks: \"" + label + "\" payload is not valid JSON - " + String(err));
26
26
  }
27
27
  }
28
28
  if (!haveParsed) {
@@ -30,7 +30,7 @@ function seedMemFromRequires(mem, requires, json, label, stubActive) {
30
30
  // this flow; when it is, fall through with an empty object so the
31
31
  // per-key loop below gets a chance to fill each one from the registry.
32
32
  if (!stubActive) {
33
- throw new Error("waygraph chain: \"" + label + "\" requires " + requires.map((k) => k.name).join(", ") +
33
+ throw new Error("waygraph run --blocks: \"" + label + "\" requires " + requires.map((k) => k.name).join(", ") +
34
34
  " - give a JSON payload (inline blockName({...}) or --data '{...}'), or enable --mem-stub" +
35
35
  " with registerMemStub for these keys");
36
36
  }
@@ -54,7 +54,7 @@ function seedMemFromRequires(mem, requires, json, label, stubActive) {
54
54
  return;
55
55
  }
56
56
  if (!isRecord(parsed)) {
57
- throw new Error("waygraph chain: \"" + label + "\" requires " + requires.length + " keys (" +
57
+ throw new Error("waygraph run --blocks: \"" + label + "\" requires " + requires.length + " keys (" +
58
58
  requires.map((k) => k.name).join(", ") + ") - payload must be an object keyed by each key's name");
59
59
  }
60
60
  const payload = parsed;
@@ -72,7 +72,7 @@ function seedMemFromRequires(mem, requires, json, label, stubActive) {
72
72
  stillMissing.push(k);
73
73
  }
74
74
  if (stillMissing.length > 0) {
75
- throw new Error("waygraph chain: \"" + label + "\" payload is missing required key(s): " +
75
+ throw new Error("waygraph run --blocks: \"" + label + "\" payload is missing required key(s): " +
76
76
  stillMissing.map((k) => "\"" + k.name + "\"").join(", "));
77
77
  }
78
78
  }
@@ -8,7 +8,7 @@ export function parseChainSpec(spec) {
8
8
  .map((s) => {
9
9
  const m = /^([A-Za-z_$][\w-]*)\s*(?:\(([\s\S]*)\))?$/.exec(s);
10
10
  if (!m) {
11
- throw new Error("waygraph chain: could not parse segment \"" + s + "\" - expected \"blockName\" or \"blockName({...json...})\"");
11
+ throw new Error("waygraph run --blocks: could not parse segment \"" + s + "\" - expected \"blockName\" or \"blockName({...json...})\"");
12
12
  }
13
13
  const ref = m[1];
14
14
  const json = m[2] ? m[2].trim() : undefined;
@@ -1,4 +1,3 @@
1
- import "../runtime/run-log.js";
2
1
  export declare function play(projectDir: string, spec: string): Promise<void>;
3
2
  /** The process entry the CLI's bootstrap calls: one run (and one run log) per invocation. */
4
3
  export declare function startRunner(): void;
@@ -2,9 +2,10 @@
2
2
  // Playwright is used) with the project dir and the spec ("cartFlow", "loginFlow then add-item"). Every mode plays the
3
3
  // same steps through the same engine; the mode decides what happens around each step.
4
4
  import { MemPage } from "../engine/mem/mem.js";
5
- import { beginRun, endRun } from "../engine/run/events.js";
5
+ import { inRun } from "../engine/run/events.js";
6
6
  import { withWebServer } from "../runtime/webserver.js";
7
- import "../runtime/run-log.js";
7
+ import { installRunLog } from "../runtime/run-log.js";
8
+ import { loadReadBlocks } from "../manifest/read-blocks.js";
8
9
  import { planSteps } from "./plan/plan.js";
9
10
  import { readOptions, slowMoFor } from "./options.js";
10
11
  import { openSession } from "./session.js";
@@ -16,6 +17,7 @@ import { reportMode } from "./modes/report.js";
16
17
  export async function play(projectDir, spec) {
17
18
  const options = readOptions();
18
19
  const mem = new MemPage();
20
+ await loadReadBlocks(projectDir);
19
21
  const { steps, flows } = await planSteps(projectDir, spec, mem, options);
20
22
  if (options.mode === "branches")
21
23
  return playBranches(flows, mem, options);
@@ -52,12 +54,10 @@ function finish(outcome) {
52
54
  }
53
55
  /** The process entry the CLI's bootstrap calls: one run (and one run log) per invocation. */
54
56
  export function startRunner() {
57
+ installRunLog();
55
58
  const projectDir = process.argv[2] ?? process.cwd();
56
59
  const spec = process.argv[3] ?? "";
57
- const opened = beginRun(spec);
58
- withWebServer(projectDir, () => play(projectDir, spec))
59
- .then(() => endRun(opened, {}), (err) => {
60
- endRun(opened, { error: err });
60
+ inRun(spec, () => withWebServer(projectDir, () => play(projectDir, spec))).catch((err) => {
61
61
  console.error(err && err.stack ? err.stack : String(err));
62
62
  process.exitCode = 1;
63
63
  });
@@ -1 +1,2 @@
1
- export {};
1
+ /** How an Engine launches its own browser: the selected config (--config), the Chromium override, the extensions. Called by the package entry. */
2
+ export declare function installFullLaunch(): void;
@@ -4,31 +4,33 @@ import { chromiumExecutablePath, extensionPathsFromEnv, launchContext } from "./
4
4
  import { resolveExtensions } from "./extensions.js";
5
5
  import { loadWaygraphConfig } from "./waygraph-config.js";
6
6
  import { envString } from "../engine/env.js";
7
- // An Engine's own browser follows the selected config (--config), the Chromium override and the configured extensions.
8
- setOwnContextLauncher(async (engineConfig) => {
9
- // A config chosen with `--config` / WAYGRAPH_CONFIG wins over the one the Engine was built with.
10
- const chosen = envString("WAYGRAPH_CONFIG") ? await loadWaygraphConfig(process.cwd()) : undefined;
11
- const config = chosen
12
- ? {
13
- ...engineConfig,
14
- ...(chosen.baseUrl !== undefined ? { baseUrl: chosen.baseUrl } : {}),
15
- ...(chosen.extensions !== undefined ? { extensions: chosen.extensions } : {}),
16
- }
17
- : engineConfig;
18
- const browserName = config?.browserName ?? "chromium";
19
- const executablePath = browserName === "chromium" ? chromiumExecutablePath() : undefined;
20
- const baseURL = config?.baseUrl ?? envString("WAYGRAPH_BASE_URL");
21
- return launchContext({
22
- launcher: resolveLauncher(browserName, config),
23
- launch: {
24
- headless: config?.headless ?? true,
25
- ...(config?.slowMo !== undefined ? { slowMo: config.slowMo } : {}),
26
- ...(executablePath ? { executablePath } : {}),
27
- },
28
- context: {
29
- ...(baseURL ? { baseURL } : {}),
30
- ...(config?.recordVideo ? { recordVideo: config.recordVideo } : {}),
31
- },
32
- extensions: await resolveExtensions(config?.extensions ?? extensionPathsFromEnv(), process.cwd()),
7
+ /** How an Engine launches its own browser: the selected config (--config), the Chromium override, the extensions. Called by the package entry. */
8
+ export function installFullLaunch() {
9
+ setOwnContextLauncher(async (engineConfig) => {
10
+ // A config chosen with `--config` / WAYGRAPH_CONFIG wins over the one the Engine was built with.
11
+ const chosen = envString("WAYGRAPH_CONFIG") ? await loadWaygraphConfig(process.cwd()) : undefined;
12
+ const config = chosen
13
+ ? {
14
+ ...engineConfig,
15
+ ...(chosen.baseUrl !== undefined ? { baseUrl: chosen.baseUrl } : {}),
16
+ ...(chosen.extensions !== undefined ? { extensions: chosen.extensions } : {}),
17
+ }
18
+ : engineConfig;
19
+ const browserName = config?.browserName ?? "chromium";
20
+ const executablePath = browserName === "chromium" ? chromiumExecutablePath() : undefined;
21
+ const baseURL = config?.baseUrl ?? envString("WAYGRAPH_BASE_URL");
22
+ return launchContext({
23
+ launcher: resolveLauncher(browserName, config),
24
+ launch: {
25
+ headless: config?.headless ?? true,
26
+ ...(config?.slowMo !== undefined ? { slowMo: config.slowMo } : {}),
27
+ ...(executablePath ? { executablePath } : {}),
28
+ },
29
+ context: {
30
+ ...(baseURL ? { baseURL } : {}),
31
+ ...(config?.recordVideo ? { recordVideo: config.recordVideo } : {}),
32
+ },
33
+ extensions: await resolveExtensions(config?.extensions ?? extensionPathsFromEnv(), process.cwd()),
34
+ });
33
35
  });
34
- });
36
+ }
@@ -1,2 +1,4 @@
1
+ /** Starts writing run events to the run log. Called by the package entry and the runner; a second call does nothing. */
2
+ export declare function installRunLog(): void;
1
3
  /** The folder run logs go to for a project unless the config or the environment says otherwise. */
2
4
  export declare function defaultRunLogDir(projectDir: string): string;
@@ -4,13 +4,20 @@ import { appendFileSync, mkdirSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { onRunEvent } from "../engine/run/events.js";
6
6
  import { envString } from "../engine/env.js";
7
- onRunEvent((event) => {
8
- const dir = envString("WAYGRAPH_RUN_LOG");
9
- if (!dir || dir === "0")
7
+ let installed = false;
8
+ /** Starts writing run events to the run log. Called by the package entry and the runner; a second call does nothing. */
9
+ export function installRunLog() {
10
+ if (installed)
10
11
  return;
11
- mkdirSync(dir, { recursive: true });
12
- appendFileSync(join(dir, `${event.runId}.jsonl`), JSON.stringify(event) + "\n");
13
- });
12
+ installed = true;
13
+ onRunEvent((event) => {
14
+ const dir = envString("WAYGRAPH_RUN_LOG");
15
+ if (!dir || dir === "0")
16
+ return;
17
+ mkdirSync(dir, { recursive: true });
18
+ appendFileSync(join(dir, `${event.runId}.jsonl`), JSON.stringify(event) + "\n");
19
+ });
20
+ }
14
21
  /** The folder run logs go to for a project unless the config or the environment says otherwise. */
15
22
  export function defaultRunLogDir(projectDir) {
16
23
  return join(projectDir, ".waygraph", "runs");
@@ -13,7 +13,7 @@ the **reference** for waygraph consumer conventions:
13
13
  - `defineApiBlock` - a real backend/HTTP check as one step in the same Flow as
14
14
  a UI step, with its own live network-activity panel during a demo - see
15
15
  `apiDemoFlow` (`src/flows/api-demo.flow.ts`)
16
- - `ctx.caption()` (bottom subtitle-bar) + `ctx.titlePos()` (banner
16
+ - `ctx.ui.caption()` (bottom subtitle-bar) + `ctx.ui.titlePos()` (banner
17
17
  left/center/right) - also on `checkoutFlow`
18
18
 
19
19
  **GitHub Pages walkthrough:**
@@ -48,7 +48,7 @@ npx playwright install chromium
48
48
  | **Device showcase** (mobile/tablet + portrait/landscape; narration in `src/stubs/device-showcase.ts`) | `npm run demo:device` |
49
49
  | Device showcase + `.webm` | `npm run demo:video` |
50
50
  | **Cart bulk** demo (add-all → remove-all) | `npm run demo:bulk` |
51
- | **Owner checkout** (inline `{ ff: true }`, `ctx.caption()`, banner position) | `npm run demo:checkout` |
51
+ | **Owner checkout** (inline `{ ff: true }`, `ctx.ui.caption()`, banner position) | `npm run demo:checkout` |
52
52
  | **FE + BE in one Flow** (`defineApiBlock`, live network panel) | `npm run demo:api` |
53
53
  | Record (headless `.webm`) | `npm run record` |
54
54
  | List flows | `npm run flows` |
@@ -9,6 +9,7 @@ src/blocks/
9
9
  saucedemo-web/ # URL /
10
10
  NAV.md
11
11
  nav-login.block.ts # Page definePageBlock url:/
12
+ header.read.block.ts # Read defineReadBlock (beta, every page: title, cart count)
12
13
  methods/
13
14
  fill-username.method.block.ts # Method defineMethodBlock (self-loop on LoginPage)
14
15
  fill-password.method.block.ts # Method defineMethodBlock (self-loop on LoginPage)
@@ -18,6 +19,7 @@ src/blocks/
18
19
  login.sel.ts # LoginSel (not a Block)
19
20
  inventory/ # URL /inventory.html
20
21
  inventory.page.block.ts # Page definePageBlock
22
+ inventory.read.block.ts # Read defineReadBlock (beta: products, prices, in cart, sort)
21
23
  NAV.md
22
24
  nav-item-detail.block.ts # Nav defineMemNavBlock (instance row)
23
25
  methods/
@@ -36,6 +38,7 @@ src/blocks/
36
38
  NAV.md
37
39
  cart.sel.ts # CartSel (not a Block)
38
40
  nav-cart.block.ts # Nav click header cart
41
+ cart.read.block.ts # Read defineReadBlock (beta: the cart's lines)
39
42
  nav-checkout-info.block.ts
40
43
  checkout-step-one/ # URL /checkout-step-one.html
41
44
  NAV.md
@@ -0,0 +1,26 @@
1
+ import { defineReadBlock } from "waygraph/beta";
2
+ import { CartPage } from "../../../states/checkout.states.js";
3
+ import { Cart } from "../../../states/checkout.reads.js";
4
+
5
+ /**
6
+ * Kind: Read (beta)
7
+ * Helper: defineReadBlock
8
+ * Route: saucedemo-web/cart/ (= /cart.html)
9
+ *
10
+ * The cart's lines: name, quantity and price.
11
+ */
12
+ export const CartReadBlock = defineReadBlock({
13
+ name: "read-cart",
14
+ description: "Each line in the cart: name, quantity, price.",
15
+ from: CartPage,
16
+ instruction: {
17
+ read: (ctx) =>
18
+ Cart({
19
+ lines: ctx.page.list(".cart_item", (row) => ({
20
+ name: row.text(".inventory_item_name"),
21
+ qty: row.number(".cart_quantity"),
22
+ price: row.number(".inventory_item_price"),
23
+ })),
24
+ }),
25
+ },
26
+ });
@@ -1,4 +1,4 @@
1
- import { defineMethodBlock, Trait, narrate } from "waygraph";
1
+ import { defineMethodBlock, Trait } from "waygraph";
2
2
  import {
3
3
  CheckoutOverviewPage,
4
4
  OrderComplete,
@@ -21,9 +21,7 @@ export const FinishOrderBlock = defineMethodBlock({
21
21
  instruction: {
22
22
  async act(page) {
23
23
  const finishButton = page.locator(CheckoutOverviewSel.finishBtn);
24
- await narrate(finishButton, "Placing the order", () =>
25
- finishButton.click(),
26
- );
24
+ await finishButton.click();
27
25
  },
28
26
  verify: [
29
27
  Trait.text(
@@ -31,35 +29,5 @@ export const FinishOrderBlock = defineMethodBlock({
31
29
  "Thank you for your order!",
32
30
  ),
33
31
  ],
34
- // Shown AFTER act (order already placed) - selectors must match the
35
- // completion page, not #finish on the overview.
36
- slides: [
37
- {
38
- caption: "Checkout is a short pipeline",
39
- detail: "Cart -> info -> overview -> done. You just placed the order.",
40
- tag: "YAP",
41
- tone: "info",
42
- duration: true,
43
- },
44
- {
45
- caption: "Completion page is the durable proof",
46
- detail:
47
- "tone: success - the thank-you header is what verify asserts on.",
48
- tag: "YAP",
49
- tone: "success",
50
- selector: CheckoutOverviewSel.completeHeader,
51
- duration: 2500,
52
- fastMode: 700,
53
- },
54
- {
55
- caption: "Episode continues after this yap",
56
- detail: "Next step rings the same banner, then you move on.",
57
- tag: "YAP",
58
- tone: "planned",
59
- selector: CheckoutOverviewSel.completeHeader,
60
- duration: true,
61
- fastMode: 500,
62
- },
63
- ],
64
32
  },
65
33
  });
@@ -0,0 +1,25 @@
1
+ import { defineReadBlock } from "waygraph/beta";
2
+ import { Header } from "../../states/checkout.reads.js";
3
+ import { InventorySel } from "./inventory/methods/inventory-items.js";
4
+
5
+ /**
6
+ * Kind: Read (beta)
7
+ * Helper: defineReadBlock
8
+ * Route: saucedemo-web/ (every signed-in page)
9
+ *
10
+ * The header every page shares: the page title and how many items are in the cart (0 when the badge is not shown).
11
+ */
12
+ export const HeaderReadBlock = defineReadBlock({
13
+ name: "read-header",
14
+ description: "The page title and the cart count.",
15
+ instruction: {
16
+ read: (ctx) =>
17
+ Header({
18
+ title: ctx.page.text(".title"),
19
+ cartCount: ctx.page.fn(async (page) => {
20
+ const badge = page.locator(InventorySel.cartBadge);
21
+ return (await badge.count()) ? Number(await badge.first().innerText()) : 0;
22
+ }),
23
+ }),
24
+ },
25
+ });
@@ -0,0 +1,28 @@
1
+ import { defineReadBlock } from "waygraph/beta";
2
+ import { ItemInCart, LoggedIn } from "../../../states/checkout.states.js";
3
+ import { Catalog } from "../../../states/checkout.reads.js";
4
+ import { InventorySel } from "./methods/inventory-items.js";
5
+
6
+ /**
7
+ * Kind: Read (beta)
8
+ * Helper: defineReadBlock
9
+ * Route: saucedemo-web/inventory/ (= /inventory.html)
10
+ *
11
+ * The catalogue as data: every product with its price and whether it is in the cart, and the sort order.
12
+ */
13
+ export const InventoryReadBlock = defineReadBlock({
14
+ name: "read-inventory",
15
+ description: "Every product, its price and whether it is in the cart; the sort order.",
16
+ from: [LoggedIn, ItemInCart],
17
+ instruction: {
18
+ read: (ctx) =>
19
+ Catalog({
20
+ items: ctx.page.list(InventorySel.card, (row) => ({
21
+ name: row.text(InventorySel.name),
22
+ price: row.number(".inventory_item_price"),
23
+ inCart: row.visible(InventorySel.removeBtnPrefix),
24
+ })),
25
+ sort: ctx.page.value(".product_sort_container"),
26
+ }),
27
+ },
28
+ });
@@ -32,7 +32,7 @@ export const apiDemoEpisode = engine
32
32
  .start()
33
33
  .method(
34
34
  CheckBackendHealth.stubBefore((ctx) => {
35
- ctx.title("Checking backend health · real HTTP request, no browser");
35
+ ctx.ui.title("Checking backend health · real HTTP request, no browser");
36
36
  }),
37
37
  )
38
38
  .end();
@@ -10,7 +10,7 @@ export const cartBulkEpisode = engine
10
10
  .start()
11
11
  .method(
12
12
  AddAllToCartBlock.stubBefore((ctx) =>
13
- ctx.title("Cart bulk add/remove all"),
13
+ ctx.ui.title("Cart bulk add/remove all"),
14
14
  ),
15
15
  )
16
16
  .method(RemoveAllFromCartBlock)
@@ -7,7 +7,7 @@ export const cartContinueFlow = engine
7
7
  .map()
8
8
  .start()
9
9
  .gotoPage(
10
- NavCartBlock.stubBefore((ctx) => ctx.title("Cart then continue shopping")),
10
+ NavCartBlock.stubBefore((ctx) => ctx.ui.title("Cart then continue shopping")),
11
11
  )
12
12
  .gotoPage(NavContinueShoppingBlock)
13
13
  .end();
@@ -9,7 +9,7 @@ export const catalogLogoutFlow = engine
9
9
  .start()
10
10
  .gotoPage(
11
11
  NavItemDetailBlock.stubBefore((ctx) =>
12
- ctx.title("Catalog browse then logout"),
12
+ ctx.ui.title("Catalog browse then logout"),
13
13
  ),
14
14
  )
15
15
  .gotoPage(NavBackToInventoryBlock)
@@ -12,6 +12,7 @@ import { FillLastNameBlock } from "../blocks/saucedemo-web/checkout-step-one/met
12
12
  import { FillPostalCodeBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.js";
13
13
  import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.js";
14
14
  import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.js";
15
+ import { orderPlacedYap, placingOrder } from "../stubs/finish-order.js";
15
16
  import { NavBackToProductsBlock } from "../blocks/saucedemo-web/inventory-item/nav-back-to-products.block.js";
16
17
 
17
18
  /** Full checkout, then the "Back Home" click-based Nav Block returns to the catalogue. */
@@ -20,7 +21,7 @@ export const checkoutThenBrowseAgainFlow = engine
20
21
  .start()
21
22
  .gotoPage(
22
23
  NavLoginBlock.stubBefore((ctx) =>
23
- ctx.title(
24
+ ctx.ui.title(
24
25
  "Owner: Checkout, then click Back Home (click-based NavBlock demo)",
25
26
  ),
26
27
  ),
@@ -35,6 +36,6 @@ export const checkoutThenBrowseAgainFlow = engine
35
36
  .method(FillLastNameBlock)
36
37
  .method(FillPostalCodeBlock)
37
38
  .method(SubmitCheckoutInfoBlock)
38
- .method(FinishOrderBlock)
39
+ .method(FinishOrderBlock.stubBefore(placingOrder).stubAfter(orderPlacedYap))
39
40
  .gotoPage(NavBackToProductsBlock)
40
41
  .end();
@@ -12,6 +12,7 @@ import { FillLastNameBlock } from "../blocks/saucedemo-web/checkout-step-one/met
12
12
  import { FillPostalCodeBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.js";
13
13
  import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.js";
14
14
  import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.js";
15
+ import { orderPlacedYap, placingOrder } from "../stubs/finish-order.js";
15
16
  import { LoginSel } from "../blocks/saucedemo-web/methods/login.sel.js";
16
17
 
17
18
  export const checkoutFlow = engine
@@ -20,7 +21,7 @@ export const checkoutFlow = engine
20
21
  .gotoPage(NavLoginBlock, { ff: true })
21
22
  .method(
22
23
  FillUsernameBlock.stubBefore((ctx) => {
23
- ctx.ring("username", {
24
+ ctx.ui.ring("username", {
24
25
  selector: LoginSel.username,
25
26
  label: "Demo user",
26
27
  detail:
@@ -34,7 +35,7 @@ export const checkoutFlow = engine
34
35
  )
35
36
  .method(
36
37
  FillPasswordBlock.stubBefore((ctx) => {
37
- ctx.ring("password", {
38
+ ctx.ui.ring("password", {
38
39
  selector: LoginSel.password,
39
40
  label: "Password",
40
41
  detail: "Filled from Mem credentials.",
@@ -46,7 +47,7 @@ export const checkoutFlow = engine
46
47
  )
47
48
  .method(
48
49
  SubmitLoginForFlow.stubBefore((ctx) => {
49
- ctx.ring("submit", {
50
+ ctx.ui.ring("submit", {
50
51
  selector: LoginSel.loginButton,
51
52
  label: "Sign in",
52
53
  tag: "GATE",
@@ -58,27 +59,27 @@ export const checkoutFlow = engine
58
59
  }),
59
60
  { ff: true },
60
61
  )
61
- .method(AddToCartBlock.stubBefore((ctx) => ctx.title("Owner: Full Checkout")))
62
+ .method(AddToCartBlock.stubBefore((ctx) => ctx.ui.title("Owner: Full Checkout")))
62
63
  .gotoPage(NavCartBlock)
63
64
  .gotoPage(NavCheckoutInfoBlock)
64
65
  .method(
65
66
  FillFirstNameBlock.stubBefore((ctx) => {
66
- ctx.caption("Enter the first name on the order");
67
- ctx.titlePos("left");
67
+ ctx.ui.caption("Enter the first name on the order");
68
+ ctx.ui.titlePos("left");
68
69
  }),
69
70
  )
70
71
  .method(
71
72
  FillLastNameBlock.stubBefore((ctx) => {
72
- ctx.caption("Enter the last name on the order");
73
- ctx.titlePos("center");
73
+ ctx.ui.caption("Enter the last name on the order");
74
+ ctx.ui.titlePos("center");
74
75
  }),
75
76
  )
76
77
  .method(
77
78
  FillPostalCodeBlock.stubBefore((ctx) => {
78
- ctx.caption("Enter a postal code to calculate shipping");
79
- ctx.titlePos("right");
79
+ ctx.ui.caption("Enter a postal code to calculate shipping");
80
+ ctx.ui.titlePos("right");
80
81
  }),
81
82
  )
82
- .method(SubmitCheckoutInfoBlock.stubBefore((ctx) => ctx.hideCaption()))
83
- .method(FinishOrderBlock)
83
+ .method(SubmitCheckoutInfoBlock.stubBefore((ctx) => ctx.ui.hideCaption()))
84
+ .method(FinishOrderBlock.stubBefore(placingOrder).stubAfter(orderPlacedYap))
84
85
  .end();
@@ -9,7 +9,7 @@ import { SubmitLoginForFlow } from "../blocks/saucedemo-web/methods/submit-login
9
9
  export const loginFlow = engine
10
10
  .map({ memStub: true })
11
11
  .start()
12
- .gotoPage(NavLoginBlock.stubBefore((ctx) => ctx.title("Sign In")))
12
+ .gotoPage(NavLoginBlock.stubBefore((ctx) => ctx.ui.title("Sign In")))
13
13
  .method(FillUsernameBlock)
14
14
  .method(FillPasswordBlock)
15
15
  .method(SubmitLoginForFlow)
@@ -8,17 +8,18 @@ import { FillLastNameBlock } from "../blocks/saucedemo-web/checkout-step-one/met
8
8
  import { FillPostalCodeBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/fill-postal-code.method.block.js";
9
9
  import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.js";
10
10
  import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.js";
11
+ import { orderPlacedYap, placingOrder } from "../stubs/finish-order.js";
11
12
 
12
13
  /** Add the selected item, check out and place the order. Starts signed in (run it after loginFlow). */
13
14
  export const shopFlow = engine
14
15
  .map({ memStub: true })
15
16
  .start()
16
- .method(AddToCartBlock.stubBefore((ctx) => ctx.title("Shop & Checkout")))
17
+ .method(AddToCartBlock.stubBefore((ctx) => ctx.ui.title("Shop & Checkout")))
17
18
  .gotoPage(NavCartBlock)
18
19
  .gotoPage(NavCheckoutInfoBlock)
19
20
  .method(FillFirstNameBlock)
20
21
  .method(FillLastNameBlock)
21
22
  .method(FillPostalCodeBlock)
22
23
  .method(SubmitCheckoutInfoBlock)
23
- .method(FinishOrderBlock)
24
+ .method(FinishOrderBlock.stubBefore(placingOrder).stubAfter(orderPlacedYap))
24
25
  .end();
@@ -19,7 +19,7 @@ export const stubOnErrorFlow = engine
19
19
  .start()
20
20
  .gotoPage(
21
21
  NavLoginBlock.stubBefore((ctx) =>
22
- ctx.title("stubOnError: locked_out fail rings"),
22
+ ctx.ui.title("stubOnError: locked_out fail rings"),
23
23
  ),
24
24
  )
25
25
  .method(FillUsernameBlock)
@@ -19,7 +19,7 @@ export const viewerBlockedFlow = engine
19
19
  .start()
20
20
  .gotoPage(
21
21
  NavLoginBlock.stubBefore((ctx) =>
22
- ctx.title("Viewer: Blocked Login Attempt"),
22
+ ctx.ui.title("Viewer: Blocked Login Attempt"),
23
23
  ),
24
24
  )
25
25
  .method(FillUsernameBlock)
@@ -0,0 +1,9 @@
1
+ import { reads } from "waygraph/beta";
2
+
3
+ // What the store's pages show, by namespace: the Read Blocks fill these, `pilot read` returns them as JSON.
4
+ export const Catalog = reads("catalog")<{
5
+ items: { name: string; price: number; inCart: boolean }[];
6
+ sort: string;
7
+ }>();
8
+ export const Cart = reads("cart")<{ lines: { name: string; qty: number; price: number }[] }>();
9
+ export const Header = reads("header")<{ title: string; cartCount: number }>();