waygraph 0.7.15 → 0.10.0
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 +77 -7
- package/dist/engine.js +108 -7
- 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 +186 -0
- 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/cart/nav-cart.block.ts +3 -3
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts +3 -3
- 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/inventory-item/nav-back-to-inventory.block.ts +3 -3
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts +3 -3
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts +3 -3
- 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 +186 -0
- 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/cart/nav-cart.block.ts +3 -3
- package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-checkout-info.block.ts +3 -3
- 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/inventory-item/nav-back-to-inventory.block.ts +3 -3
- package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts +3 -3
- package/templates/quickstart/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts +3 -3
- 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 { collectAddableItems, InventorySel } from "./inventory-items.js";
|
|
4
|
+
|
|
5
|
+
type BulkObserve = "added" | "nothing";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Kind: Method
|
|
9
|
+
* Helper: defineMethodBlock
|
|
10
|
+
* Route: saucedemo-web/inventory/methods/ (= /inventory.html)
|
|
11
|
+
*
|
|
12
|
+
* Bulk: click every live "Add to cart" once. No instanceOptions - one auto
|
|
13
|
+
* menu row. In is Checkpoint<string> (from: "*") so back-and-forth works:
|
|
14
|
+
* leave inventory and return, collectors re-read Add buttons from the DOM.
|
|
15
|
+
* Out: ItemInCart if the cart has items after; LoggedIn if still empty.
|
|
16
|
+
*/
|
|
17
|
+
export const AddAllToCartBlock = defineMethodBlock<
|
|
18
|
+
Checkpoint<string>,
|
|
19
|
+
RemoveFromCartOutcome
|
|
20
|
+
>({
|
|
21
|
+
name: "add-all-to-cart",
|
|
22
|
+
description: "Adds every product still showing Add to cart on this page.",
|
|
23
|
+
instruction: {
|
|
24
|
+
async act(page) {
|
|
25
|
+
const items = await collectAddableItems(page);
|
|
26
|
+
for (const item of items) {
|
|
27
|
+
await page.locator(InventorySel.addBtn(item.id)).click();
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
async observe(page): Promise<BulkObserve> {
|
|
31
|
+
const badge = await page.locator(InventorySel.cartBadge).count();
|
|
32
|
+
return badge > 0 ? "added" : "nothing";
|
|
33
|
+
},
|
|
34
|
+
resolve: (observed) =>
|
|
35
|
+
checkpoint(observed === "added" ? "ItemInCart" : "LoggedIn"),
|
|
36
|
+
verify: (out) =>
|
|
37
|
+
out.__state === "ItemInCart"
|
|
38
|
+
? [
|
|
39
|
+
{
|
|
40
|
+
name: "cart-has-items",
|
|
41
|
+
async check(page) {
|
|
42
|
+
return (await page.locator(InventorySel.cartBadge).count()) > 0;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
: [
|
|
47
|
+
{
|
|
48
|
+
name: "cart-still-empty",
|
|
49
|
+
async check(page) {
|
|
50
|
+
return (await page.locator(InventorySel.cartBadge).count()) === 0;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { defineEffectBlock, checkpoint, type Checkpoint, type WaygraphInstanceOption } from "waygraph";
|
|
2
2
|
import type { ItemInCart } from "../../../../states/checkout.states.js";
|
|
3
3
|
import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
|
|
4
|
-
import { collectAddableItems } from "./inventory-items.js";
|
|
4
|
+
import { collectAddableItems, InventorySel } from "./inventory-items.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Kind: Effect
|
|
8
8
|
* Helper: defineEffectBlock
|
|
9
|
-
* Route: saucedemo-web/inventory/
|
|
9
|
+
* Route: saucedemo-web/inventory/methods/ (= /inventory.html)
|
|
10
10
|
*
|
|
11
11
|
* Mutates cart for the mem-picked product. waygraph auto lists one
|
|
12
12
|
* "Add ..." row per live Add button via instanceOptions.
|
|
@@ -18,7 +18,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
|
18
18
|
instruction: {
|
|
19
19
|
async act(page, _input, mem) {
|
|
20
20
|
const { id } = mem.get(SelectedItem.key);
|
|
21
|
-
await page.locator(
|
|
21
|
+
await page.locator(InventorySel.addBtn(id)).click();
|
|
22
22
|
},
|
|
23
23
|
resolve: () => checkpoint("ItemInCart"),
|
|
24
24
|
verify: [
|
|
@@ -26,11 +26,11 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
|
26
26
|
name: "item-added-to-cart",
|
|
27
27
|
async check(page, mem) {
|
|
28
28
|
const { id } = mem.get(SelectedItem.key);
|
|
29
|
-
return await page.locator(
|
|
29
|
+
return await page.locator(InventorySel.removeBtn(id)).first().isVisible();
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
],
|
|
33
|
-
highlights: () => [{ selector:
|
|
33
|
+
highlights: () => [{ selector: InventorySel.cartBadge, label: "remember this: cart now has the item" }],
|
|
34
34
|
},
|
|
35
35
|
async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
|
|
36
36
|
const items = await collectAddableItems(page);
|
|
@@ -39,7 +39,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
|
39
39
|
label: `Add "${item.name}" to cart`,
|
|
40
40
|
key: SelectedItem.key,
|
|
41
41
|
value: item,
|
|
42
|
-
highlight:
|
|
42
|
+
highlight: InventorySel.addBtn(item.id),
|
|
43
43
|
}));
|
|
44
44
|
},
|
|
45
45
|
});
|
package/examples/saucedemo/src/blocks/saucedemo-web/inventory/{actions → methods}/inventory-items.ts
RENAMED
|
@@ -2,27 +2,45 @@ import type { Page, Locator } from "@playwright/test";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Kind: helper (not a Block)
|
|
5
|
-
* Route: saucedemo-web/inventory/
|
|
5
|
+
* Route: saucedemo-web/inventory/methods/ (= /inventory.html)
|
|
6
6
|
*
|
|
7
|
-
* DOM collectors for Effect / MemNav instanceOptions. Discovery
|
|
8
|
-
* file (no *.block.ts suffix).
|
|
7
|
+
* DOM selectors + collectors for Effect / MemNav instanceOptions. Discovery
|
|
8
|
+
* ignores this file (no *.block.ts suffix). InventorySel = strings only;
|
|
9
|
+
* collectors use Sel to spawn auto menu rows.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
/** Product slug + display name (saucedemo inventory / detail / cart). */
|
|
12
13
|
export type InventoryItem = { id: string; name: string };
|
|
13
14
|
|
|
15
|
+
/** DOM selectors for this screen - static strings + per-id factories. */
|
|
16
|
+
export const InventorySel = {
|
|
17
|
+
list: ".inventory_list",
|
|
18
|
+
card: ".inventory_item",
|
|
19
|
+
name: ".inventory_item_name",
|
|
20
|
+
detailName: ".inventory_details_name",
|
|
21
|
+
cartItem: ".cart_item",
|
|
22
|
+
cartBadge: ".shopping_cart_badge",
|
|
23
|
+
addBtn: (id: string) => `[data-test="add-to-cart-${id}"]`,
|
|
24
|
+
removeBtn: (id: string) => `[data-test="remove-${id}"]`,
|
|
25
|
+
addBtnPrefix: 'button[data-test^="add-to-cart-"]',
|
|
26
|
+
removeBtnPrefix: 'button[data-test^="remove-"]',
|
|
27
|
+
title: (id: string) =>
|
|
28
|
+
`div.inventory_item:has(button[data-test="add-to-cart-${id}"], button[data-test="remove-${id}"]) ` +
|
|
29
|
+
`.inventory_item_name`,
|
|
30
|
+
};
|
|
31
|
+
|
|
14
32
|
async function readItemName(page: Page, button: Locator, id: string): Promise<string> {
|
|
15
33
|
const card = button.locator("xpath=ancestor::div[contains(@class,'inventory_item')][1]");
|
|
16
34
|
if ((await card.count()) > 0) {
|
|
17
|
-
const text = await card.locator(
|
|
35
|
+
const text = await card.locator(InventorySel.name).first().textContent({ timeout: 500 }).catch(() => null);
|
|
18
36
|
if (text?.trim()) return text.trim();
|
|
19
37
|
}
|
|
20
38
|
const cartRow = button.locator("xpath=ancestor::div[contains(@class,'cart_item')][1]");
|
|
21
39
|
if ((await cartRow.count()) > 0) {
|
|
22
|
-
const text = await cartRow.locator(
|
|
40
|
+
const text = await cartRow.locator(InventorySel.name).first().textContent({ timeout: 500 }).catch(() => null);
|
|
23
41
|
if (text?.trim()) return text.trim();
|
|
24
42
|
}
|
|
25
|
-
const detailName = await page.locator(
|
|
43
|
+
const detailName = await page.locator(InventorySel.detailName).first().textContent({ timeout: 500 }).catch(() => null);
|
|
26
44
|
if (detailName?.trim()) return detailName.trim();
|
|
27
45
|
return id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
28
46
|
}
|
|
@@ -33,7 +51,7 @@ async function collectByPrefix(page: Page, prefix: "add-to-cart-" | "remove-"):
|
|
|
33
51
|
const items: InventoryItem[] = [];
|
|
34
52
|
for (let i = 0; i < count; i++) {
|
|
35
53
|
const button = buttons.nth(i);
|
|
36
|
-
const testId = await button.getAttribute("data-test");
|
|
54
|
+
const testId = await button.getAttribute("data-test", { timeout: 500 }).catch(() => null);
|
|
37
55
|
const id = testId?.replace(new RegExp(`^${prefix}`), "");
|
|
38
56
|
if (!id) continue;
|
|
39
57
|
items.push({ id, name: await readItemName(page, button, id) });
|
|
@@ -56,32 +74,34 @@ export function collectRemovableItems(page: Page): Promise<InventoryItem[]> {
|
|
|
56
74
|
* Used by MemNavBlock (open item details) - not filtered by cart state.
|
|
57
75
|
*/
|
|
58
76
|
export async function collectInventoryItems(page: Page): Promise<InventoryItem[]> {
|
|
59
|
-
const cards = page.locator(
|
|
77
|
+
const cards = page.locator(InventorySel.card);
|
|
60
78
|
const count = await cards.count();
|
|
61
79
|
const items: InventoryItem[] = [];
|
|
62
80
|
for (let i = 0; i < count; i++) {
|
|
63
81
|
const card = cards.nth(i);
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
82
|
+
// count() first - getAttribute on a missing locator waits the full default timeout.
|
|
83
|
+
const addLoc = card.locator(InventorySel.addBtnPrefix);
|
|
84
|
+
const removeLoc = card.locator(InventorySel.removeBtnPrefix);
|
|
85
|
+
const addTest =
|
|
86
|
+
(await addLoc.count()) > 0
|
|
87
|
+
? await addLoc.first().getAttribute("data-test", { timeout: 500 }).catch(() => null)
|
|
88
|
+
: null;
|
|
89
|
+
const removeTest =
|
|
90
|
+
(await removeLoc.count()) > 0
|
|
91
|
+
? await removeLoc.first().getAttribute("data-test", { timeout: 500 }).catch(() => null)
|
|
92
|
+
: null;
|
|
70
93
|
const id =
|
|
71
94
|
addTest?.replace(/^add-to-cart-/, "") ?? removeTest?.replace(/^remove-/, "") ?? null;
|
|
72
95
|
if (!id) continue;
|
|
73
96
|
const name =
|
|
74
|
-
(await card.locator(
|
|
97
|
+
(await card.locator(InventorySel.name).first().textContent({ timeout: 500 }).catch(() => null))?.trim() ??
|
|
75
98
|
id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
76
99
|
items.push({ id, name });
|
|
77
100
|
}
|
|
78
101
|
return items;
|
|
79
102
|
}
|
|
80
103
|
|
|
81
|
-
/**
|
|
104
|
+
/** @deprecated Prefer {@link InventorySel.title} */
|
|
82
105
|
export function inventoryItemTitleSelector(id: string): string {
|
|
83
|
-
return (
|
|
84
|
-
`div.inventory_item:has(button[data-test="add-to-cart-${id}"], button[data-test="remove-${id}"]) ` +
|
|
85
|
-
`.inventory_item_name`
|
|
86
|
-
);
|
|
106
|
+
return InventorySel.title(id);
|
|
87
107
|
}
|
|
@@ -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/inventory-item/nav-back-to-inventory.block.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineNavClickBlock, Trait } from "waygraph";
|
|
2
2
|
import type { LoggedIn } from "../../../states/checkout.states.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Kind: Nav
|
|
6
|
-
* Helper:
|
|
6
|
+
* Helper: defineNavClickBlock
|
|
7
7
|
* Route: saucedemo-web/inventory-item/ (= /inventory-item.html From)
|
|
8
8
|
*
|
|
9
9
|
* click, not url - detail page "Back to products". Separate Block from
|
|
10
10
|
* nav-back-to-products (different From Checkpoint; atomic convention).
|
|
11
11
|
*/
|
|
12
|
-
export const NavBackToInventoryBlock =
|
|
12
|
+
export const NavBackToInventoryBlock = defineNavClickBlock<LoggedIn>({
|
|
13
13
|
name: "nav-back-to-inventory",
|
|
14
14
|
description: "Clicks 'Back to products' on the item detail page.",
|
|
15
15
|
checkpoint: "LoggedIn",
|
package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-back-to-products.block.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineNavClickBlock, Trait } from "waygraph";
|
|
2
2
|
import type { LoggedIn } from "../../../states/checkout.states.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Kind: Nav
|
|
6
|
-
* Helper:
|
|
6
|
+
* Helper: defineNavClickBlock
|
|
7
7
|
* Route: saucedemo-web/inventory-item/ (lands on /inventory.html)
|
|
8
8
|
*
|
|
9
9
|
* click, not url - order-complete "Back Home" proves the button path
|
|
10
10
|
* (a url NavBlock would teleport without exercising the control).
|
|
11
11
|
*/
|
|
12
|
-
export const NavBackToProductsBlock =
|
|
12
|
+
export const NavBackToProductsBlock = defineNavClickBlock<LoggedIn>({
|
|
13
13
|
name: "nav-back-to-products",
|
|
14
14
|
description: "Clicks 'Back Home' on the order confirmation page.",
|
|
15
15
|
checkpoint: "LoggedIn",
|
package/examples/saucedemo/src/blocks/saucedemo-web/inventory-item/nav-continue-shopping.block.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineNavClickBlock, Trait } from "waygraph";
|
|
2
2
|
import type { LoggedIn } from "../../../states/checkout.states.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Kind: Nav
|
|
6
|
-
* Helper:
|
|
6
|
+
* Helper: defineNavClickBlock
|
|
7
7
|
* Route: saucedemo-web/inventory-item/ (lands on /inventory.html)
|
|
8
8
|
*
|
|
9
9
|
* click, not url - cart page "Continue Shopping" proves the app's own path.
|
|
10
10
|
*/
|
|
11
|
-
export const NavContinueShoppingBlock =
|
|
11
|
+
export const NavContinueShoppingBlock = defineNavClickBlock<LoggedIn>({
|
|
12
12
|
name: "nav-continue-shopping",
|
|
13
13
|
description: "Clicks 'Continue Shopping' on the cart page.",
|
|
14
14
|
checkpoint: "LoggedIn",
|
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 () => {
|