waygraph 0.7.16 → 0.10.1
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 +90 -52
- package/dist/auto-explore-run.js +13 -7
- package/dist/auto-explore.js +47 -36
- package/dist/cli.d.ts +8 -12
- package/dist/cli.js +229 -249
- package/dist/engine.d.ts +53 -11
- package/dist/engine.js +81 -8
- package/dist/graph.d.ts +2 -1
- package/dist/graph.js +51 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/examples/saucedemo/README.md +19 -5
- package/examples/saucedemo/docs/CONVENTION.md +22 -18
- package/examples/saucedemo/docs/HELPERS.md +152 -8
- package/examples/saucedemo/package-lock.json +1 -1
- package/examples/saucedemo/package.json +11 -5
- package/examples/saucedemo/scripts/capture-wrong-login.mjs +1 -1
- package/examples/saucedemo/scripts/drive-auto-cli.mjs +93 -0
- package/examples/saucedemo/scripts/prove-wrong-login-runtime.mjs +1 -1
- package/examples/saucedemo/scripts/show-auto-menu.mjs +72 -0
- package/examples/saucedemo/src/blocks/SITE-MAP.md +11 -8
- package/examples/saucedemo/src/blocks/saucedemo-web/NAV.md +3 -3
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/NAV.md +1 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/NAV.md +1 -1
- package/{templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/actions/submit-checkout-info.action.block.ts → examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts} +5 -5
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/NAV.md +1 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/{actions/finish-order.action.block.ts → methods/finish-order.method.block.ts} +5 -5
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/NAV.md +17 -5
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +37 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts +55 -0
- package/{templates/quickstart/src/blocks/saucedemo-web/inventory/actions → examples/saucedemo/src/blocks/saucedemo-web/inventory/methods}/add-to-cart.effect.block.ts +6 -6
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts +40 -20
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts +55 -0
- package/{templates/quickstart/src/blocks/saucedemo-web/inventory/actions → examples/saucedemo/src/blocks/saucedemo-web/inventory/methods}/remove-from-cart.effect.block.ts +5 -5
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +4 -4
- package/examples/saucedemo/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts +2 -2
- package/examples/saucedemo/src/blocks/saucedemo-web/{actions/submit-login.action.block.ts → methods/submit-login.method.block.ts} +5 -5
- package/{templates/quickstart/src/blocks/saucedemo-web/actions/submit-logout.action.block.ts → examples/saucedemo/src/blocks/saucedemo-web/methods/submit-logout.method.block.ts} +5 -5
- package/examples/saucedemo/src/flows/cart-bulk.flow.ts +19 -0
- package/examples/saucedemo/src/flows/cart-mem.flow.ts +1 -1
- package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
- package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +4 -4
- package/examples/saucedemo/src/flows/checkout.flow.ts +4 -4
- package/examples/saucedemo/src/flows/login.flow.ts +1 -1
- package/examples/saucedemo/src/flows/shop.flow.ts +3 -3
- package/examples/saucedemo/src/flows/smoke.flow.ts +2 -2
- package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
- package/examples/saucedemo/tests/flow-block-verify.spec.ts +1 -1
- package/examples/saucedemo/tests/submit-login-branch.spec.ts +1 -1
- package/package.json +1 -1
- package/templates/quickstart/README-saucedemo-commands.md +72 -0
- package/templates/quickstart/docs/CONVENTION.md +5 -5
- package/templates/quickstart/docs/HELPERS.md +152 -8
- package/templates/quickstart/package.json +9 -5
- package/templates/quickstart/scripts/drive-auto-cli.mjs +93 -0
- package/templates/quickstart/src/blocks/SITE-MAP.md +11 -8
- package/templates/quickstart/src/blocks/saucedemo-web/NAV.md +3 -3
- package/templates/quickstart/src/blocks/saucedemo-web/cart/NAV.md +1 -1
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/NAV.md +1 -1
- package/{examples/saucedemo/src/blocks/saucedemo-web/checkout-step-one/actions/submit-checkout-info.action.block.ts → templates/quickstart/src/blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.ts} +5 -5
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/NAV.md +1 -1
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/{actions/finish-order.action.block.ts → methods/finish-order.method.block.ts} +5 -5
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/NAV.md +17 -5
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +37 -0
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.ts +55 -0
- package/{examples/saucedemo/src/blocks/saucedemo-web/inventory/actions → templates/quickstart/src/blocks/saucedemo-web/inventory/methods}/add-to-cart.effect.block.ts +6 -6
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts +40 -20
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.ts +55 -0
- package/{examples/saucedemo/src/blocks/saucedemo-web/inventory/actions → templates/quickstart/src/blocks/saucedemo-web/inventory/methods}/remove-from-cart.effect.block.ts +5 -5
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/nav-item-detail.block.ts +1 -1
- package/templates/quickstart/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts +2 -2
- package/templates/quickstart/src/blocks/saucedemo-web/{actions/submit-login.action.block.ts → methods/submit-login.method.block.ts} +5 -5
- package/{examples/saucedemo/src/blocks/saucedemo-web/actions/submit-logout.action.block.ts → templates/quickstart/src/blocks/saucedemo-web/methods/submit-logout.method.block.ts} +5 -5
- package/templates/quickstart/src/flows/cart-bulk.flow.ts +19 -0
- package/templates/quickstart/src/flows/cart-mem.flow.ts +1 -1
- package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
- package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +4 -4
- package/templates/quickstart/src/flows/checkout.flow.ts +4 -4
- package/templates/quickstart/src/flows/login.flow.ts +1 -1
- package/templates/quickstart/src/flows/shop.flow.ts +3 -3
- package/templates/quickstart/src/flows/smoke.flow.ts +2 -2
- package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
- package/templates/scaffold/package.json +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { defineMethodBlock, checkpoint, type Checkpoint } from "waygraph";
|
|
2
|
+
import type { RemoveFromCartOutcome } from "../../../../states/checkout.states.js";
|
|
3
|
+
import { collectRemovableItems, InventorySel } from "./inventory-items.js";
|
|
4
|
+
|
|
5
|
+
type BulkObserve = "still-has-items" | "cart-empty";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Kind: Method
|
|
9
|
+
* Helper: defineMethodBlock
|
|
10
|
+
* Route: saucedemo-web/inventory/methods/ (= /inventory.html)
|
|
11
|
+
*
|
|
12
|
+
* Bulk: click every live "Remove" once. No instanceOptions - one auto menu
|
|
13
|
+
* row. In is Checkpoint<string> (from: "*") so round-trips work the same as
|
|
14
|
+
* single-item remove. Out: LoggedIn when cart empty; ItemInCart if anything
|
|
15
|
+
* remains (partial failure / race).
|
|
16
|
+
*/
|
|
17
|
+
export const RemoveAllFromCartBlock = defineMethodBlock<
|
|
18
|
+
Checkpoint<string>,
|
|
19
|
+
RemoveFromCartOutcome
|
|
20
|
+
>({
|
|
21
|
+
name: "remove-all-from-cart",
|
|
22
|
+
description: "Removes every product still showing Remove on this page.",
|
|
23
|
+
instruction: {
|
|
24
|
+
async act(page) {
|
|
25
|
+
const items = await collectRemovableItems(page);
|
|
26
|
+
for (const item of items) {
|
|
27
|
+
await page.locator(InventorySel.removeBtn(item.id)).click();
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
async observe(page): Promise<BulkObserve> {
|
|
31
|
+
const left = await collectRemovableItems(page);
|
|
32
|
+
return left.length > 0 ? "still-has-items" : "cart-empty";
|
|
33
|
+
},
|
|
34
|
+
resolve: (observed) =>
|
|
35
|
+
checkpoint(observed === "still-has-items" ? "ItemInCart" : "LoggedIn"),
|
|
36
|
+
verify: (out) =>
|
|
37
|
+
out.__state === "ItemInCart"
|
|
38
|
+
? [
|
|
39
|
+
{
|
|
40
|
+
name: "cart-still-has-items",
|
|
41
|
+
async check(page) {
|
|
42
|
+
return (await collectRemovableItems(page)).length > 0;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
: [
|
|
47
|
+
{
|
|
48
|
+
name: "cart-empty",
|
|
49
|
+
async check(page) {
|
|
50
|
+
return (await page.locator(InventorySel.cartBadge).count()) === 0;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { defineEffectBlock, checkpoint, type Checkpoint, type WaygraphInstanceOption } from "waygraph";
|
|
2
2
|
import type { RemoveFromCartOutcome } from "../../../../states/checkout.states.js";
|
|
3
3
|
import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
|
|
4
|
-
import { collectRemovableItems } from "./inventory-items.js";
|
|
4
|
+
import { collectRemovableItems, InventorySel } from "./inventory-items.js";
|
|
5
5
|
|
|
6
6
|
type RemoveObserve = "still-has-items" | "cart-empty";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Kind: Effect
|
|
10
10
|
* Helper: defineEffectBlock
|
|
11
|
-
* Route: saucedemo-web/inventory/
|
|
11
|
+
* Route: saucedemo-web/inventory/methods/ (= /inventory.html)
|
|
12
12
|
*
|
|
13
13
|
* Removes the mem-picked product. After Add, waygraph auto shows matching
|
|
14
14
|
* "Remove ..." rows from live Remove buttons.
|
|
@@ -20,7 +20,7 @@ export const RemoveFromCartBlock = defineEffectBlock<Checkpoint<string>, RemoveF
|
|
|
20
20
|
instruction: {
|
|
21
21
|
async act(page, _input, mem) {
|
|
22
22
|
const { id } = mem.get(SelectedItem.key);
|
|
23
|
-
await page.locator(
|
|
23
|
+
await page.locator(InventorySel.removeBtn(id)).click();
|
|
24
24
|
},
|
|
25
25
|
async observe(page): Promise<RemoveObserve> {
|
|
26
26
|
const left = await collectRemovableItems(page);
|
|
@@ -42,7 +42,7 @@ export const RemoveFromCartBlock = defineEffectBlock<Checkpoint<string>, RemoveF
|
|
|
42
42
|
{
|
|
43
43
|
name: "cart-empty",
|
|
44
44
|
async check(page) {
|
|
45
|
-
return (await page.locator(
|
|
45
|
+
return (await page.locator(InventorySel.cartBadge).count()) === 0;
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
],
|
|
@@ -54,7 +54,7 @@ export const RemoveFromCartBlock = defineEffectBlock<Checkpoint<string>, RemoveF
|
|
|
54
54
|
label: `Remove "${item.name}" from cart`,
|
|
55
55
|
key: SelectedItem.key,
|
|
56
56
|
value: item,
|
|
57
|
-
highlight:
|
|
57
|
+
highlight: InventorySel.removeBtn(item.id),
|
|
58
58
|
}));
|
|
59
59
|
},
|
|
60
60
|
});
|
|
@@ -3,8 +3,8 @@ import type { ItemDetailPage } from "../../../states/checkout.states.js";
|
|
|
3
3
|
import { SelectedItem } from "../../../states/checkout.mem-keys.js";
|
|
4
4
|
import {
|
|
5
5
|
collectInventoryItems,
|
|
6
|
-
|
|
7
|
-
} from "./
|
|
6
|
+
InventorySel,
|
|
7
|
+
} from "./methods/inventory-items.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Kind: Nav
|
|
@@ -19,7 +19,7 @@ export const NavItemDetailBlock = defineMemNavBlock<ItemDetailPage>({
|
|
|
19
19
|
description: "Opens the product named by mem (saucedemo.selectedItem) on its detail page.",
|
|
20
20
|
checkpoint: "ItemDetailPage",
|
|
21
21
|
requires: [SelectedItem.key],
|
|
22
|
-
click: (mem) =>
|
|
22
|
+
click: (mem) => InventorySel.title(mem.get(SelectedItem.key).id),
|
|
23
23
|
verify: [Trait.url({ pathname: "/inventory-item.html" })],
|
|
24
24
|
async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
|
|
25
25
|
const items = await collectInventoryItems(page);
|
|
@@ -28,7 +28,7 @@ export const NavItemDetailBlock = defineMemNavBlock<ItemDetailPage>({
|
|
|
28
28
|
label: `Open "${item.name}" details`,
|
|
29
29
|
key: SelectedItem.key,
|
|
30
30
|
value: item,
|
|
31
|
-
highlight:
|
|
31
|
+
highlight: InventorySel.title(item.id),
|
|
32
32
|
}));
|
|
33
33
|
},
|
|
34
34
|
});
|
package/examples/saucedemo/src/blocks/saucedemo-web/{actions → methods}/submit-login-for-flow.ts
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { DefinedBlock } from "waygraph";
|
|
2
2
|
import type { LoginPage, LoggedIn } from "../../../states/checkout.states.js";
|
|
3
|
-
import { SubmitLoginActionBlock } from "./submit-login.
|
|
3
|
+
import { SubmitLoginActionBlock } from "./submit-login.method.block.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Kind: helper (not a discoverable Block file)
|
|
7
7
|
* Helper: typed alias over SubmitLoginActionBlock
|
|
8
|
-
* Route: saucedemo-web/
|
|
8
|
+
* Route: saucedemo-web/methods/ (page URL /)
|
|
9
9
|
*
|
|
10
10
|
* DefinedBlock cast so linear Flows can chain .withVerify on Out=LoggedIn;
|
|
11
11
|
* runtime still branches on bad credentials via SubmitLoginActionBlock.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineMethodBlock, checkpoint, Trait } from "waygraph";
|
|
2
2
|
import type { LoginPage, LoginSubmitOutcome } from "../../../states/checkout.states.js";
|
|
3
3
|
import { LoginCreds } from "../../../states/checkout.mem-keys.js";
|
|
4
4
|
|
|
5
5
|
type LoginObserve = "success" | "failure";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Kind:
|
|
9
|
-
* Helper:
|
|
10
|
-
* Route: saucedemo-web/
|
|
8
|
+
* Kind: Method
|
|
9
|
+
* Helper: defineMethodBlock
|
|
10
|
+
* Route: saucedemo-web/methods/ (page URL /)
|
|
11
11
|
*
|
|
12
12
|
* Fills credentials and submits - never page.goto (nav-login owns that).
|
|
13
13
|
* Branches: good creds -> LoggedIn; bad/locked -> LoginPage with error banner.
|
|
14
14
|
*/
|
|
15
|
-
export const SubmitLoginActionBlock =
|
|
15
|
+
export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOutcome>({
|
|
16
16
|
name: "submit-login",
|
|
17
17
|
description: "Logs a user in when credentials are valid; stays on LoginPage when auth fails.",
|
|
18
18
|
requires: [LoginCreds.key],
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineMethodBlock, checkpoint, Trait } from "waygraph";
|
|
2
2
|
import type { LoggedIn, LoginPage } from "../../../states/checkout.states.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Kind:
|
|
6
|
-
* Helper:
|
|
7
|
-
* Route: saucedemo-web/
|
|
5
|
+
* Kind: Method
|
|
6
|
+
* Helper: defineMethodBlock
|
|
7
|
+
* Route: saucedemo-web/methods/ (page URL / after logout)
|
|
8
8
|
*
|
|
9
9
|
* Burger menu Logout - real cycle back to LoginPage (same Checkpoint nav-login lands on).
|
|
10
10
|
*/
|
|
11
|
-
export const SubmitLogoutBlock =
|
|
11
|
+
export const SubmitLogoutBlock = defineMethodBlock<LoggedIn, LoginPage>({
|
|
12
12
|
name: "submit-logout",
|
|
13
13
|
description: "Opens the burger menu and logs out.",
|
|
14
14
|
instruction: {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Engine, start, end, withTitle, chainFlow } from "waygraph";
|
|
2
|
+
import { loginFlow } from "./login.flow.js";
|
|
3
|
+
import { AddAllToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.js";
|
|
4
|
+
import { RemoveAllFromCartBlock } from "../blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.js";
|
|
5
|
+
|
|
6
|
+
const engine = new Engine();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Episode after login: drain every Add button, then every Remove button.
|
|
10
|
+
* Proves bulk Methods + round-trip (LoggedIn <-> ItemInCart) without
|
|
11
|
+
* instanceOptions. Pair with loginFlow via chainFlow for `waygraph demo`.
|
|
12
|
+
*/
|
|
13
|
+
export const cartBulkEpisode = withTitle(
|
|
14
|
+
engine.defineFlow([start, AddAllToCartBlock, RemoveAllFromCartBlock, end]),
|
|
15
|
+
"Cart bulk add/remove all",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/** Full demo: Sign In -> Add all -> Remove all. */
|
|
19
|
+
export const cartBulkFlow = chainFlow(loginFlow, cartBulkEpisode);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Engine, start, end } from "waygraph";
|
|
2
|
-
import { RemoveFromCartBlock } from "../blocks/saucedemo-web/inventory/
|
|
2
|
+
import { RemoveFromCartBlock } from "../blocks/saucedemo-web/inventory/methods/remove-from-cart.effect.block.js";
|
|
3
3
|
|
|
4
4
|
const engine = new Engine();
|
|
5
5
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Engine, start, end, withTitle } from "waygraph";
|
|
2
2
|
import { NavItemDetailBlock } from "../blocks/saucedemo-web/inventory/nav-item-detail.block.js";
|
|
3
3
|
import { NavBackToInventoryBlock } from "../blocks/saucedemo-web/inventory-item/nav-back-to-inventory.block.js";
|
|
4
|
-
import { SubmitLogoutBlock } from "../blocks/saucedemo-web/
|
|
4
|
+
import { SubmitLogoutBlock } from "../blocks/saucedemo-web/methods/submit-logout.method.block.js";
|
|
5
5
|
|
|
6
6
|
const engine = new Engine();
|
|
7
7
|
|
|
@@ -9,12 +9,12 @@ import type {
|
|
|
9
9
|
OrderComplete,
|
|
10
10
|
} from "../states/checkout.states.js";
|
|
11
11
|
import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
|
|
12
|
-
import { SubmitLoginForFlow } from "../blocks/saucedemo-web/
|
|
13
|
-
import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/
|
|
12
|
+
import { SubmitLoginForFlow } from "../blocks/saucedemo-web/methods/submit-login-for-flow.js";
|
|
13
|
+
import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.js";
|
|
14
14
|
import { NavCartBlock } from "../blocks/saucedemo-web/cart/nav-cart.block.js";
|
|
15
15
|
import { NavCheckoutInfoBlock } from "../blocks/saucedemo-web/cart/nav-checkout-info.block.js";
|
|
16
|
-
import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/
|
|
17
|
-
import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/
|
|
16
|
+
import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.js";
|
|
17
|
+
import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.js";
|
|
18
18
|
import { NavBackToProductsBlock } from "../blocks/saucedemo-web/inventory-item/nav-back-to-products.block.js";
|
|
19
19
|
|
|
20
20
|
const engine = new Engine({ headless: false, slowMo: 250 });
|
|
@@ -9,12 +9,12 @@ import type {
|
|
|
9
9
|
OrderComplete,
|
|
10
10
|
} from "../states/checkout.states.js";
|
|
11
11
|
import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
|
|
12
|
-
import { SubmitLoginForFlow } from "../blocks/saucedemo-web/
|
|
13
|
-
import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/
|
|
12
|
+
import { SubmitLoginForFlow } from "../blocks/saucedemo-web/methods/submit-login-for-flow.js";
|
|
13
|
+
import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.js";
|
|
14
14
|
import { NavCartBlock } from "../blocks/saucedemo-web/cart/nav-cart.block.js";
|
|
15
15
|
import { NavCheckoutInfoBlock } from "../blocks/saucedemo-web/cart/nav-checkout-info.block.js";
|
|
16
|
-
import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/
|
|
17
|
-
import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/
|
|
16
|
+
import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.js";
|
|
17
|
+
import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.js";
|
|
18
18
|
|
|
19
19
|
const engine = new Engine({ headless: false, slowMo: 250 });
|
|
20
20
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Engine, start, end, withTitle } from "waygraph";
|
|
2
2
|
import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
|
|
3
|
-
import { SubmitLoginForFlow } from "../blocks/saucedemo-web/
|
|
3
|
+
import { SubmitLoginForFlow } from "../blocks/saucedemo-web/methods/submit-login-for-flow.js";
|
|
4
4
|
|
|
5
5
|
const engine = new Engine();
|
|
6
6
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Engine, start, end, withTitle } from "waygraph";
|
|
2
|
-
import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/
|
|
2
|
+
import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.js";
|
|
3
3
|
import { NavCartBlock } from "../blocks/saucedemo-web/cart/nav-cart.block.js";
|
|
4
4
|
import { NavCheckoutInfoBlock } from "../blocks/saucedemo-web/cart/nav-checkout-info.block.js";
|
|
5
|
-
import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/
|
|
6
|
-
import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/
|
|
5
|
+
import { SubmitCheckoutInfoBlock } from "../blocks/saucedemo-web/checkout-step-one/methods/submit-checkout-info.method.block.js";
|
|
6
|
+
import { FinishOrderBlock } from "../blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.js";
|
|
7
7
|
|
|
8
8
|
const engine = new Engine();
|
|
9
9
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Engine, start, end } from "waygraph";
|
|
2
2
|
import type { LoginPage, LoggedIn, ItemInCart } from "../states/checkout.states.js";
|
|
3
3
|
import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
|
|
4
|
-
import { SubmitLoginForFlow } from "../blocks/saucedemo-web/
|
|
5
|
-
import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/
|
|
4
|
+
import { SubmitLoginForFlow } from "../blocks/saucedemo-web/methods/submit-login-for-flow.js";
|
|
5
|
+
import { AddToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.js";
|
|
6
6
|
|
|
7
7
|
const engine = new Engine();
|
|
8
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Engine, start, end, withTitle, withSessionReset } from "waygraph";
|
|
2
2
|
import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
|
|
3
|
-
import { SubmitLoginActionBlock } from "../blocks/saucedemo-web/
|
|
3
|
+
import { SubmitLoginActionBlock } from "../blocks/saucedemo-web/methods/submit-login.method.block.js";
|
|
4
4
|
|
|
5
5
|
const engine = new Engine();
|
|
6
6
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test, expect } from "@playwright/test";
|
|
2
2
|
import { MemPage, checkpoint, Trait } from "waygraph";
|
|
3
3
|
import { checkoutFlow } from "../src/flows/checkout.flow.js";
|
|
4
|
-
import { SubmitLoginActionBlock } from "../src/blocks/saucedemo-web/
|
|
4
|
+
import { SubmitLoginActionBlock } from "../src/blocks/saucedemo-web/methods/submit-login.method.block.js";
|
|
5
5
|
import { LoginCreds, SelectedItem } from "../src/states/checkout.mem-keys.js";
|
|
6
6
|
|
|
7
7
|
// checkoutFlow.withBlockVerify/modBlockVerify - a spec that only imports the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test, expect } from "@playwright/test";
|
|
2
2
|
import { MemPage, Engine, start, end } from "waygraph";
|
|
3
3
|
import { NavLoginBlock } from "../src/blocks/saucedemo-web/nav-login.block.js";
|
|
4
|
-
import { SubmitLoginActionBlock } from "../src/blocks/saucedemo-web/
|
|
4
|
+
import { SubmitLoginActionBlock } from "../src/blocks/saucedemo-web/methods/submit-login.method.block.js";
|
|
5
5
|
import { LoginCreds } from "../src/states/checkout.mem-keys.js";
|
|
6
6
|
|
|
7
7
|
test("submit-login: wrong password branches to LoginPage with error banner", async () => {
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Sauce Demo - convention showcase
|
|
2
|
+
|
|
3
|
+
Full live consumer for [saucedemo.com](https://www.saucedemo.com). This tree is
|
|
4
|
+
the **reference** for waygraph consumer conventions:
|
|
5
|
+
|
|
6
|
+
- Route folders under `src/blocks/saucedemo-web/` = real Swag Labs URLs
|
|
7
|
+
- **Nav** / **Effect** / **Action** file kinds (helpers are TypeScript salt;
|
|
8
|
+
runtime is still `Block`)
|
|
9
|
+
- `instanceOptions` menus for `waygraph auto` (Add/Remove + Open details)
|
|
10
|
+
- Detailed Kind / Helper / Route comments on every Block file
|
|
11
|
+
- Per-route `NAV.md` + root `src/blocks/SITE-MAP.md`
|
|
12
|
+
|
|
13
|
+
**GitHub Pages walkthrough:**
|
|
14
|
+
[docs/saucedemo](https://deviate-dv8.github.io/waygraph/saucedemo/)
|
|
15
|
+
(`docs/saucedemo/` in this repo - README, SITE-MAP, kinds, layout, auto, comments).
|
|
16
|
+
|
|
17
|
+
`waygraph try demo` copies the sibling template at `templates/quickstart`
|
|
18
|
+
(same Blocks) into an OS temp dir.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
From the waygraph package root (after `npm run build`):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd examples/saucedemo
|
|
26
|
+
npm install
|
|
27
|
+
npx playwright install chromium
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
| Want | Command |
|
|
33
|
+
|------|---------|
|
|
34
|
+
| Headless suite | `npm test` |
|
|
35
|
+
| Interactive explore (headed panel) | `npm run auto` |
|
|
36
|
+
| Interactive explore (terminal menu) | `npm run auto:cli` |
|
|
37
|
+
| Shop demo stepper | `npm run demo:step` |
|
|
38
|
+
| Shop demo autoplay | `npm run demo:autoplay` |
|
|
39
|
+
| **Cart bulk** demo (named flow) | `npm run demo:bulk` |
|
|
40
|
+
| Cart bulk stepper | `npm run demo:bulk:step` |
|
|
41
|
+
| **Cart bulk** chain (Block names only) | `npm run chain:bulk` |
|
|
42
|
+
| List flows | `npm run flows` |
|
|
43
|
+
|
|
44
|
+
**Demo vs auto vs chain for cart bulk**
|
|
45
|
+
|
|
46
|
+
| | CLI | Needs `.flow.ts`? |
|
|
47
|
+
|--|-----|-------------------|
|
|
48
|
+
| **`chain` (block names)** | `npm run chain:bulk` → `loginFlow({…}) then add-all-to-cart then remove-all-from-cart` | No — Block `.name` only |
|
|
49
|
+
| **`demo` (named flow)** | `npm run demo:bulk` → `cartBulkFlow` | Yes — `cart-bulk.flow.ts` |
|
|
50
|
+
| **`auto` (pick live)** | `npm run auto` / `auto:cli` | No — menu from graph |
|
|
51
|
+
|
|
52
|
+
Same Blocks either way. `chain` is the ad-hoc path (handout-style `"A then B then C"`).
|
|
53
|
+
|
|
54
|
+
## Layout (short)
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
src/blocks/
|
|
58
|
+
SITE-MAP.md
|
|
59
|
+
saucedemo-web/ # URL /
|
|
60
|
+
NAV.md
|
|
61
|
+
nav-login.block.ts
|
|
62
|
+
methods/ # submit-login, logout, …
|
|
63
|
+
inventory/ # URL /inventory.html
|
|
64
|
+
nav-item-detail.block.ts # MemNav
|
|
65
|
+
methods/*.effect.block.ts # Add / Remove
|
|
66
|
+
cart/ # URL /cart.html
|
|
67
|
+
checkout-step-one/
|
|
68
|
+
checkout-step-two/
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Docs: [Sauce Demo showcase](../../docs/saucedemo/) ·
|
|
72
|
+
[Auto explore](../../docs/auto.html) · [Consumer layout](../../docs/consumer.html)
|
|
@@ -17,7 +17,7 @@ see package docs `docs/consumer.html` (no invented `root/`, `landing/`, or
|
|
|
17
17
|
|------|------|--------|--------------|
|
|
18
18
|
| Nav | `nav-*.block.ts` | `defineNavBlock` / `defineMemNavBlock` | Yes (`url` XOR `click`) |
|
|
19
19
|
| Effect | `*.effect.block.ts` | `defineEffectBlock` | No - mutates a live instance (add/remove/toggle) via mem + `instanceOptions` |
|
|
20
|
-
| Action | `*.
|
|
20
|
+
| Action | `*.method.block.ts` | `defineBlock` | No - forms, submit, logout, etc. |
|
|
21
21
|
|
|
22
22
|
Only an explicit `page.goto()` (or NavBlock `click`) counts as navigation - an
|
|
23
23
|
action/effect that fills a form or clicks a button stays non-nav even when the
|
|
@@ -32,9 +32,9 @@ src/blocks/
|
|
|
32
32
|
NAV.md
|
|
33
33
|
nav-login.block.ts
|
|
34
34
|
actions/
|
|
35
|
-
submit-login.
|
|
35
|
+
submit-login.method.block.ts
|
|
36
36
|
submit-login-for-flow.ts
|
|
37
|
-
submit-logout.
|
|
37
|
+
submit-logout.method.block.ts
|
|
38
38
|
inventory/ # /inventory.html
|
|
39
39
|
NAV.md
|
|
40
40
|
nav-item-detail.block.ts
|
|
@@ -54,11 +54,11 @@ src/blocks/
|
|
|
54
54
|
checkout-step-one/ # /checkout-step-one.html
|
|
55
55
|
NAV.md
|
|
56
56
|
actions/
|
|
57
|
-
submit-checkout-info.
|
|
57
|
+
submit-checkout-info.method.block.ts
|
|
58
58
|
checkout-step-two/ # /checkout-step-two.html
|
|
59
59
|
NAV.md
|
|
60
60
|
actions/
|
|
61
|
-
finish-order.
|
|
61
|
+
finish-order.method.block.ts
|
|
62
62
|
src/states/
|
|
63
63
|
src/flows/
|
|
64
64
|
```
|
|
@@ -4,23 +4,76 @@ Runtime is always a `Block`. Helpers are TypeScript salt + clear intent.
|
|
|
4
4
|
|
|
5
5
|
| Helper | Use when | File |
|
|
6
6
|
|--------|----------|------|
|
|
7
|
+
| `definePageBlock` | Screen hub (checkpoint + methods + optional Sel) | `*.page.block.ts` |
|
|
7
8
|
| `defineNavBlock` | URL / `goto` only | `nav-*.block.ts` |
|
|
8
9
|
| `defineNavClickBlock` | Click-to-navigate only | `nav-*.block.ts` |
|
|
9
10
|
| `defineMemNavBlock` | Nav + per-row `instanceOptions` | `nav-*.block.ts` |
|
|
10
|
-
| `
|
|
11
|
-
| `defineEffectBlock` | Instance mutate + auto menu rows | `*.effect.block.ts` |
|
|
11
|
+
| `defineMethodBlock` | One-shot non-nav app step | `*.method.block.ts` under `methods/` |
|
|
12
|
+
| `defineEffectBlock` | Instance mutate + auto menu rows | `*.effect.block.ts` under `methods/` |
|
|
12
13
|
| `defineBlock` | Escape hatch: tests, unsure, probes | any |
|
|
14
|
+
| `defineActionBlock` | **Deprecated** alias of `defineMethodBlock` | — |
|
|
13
15
|
|
|
14
16
|
`defineNavBlock({ click })` still works (compat) - prefer `defineNavClickBlock` in app code.
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
---
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
## Page hub + methods + Sel
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
One **PageBlock** = this screen. Methods hang off it. DOM strings live in `*Sel` (not in mem).
|
|
21
23
|
|
|
22
24
|
```ts
|
|
23
|
-
|
|
25
|
+
export const InventorySel = {
|
|
26
|
+
list: ".inventory_list",
|
|
27
|
+
addBtn: (id: string) => `[data-test="add-to-cart-${id}"]`,
|
|
28
|
+
// …
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const InventoryPageBlock = definePageBlock({
|
|
32
|
+
name: "page-inventory",
|
|
33
|
+
checkpoint: "LoggedIn",
|
|
34
|
+
verify: [Trait.url({ pathname: "/inventory.html" }), Trait.visible(InventorySel.list)],
|
|
35
|
+
methods: {
|
|
36
|
+
addToCart: () => AddToCartBlock,
|
|
37
|
+
removeFromCart: () => RemoveFromCartBlock,
|
|
38
|
+
addAllToCart: () => AddAllToCartBlock,
|
|
39
|
+
removeAllFromCart: () => RemoveAllFromCartBlock,
|
|
40
|
+
openDetail: () => NavItemDetailBlock,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Piece | Stores |
|
|
46
|
+
|-------|--------|
|
|
47
|
+
| `*Sel` | DOM selectors only (static + `(id) => …`) |
|
|
48
|
+
| Mem keys | Typed values (which item / queue) — not selectors |
|
|
49
|
+
| `instanceOptions` | Live DOM → menu rows → `mem.set` on pick |
|
|
50
|
+
| Predefined method | No `instanceOptions`; flow seeds mem first |
|
|
51
|
+
|
|
52
|
+
Live: `inventory/inventory.page.block.ts`, `inventory/methods/`.
|
|
53
|
+
|
|
54
|
+
**Round-trip (not forward-only):** cart Methods/Effects use `In = Checkpoint<string>`
|
|
55
|
+
so the graph edge is `from: "*"`. Auto lists them whenever the live page has matching
|
|
56
|
+
buttons (Add vs Remove), including after continue-shopping / back to inventory. Bulk
|
|
57
|
+
`add-all-to-cart` / `remove-all-from-cart` are one menu row each (no `instanceOptions`);
|
|
58
|
+
Out is `ItemInCart` vs `LoggedIn` from cart emptiness.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Checkpoint vs `branch()`
|
|
63
|
+
|
|
64
|
+
| | **Checkpoint** | **`branch()`** |
|
|
65
|
+
|--|----------------|----------------|
|
|
66
|
+
| Answers | *Where* are you? (`LoggedIn`, `LoginPage`) | *Which Block runs next?* |
|
|
67
|
+
| How | `observe` + `resolve` -> union `Out` | `branch(block, { Tag: nextBlock \| null })` |
|
|
68
|
+
| Alone can | Label state; graph edges; verify | Route a `runGraph` walk |
|
|
69
|
+
| Alone cannot | Pick the next Block | Decide the state |
|
|
70
|
+
|
|
71
|
+
Checkpoint = place on the map. `branch()` = which road to take from that place.
|
|
72
|
+
|
|
73
|
+
Conditional Out (no `branch()` needed for auto/graph):
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
defineMethodBlock<LoginPage, LoggedIn | LoginPage>({
|
|
24
77
|
name: "submit-login",
|
|
25
78
|
instruction: {
|
|
26
79
|
async act(page, _in, mem) { /* fill + click */ },
|
|
@@ -37,6 +90,97 @@ defineActionBlock<LoginPage, LoggedIn | LoginPage>({
|
|
|
37
90
|
});
|
|
38
91
|
```
|
|
39
92
|
|
|
40
|
-
|
|
93
|
+
Optional routing for `runGraph`:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
branch(SubmitLoginActionBlock, {
|
|
97
|
+
LoggedIn: AddToCartBlock,
|
|
98
|
+
LoginPage: null, // stop
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Linear `defineFlow([start, …, end])` usually skips `branch()` (one happy path).
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Mem / `instanceOptions` (spawns menu rows)
|
|
107
|
+
|
|
108
|
+
Not a separate runtime type. One Block + `requires` + `instanceOptions(page)` ->
|
|
109
|
+
`waygraph auto` lists **one row per live DOM match**. On pick: `mem.set(key, value)`
|
|
110
|
+
then run the Block (act reads mem as usual).
|
|
111
|
+
|
|
112
|
+
### Effect - spawn Method-like rows (Add / Remove)
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
defineEffectBlock<Checkpoint<string>, ItemInCart>({
|
|
116
|
+
name: "add-to-cart",
|
|
117
|
+
requires: [SelectedItem.key],
|
|
118
|
+
instruction: {
|
|
119
|
+
async act(page, _in, mem) {
|
|
120
|
+
const { id } = mem.get(SelectedItem.key);
|
|
121
|
+
await page.locator(InventorySel.addBtn(id)).click();
|
|
122
|
+
},
|
|
123
|
+
resolve: () => checkpoint("ItemInCart"),
|
|
124
|
+
},
|
|
125
|
+
async instanceOptions(page) {
|
|
126
|
+
const items = await collectAddableItems(page);
|
|
127
|
+
return items.map((item) => ({
|
|
128
|
+
id: item.id,
|
|
129
|
+
label: `Add "${item.name}" to cart`,
|
|
130
|
+
key: SelectedItem.key,
|
|
131
|
+
value: item,
|
|
132
|
+
highlight: InventorySel.addBtn(item.id),
|
|
133
|
+
}));
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Auto shows: `Add "Sauce Labs Backpack" to cart`, `Add "Bike Light"…`, …
|
|
139
|
+
|
|
140
|
+
### MemNav - spawn Nav rows (Open details)
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
defineMemNavBlock<ItemDetailPage>({
|
|
144
|
+
name: "nav-item-detail",
|
|
145
|
+
checkpoint: "ItemDetailPage",
|
|
146
|
+
requires: [SelectedItem.key],
|
|
147
|
+
click: (mem) => InventorySel.title(mem.get(SelectedItem.key).id),
|
|
148
|
+
verify: [Trait.url({ pathname: "/inventory-item.html" })],
|
|
149
|
+
async instanceOptions(page) {
|
|
150
|
+
const items = await collectInventoryItems(page);
|
|
151
|
+
return items.map((item) => ({
|
|
152
|
+
id: item.id,
|
|
153
|
+
label: `Open "${item.name}" details`,
|
|
154
|
+
key: SelectedItem.key,
|
|
155
|
+
value: item,
|
|
156
|
+
highlight: InventorySel.title(item.id),
|
|
157
|
+
}));
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Auto shows: `Open "Backpack" details`, … - still a NavBlock (click nav), salt only.
|
|
163
|
+
|
|
164
|
+
| | Effect | MemNav |
|
|
165
|
+
|--|--------|--------|
|
|
166
|
+
| Helper | `defineEffectBlock` | `defineMemNavBlock` |
|
|
167
|
+
| Spawns | mutate rows | navigate rows |
|
|
168
|
+
| `act` / nav | click Add/Remove | `click: (mem) => …` title link |
|
|
169
|
+
| Mem | same pattern: `requires` + row `key`/`value` | same |
|
|
170
|
+
|
|
171
|
+
Live source: `inventory/methods/add-to-cart.effect.block.ts`,
|
|
172
|
+
`inventory/nav-item-detail.block.ts`.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Cart bulk: demo / run / auto
|
|
177
|
+
|
|
178
|
+
| | Command |
|
|
179
|
+
|--|---------|
|
|
180
|
+
| **Run (Block names)** | `npm run run:bulk` — `run --blocks "loginFlow then add-all…" --data '{…}'` |
|
|
181
|
+
| **Demo (named flow)** | `npm run demo:bulk` — `--blocks cartBulkFlow --data '{…}'` |
|
|
182
|
+
| **Demo QA video** | `npm run demo:bulk:video` — `--auto-play-video` |
|
|
183
|
+
| **Auto headed / CLI** | `npm run auto` / `auto:cli` |
|
|
184
|
+
| **Auto path-find** | `waygraph auto --blocks LoginPage OrderComplete` |
|
|
185
|
+
|
|
41
186
|
|
|
42
|
-
No breaking change for atomics - document observe/resolve (+ optional `branch`) as the Branch pattern.
|