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 +21 -0
- package/dist/cli.js +119 -24
- package/package.json +2 -1
- package/templates/quickstart/node_modules/.package-lock.json +114 -13
- package/templates/quickstart/package.json +4 -4
- package/templates/quickstart/src/blocks/actions/add-to-cart.action.block.ts +15 -0
- package/templates/quickstart/src/blocks/actions/finish-order.action.block.ts +16 -0
- package/templates/quickstart/src/blocks/actions/submit-checkout-info.action.block.ts +17 -0
- package/templates/quickstart/src/blocks/nav-cart.block.ts +10 -0
- package/templates/quickstart/src/blocks/nav-checkout-info.block.ts +10 -0
- package/templates/quickstart/src/flows/login.flow.ts +1 -1
- package/templates/quickstart/src/flows/shop.flow.ts +26 -0
- package/templates/quickstart/src/states/checkout.states.ts +5 -0
- package/templates/quickstart/tests/chain-flow.spec.ts +14 -9
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
|
-
|
|
1920
|
+
const blockInfos = flow.blocks();
|
|
1921
|
+
resolved = blockInfos.map((bi, idx) => ({
|
|
1875
1922
|
block: bi.block,
|
|
1876
1923
|
exportName: bi.name,
|
|
1877
|
-
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:
|
|
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
|
-
|
|
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
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
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 = '
|
|
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
|
-
|
|
2309
|
-
|
|
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
|
|
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
|
-
` ${
|
|
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
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
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.
|
|
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.
|
|
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 -
|
|
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 \"
|
|
9
|
-
"record": "waygraph chain \"
|
|
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.
|
|
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
|
-
//
|
|
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 {
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
21
|
+
const result = await shopFlow.run(mem);
|
|
22
22
|
|
|
23
|
-
expect(result).toEqual(checkpoint("
|
|
23
|
+
expect(result).toEqual(checkpoint("OrderComplete"));
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
test("chainFlow blocks() lists the flattened episode chain in order", () => {
|
|
27
|
-
expect(
|
|
27
|
+
expect(shopFlow.title).toBe("Shop & Checkout");
|
|
28
28
|
expect(viewerBlockedFlow.title).toBe("Viewer: Blocked Login Attempt");
|
|
29
29
|
|
|
30
|
-
const blocks = chainFlow(
|
|
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[
|
|
43
|
+
expect(blocks[7]!.resetSessionBefore).toBe(true);
|
|
39
44
|
});
|