waygraph 0.7.6 → 0.7.9

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Deviate-DV8
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/cli.js CHANGED
@@ -1772,6 +1772,16 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1772
1772
  i = episodeStartIndex - 1;
1773
1773
  continue;
1774
1774
  }
1775
+ // withExpectedFailure episode (e.g. locked_out_user) - throwing here is
1776
+ // the demo working, not a broken run.
1777
+ if (r.expectedFailureReason) {
1778
+ if (process.env.WAYGRAPH_JSON !== "1") {
1779
+ console.log(
1780
+ "waygraph: expected failure on " + r.block.name + " - " + r.expectedFailureReason,
1781
+ );
1782
+ }
1783
+ break;
1784
+ }
1775
1785
  throw err;
1776
1786
  }
1777
1787
  await page
@@ -1829,6 +1839,21 @@ async function runJsonReportMode(engine, start, end, context, page, mem, resolve
1829
1839
  const outcome = await stepFlow.run(context, mem, { page, closeOnFinish: false });
1830
1840
  steps.push({ name: r.block.name, checkpoint: outcome.result, ms: Date.now() - startedAt });
1831
1841
  } catch (err) {
1842
+ if (r.expectedFailureReason) {
1843
+ steps.push({
1844
+ name: r.block.name,
1845
+ expectedFailure: true,
1846
+ error: err && err.message ? err.message : String(err),
1847
+ ms: Date.now() - startedAt,
1848
+ });
1849
+ return {
1850
+ ok: true,
1851
+ expectedFailure: true,
1852
+ failedAt: r.block.name,
1853
+ reason: r.expectedFailureReason,
1854
+ steps,
1855
+ };
1856
+ }
1832
1857
  let screenshot = null;
1833
1858
  try {
1834
1859
  screenshot = join(tmpdir(), "waygraph-chain-failure-" + Date.now() + ".png");
@@ -1850,6 +1875,26 @@ async function runJsonReportMode(engine, start, end, context, page, mem, resolve
1850
1875
  return { ok: true, result: steps.length > 0 ? steps[steps.length - 1].checkpoint : null, steps };
1851
1876
  }
1852
1877
 
1878
+ function isExpectedChainFailure(chainFlows, err) {
1879
+ if (!chainFlows || chainFlows.length === 0) return false;
1880
+ const last = chainFlows[chainFlows.length - 1];
1881
+ if (!last?.expectedFailureReason) return false;
1882
+ const msg = err && err.message ? err.message : String(err);
1883
+ return msg.includes("viewer-login") || msg.includes("reached-inventory-or-genuinely-blocked");
1884
+ }
1885
+
1886
+ async function runChainedFlows(chainFlow, chainFlows, context, mem, page) {
1887
+ try {
1888
+ const outcome = await chainFlow(...chainFlows).run(context, mem, { page, closeOnFinish: false });
1889
+ return outcome?.result !== undefined ? outcome.result : outcome;
1890
+ } catch (err) {
1891
+ if (isExpectedChainFailure(chainFlows, err)) {
1892
+ return { __expectedFailure: true, message: err && err.message ? err.message : String(err) };
1893
+ }
1894
+ throw err;
1895
+ }
1896
+ }
1897
+
1853
1898
  async function main() {
1854
1899
  const projectDir = process.argv[2];
1855
1900
  const spec = process.argv[3];
@@ -1863,6 +1908,7 @@ async function main() {
1863
1908
  // reaches stdout.
1864
1909
  const jsonReport = process.env.WAYGRAPH_JSON === "1";
1865
1910
  let resolved = [];
1911
+ let chainFlows = null;
1866
1912
  // A bare identifier (no "(", no "then") might name an existing Flow
1867
1913
  // that's already wired up (e.g. loginFlow) - try that FIRST so pointing
1868
1914
  // at real, already-built flows needs no chain-spec typing at all. Falls
@@ -1871,11 +1917,16 @@ async function main() {
1871
1917
  if (bareRef) {
1872
1918
  const flow = await findFlow(projectDir, bareRef);
1873
1919
  if (flow && typeof flow.blocks === "function") {
1874
- resolved = flow.blocks().map((bi) => ({
1920
+ const blockInfos = flow.blocks();
1921
+ resolved = blockInfos.map((bi, idx) => ({
1875
1922
  block: bi.block,
1876
1923
  exportName: bi.name,
1877
- expectedFailureReason: flow.expectedFailureReason,
1924
+ expectedFailureReason:
1925
+ flow.expectedFailureReason && idx === blockInfos.length - 1
1926
+ ? flow.expectedFailureReason
1927
+ : undefined,
1878
1928
  }));
1929
+ chainFlows = [flow];
1879
1930
  if (!jsonReport) {
1880
1931
  console.log(
1881
1932
  "waygraph: running existing flow \\"" + bareRef + "\\" - " +
@@ -1960,13 +2011,15 @@ async function main() {
1960
2011
  resetSession: bi.resetSessionBefore === true,
1961
2012
  episodeNumber: meta.episodeNumber,
1962
2013
  episodeTitle: meta.episodeTitle,
1963
- expectedFailureReason: meta.expectedFailureReason,
2014
+ expectedFailureReason:
2015
+ meta.expectedFailureReason && remainingInFlow === 1 ? meta.expectedFailureReason : undefined,
1964
2016
  seedMem: isFirstOfSegment ? meta.seedMem : undefined,
1965
2017
  };
1966
2018
  remainingInFlow -= 1;
1967
2019
  isFirstOfSegment = false;
1968
2020
  return entry;
1969
2021
  });
2022
+ chainFlows = flows;
1970
2023
  if (!jsonReport) {
1971
2024
  console.log(
1972
2025
  "waygraph: chaining " + resolved.map((r) => r.block.name).join(" -> ") +
@@ -2072,15 +2125,19 @@ async function main() {
2072
2125
  if (!report.ok) process.exitCode = 1;
2073
2126
  return;
2074
2127
  } else {
2075
- const blocks = resolved.map((r) => r.block);
2076
- const chained = blocks.reduce((a, b) => connect(a, b));
2077
- const flow = engine.defineFlow([start, chained, end]);
2078
2128
  const page = await context.newPage();
2079
2129
  pageForVideo = page;
2080
2130
  if (baseURL) {
2081
2131
  await page.goto(baseURL).catch(() => {});
2082
2132
  }
2083
- result = await flow.run(context, mem, { page, closeOnFinish: false });
2133
+ if (chainFlows && chainFlows.length > 0) {
2134
+ result = await runChainedFlows(chainFlow, chainFlows, context, mem, page);
2135
+ } else {
2136
+ const blocks = resolved.map((r) => r.block);
2137
+ const chained = blocks.reduce((a, b) => connect(a, b));
2138
+ const flow = engine.defineFlow([start, chained, end]);
2139
+ result = await flow.run(context, mem, { page, closeOnFinish: false });
2140
+ }
2084
2141
  }
2085
2142
  } finally {
2086
2143
  await context.close();
@@ -2094,13 +2151,29 @@ async function main() {
2094
2151
  await browser.close();
2095
2152
  }
2096
2153
  } else {
2097
- const blocks = resolved.map((r) => r.block);
2098
- const chained = blocks.reduce((a, b) => connect(a, b));
2099
- const flow = engine.defineFlow([start, chained, end]);
2100
- const recordVideo = videoDir ? { dir: videoDir } : undefined;
2101
- result = await flow.run(mem, recordVideo ? { recordVideo } : undefined);
2154
+ if (chainFlows && chainFlows.length > 0) {
2155
+ try {
2156
+ result = await chainFlow(...chainFlows).run(mem);
2157
+ } catch (err) {
2158
+ if (isExpectedChainFailure(chainFlows, err)) {
2159
+ result = { __expectedFailure: true, message: err && err.message ? err.message : String(err) };
2160
+ } else {
2161
+ throw err;
2162
+ }
2163
+ }
2164
+ } else {
2165
+ const blocks = resolved.map((r) => r.block);
2166
+ const chained = blocks.reduce((a, b) => connect(a, b));
2167
+ const flow = engine.defineFlow([start, chained, end]);
2168
+ const recordVideo = videoDir ? { dir: videoDir } : undefined;
2169
+ result = await flow.run(mem, recordVideo ? { recordVideo } : undefined);
2170
+ }
2171
+ }
2172
+ if (result && result.__expectedFailure) {
2173
+ console.log("waygraph: chain finished (expected failure on last episode) -- " + result.message);
2174
+ } else {
2175
+ console.log("waygraph: chain finished -- " + JSON.stringify(result));
2102
2176
  }
2103
- console.log("waygraph: chain finished -- " + JSON.stringify(result));
2104
2177
  }
2105
2178
 
2106
2179
  main().catch((err) => {
@@ -2234,6 +2307,8 @@ async function ensureDirPrereqs(dir, label) {
2234
2307
  return `${label}: no package.json in ${dir}`;
2235
2308
  }
2236
2309
  if (status.missing === "waygraph" || status.missing === "playwright-test" || status.missing === "unknown") {
2310
+ // Copied quickstart must not keep a workspace-tainted lockfile (breaks temp-dir npm install).
2311
+ rmSync(join(dir, "package-lock.json"), { force: true });
2237
2312
  console.log(`${label}: installing dependencies in ${dir} ...`);
2238
2313
  const installCode = await runInherited("npm", ["install"], dir);
2239
2314
  if (installCode !== 0) {
@@ -2258,7 +2333,7 @@ async function ensureDirPrereqs(dir, label) {
2258
2333
  return null;
2259
2334
  return status.detail ?? "prerequisites still aren't ready after attempting to install them.";
2260
2335
  }
2261
- const TRY_DEMO_CHAIN = 'loginFlow({"username":"standard_user","password":"secret_sauce"}) then viewerBlockedFlow({"username":"locked_out_user","password":"secret_sauce"})';
2336
+ const TRY_DEMO_CHAIN = 'shopFlow({"username":"standard_user","password":"secret_sauce"}) then viewerBlockedFlow({"username":"locked_out_user","password":"secret_sauce"})';
2262
2337
  /**
2263
2338
  * Point a copied quickstart at this checkout's waygraph build (not npm registry).
2264
2339
  * Uses `npm pack` into destDir - a file: symlink to the dev tree can double-load
@@ -2287,6 +2362,10 @@ async function wireQuickstartToPackageRoot(destDir) {
2287
2362
  async function runTryDemo() {
2288
2363
  const destDir = mkdtempSync(join(tmpdir(), "waygraph-try-demo-"));
2289
2364
  cpSync(join(packageRoot(), "templates", "quickstart"), destDir, { recursive: true });
2365
+ // Never copy dev node_modules / test output into the temp demo (Playwright double-load).
2366
+ rmSync(join(destDir, "node_modules"), { recursive: true, force: true });
2367
+ rmSync(join(destDir, "test-results"), { recursive: true, force: true });
2368
+ rmSync(join(destDir, "package-lock.json"), { force: true });
2290
2369
  try {
2291
2370
  await wireQuickstartToPackageRoot(destDir);
2292
2371
  }
@@ -2301,12 +2380,20 @@ async function runTryDemo() {
2301
2380
  process.exitCode = 1;
2302
2381
  return;
2303
2382
  }
2383
+ process.env.WAYGRAPH_BASE_URL ??= "https://www.saucedemo.com";
2384
+ // Default: headed stepper, manual Next. --video records that session (not headless rush).
2304
2385
  process.env.WAYGRAPH_HEADED ??= "1";
2305
2386
  process.env.WAYGRAPH_STEP ??= "1";
2306
- // Manual Next by default - onboarding should not race ahead on autoplay.
2307
2387
  process.env.WAYGRAPH_AUTOPLAY ??= "0";
2308
- process.env.WAYGRAPH_BASE_URL ??= "https://www.saucedemo.com";
2309
- console.log("waygraph try demo: step-through chainFlow (Episode 1: Sign In -> Episode 2: Viewer blocked login) - click Next for each step ...");
2388
+ // Explicit --no-step --video = unattended headless .webm only.
2389
+ if (process.env.WAYGRAPH_VIDEO && process.env.WAYGRAPH_STEP === "0") {
2390
+ process.env.WAYGRAPH_AUTOPLAY ??= "1";
2391
+ }
2392
+ const videoTo = process.env.WAYGRAPH_VIDEO;
2393
+ console.log(videoTo
2394
+ ? "waygraph try demo: step-through + record (Episode 1: Shop & Checkout -> Episode 2: blocked login) - click Next; video -> " +
2395
+ (videoTo === "1" ? ".waygraph-videos/" : videoTo)
2396
+ : "waygraph try demo: step-through (Episode 1: Shop & Checkout -> Episode 2: blocked login) - click Next for each step ...");
2310
2397
  await runChain(destDir, TRY_DEMO_CHAIN);
2311
2398
  if (process.exitCode)
2312
2399
  return;
@@ -2317,13 +2404,13 @@ async function runTryDemo() {
2317
2404
  return;
2318
2405
  }
2319
2406
  const testFile = join(destDir, "tests", "chain-flow.spec.ts");
2320
- const loginFlowFile = join(destDir, "src", "flows", "login.flow.ts");
2407
+ const shopFlowFile = join(destDir, "src", "flows", "shop.flow.ts");
2321
2408
  const viewerBlockedFlowFile = join(destDir, "src", "flows", "viewer-blocked.flow.ts");
2322
2409
  console.log("\nwaygraph try demo: done.\n\n" +
2323
2410
  "Temp project (does not touch your cwd - lives under the OS temp dir):\n" +
2324
2411
  ` ${destDir}\n\n` +
2325
2412
  "Episodes you just watched (same Flow objects the test imports):\n" +
2326
- ` ${loginFlowFile}\n` +
2413
+ ` ${shopFlowFile}\n` +
2327
2414
  ` ${viewerBlockedFlowFile}\n\n` +
2328
2415
  "Headless chainFlow test (automated - no clicking):\n" +
2329
2416
  ` ${testFile}\n\n` +
@@ -2551,11 +2638,14 @@ function parseRunFlags(argv) {
2551
2638
  }
2552
2639
  out.title = v;
2553
2640
  }
2554
- else if (a === "--video") {
2555
- out.video = "";
2556
- }
2557
- else if (a.startsWith("--video=")) {
2558
- out.video = a.slice("--video=".length);
2641
+ else if (a === "--video" || a.startsWith("--video=")) {
2642
+ const v = a.includes("=") ? a.slice("--video=".length) : argv[++i];
2643
+ if (v === undefined || (v.startsWith("-") && !a.includes("="))) {
2644
+ out.video = "";
2645
+ }
2646
+ else {
2647
+ out.video = v;
2648
+ }
2559
2649
  }
2560
2650
  else {
2561
2651
  out.positionals.push(a);
@@ -2629,6 +2719,11 @@ function applyDemoDefaults(projectDir) {
2629
2719
  if (process.env.WAYGRAPH_STEP === "1" && process.env.WAYGRAPH_HEADED === undefined) {
2630
2720
  process.env.WAYGRAPH_HEADED = "1";
2631
2721
  }
2722
+ if (process.env.WAYGRAPH_VIDEO &&
2723
+ process.env.WAYGRAPH_STEP === "0" &&
2724
+ process.env.WAYGRAPH_AUTOPLAY === undefined) {
2725
+ process.env.WAYGRAPH_AUTOPLAY = "1";
2726
+ }
2632
2727
  if (!process.env.WAYGRAPH_BASE_URL) {
2633
2728
  const resolved = resolveBaseUrl(projectDir);
2634
2729
  if (resolved)
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.7.6",
3
+ "version": "0.7.9",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
+ "license": "MIT",
6
7
  "main": "./dist/index.js",
7
8
  "types": "./dist/index.d.ts",
8
9
  "exports": {
@@ -4,7 +4,8 @@
4
4
  "requires": true,
5
5
  "packages": {
6
6
  "../..": {
7
- "version": "0.7.5",
7
+ "version": "0.7.7",
8
+ "license": "MIT",
8
9
  "dependencies": {
9
10
  "tsx": "^4.0.0"
10
11
  },
@@ -20,10 +21,120 @@
20
21
  "@playwright/test": "^1.40.0"
21
22
  }
22
23
  },
24
+ "../../../../projects_waygraph/node_modules/@playwright/test": {
25
+ "version": "1.63.0",
26
+ "dev": true,
27
+ "license": "Apache-2.0",
28
+ "dependencies": {
29
+ "playwright": "1.63.0"
30
+ },
31
+ "bin": {
32
+ "playwright": "cli.js"
33
+ },
34
+ "engines": {
35
+ "node": ">=20"
36
+ }
37
+ },
38
+ "../../node_modules/@esbuild/linux-x64": {
39
+ "version": "0.28.2",
40
+ "cpu": [
41
+ "x64"
42
+ ],
43
+ "license": "MIT",
44
+ "optional": true,
45
+ "os": [
46
+ "linux"
47
+ ],
48
+ "engines": {
49
+ "node": ">=18"
50
+ }
51
+ },
52
+ "../../node_modules/@playwright/test": {
53
+ "resolved": "../../../../projects_waygraph/node_modules/@playwright/test",
54
+ "link": true
55
+ },
56
+ "../../node_modules/@types/node": {
57
+ "version": "26.4.0",
58
+ "dev": true,
59
+ "license": "MIT",
60
+ "dependencies": {
61
+ "undici-types": "~8.3.0"
62
+ }
63
+ },
64
+ "../../node_modules/esbuild": {
65
+ "version": "0.28.2",
66
+ "hasInstallScript": true,
67
+ "license": "MIT",
68
+ "bin": {
69
+ "esbuild": "bin/esbuild"
70
+ },
71
+ "engines": {
72
+ "node": ">=18"
73
+ },
74
+ "optionalDependencies": {
75
+ "@esbuild/aix-ppc64": "0.28.2",
76
+ "@esbuild/android-arm": "0.28.2",
77
+ "@esbuild/android-arm64": "0.28.2",
78
+ "@esbuild/android-x64": "0.28.2",
79
+ "@esbuild/darwin-arm64": "0.28.2",
80
+ "@esbuild/darwin-x64": "0.28.2",
81
+ "@esbuild/freebsd-arm64": "0.28.2",
82
+ "@esbuild/freebsd-x64": "0.28.2",
83
+ "@esbuild/linux-arm": "0.28.2",
84
+ "@esbuild/linux-arm64": "0.28.2",
85
+ "@esbuild/linux-ia32": "0.28.2",
86
+ "@esbuild/linux-loong64": "0.28.2",
87
+ "@esbuild/linux-mips64el": "0.28.2",
88
+ "@esbuild/linux-ppc64": "0.28.2",
89
+ "@esbuild/linux-riscv64": "0.28.2",
90
+ "@esbuild/linux-s390x": "0.28.2",
91
+ "@esbuild/linux-x64": "0.28.2",
92
+ "@esbuild/netbsd-arm64": "0.28.2",
93
+ "@esbuild/netbsd-x64": "0.28.2",
94
+ "@esbuild/openbsd-arm64": "0.28.2",
95
+ "@esbuild/openbsd-x64": "0.28.2",
96
+ "@esbuild/openharmony-arm64": "0.28.2",
97
+ "@esbuild/sunos-x64": "0.28.2",
98
+ "@esbuild/win32-arm64": "0.28.2",
99
+ "@esbuild/win32-ia32": "0.28.2",
100
+ "@esbuild/win32-x64": "0.28.2"
101
+ }
102
+ },
103
+ "../../node_modules/tsx": {
104
+ "version": "4.23.13",
105
+ "license": "MIT",
106
+ "dependencies": {
107
+ "esbuild": "~0.28.0"
108
+ },
109
+ "bin": {
110
+ "tsx": "dist/cli.mjs"
111
+ },
112
+ "engines": {
113
+ "node": ">=18.0.0"
114
+ },
115
+ "optionalDependencies": {
116
+ "fsevents": "~2.3.3"
117
+ }
118
+ },
119
+ "../../node_modules/typescript": {
120
+ "version": "5.9.3",
121
+ "dev": true,
122
+ "license": "Apache-2.0",
123
+ "bin": {
124
+ "tsc": "bin/tsc",
125
+ "tsserver": "bin/tsserver"
126
+ },
127
+ "engines": {
128
+ "node": ">=14.17"
129
+ }
130
+ },
131
+ "../../node_modules/undici-types": {
132
+ "version": "8.3.0",
133
+ "dev": true,
134
+ "license": "MIT"
135
+ },
23
136
  "node_modules/@playwright/test": {
24
137
  "version": "1.63.0",
25
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
26
- "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
27
138
  "dev": true,
28
139
  "license": "Apache-2.0",
29
140
  "dependencies": {
@@ -38,8 +149,6 @@
38
149
  },
39
150
  "node_modules/@types/node": {
40
151
  "version": "26.6.1",
41
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.1.tgz",
42
- "integrity": "sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA==",
43
152
  "dev": true,
44
153
  "license": "MIT",
45
154
  "dependencies": {
@@ -48,8 +157,6 @@
48
157
  },
49
158
  "node_modules/playwright": {
50
159
  "version": "1.63.0",
51
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
52
- "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
53
160
  "dev": true,
54
161
  "license": "Apache-2.0",
55
162
  "dependencies": {
@@ -64,8 +171,6 @@
64
171
  },
65
172
  "node_modules/playwright-core": {
66
173
  "version": "1.63.0",
67
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
68
- "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
69
174
  "dev": true,
70
175
  "license": "Apache-2.0",
71
176
  "bin": {
@@ -77,8 +182,6 @@
77
182
  },
78
183
  "node_modules/typescript": {
79
184
  "version": "5.9.3",
80
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
81
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
82
185
  "dev": true,
83
186
  "license": "Apache-2.0",
84
187
  "bin": {
@@ -91,8 +194,6 @@
91
194
  },
92
195
  "node_modules/undici-types": {
93
196
  "version": "8.9.0",
94
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
95
- "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
96
197
  "dev": true,
97
198
  "license": "MIT"
98
199
  },
@@ -2,17 +2,17 @@
2
2
  "name": "waygraph-quickstart",
3
3
  "private": true,
4
4
  "type": "module",
5
- "description": "Self-contained saucedemo quickstart - 2-episode chainFlow demo (Sign In + blocked Viewer login).",
5
+ "description": "Self-contained saucedemo quickstart - Episode 1 Shop & Checkout, Episode 2 blocked login.",
6
6
  "scripts": {
7
7
  "test": "playwright test",
8
- "demo": "waygraph chain \"loginFlow({\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}) then viewerBlockedFlow({\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"})\" . --step --no-autoplay",
9
- "record": "waygraph chain \"loginFlow({\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}) then viewerBlockedFlow({\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"})\" . --video --no-step"
8
+ "demo": "waygraph chain \"shopFlow({\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}) then viewerBlockedFlow({\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"})\" . --step --no-autoplay",
9
+ "record": "waygraph chain \"shopFlow({\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}) then viewerBlockedFlow({\\\"username\\\":\\\"locked_out_user\\\",\\\"password\\\":\\\"secret_sauce\\\"})\" . --video --no-step"
10
10
  },
11
11
  "waygraph": {
12
12
  "baseUrl": "https://www.saucedemo.com"
13
13
  },
14
14
  "dependencies": {
15
- "waygraph": "^0.7.6"
15
+ "waygraph": "^0.7.7"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@playwright/test": "^1.55.0",
@@ -0,0 +1,15 @@
1
+ import { defineBlock, checkpoint, Trait } from "waygraph";
2
+ import type { LoggedIn, ItemInCart } from "../../states/checkout.states.js";
3
+
4
+ export const AddToCartBlock = defineBlock<LoggedIn, ItemInCart>({
5
+ name: "add-to-cart",
6
+ description: "Adds the Sauce Labs Backpack and confirms the cart badge shows 1 item.",
7
+ instruction: {
8
+ async act(page) {
9
+ await page.locator("#add-to-cart-sauce-labs-backpack").click();
10
+ },
11
+ resolve: () => checkpoint("ItemInCart"),
12
+ verify: [Trait.text(".shopping_cart_badge", "1")],
13
+ highlights: () => [{ selector: ".shopping_cart_badge", label: "remember this: cart now has 1 item" }],
14
+ },
15
+ });
@@ -0,0 +1,16 @@
1
+ import { defineBlock, checkpoint, Trait, narrate } from "waygraph";
2
+ import type { CheckoutOverviewPage, OrderComplete } from "../../states/checkout.states.js";
3
+
4
+ export const FinishOrderBlock = defineBlock<CheckoutOverviewPage, OrderComplete>({
5
+ name: "finish-order",
6
+ description: "Places the order and confirms the completion page.",
7
+ instruction: {
8
+ async act(page) {
9
+ const finishButton = page.locator("#finish");
10
+ await narrate(finishButton, "Placing the order", () => finishButton.click());
11
+ },
12
+ resolve: () => checkpoint("OrderComplete"),
13
+ verify: [Trait.text(".complete-header", "Thank you for your order!")],
14
+ highlights: () => [{ selector: ".complete-header", label: "remember this: order confirmed" }],
15
+ },
16
+ });
@@ -0,0 +1,17 @@
1
+ import { defineBlock, checkpoint, Trait } from "waygraph";
2
+ import type { CheckoutInfoPage, CheckoutOverviewPage } from "../../states/checkout.states.js";
3
+
4
+ export const SubmitCheckoutInfoBlock = defineBlock<CheckoutInfoPage, CheckoutOverviewPage>({
5
+ name: "submit-checkout-info",
6
+ description: "Fills shipping info and continues to the order overview.",
7
+ instruction: {
8
+ async act(page) {
9
+ await page.locator("#first-name").fill("Ada");
10
+ await page.locator("#last-name").fill("Lovelace");
11
+ await page.locator("#postal-code").fill("1000");
12
+ await page.locator("#continue").click();
13
+ },
14
+ resolve: () => checkpoint("CheckoutOverviewPage"),
15
+ verify: [Trait.url({ pathname: "/checkout-step-two.html" })],
16
+ },
17
+ });
@@ -0,0 +1,10 @@
1
+ import { defineNavBlock, Trait } from "waygraph";
2
+ import type { CartPage } from "../states/checkout.states.js";
3
+
4
+ export const NavCartBlock = defineNavBlock<CartPage>({
5
+ name: "nav-cart",
6
+ description: "Clicks the cart link in the header.",
7
+ checkpoint: "CartPage",
8
+ click: ".shopping_cart_link",
9
+ verify: [Trait.url({ pathname: "/cart.html" })],
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineNavBlock, Trait } from "waygraph";
2
+ import type { CheckoutInfoPage } from "../states/checkout.states.js";
3
+
4
+ export const NavCheckoutInfoBlock = defineNavBlock<CheckoutInfoPage>({
5
+ name: "nav-checkout-info",
6
+ description: "Clicks Checkout on the cart page.",
7
+ checkpoint: "CheckoutInfoPage",
8
+ click: "#checkout",
9
+ verify: [Trait.url({ pathname: "/checkout-step-one.html" })],
10
+ });
@@ -4,7 +4,7 @@ import { SubmitLoginActionBlock } from "../blocks/actions/submit-login.action.bl
4
4
 
5
5
  const engine = new Engine();
6
6
 
7
- // Episode 1 - the step overlay shows "Episode 1: Sign In".
7
+ // Optional standalone sign-in only (try demo uses shopFlow as Episode 1).
8
8
  export const loginFlow = withTitle(
9
9
  engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end]),
10
10
  "Sign In",
@@ -0,0 +1,26 @@
1
+ import { Engine, start, end, withTitle } from "waygraph";
2
+ import { NavLoginBlock } from "../blocks/nav-login.block.js";
3
+ import { SubmitLoginActionBlock } from "../blocks/actions/submit-login.action.block.js";
4
+ import { AddToCartBlock } from "../blocks/actions/add-to-cart.action.block.js";
5
+ import { NavCartBlock } from "../blocks/nav-cart.block.js";
6
+ import { NavCheckoutInfoBlock } from "../blocks/nav-checkout-info.block.js";
7
+ import { SubmitCheckoutInfoBlock } from "../blocks/actions/submit-checkout-info.action.block.js";
8
+ import { FinishOrderBlock } from "../blocks/actions/finish-order.action.block.js";
9
+
10
+ const engine = new Engine();
11
+
12
+ // Episode 1 - sign in, shop, checkout, place order.
13
+ export const shopFlow = withTitle(
14
+ engine.defineFlow([
15
+ start,
16
+ NavLoginBlock,
17
+ SubmitLoginActionBlock,
18
+ AddToCartBlock,
19
+ NavCartBlock,
20
+ NavCheckoutInfoBlock,
21
+ SubmitCheckoutInfoBlock,
22
+ FinishOrderBlock,
23
+ end,
24
+ ]),
25
+ "Shop & Checkout",
26
+ );
@@ -2,3 +2,8 @@ import type { Checkpoint } from "waygraph";
2
2
 
3
3
  export type LoginPage = Checkpoint<"LoginPage">;
4
4
  export type LoggedIn = Checkpoint<"LoggedIn">;
5
+ export type ItemInCart = Checkpoint<"ItemInCart">;
6
+ export type CartPage = Checkpoint<"CartPage">;
7
+ export type CheckoutInfoPage = Checkpoint<"CheckoutInfoPage">;
8
+ export type CheckoutOverviewPage = Checkpoint<"CheckoutOverviewPage">;
9
+ export type OrderComplete = Checkpoint<"OrderComplete">;
@@ -1,39 +1,44 @@
1
1
  import { test, expect } from "@playwright/test";
2
2
  import { MemPage, checkpoint, chainFlow } from "waygraph";
3
- import { loginFlow } from "../src/flows/login.flow.js";
3
+ import { shopFlow } from "../src/flows/shop.flow.js";
4
4
  import { viewerBlockedFlow } from "../src/flows/viewer-blocked.flow.js";
5
5
  import { LoginCreds, ViewerCreds } from "../src/states/checkout.mem-keys.js";
6
6
 
7
- test("chainFlow: Episode 1 signs in, Episode 2 blocked login genuinely fails", async () => {
7
+ test("chainFlow: Episode 1 shops through checkout, Episode 2 blocked login genuinely fails", async () => {
8
8
  const mem = new MemPage();
9
9
  mem.set(LoginCreds({ username: "standard_user", password: "secret_sauce" }));
10
10
  mem.set(ViewerCreds({ username: "locked_out_user", password: "secret_sauce" }));
11
11
 
12
- const combined = chainFlow(loginFlow, viewerBlockedFlow);
12
+ const combined = chainFlow(shopFlow, viewerBlockedFlow);
13
13
 
14
14
  await expect(combined.run(mem)).rejects.toThrow(/viewer-login/);
15
15
  });
16
16
 
17
- test("episode 1 alone: standard_user reaches inventory", async () => {
17
+ test("episode 1 alone: standard_user completes checkout", async () => {
18
18
  const mem = new MemPage();
19
19
  mem.set(LoginCreds({ username: "standard_user", password: "secret_sauce" }));
20
20
 
21
- const result = await loginFlow.run(mem);
21
+ const result = await shopFlow.run(mem);
22
22
 
23
- expect(result).toEqual(checkpoint("LoggedIn"));
23
+ expect(result).toEqual(checkpoint("OrderComplete"));
24
24
  });
25
25
 
26
26
  test("chainFlow blocks() lists the flattened episode chain in order", () => {
27
- expect(loginFlow.title).toBe("Sign In");
27
+ expect(shopFlow.title).toBe("Shop & Checkout");
28
28
  expect(viewerBlockedFlow.title).toBe("Viewer: Blocked Login Attempt");
29
29
 
30
- const blocks = chainFlow(loginFlow, viewerBlockedFlow).blocks();
30
+ const blocks = chainFlow(shopFlow, viewerBlockedFlow).blocks();
31
31
  expect(blocks.map((b) => b.name)).toEqual([
32
32
  "nav-login",
33
33
  "submit-login",
34
+ "add-to-cart",
35
+ "nav-cart",
36
+ "nav-checkout-info",
37
+ "submit-checkout-info",
38
+ "finish-order",
34
39
  "nav-login",
35
40
  "viewer-login",
36
41
  ]);
37
42
  expect(blocks[0]!.resetSessionBefore).toBeUndefined();
38
- expect(blocks[2]!.resetSessionBefore).toBe(true);
43
+ expect(blocks[7]!.resetSessionBefore).toBe(true);
39
44
  });