waygraph 0.15.52 → 0.15.54
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/README.md +8 -1
- package/dist/analysis/graph.d.ts +3 -0
- package/dist/analysis/map-check.d.ts +3 -2
- package/dist/auto-session/helpers.d.ts +10 -0
- package/dist/blocks-select.d.ts +3 -0
- package/dist/blocks-select.js +2 -0
- package/dist/cli/flags.js +3 -1
- package/dist/cli/usage.js +4 -1
- package/dist/cli.js +4 -1
- package/dist/commands/agent-dive-scaffold.d.ts +0 -20
- package/dist/commands/agent-dive-scaffold.js +3 -3
- package/dist/commands/init.js +1 -1
- package/dist/commands/upgrade.d.ts +1 -1
- package/dist/commands/upgrade.js +7 -1
- package/dist/config.d.ts +55 -19
- package/dist/config.js +55 -13
- package/dist/core/block.d.ts +7 -2
- package/dist/core/block.js +7 -3
- package/dist/core/blocks/api.d.ts +1 -0
- package/dist/core/blocks/method.d.ts +2 -0
- package/dist/core/blocks/method.js +1 -0
- package/dist/core/blocks/nav.d.ts +1 -0
- package/dist/core/blocks/page.d.ts +1 -0
- package/dist/core/branch-regression.d.ts +1 -0
- package/dist/core/compose.d.ts +1 -0
- package/dist/core/engine.d.ts +2 -0
- package/dist/core/engine.js +3 -1
- package/dist/core/flow.d.ts +11 -0
- package/dist/core/flow.js +32 -8
- package/dist/core/map-builder.d.ts +34 -1
- package/dist/core/map-builder.js +42 -4
- package/dist/core/run-graph.d.ts +2 -0
- package/dist/core/run-graph.js +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/extensions.d.ts +1 -0
- package/dist/extensions.js +1 -0
- package/dist/highlights/demo-pace.d.ts +4 -1
- package/dist/highlights/demo-pace.js +28 -3
- package/dist/highlights/device.d.ts +2 -0
- package/dist/highlights/device.js +2 -0
- package/dist/highlights/types.d.ts +6 -0
- package/dist/index.d.ts +2 -2
- package/dist/pilot-overlay/action-ring.d.ts +0 -9
- package/dist/pilot-overlay/action-ring.js +1 -1
- package/dist/pilot-overlay/panel.d.ts +2 -1
- package/dist/runner/interaction-patch.js +32 -1
- package/dist/runner/main.js +5 -2
- package/dist/runner/step-mode.js +10 -4
- package/dist/session/auto-explore-run.js +6 -0
- package/dist/session/auto-explore.d.ts +2 -1
- package/dist/session/auto-session-ipc.d.ts +1 -2
- package/dist/session/auto-session-ipc.js +1 -1
- package/dist/session/block-inject.d.ts +0 -13
- package/dist/session/block-inject.js +3 -3
- package/dist/session/overlay-beacon.d.ts +7 -0
- package/dist/session/overlay-beacon.js +5 -0
- package/dist/session/pilot.d.ts +1 -2
- package/dist/session/pilot.js +1 -2
- package/dist/traverse/coverage.d.ts +4 -4
- package/dist/traverse/coverage.js +4 -1
- package/dist/traverse/lease.d.ts +1 -0
- package/dist/traverse/lease.js +1 -0
- package/dist/traverse/run.d.ts +3 -0
- package/dist/types.d.ts +1 -1
- package/examples/routed-demo/src/flows/routed-demo-map.flow.ts +1 -1
- package/examples/routed-demo/src/flows/routed-demo.flow.ts +2 -2
- package/examples/saucedemo/README.md +11 -12
- package/examples/saucedemo/config/waygraph.config.ts +17 -0
- package/examples/saucedemo/docs/CONVENTION.md +1 -2
- package/examples/saucedemo/docs/HELPERS.md +1 -1
- package/examples/saucedemo/package-lock.json +10 -3
- package/examples/saucedemo/package.json +12 -29
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-cart.block.ts +0 -49
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +22 -25
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +0 -6
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +38 -96
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +1 -26
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +1 -45
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +14 -77
- package/examples/saucedemo/src/flows/api-demo.flow.ts +17 -11
- package/examples/saucedemo/src/flows/cart-bulk.flow.ts +19 -14
- package/examples/saucedemo/src/flows/cart-continue.flow.ts +10 -8
- package/examples/saucedemo/src/flows/cart-mem.flow.ts +7 -7
- package/examples/saucedemo/src/flows/catalog-logout.flow.ts +13 -8
- package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +26 -48
- package/examples/saucedemo/src/flows/checkout.flow.ts +70 -86
- package/examples/saucedemo/src/flows/device-showcase.flow.ts +24 -25
- package/examples/saucedemo/src/flows/login.flow.ts +11 -18
- package/examples/saucedemo/src/flows/mem-stub-demo.flow.ts +9 -20
- package/examples/saucedemo/src/flows/shop.flow.ts +15 -20
- package/examples/saucedemo/src/flows/smoke.flow.ts +12 -27
- package/examples/saucedemo/src/flows/stub-on-error.flow.ts +24 -43
- package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +26 -34
- package/examples/saucedemo/src/states/checkout.mem-stubs.ts +11 -0
- package/examples/saucedemo/src/stubs/device-showcase.ts +193 -0
- package/examples/saucedemo/src/stubs/login.ts +37 -0
- package/examples/saucedemo/tests/chain-flow-episodes.spec.ts +7 -9
- package/examples/saucedemo/tests/submit-login-branch.spec.ts +18 -5
- package/package.json +12 -5
- package/templates/quickstart/README.md +4 -7
- package/templates/quickstart/config/waygraph.config.ts +17 -0
- package/templates/quickstart/docs/CONVENTION.md +1 -2
- package/templates/quickstart/docs/HELPERS.md +1 -1
- package/templates/quickstart/package.json +14 -14
- package/templates/quickstart/src/blocks/SITE-MAP.md +14 -3
- package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +0 -49
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +22 -25
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +0 -6
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +38 -96
- package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +1 -26
- package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +1 -45
- package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +14 -77
- package/templates/quickstart/src/flows/api-demo.flow.ts +43 -0
- package/templates/quickstart/src/flows/cart-bulk.flow.ts +19 -14
- package/templates/quickstart/src/flows/cart-continue.flow.ts +10 -8
- package/templates/quickstart/src/flows/cart-mem.flow.ts +7 -7
- package/templates/quickstart/src/flows/catalog-logout.flow.ts +13 -8
- package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +26 -48
- package/templates/quickstart/src/flows/checkout.flow.ts +70 -63
- package/templates/quickstart/src/flows/device-showcase.flow.ts +24 -25
- package/templates/quickstart/src/flows/login.flow.ts +11 -18
- package/templates/quickstart/src/flows/mem-stub-demo.flow.ts +12 -0
- package/templates/quickstart/src/flows/shop.flow.ts +15 -20
- package/templates/quickstart/src/flows/smoke.flow.ts +12 -27
- package/templates/quickstart/src/flows/stub-on-error.flow.ts +31 -0
- package/templates/quickstart/src/flows/viewer-blocked.flow.ts +26 -34
- package/templates/quickstart/src/states/checkout.mem-stubs.ts +11 -0
- package/templates/quickstart/src/stubs/device-showcase.ts +193 -0
- package/templates/quickstart/src/stubs/login.ts +37 -0
- package/templates/quickstart/tests/chain-flow.spec.ts +7 -10
- package/templates/scaffold/README.md +1 -1
- package/templates/scaffold/STRUCTURE.md +5 -2
- package/templates/scaffold/config/waygraph.config.ts +24 -5
- package/templates/scaffold/package.json +6 -6
- package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +36 -0
- package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +24 -0
- package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +34 -0
- package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +47 -0
- package/templates/scaffold/src/flows/README.md +35 -14
- package/templates/scaffold/src/flows/{shop.flow.ts → home/cart.flow.ts} +18 -17
- package/templates/scaffold/src/flows/{example.flow.ts → home/hello.flow.ts} +12 -13
- package/templates/scaffold/tests/example.spec.ts +5 -5
- package/templates/scaffold/tests/hoppscotch.spec.ts +3 -3
- package/templates/scaffold/tests/mail-verify.spec.ts +7 -7
- package/templates/skills/waygraph-convention.skill.md +14 -6
- package/examples/saucedemo/playwright.config.ts +0 -19
- package/examples/saucedemo/scripts/drive-auto-cli.mjs +0 -93
- package/examples/saucedemo/scripts/prove-add-to-cart-menu.mjs +0 -126
- package/examples/saucedemo/scripts/prove-auto-graph.mjs +0 -92
- package/examples/saucedemo/scripts/prove-wrong-login-runtime.mjs +0 -63
- package/templates/quickstart/playwright.config.ts +0 -14
- package/templates/quickstart/scripts/drive-auto-cli.mjs +0 -93
- package/templates/scaffold/src/flows/hoppscotch-auth.flow.ts +0 -35
- package/templates/scaffold/src/flows/hoppscotch-get.flow.ts +0 -23
- package/templates/scaffold/src/flows/hoppscotch-post.flow.ts +0 -33
- package/templates/scaffold/src/flows/mail-verify.flow.ts +0 -46
|
@@ -1,21 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { NavDocsBlock } from "../map/(app_base)/docs/_nav.block.js";
|
|
1
|
+
import { engine } from "../../../config/waygraph.config.js";
|
|
2
|
+
import { DemoSel } from "../../map/(app_base)/home/_sel.js";
|
|
3
|
+
import { NavHomeBlock } from "../../map/(app_base)/home/_nav.block.js";
|
|
4
|
+
import { AssertHelloBlock } from "../../map/(app_base)/home/_methods/assert-hello.method.block.js";
|
|
5
|
+
import { NavDocsBlock } from "../../map/(app_base)/docs/_nav.block.js";
|
|
7
6
|
// Page hubs registered for auto/docs grouping (arrival-only after nav).
|
|
8
|
-
import "
|
|
9
|
-
import "
|
|
10
|
-
|
|
11
|
-
const engine = new Engine(config);
|
|
7
|
+
import "../../map/(app_base)/home/_page.block.js";
|
|
8
|
+
import "../../map/(app_base)/docs/_page.block.js";
|
|
12
9
|
|
|
13
10
|
/** Smoke: nav + assert (no mem), then nav to the sibling docs/ page. Green offline. */
|
|
14
|
-
export const
|
|
11
|
+
export const homeHelloFlow = engine
|
|
15
12
|
.map()
|
|
16
13
|
.start()
|
|
17
14
|
.gotoPage(
|
|
18
|
-
NavHomeBlock.
|
|
15
|
+
NavHomeBlock.stubBefore((ctx) => {
|
|
16
|
+
ctx.title("Scaffold: Hello Waygraph");
|
|
17
|
+
}).stubAfter((ctx) => {
|
|
19
18
|
ctx.ring("title", { selector: DemoSel.title, label: "Home heading", duration: true });
|
|
20
19
|
}).stubOnError((ctx) => {
|
|
21
20
|
ctx.ring("title", { selector: DemoSel.title, label: "Home heading at failure" });
|
|
@@ -40,4 +39,4 @@ export const exampleFlow = engine
|
|
|
40
39
|
}),
|
|
41
40
|
)
|
|
42
41
|
.gotoPage(NavDocsBlock)
|
|
43
|
-
.end(
|
|
42
|
+
.end();
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { test, expect } from "@playwright/test";
|
|
2
2
|
import { MemPage, checkpoint, seedMemStub } from "waygraph";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { homeHelloFlow } from "../src/flows/home/hello.flow.js";
|
|
4
|
+
import { homeCartFlow } from "../src/flows/home/cart.flow.js";
|
|
5
5
|
|
|
6
6
|
test("example flow reaches Docs (home -> assert -> sibling docs/ page)", async ({ context }) => {
|
|
7
|
-
const result = await
|
|
7
|
+
const result = await homeHelloFlow.run(context, new MemPage());
|
|
8
8
|
expect(result).toEqual(checkpoint("Docs"));
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
test("shop flow adds then clears (Effect + Method)", async ({ context }) => {
|
|
12
12
|
const mem = new MemPage();
|
|
13
|
-
seedMemStub(mem,
|
|
14
|
-
const result = await
|
|
13
|
+
seedMemStub(mem, homeCartFlow);
|
|
14
|
+
const result = await homeCartFlow.run(context, mem);
|
|
15
15
|
expect(result).toEqual(checkpoint("CartEmpty"));
|
|
16
16
|
});
|
|
@@ -2,9 +2,9 @@ import { test, expect } from "@playwright/test";
|
|
|
2
2
|
import { createServer, type Server } from "node:http";
|
|
3
3
|
import { MemPage, checkpoint } from "waygraph";
|
|
4
4
|
import config from "../config/waygraph.config.js";
|
|
5
|
-
import { hoppscotchGetFlow } from "../src/flows/hoppscotch
|
|
6
|
-
import { hoppscotchAuthFlow } from "../src/flows/hoppscotch
|
|
7
|
-
import { hoppscotchPostFlow } from "../src/flows/hoppscotch
|
|
5
|
+
import { hoppscotchGetFlow } from "../src/flows/(external)/hoppscotch/get.flow.js";
|
|
6
|
+
import { hoppscotchAuthFlow } from "../src/flows/(external)/hoppscotch/auth.flow.js";
|
|
7
|
+
import { hoppscotchPostFlow } from "../src/flows/(external)/hoppscotch/post.flow.js";
|
|
8
8
|
import {
|
|
9
9
|
ExpectedResponseHeader,
|
|
10
10
|
ExpectedResponseText,
|
|
@@ -2,12 +2,12 @@ import { test, expect } from "@playwright/test";
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import { MemPage, checkpoint } from "waygraph";
|
|
5
|
-
import {
|
|
5
|
+
import { emailVerificationFlow } from "../src/flows/(user-stories)/email-verification.flow.js";
|
|
6
6
|
import { ExpectedRecipient, ExpectedLinkPattern, ExpectedEmailContent } from "../src/states/demo.mem-keys.js";
|
|
7
7
|
import { HOME_URL } from "../src/map/(app_base)/home/_sel.js";
|
|
8
8
|
|
|
9
9
|
// Plays "the app sent a verification email": starts a throwaway Mailpit container, sends a real SMTP message,
|
|
10
|
-
// then runs
|
|
10
|
+
// then runs emailVerificationFlow against it, including the no-email and wrong-content failure paths.
|
|
11
11
|
|
|
12
12
|
function runMailpit(): { containerId: string; smtpPort: number; httpPort: number } {
|
|
13
13
|
const run = spawnSync("docker", ["run", "--rm", "-d", "-p", "0:1025", "-p", "0:8025", "axllent/mailpit"], {
|
|
@@ -80,7 +80,7 @@ async function withMailpit<T>(fn: (opts: { httpPort: number; smtpPort: number })
|
|
|
80
80
|
|
|
81
81
|
const VERIFICATION_CONTENT = "Please confirm your email address to finish setting up your account.";
|
|
82
82
|
|
|
83
|
-
test("
|
|
83
|
+
test("emailVerificationFlow: confirms arrival, confirms content, reads the real link, navigates to it", async ({
|
|
84
84
|
context,
|
|
85
85
|
}) => {
|
|
86
86
|
test.skip(!dockerAvailable(), "Docker not reachable in this environment");
|
|
@@ -100,7 +100,7 @@ test("mailVerifyFlow: confirms arrival, confirms content, reads the real link, n
|
|
|
100
100
|
mem.set(ExpectedRecipient({ email: recipient }));
|
|
101
101
|
mem.set(ExpectedLinkPattern, "verified=1");
|
|
102
102
|
mem.set(ExpectedEmailContent, VERIFICATION_CONTENT);
|
|
103
|
-
const result = await
|
|
103
|
+
const result = await emailVerificationFlow.run(context, mem);
|
|
104
104
|
expect(result).toEqual(checkpoint("HomeVerified"));
|
|
105
105
|
});
|
|
106
106
|
});
|
|
@@ -130,7 +130,7 @@ test("the same four mail-reading Blocks handle a second, unrelated email scenari
|
|
|
130
130
|
mem.set(ExpectedRecipient({ email: recipient }));
|
|
131
131
|
mem.set(ExpectedLinkPattern, "verified=1");
|
|
132
132
|
mem.set(ExpectedEmailContent, resetContent);
|
|
133
|
-
const result = await
|
|
133
|
+
const result = await emailVerificationFlow.run(context, mem);
|
|
134
134
|
expect(result).toEqual(checkpoint("HomeVerified"));
|
|
135
135
|
});
|
|
136
136
|
});
|
|
@@ -149,7 +149,7 @@ test("assert-email-received fails loud, naming itself, when no email ever arrive
|
|
|
149
149
|
// not just the leaf Block - real, useful QA context, not a bug - so this
|
|
150
150
|
// matches the accumulated name ending at assert-email-received, not an
|
|
151
151
|
// exact "failed after \"assert-email-received\"" alone.
|
|
152
|
-
await expect(
|
|
152
|
+
await expect(emailVerificationFlow.run(context, mem)).rejects.toThrow(
|
|
153
153
|
/trait "email-received" failed after ".*assert-email-received"/,
|
|
154
154
|
);
|
|
155
155
|
});
|
|
@@ -175,7 +175,7 @@ test("assert-email-content fails loud, naming itself, when the email's copy does
|
|
|
175
175
|
mem.set(ExpectedRecipient({ email: recipient }));
|
|
176
176
|
mem.set(ExpectedLinkPattern, "verified=1");
|
|
177
177
|
mem.set(ExpectedEmailContent, VERIFICATION_CONTENT);
|
|
178
|
-
await expect(
|
|
178
|
+
await expect(emailVerificationFlow.run(context, mem)).rejects.toThrow(
|
|
179
179
|
/trait "body-contains-expected-text" failed after ".*assert-email-content"/,
|
|
180
180
|
);
|
|
181
181
|
});
|
|
@@ -68,10 +68,14 @@ against a live site, use `--skill-pilot-blind`.
|
|
|
68
68
|
|
|
69
69
|
## Flows, stubs and config (the scaffold's shape)
|
|
70
70
|
|
|
71
|
-
- **Build flows with `map()`** (
|
|
72
|
-
|
|
73
|
-
`.end(
|
|
74
|
-
|
|
71
|
+
- **Build flows with `map()`** (`engine.map({ memStub }).start().gotoPage(Nav).method(B, { pace }).assert(A).end()`;
|
|
72
|
+
`.gotoExternal` for another origin), never a raw `defineFlow([start, ...])`. Flow settings (`memStub`, `expectedFailure`, `demoPace`, `sessionReset`) go in `map({ … })` before the chain
|
|
73
|
+
and `.end()` takes nothing. The flow's title is `ctx.title(...)` in a stub. **Nest flows with `.flow(sub, { reset: true })`**,
|
|
74
|
+
not `chainFlow` / `withSessionReset`. No `withMemStub(withTitle(...))` wrappers.
|
|
75
|
+
- **`src/flows/` holds use cases and mirrors the front end's endpoints**: a folder per page (`flows/auth/login/`) with one
|
|
76
|
+
file per scenario named for its outcome (`success.flow.ts`, `wrong-password.flow.ts`, `empty-fields.flow.ts`), and
|
|
77
|
+
`(user-stories)/` (any `(group)`) for flows that cross pages. Export names are `<page><Outcome>Flow`. Not one file per
|
|
78
|
+
Block chain, not a flat folder of unrelated flows.
|
|
75
79
|
- **Blocks carry no stubs; the flow does.** Demo highlights are ctx stubs chained on the Block where the flow uses it:
|
|
76
80
|
`AddItemBlock.stubAfter((ctx) => { ctx.ring("cart", { selector: DemoSel.cartCount, label: "AC · item in cart" }); })`.
|
|
77
81
|
Not a static `withHighlightFixtures({ "add-item": { stubAfter: {...} } })` map, and not an inline
|
|
@@ -79,19 +83,23 @@ against a live site, use `--skill-pilot-blind`.
|
|
|
79
83
|
- **Reading a value into `mem` happens in `act`** (`mem.set(Key, value)`), like Mailpit's `extract-email-link`.
|
|
80
84
|
`observe` is only for a Block that branches (it feeds `resolve`); an empty `act(){}` plus an `observe` that
|
|
81
85
|
writes `mem` is a misuse.
|
|
86
|
+
- **Demo pace is a map option or config, never a wrapper.** `.method(SetBearerTokenBlock, { pace: "blitz" })` speeds one step,
|
|
87
|
+
`map({ demoPace: "fast" })` a flow, `demo: { pace, typingMs, slowMo }` in the config the whole project. Not `withBlockPace(...)`.
|
|
82
88
|
- **Per-environment values** (a different `baseUrl` for staging) come from `.env` files read in the config function:
|
|
83
89
|
`defineConfig(({ env }) => ({ baseUrl: env.BASE_URL }))`; `--env staging` loads `.env.staging`. Never edit the config to switch.
|
|
90
|
+
- **More than one config** (staging, another app) is another file, `config/waygraph.<name>.config.ts`, chosen with `--config <name>`.
|
|
91
|
+
The engine a config exports, and every engine in a flow, follows the selected config.
|
|
84
92
|
- **A browser dialog (`confirm`, `beforeunload`) is answered with `withDialog(block, { accept, into })`**, not inside the
|
|
85
93
|
Block's `act`; the click Block stays a plain click.
|
|
86
94
|
- **Project settings live in `config/waygraph.config.ts`** (`defineConfig({ baseUrl, testDir, webServer,
|
|
87
|
-
extensions, playwright: {...} })`).
|
|
95
|
+
extensions, playwright: {...} })`). The config file also exports `engine = new Engine(config)`; flows `import { engine } from "../../config/waygraph.config.js"`; a project needs no
|
|
88
96
|
`playwright.config.ts` - `waygraph test` builds one from it. A leftover `playwright.config.*` is deprecated.
|
|
89
97
|
- **Another origin the test needs is a Map external** under `src/map/(external)/<tool>/`. For REST API
|
|
90
98
|
tests, `(external)/hoppscotch` (list `extensions: ["hoppscotch"]` in the config) drives hoppscotch.io's client
|
|
91
99
|
with the extension, so requests skip CORS: `set-method`, `set-url`, `set-headers`, `set-json-body`,
|
|
92
100
|
`send-request`, `assert-status`, `read-response-body`, `assert-response-body`.
|
|
93
101
|
- **Updating an older project:** `npm install waygraph@latest`, then `waygraph upgrade` (dry run) to see new
|
|
94
|
-
and differing scaffold files; copy the paths you want with `--apply`.
|
|
102
|
+
and differing scaffold files; copy the paths you want with `--apply`. `waygraph upgrade --skills` refreshes these skills.
|
|
95
103
|
|
|
96
104
|
## Block kinds (file convention)
|
|
97
105
|
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "@playwright/test";
|
|
2
|
-
import { chromiumExecutablePath } from "waygraph";
|
|
3
|
-
|
|
4
|
-
export default defineConfig({
|
|
5
|
-
testDir: "./tests",
|
|
6
|
-
fullyParallel: false,
|
|
7
|
-
workers: 1,
|
|
8
|
-
timeout: 30_000,
|
|
9
|
-
reporter: [["list"], ["html", { open: "never" }]],
|
|
10
|
-
use: {
|
|
11
|
-
headless: true,
|
|
12
|
-
baseURL: "https://www.saucedemo.com",
|
|
13
|
-
trace: "retain-on-failure",
|
|
14
|
-
launchOptions: {
|
|
15
|
-
executablePath: chromiumExecutablePath(),
|
|
16
|
-
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
});
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Drive `waygraph auto --cli` through login -> add-all -> remove-all -> quit.
|
|
4
|
-
* Waits for each Choose: prompt so picks are not sent early.
|
|
5
|
-
*
|
|
6
|
-
* Usage (from examples/saucedemo):
|
|
7
|
-
* node scripts/drive-auto-cli.mjs
|
|
8
|
-
*/
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
10
|
-
import { createInterface } from "node:readline";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
import { dirname, join } from "node:path";
|
|
13
|
-
|
|
14
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
-
const waygraphBin = join(root, "../../bin/waygraph");
|
|
16
|
-
const DATA =
|
|
17
|
-
process.env.WAYGRAPH_DATA ??
|
|
18
|
-
'{"saucedemo.credentials":{"username":"standard_user","password":"secret_sauce"}}';
|
|
19
|
-
|
|
20
|
-
function pickByLabel(menuText, label) {
|
|
21
|
-
const re = new RegExp(`\\[(\\d+)\\]\\s+${label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
|
|
22
|
-
const m = menuText.match(re);
|
|
23
|
-
if (!m) throw new Error(`menu missing "${label}"\n${menuText}`);
|
|
24
|
-
return m[1];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function main() {
|
|
28
|
-
const child = spawn(waygraphBin, ["auto", "--cli", "--data", DATA, "."], {
|
|
29
|
-
cwd: root,
|
|
30
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
31
|
-
env: process.env,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
let buf = "";
|
|
35
|
-
const dump = (chunk, stream) => {
|
|
36
|
-
const s = chunk.toString();
|
|
37
|
-
buf += s;
|
|
38
|
-
process[stream].write(s);
|
|
39
|
-
};
|
|
40
|
-
child.stdout.on("data", (c) => dump(c, "stdout"));
|
|
41
|
-
child.stderr.on("data", (c) => dump(c, "stderr"));
|
|
42
|
-
|
|
43
|
-
const waitChoose = () =>
|
|
44
|
-
new Promise((resolve, reject) => {
|
|
45
|
-
const start = Date.now();
|
|
46
|
-
const tick = () => {
|
|
47
|
-
if (buf.includes("Choose:")) {
|
|
48
|
-
const i = buf.lastIndexOf("Choose:");
|
|
49
|
-
const slice = buf.slice(Math.max(0, i - 4000), i + 8);
|
|
50
|
-
resolve(slice);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
if (Date.now() - start > 120_000) {
|
|
54
|
-
reject(new Error(`timeout waiting for Choose:\n${buf.slice(-2000)}`));
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
setTimeout(tick, 100);
|
|
58
|
-
};
|
|
59
|
-
tick();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const send = (line) => {
|
|
63
|
-
console.error(`\n>> pick ${line.trim()}`);
|
|
64
|
-
buf = buf.replace(/Choose:/g, "Choose(done):");
|
|
65
|
-
child.stdin.write(line);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
let menu = await waitChoose();
|
|
70
|
-
send("1\n"); // submit-login
|
|
71
|
-
menu = await waitChoose();
|
|
72
|
-
if (!/LoggedIn|Add all to cart/.test(menu)) {
|
|
73
|
-
throw new Error(`expected inventory after login, got:\n${menu}`);
|
|
74
|
-
}
|
|
75
|
-
send(`${pickByLabel(menu, "Add all to cart")}\n`);
|
|
76
|
-
menu = await waitChoose();
|
|
77
|
-
send(`${pickByLabel(menu, "Remove all from cart")}\n`);
|
|
78
|
-
menu = await waitChoose();
|
|
79
|
-
if (!/Add all to cart/.test(menu)) {
|
|
80
|
-
throw new Error(`expected Add all again after remove-all, got:\n${menu}`);
|
|
81
|
-
}
|
|
82
|
-
send("q\n");
|
|
83
|
-
const code = await new Promise((res) => child.on("exit", res));
|
|
84
|
-
if (code !== 0 && code !== null) process.exitCode = code;
|
|
85
|
-
else console.error("\ndrive-auto-cli: PASS login -> add-all -> remove-all -> quit");
|
|
86
|
-
} catch (err) {
|
|
87
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
88
|
-
child.kill("SIGTERM");
|
|
89
|
-
process.exitCode = 1;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
main();
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// Proves the dynamic add-to-cart menu for real: buildExploreMenu must offer
|
|
2
|
-
// one row PER item still on the page (not one generic "add-to-cart" row tied
|
|
3
|
-
// to the backpack), picking a non-default item must add THAT item (not the
|
|
4
|
-
// backpack), and the menu must stop offering an item once it is in the cart.
|
|
5
|
-
import { chromium } from "@playwright/test";
|
|
6
|
-
import { join, dirname } from "node:path";
|
|
7
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { createRequire } from "node:module";
|
|
9
|
-
import { chromiumExecutablePath } from "waygraph";
|
|
10
|
-
|
|
11
|
-
const require = createRequire(import.meta.url);
|
|
12
|
-
await import(require.resolve("tsx/esm"));
|
|
13
|
-
|
|
14
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
-
const waygraphDist = join(root, "../../dist");
|
|
16
|
-
|
|
17
|
-
const { buildExploreContext, buildExploreMenu } = await import(
|
|
18
|
-
pathToFileURL(join(waygraphDist, "auto-explore.js")).href
|
|
19
|
-
);
|
|
20
|
-
const { MemPage, Engine, start, end } = await import(pathToFileURL(join(waygraphDist, "index.js")).href);
|
|
21
|
-
|
|
22
|
-
const exe = chromiumExecutablePath();
|
|
23
|
-
|
|
24
|
-
async function runBlock(engine, entry, context, page, mem) {
|
|
25
|
-
const flow = engine.defineFlow([start, entry.block, end]);
|
|
26
|
-
await flow.run(context, mem, { page, closeOnFinish: false });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const { graph, library } = await buildExploreContext(root);
|
|
30
|
-
const mem = new MemPage();
|
|
31
|
-
mem.set(library.byName.get("submit-login").block.requires[0], {
|
|
32
|
-
username: "standard_user",
|
|
33
|
-
password: "secret_sauce",
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const browser = await chromium.launch({
|
|
37
|
-
headless: true,
|
|
38
|
-
...(exe ? { executablePath: exe, args: ["--no-sandbox"] } : {}),
|
|
39
|
-
});
|
|
40
|
-
const context = await browser.newContext({
|
|
41
|
-
baseURL: "https://www.saucedemo.com",
|
|
42
|
-
viewport: { width: 1280, height: 900 },
|
|
43
|
-
});
|
|
44
|
-
const page = await context.newPage();
|
|
45
|
-
const engine = new Engine({ headless: true });
|
|
46
|
-
|
|
47
|
-
await page.goto("/", { waitUntil: "load" });
|
|
48
|
-
await runBlock(engine, library.byName.get("submit-login"), context, page, mem);
|
|
49
|
-
await page.waitForURL(/inventory\.html/);
|
|
50
|
-
|
|
51
|
-
// 1. Menu on LoggedIn must offer one distinct add-to-cart row per product on
|
|
52
|
-
// the inventory page - not a single generic row.
|
|
53
|
-
const menu1 = await buildExploreMenu(page, graph, library, "LoggedIn");
|
|
54
|
-
const addRows1 = menu1.flat.filter((e) => e.block === "add-to-cart");
|
|
55
|
-
console.log(`add-to-cart rows on LoggedIn: ${addRows1.length}`);
|
|
56
|
-
for (const row of addRows1) console.log(` - ${row.label}`);
|
|
57
|
-
if (addRows1.length < 2) {
|
|
58
|
-
throw new Error(`expected several distinct add-to-cart rows, got ${addRows1.length}`);
|
|
59
|
-
}
|
|
60
|
-
if (addRows1.some((r) => !r.instanceOption)) {
|
|
61
|
-
throw new Error("every add-to-cart row must carry an instanceOption");
|
|
62
|
-
}
|
|
63
|
-
const backpackRow = addRows1.find((r) => r.instanceOption.value.id === "sauce-labs-backpack");
|
|
64
|
-
const bikeLightRow = addRows1.find((r) => r.instanceOption.value.id === "sauce-labs-bike-light");
|
|
65
|
-
if (!backpackRow || !bikeLightRow) {
|
|
66
|
-
throw new Error("expected both the backpack and the bike light as separate rows");
|
|
67
|
-
}
|
|
68
|
-
if (backpackRow.label === bikeLightRow.label) {
|
|
69
|
-
throw new Error("rows must have distinct, item-specific labels");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 2. Picking the NON-default item (bike light, not the backpack) must add
|
|
73
|
-
// exactly that item - proving mem really drives which product act() clicks.
|
|
74
|
-
mem.set(bikeLightRow.instanceOption.key, bikeLightRow.instanceOption.value);
|
|
75
|
-
await runBlock(engine, library.byName.get("add-to-cart"), context, page, mem);
|
|
76
|
-
|
|
77
|
-
const bikeLightInCart = await page.locator('[data-test="remove-sauce-labs-bike-light"]').first().isVisible();
|
|
78
|
-
const backpackStillInCart = await page.locator('[data-test="remove-sauce-labs-backpack"]').first().isVisible();
|
|
79
|
-
console.log(`bike light in cart: ${bikeLightInCart}, backpack in cart (should be false): ${backpackStillInCart}`);
|
|
80
|
-
if (!bikeLightInCart) throw new Error("expected the picked item (bike light) to be in the cart");
|
|
81
|
-
if (backpackStillInCart) throw new Error("backpack should NOT be in the cart - it was never picked");
|
|
82
|
-
|
|
83
|
-
// 3. Rebuilding the menu must now offer bike light's "Remove" row instead,
|
|
84
|
-
// and drop it from the add-to-cart options - the menu reflects the live page.
|
|
85
|
-
const menu2 = await buildExploreMenu(page, graph, library, "LoggedIn");
|
|
86
|
-
const addRows2 = menu2.flat.filter((e) => e.block === "add-to-cart");
|
|
87
|
-
if (addRows2.some((r) => r.instanceOption.value.id === "sauce-labs-bike-light")) {
|
|
88
|
-
throw new Error("bike light should no longer be offered - it is already in the cart");
|
|
89
|
-
}
|
|
90
|
-
if (!addRows2.some((r) => r.instanceOption.value.id === "sauce-labs-backpack")) {
|
|
91
|
-
throw new Error("backpack should still be offered - it was never added");
|
|
92
|
-
}
|
|
93
|
-
console.log(`add-to-cart rows after adding bike light: ${addRows2.length} (one fewer than before)`);
|
|
94
|
-
if (addRows2.length !== addRows1.length - 1) {
|
|
95
|
-
throw new Error(`expected exactly one fewer row (${addRows1.length - 1}), got ${addRows2.length}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 4. Same rebuild must now offer Remove for the bike light (mem branch for
|
|
99
|
-
// items already in the cart) - not only shrink the Add list.
|
|
100
|
-
const removeRows = menu2.flat.filter((e) => e.block === "remove-from-cart");
|
|
101
|
-
console.log(`remove-from-cart rows after add: ${removeRows.length}`);
|
|
102
|
-
for (const row of removeRows) console.log(` - ${row.label}`);
|
|
103
|
-
if (removeRows.length !== 1) {
|
|
104
|
-
throw new Error(`expected exactly 1 Remove row (bike light), got ${removeRows.length}`);
|
|
105
|
-
}
|
|
106
|
-
if (removeRows[0].instanceOption.value.id !== "sauce-labs-bike-light") {
|
|
107
|
-
throw new Error(`expected Remove for bike light, got ${removeRows[0].label}`);
|
|
108
|
-
}
|
|
109
|
-
if (!/Remove "Sauce Labs Bike Light" from cart/.test(removeRows[0].label)) {
|
|
110
|
-
throw new Error(`unexpected Remove label: ${removeRows[0].label}`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 5. MemNav: Open "…" details for every inventory card (including ones already
|
|
114
|
-
// in the cart) - separate from Add/Remove effect rows.
|
|
115
|
-
const openRows = menu2.flat.filter((e) => e.block === "nav-item-detail");
|
|
116
|
-
console.log(`nav-item-detail (MemNav) rows: ${openRows.length}`);
|
|
117
|
-
for (const row of openRows) console.log(` - ${row.label}`);
|
|
118
|
-
if (openRows.length < 6) {
|
|
119
|
-
throw new Error(`expected 6 Open details rows, got ${openRows.length}`);
|
|
120
|
-
}
|
|
121
|
-
if (!openRows.some((r) => /Open "Test\.allTheThings\(\) T-Shirt \(Red\)" details/.test(r.label))) {
|
|
122
|
-
throw new Error("missing Open details row for Test.allTheThings() T-Shirt (Red)");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
console.log("\nPASS: add/remove Effect + open-details MemNav reflect the live page");
|
|
126
|
-
await browser.close();
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { chromium } from "@playwright/test";
|
|
2
|
-
import { join, dirname } from "node:path";
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { chromiumExecutablePath } from "waygraph";
|
|
6
|
-
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
await import(require.resolve("tsx/esm"));
|
|
9
|
-
|
|
10
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
const outDir = join(root, "../../.tmp/waygraph-auto");
|
|
12
|
-
const waygraphDist = join(root, "../../dist");
|
|
13
|
-
|
|
14
|
-
const { buildExploreContext, buildExploreMenu } = await import(
|
|
15
|
-
pathToFileURL(join(waygraphDist, "auto-explore.js")).href
|
|
16
|
-
);
|
|
17
|
-
const { locate, MemPage, Engine, start, end } = await import(pathToFileURL(join(waygraphDist, "index.js")).href);
|
|
18
|
-
|
|
19
|
-
const exe = chromiumExecutablePath();
|
|
20
|
-
|
|
21
|
-
async function snap(page, name) {
|
|
22
|
-
const path = join(outDir, `graph-proof-${name}.png`);
|
|
23
|
-
await page.screenshot({ path, fullPage: false });
|
|
24
|
-
console.log("screenshot", path);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function resolveHere(page, navTags) {
|
|
28
|
-
const path = new URL(page.url()).pathname;
|
|
29
|
-
if (path.includes("inventory-item.html")) return "ItemDetailPage";
|
|
30
|
-
if (path.includes("inventory.html")) return "LoggedIn";
|
|
31
|
-
if (path.includes("cart.html")) return "CartPage";
|
|
32
|
-
if (path.includes("checkout-complete.html")) return "OrderComplete";
|
|
33
|
-
if (path === "/" || path === "") {
|
|
34
|
-
if (await page.locator("#login-button").isVisible({ timeout: 500 }).catch(() => false)) return "LoginPage";
|
|
35
|
-
}
|
|
36
|
-
return locate(page, navTags, { timeoutMs: 1500 });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function menuSummary(page, graph, library, label) {
|
|
40
|
-
const navTags = library.navBlocks.map((n) => n.block);
|
|
41
|
-
const resolved = await resolveHere(page, navTags);
|
|
42
|
-
const menu = await buildExploreMenu(page, graph, library, resolved);
|
|
43
|
-
console.log(`\n=== ${label} | here=${resolved} ===`);
|
|
44
|
-
for (const s of menu.sections) {
|
|
45
|
-
console.log(` ${s.title}:`);
|
|
46
|
-
for (const e of s.edges) console.log(` ${e.block} (${e.kind}) → ${e.to}`);
|
|
47
|
-
}
|
|
48
|
-
return resolved;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function runBlock(engine, entry, context, page, mem) {
|
|
52
|
-
const flow = engine.defineFlow([start, entry.block, end]);
|
|
53
|
-
await flow.run(context, mem, { page, closeOnFinish: false });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const { graph, library } = await buildExploreContext(root);
|
|
57
|
-
const mem = new MemPage();
|
|
58
|
-
mem.set(
|
|
59
|
-
library.byName.get("submit-login").block.requires[0],
|
|
60
|
-
{ username: "standard_user", password: "secret_sauce" },
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const browser = await chromium.launch({
|
|
64
|
-
headless: true,
|
|
65
|
-
...(exe ? { executablePath: exe, args: ["--no-sandbox"] } : {}),
|
|
66
|
-
});
|
|
67
|
-
const context = await browser.newContext({ baseURL: "https://www.saucedemo.com", viewport: { width: 1280, height: 900 } });
|
|
68
|
-
const page = await context.newPage();
|
|
69
|
-
const engine = new Engine({ headless: true });
|
|
70
|
-
|
|
71
|
-
await page.goto("/", { waitUntil: "load" });
|
|
72
|
-
await menuSummary(page, graph, library, "1. Login page");
|
|
73
|
-
await snap(page, "1-login-menu");
|
|
74
|
-
|
|
75
|
-
await runBlock(engine, library.byName.get("submit-login"), context, page, mem);
|
|
76
|
-
await page.waitForURL(/inventory\.html/);
|
|
77
|
-
const here2 = await menuSummary(page, graph, library, "2. After submit-login → LoggedIn");
|
|
78
|
-
if (here2 !== "LoggedIn") throw new Error(`expected LoggedIn got ${here2}`);
|
|
79
|
-
await snap(page, "2-logged-in-menu");
|
|
80
|
-
|
|
81
|
-
const logout = library.byName.get("submit-logout");
|
|
82
|
-
const hasLogout = (await buildExploreMenu(page, graph, library, "LoggedIn")).flat.some((e) => e.block === "submit-logout");
|
|
83
|
-
if (!hasLogout) throw new Error("graph menu missing submit-logout on LoggedIn");
|
|
84
|
-
|
|
85
|
-
await runBlock(engine, logout, context, page, mem);
|
|
86
|
-
await page.waitForSelector("#login-button");
|
|
87
|
-
const here3 = await menuSummary(page, graph, library, "3. After submit-logout → LoginPage");
|
|
88
|
-
if (here3 !== "LoginPage") throw new Error(`expected LoginPage got ${here3}`);
|
|
89
|
-
await snap(page, "3-back-to-login-menu");
|
|
90
|
-
|
|
91
|
-
console.log("\nPASS: graph cycle LoginPage → LoggedIn → LoginPage");
|
|
92
|
-
await browser.close();
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Headless runtime proof: wrong password -> LoginPage + error banner.
|
|
3
|
-
* Prints PASS/FAIL to stdout only (no screenshots, no visible browser).
|
|
4
|
-
*/
|
|
5
|
-
import { chromium } from "@playwright/test";
|
|
6
|
-
import { join, dirname } from "node:path";
|
|
7
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
-
import { createRequire } from "node:module";
|
|
9
|
-
import { chromiumExecutablePath } from "waygraph";
|
|
10
|
-
|
|
11
|
-
const require = createRequire(import.meta.url);
|
|
12
|
-
await import(require.resolve("tsx/esm"));
|
|
13
|
-
|
|
14
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
-
const waygraphDist = join(root, "../../dist");
|
|
16
|
-
const { Engine, start, end, MemPage } = await import(pathToFileURL(join(waygraphDist, "index.js")).href);
|
|
17
|
-
const { NavLoginBlock } = await import(pathToFileURL(join(root, "src/blocks/saucedemo-web/nav-login.block.js")).href);
|
|
18
|
-
const { FillUsernameBlock } = await import(
|
|
19
|
-
pathToFileURL(join(root, "src/blocks/saucedemo-web/methods/fill-username.method.block.js")).href
|
|
20
|
-
);
|
|
21
|
-
const { FillPasswordBlock } = await import(
|
|
22
|
-
pathToFileURL(join(root, "src/blocks/saucedemo-web/methods/fill-password.method.block.js")).href
|
|
23
|
-
);
|
|
24
|
-
const { SubmitLoginBlock } = await import(
|
|
25
|
-
pathToFileURL(join(root, "src/blocks/saucedemo-web/methods/submit-login.method.block.js")).href
|
|
26
|
-
);
|
|
27
|
-
const { LoginCreds } = await import(pathToFileURL(join(root, "src/states/checkout.mem-keys.js")).href);
|
|
28
|
-
|
|
29
|
-
const exe = chromiumExecutablePath();
|
|
30
|
-
const baseURL = "https://www.saucedemo.com";
|
|
31
|
-
|
|
32
|
-
console.log("RUNTIME: headless browser (wrong password test)...");
|
|
33
|
-
console.log("RUNTIME: baseURL =", baseURL);
|
|
34
|
-
|
|
35
|
-
const browser = await chromium.launch({
|
|
36
|
-
headless: true,
|
|
37
|
-
...(exe ? { executablePath: exe, args: ["--no-sandbox", "--disable-dev-shm-usage"] } : {}),
|
|
38
|
-
});
|
|
39
|
-
const context = await browser.newContext({ baseURL, viewport: { width: 1280, height: 900 } });
|
|
40
|
-
const page = await context.newPage();
|
|
41
|
-
const engine = new Engine({ headless: true });
|
|
42
|
-
const mem = new MemPage();
|
|
43
|
-
mem.set(LoginCreds({ username: "standard_user", password: "wrong-password" }));
|
|
44
|
-
|
|
45
|
-
const flow = engine.defineFlow([start, NavLoginBlock, FillUsernameBlock, FillPasswordBlock, SubmitLoginBlock, end]);
|
|
46
|
-
const runOut = await flow.run(context, mem, { page, closeOnFinish: false });
|
|
47
|
-
const result = runOut && typeof runOut === "object" && "result" in runOut ? runOut.result : runOut;
|
|
48
|
-
|
|
49
|
-
const errorVisible = await page.locator('[data-test="error"]').isVisible();
|
|
50
|
-
const url = page.url();
|
|
51
|
-
|
|
52
|
-
console.log("RUNTIME: checkpoint =", result.__state);
|
|
53
|
-
console.log("RUNTIME: url =", url);
|
|
54
|
-
console.log("RUNTIME: error banner visible =", errorVisible);
|
|
55
|
-
|
|
56
|
-
if (result.__state !== "LoginPage" || !errorVisible) {
|
|
57
|
-
console.error("RUNTIME: FAIL");
|
|
58
|
-
await browser.close();
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log("RUNTIME: PASS — stayed on LoginPage with error banner.");
|
|
63
|
-
await browser.close();
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "@playwright/test";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
testDir: "./tests",
|
|
5
|
-
fullyParallel: false,
|
|
6
|
-
workers: 1,
|
|
7
|
-
timeout: 30_000,
|
|
8
|
-
reporter: [["list"], ["html", { open: "never" }]],
|
|
9
|
-
use: {
|
|
10
|
-
headless: true,
|
|
11
|
-
baseURL: "https://www.saucedemo.com",
|
|
12
|
-
trace: "retain-on-failure",
|
|
13
|
-
},
|
|
14
|
-
});
|