waygraph 0.15.53 → 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/dist/analysis/graph.d.ts +3 -0
- 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/config.d.ts +2 -0
- 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/flow.d.ts +1 -0
- package/dist/core/map-builder.d.ts +8 -1
- package/dist/core/map-builder.js +1 -1
- 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 -0
- package/dist/highlights/demo-pace.js +4 -0
- 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/session/auto-explore-run.js +6 -0
- package/dist/session/overlay-beacon.d.ts +7 -0
- package/dist/session/overlay-beacon.js +5 -0
- package/dist/session/pilot.d.ts +1 -0
- package/dist/traverse/coverage.d.ts +4 -0
- package/dist/traverse/coverage.js +3 -0
- package/dist/traverse/lease.d.ts +1 -0
- package/dist/traverse/lease.js +1 -0
- package/dist/traverse/run.d.ts +3 -0
- 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 +3 -2
- 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/skills/waygraph-convention.skill.md +1 -1
- 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
|
@@ -4,30 +4,30 @@
|
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Self-contained saucedemo quickstart (copied into a temp dir by `waygraph try demo`).",
|
|
6
6
|
"scripts": {
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
7
8
|
"test": "waygraph test",
|
|
8
9
|
"test:ui": "waygraph test ui",
|
|
9
|
-
"
|
|
10
|
+
"flows": "waygraph list",
|
|
10
11
|
"check": "waygraph check",
|
|
11
|
-
"auto": "waygraph auto",
|
|
12
|
-
"auto:cli": "waygraph auto --cli --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
|
|
13
|
-
"traverse": "waygraph traverse --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 40",
|
|
14
|
-
"traverse:parallel": "waygraph traverse --parallel 2 --session clone --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 30",
|
|
15
|
-
"traverse:coverage": "waygraph traverse --min-edge-coverage 10% --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 40",
|
|
16
12
|
"graph": "waygraph graph",
|
|
17
13
|
"graph:mermaid": "waygraph graph --mermaid",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"demo:
|
|
22
|
-
"demo:
|
|
23
|
-
"demo:
|
|
24
|
-
"
|
|
14
|
+
"auto": "waygraph auto",
|
|
15
|
+
"auto:cli": "waygraph auto --cli",
|
|
16
|
+
"demo": "waygraph demo --blocks \"loginFlow then shopFlow\"",
|
|
17
|
+
"demo:auto-next": "waygraph demo --blocks \"loginFlow then shopFlow\" --auto-next",
|
|
18
|
+
"demo:checkout": "waygraph demo --blocks checkoutFlow --auto-next",
|
|
19
|
+
"demo:device": "waygraph demo --blocks deviceShowcaseFlow --auto-next",
|
|
20
|
+
"demo:bulk": "waygraph demo --blocks cartBulkFlow --auto-next",
|
|
21
|
+
"demo:api": "waygraph demo --blocks apiDemoFlow --auto-next",
|
|
22
|
+
"demo:video": "waygraph demo --blocks deviceShowcaseFlow --auto-play-video",
|
|
23
|
+
"record": "waygraph run --blocks \"loginFlow then shopFlow\" --video --no-step",
|
|
24
|
+
"traverse": "waygraph traverse --max-steps 40"
|
|
25
25
|
},
|
|
26
26
|
"waygraph": {
|
|
27
27
|
"baseUrl": "https://www.saucedemo.com"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"waygraph": "
|
|
30
|
+
"waygraph": "latest"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@playwright/test": "^1.55.0",
|
|
@@ -8,11 +8,15 @@ src/blocks/
|
|
|
8
8
|
SITE-MAP.md
|
|
9
9
|
saucedemo-web/ # URL /
|
|
10
10
|
NAV.md
|
|
11
|
-
nav-login.block.ts #
|
|
11
|
+
nav-login.block.ts # Page definePageBlock url:/
|
|
12
|
+
ff-owner-auth.block.ts # FFCompose (nav-login + fill-username + fill-password + submit-login)
|
|
12
13
|
methods/
|
|
13
|
-
|
|
14
|
+
fill-username.method.block.ts # Method defineMethodBlock (self-loop on LoginPage)
|
|
15
|
+
fill-password.method.block.ts # Method defineMethodBlock (self-loop on LoginPage)
|
|
16
|
+
submit-login.method.block.ts # Method defineMethodBlock (click only; branches)
|
|
14
17
|
submit-login-for-flow.ts # helper (not a discoverable *.block.ts alone)
|
|
15
18
|
submit-logout.method.block.ts # Method defineMethodBlock
|
|
19
|
+
login.sel.ts # LoginSel (not a Block)
|
|
16
20
|
inventory/ # URL /inventory.html
|
|
17
21
|
inventory.page.block.ts # Page definePageBlock
|
|
18
22
|
NAV.md
|
|
@@ -25,20 +29,27 @@ src/blocks/
|
|
|
25
29
|
remove-from-cart.effect.block.ts
|
|
26
30
|
inventory-item/ # URL /inventory-item.html
|
|
27
31
|
NAV.md
|
|
32
|
+
item-detail.sel.ts # ItemDetailSel (not a Block)
|
|
28
33
|
nav-continue-shopping.block.ts
|
|
29
34
|
nav-back-to-products.block.ts
|
|
30
35
|
nav-back-to-inventory.block.ts
|
|
31
36
|
cart/ # URL /cart.html
|
|
32
37
|
NAV.md
|
|
38
|
+
cart.sel.ts # CartSel (not a Block)
|
|
33
39
|
nav-cart.block.ts # Nav click header cart
|
|
34
40
|
nav-checkout-info.block.ts
|
|
35
41
|
checkout-step-one/ # URL /checkout-step-one.html
|
|
36
42
|
NAV.md
|
|
37
43
|
methods/
|
|
38
|
-
|
|
44
|
+
checkout-info.sel.ts # CheckoutInfoSel (not a Block)
|
|
45
|
+
fill-first-name.method.block.ts # Method defineMethodBlock (self-loop on CheckoutInfoPage)
|
|
46
|
+
fill-last-name.method.block.ts # Method defineMethodBlock (self-loop on CheckoutInfoPage)
|
|
47
|
+
fill-postal-code.method.block.ts # Method defineMethodBlock (self-loop on CheckoutInfoPage)
|
|
48
|
+
submit-checkout-info.method.block.ts # Method defineMethodBlock (click only)
|
|
39
49
|
checkout-step-two/ # URL /checkout-step-two.html
|
|
40
50
|
NAV.md
|
|
41
51
|
methods/
|
|
52
|
+
checkout-overview.sel.ts # CheckoutOverviewSel (not a Block)
|
|
42
53
|
finish-order.method.block.ts
|
|
43
54
|
```
|
|
44
55
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { defineNavClickBlock, Trait } from "waygraph";
|
|
2
|
-
import type { StubCtx } from "waygraph";
|
|
3
2
|
import type { CartPage } from "../../../states/checkout.states.js";
|
|
4
|
-
import { InventorySel } from "../inventory/methods/inventory-items.js";
|
|
5
3
|
import { CartSel } from "./cart.sel.js";
|
|
6
4
|
|
|
7
5
|
/**
|
|
@@ -17,51 +15,4 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
|
|
|
17
15
|
checkpoint: "CartPage",
|
|
18
16
|
click: CartSel.cartLink,
|
|
19
17
|
verify: [Trait.url({ pathname: "/cart.html" })],
|
|
20
|
-
stubBefore: (ctx: StubCtx) => {
|
|
21
|
-
ctx.title("Open cart · landscape");
|
|
22
|
-
ctx.landscape();
|
|
23
|
-
ctx.todos(
|
|
24
|
-
"saucedemo",
|
|
25
|
-
[
|
|
26
|
-
{ id: "login-user", text: "Enter username", done: true },
|
|
27
|
-
{ id: "login-pass", text: "Enter password", done: true },
|
|
28
|
-
{ id: "login-submit", text: "Tap Login", done: true },
|
|
29
|
-
{ id: "cart-find", text: "Find the product", done: true },
|
|
30
|
-
{ id: "cart-add", text: "Add to cart", done: true },
|
|
31
|
-
{ id: "cart-open", text: "Open cart" },
|
|
32
|
-
],
|
|
33
|
-
{ title: "Device showcase", index: 5 },
|
|
34
|
-
);
|
|
35
|
-
ctx.zoom(1.4);
|
|
36
|
-
ctx.zoomOut(false);
|
|
37
|
-
ctx.highlights({
|
|
38
|
-
cart: {
|
|
39
|
-
selector: CartSel.cartLink,
|
|
40
|
-
label: "Cart",
|
|
41
|
-
detail: "tone: planned (purple) - open cart",
|
|
42
|
-
tone: "planned",
|
|
43
|
-
focus: true,
|
|
44
|
-
zoomOut: false,
|
|
45
|
-
followMouse: false,
|
|
46
|
-
weight: "bold",
|
|
47
|
-
gesture: "tap",
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
},
|
|
51
|
-
stubAfter: (ctx: StubCtx) => {
|
|
52
|
-
ctx.banner("Your cart · desktop");
|
|
53
|
-
ctx.clearDevice();
|
|
54
|
-
ctx.hideTodos();
|
|
55
|
-
ctx.zoomOut(true);
|
|
56
|
-
ctx.ring("items", {
|
|
57
|
-
selector: InventorySel.cartItem,
|
|
58
|
-
label: "Cart items",
|
|
59
|
-
detail: "tone: success (green) - product in cart",
|
|
60
|
-
tone: "success",
|
|
61
|
-
focus: true,
|
|
62
|
-
zoomOut: true,
|
|
63
|
-
followMouse: false,
|
|
64
|
-
duration: true,
|
|
65
|
-
});
|
|
66
|
-
},
|
|
67
18
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { defineMethodBlock, checkpoint, Trait, narrate
|
|
2
|
-
import type {
|
|
1
|
+
import { defineMethodBlock, checkpoint, Trait, narrate } from "waygraph";
|
|
2
|
+
import type {
|
|
3
|
+
CheckoutOverviewPage,
|
|
4
|
+
OrderComplete,
|
|
5
|
+
} from "../../../../states/checkout.states.js";
|
|
3
6
|
import { CheckoutOverviewSel } from "./checkout-overview.sel.js";
|
|
4
7
|
|
|
5
8
|
/**
|
|
@@ -10,34 +13,27 @@ import { CheckoutOverviewSel } from "./checkout-overview.sel.js";
|
|
|
10
13
|
* Places the order on the overview page - form/finish click, not a NavBlock.
|
|
11
14
|
* Stubs use open ctx lifecycles (preferred) - not static slot maps.
|
|
12
15
|
*/
|
|
13
|
-
export const FinishOrderBlock = defineMethodBlock<
|
|
16
|
+
export const FinishOrderBlock = defineMethodBlock<
|
|
17
|
+
CheckoutOverviewPage,
|
|
18
|
+
OrderComplete
|
|
19
|
+
>({
|
|
14
20
|
name: "finish-order",
|
|
15
|
-
description:
|
|
21
|
+
description:
|
|
22
|
+
"Places the order and confirms the 'Thank you for your order!' completion page.",
|
|
16
23
|
instruction: {
|
|
17
24
|
async act(page) {
|
|
18
25
|
const finishButton = page.locator(CheckoutOverviewSel.finishBtn);
|
|
19
|
-
await narrate(finishButton, "Placing the order", () =>
|
|
26
|
+
await narrate(finishButton, "Placing the order", () =>
|
|
27
|
+
finishButton.click(),
|
|
28
|
+
);
|
|
20
29
|
},
|
|
21
30
|
resolve: () => checkpoint("OrderComplete"),
|
|
22
|
-
verify: [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
tone: "warning",
|
|
29
|
-
weight: "bold",
|
|
30
|
-
});
|
|
31
|
-
},
|
|
32
|
-
stubAfter: (ctx: StubCtx) => {
|
|
33
|
-
ctx.ring("thanks", {
|
|
34
|
-
selector: CheckoutOverviewSel.completeHeader,
|
|
35
|
-
label: "Order confirmed",
|
|
36
|
-
detail: "tone: success (green) - thank you!",
|
|
37
|
-
tone: "success",
|
|
38
|
-
duration: true,
|
|
39
|
-
});
|
|
40
|
-
},
|
|
31
|
+
verify: [
|
|
32
|
+
Trait.text(
|
|
33
|
+
CheckoutOverviewSel.completeHeader,
|
|
34
|
+
"Thank you for your order!",
|
|
35
|
+
),
|
|
36
|
+
],
|
|
41
37
|
// Shown AFTER act (order already placed) - selectors must match the
|
|
42
38
|
// completion page, not #finish on the overview.
|
|
43
39
|
slides: [
|
|
@@ -50,7 +46,8 @@ export const FinishOrderBlock = defineMethodBlock<CheckoutOverviewPage, OrderCom
|
|
|
50
46
|
},
|
|
51
47
|
{
|
|
52
48
|
caption: "Completion page is the durable proof",
|
|
53
|
-
detail:
|
|
49
|
+
detail:
|
|
50
|
+
"tone: success - the thank-you header is what verify asserts on.",
|
|
54
51
|
tag: "YAP",
|
|
55
52
|
tone: "success",
|
|
56
53
|
selector: CheckoutOverviewSel.completeHeader,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { definePageBlock, Trait } from "waygraph";
|
|
2
|
-
import type { StubCtx } from "waygraph";
|
|
3
2
|
import type { LoggedIn } from "../../../states/checkout.states.js";
|
|
4
3
|
import { AddToCartBlock } from "./methods/add-to-cart.effect.block.js";
|
|
5
4
|
import { RemoveFromCartBlock } from "./methods/remove-from-cart.effect.block.js";
|
|
@@ -28,11 +27,6 @@ export const InventoryPageBlock = definePageBlock<LoggedIn>({
|
|
|
28
27
|
Trait.url({ pathname: "/inventory.html" }),
|
|
29
28
|
Trait.visible(InventorySel.list),
|
|
30
29
|
],
|
|
31
|
-
// Showcase responsive dashboard: tablet after mobile login (toast + lerp).
|
|
32
|
-
stubBefore: (ctx: StubCtx) => {
|
|
33
|
-
ctx.title("Inventory · tablet");
|
|
34
|
-
ctx.device("tablet");
|
|
35
|
-
},
|
|
36
30
|
methods: {
|
|
37
31
|
addToCart: () => AddToCartBlock,
|
|
38
32
|
removeFromCart: () => RemoveFromCartBlock,
|
package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
defineEffectBlock,
|
|
3
|
+
checkpoint,
|
|
4
|
+
type Checkpoint,
|
|
5
|
+
type WaygraphInstanceOption,
|
|
6
|
+
} from "waygraph";
|
|
3
7
|
import type { ItemInCart } from "../../../../states/checkout.states.js";
|
|
4
8
|
import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
|
|
5
9
|
import { collectAddableItems, InventorySel } from "./inventory-items.js";
|
|
6
10
|
|
|
7
|
-
/** Showcase product for headed demos (matches shopFlow --data selectedItem). */
|
|
8
|
-
const DEMO_PRODUCT_ID = "sauce-labs-backpack";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Kind: Effect
|
|
@@ -15,100 +17,40 @@ const DEMO_PRODUCT_ID = "sauce-labs-backpack";
|
|
|
15
17
|
* Mutates cart for the mem-picked product. waygraph auto lists one
|
|
16
18
|
* "Add ..." row per live Add button via instanceOptions.
|
|
17
19
|
*/
|
|
18
|
-
export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
verify: [
|
|
29
|
-
{
|
|
30
|
-
name: "item-added-to-cart",
|
|
31
|
-
async check(page, mem) {
|
|
32
|
-
const { id } = mem.get(SelectedItem.key);
|
|
33
|
-
return await page.locator(InventorySel.removeBtn(id)).first().isVisible();
|
|
34
|
-
},
|
|
20
|
+
export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
21
|
+
{
|
|
22
|
+
name: "add-to-cart",
|
|
23
|
+
description:
|
|
24
|
+
"Adds the item named by mem (saucedemo.selectedItem) to the cart.",
|
|
25
|
+
requires: [SelectedItem.key],
|
|
26
|
+
instruction: {
|
|
27
|
+
async act(page, _input, mem) {
|
|
28
|
+
const { id } = mem.get(SelectedItem.key);
|
|
29
|
+
await page.locator(InventorySel.addBtn(id)).click();
|
|
35
30
|
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{ id: "login-submit", text: "Tap Login", done: true },
|
|
48
|
-
{ id: "cart-find", text: "Find the product" },
|
|
49
|
-
{ id: "cart-add", text: "Add to cart" },
|
|
50
|
-
{ id: "cart-open", text: "Open cart" },
|
|
51
|
-
],
|
|
52
|
-
{ title: "Device showcase", index: 3 },
|
|
53
|
-
);
|
|
54
|
-
ctx.zoom(1.35);
|
|
55
|
-
ctx.zoomOut(false);
|
|
56
|
-
ctx.highlights({
|
|
57
|
-
product: {
|
|
58
|
-
selector: InventorySel.title(DEMO_PRODUCT_ID),
|
|
59
|
-
label: "Sauce Labs Backpack",
|
|
60
|
-
detail: "tone: info (blue)",
|
|
61
|
-
tone: "info",
|
|
62
|
-
focus: true,
|
|
63
|
-
zoom: 1.45,
|
|
64
|
-
zoomOut: false,
|
|
31
|
+
resolve: () => checkpoint("ItemInCart"),
|
|
32
|
+
verify: [
|
|
33
|
+
{
|
|
34
|
+
name: "item-added-to-cart",
|
|
35
|
+
async check(page, mem) {
|
|
36
|
+
const { id } = mem.get(SelectedItem.key);
|
|
37
|
+
return await page
|
|
38
|
+
.locator(InventorySel.removeBtn(id))
|
|
39
|
+
.first()
|
|
40
|
+
.isVisible();
|
|
41
|
+
},
|
|
65
42
|
},
|
|
66
|
-
|
|
67
|
-
selector: InventorySel.addBtn(DEMO_PRODUCT_ID),
|
|
68
|
-
label: "Add to cart",
|
|
69
|
-
detail: "tone: success (green)",
|
|
70
|
-
tone: "success",
|
|
71
|
-
focus: true,
|
|
72
|
-
zoom: 1.5,
|
|
73
|
-
zoomOut: false,
|
|
74
|
-
weight: "bold",
|
|
75
|
-
},
|
|
76
|
-
});
|
|
43
|
+
],
|
|
77
44
|
},
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{ id: "cart-add", text: "Add to cart" },
|
|
88
|
-
{ id: "cart-open", text: "Open cart" },
|
|
89
|
-
],
|
|
90
|
-
{ title: "Device showcase", index: 4 },
|
|
91
|
-
);
|
|
92
|
-
ctx.zoomOut(false);
|
|
93
|
-
ctx.ring("badge", {
|
|
94
|
-
selector: InventorySel.cartBadge,
|
|
95
|
-
label: "Cart badge",
|
|
96
|
-
detail: "tone: success (green) - count updated",
|
|
97
|
-
tone: "success",
|
|
98
|
-
focus: true,
|
|
99
|
-
zoomOut: false,
|
|
100
|
-
duration: true,
|
|
101
|
-
});
|
|
45
|
+
async instanceOptions(page): Promise<readonly WaygraphInstanceOption[]> {
|
|
46
|
+
const items = await collectAddableItems(page);
|
|
47
|
+
return items.map((item) => ({
|
|
48
|
+
id: item.id,
|
|
49
|
+
label: `Add "${item.name}" to cart`,
|
|
50
|
+
key: SelectedItem.key,
|
|
51
|
+
value: item,
|
|
52
|
+
highlight: InventorySel.addBtn(item.id),
|
|
53
|
+
}));
|
|
102
54
|
},
|
|
103
55
|
},
|
|
104
|
-
|
|
105
|
-
const items = await collectAddableItems(page);
|
|
106
|
-
return items.map((item) => ({
|
|
107
|
-
id: item.id,
|
|
108
|
-
label: `Add "${item.name}" to cart`,
|
|
109
|
-
key: SelectedItem.key,
|
|
110
|
-
value: item,
|
|
111
|
-
highlight: InventorySel.addBtn(item.id),
|
|
112
|
-
}));
|
|
113
|
-
},
|
|
114
|
-
});
|
|
56
|
+
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineMethodBlock, checkpoint
|
|
1
|
+
import { defineMethodBlock, checkpoint } from "waygraph";
|
|
2
2
|
import type { LoginPage } from "../../../states/checkout.states.js";
|
|
3
3
|
import { LoginCreds } from "../../../states/checkout.mem-keys.js";
|
|
4
4
|
import { LoginSel } from "./login.sel.js";
|
|
@@ -21,30 +21,5 @@ export const FillPasswordBlock = defineMethodBlock<LoginPage, LoginPage>({
|
|
|
21
21
|
await page.locator(LoginSel.password).fill(password);
|
|
22
22
|
},
|
|
23
23
|
resolve: () => checkpoint("LoginPage"),
|
|
24
|
-
stubBefore: (ctx: StubCtx) => {
|
|
25
|
-
ctx.ring("password", {
|
|
26
|
-
selector: LoginSel.password,
|
|
27
|
-
label: "Password",
|
|
28
|
-
detail: "tone: planned (purple)",
|
|
29
|
-
todo: "login-pass",
|
|
30
|
-
tone: "planned",
|
|
31
|
-
focus: true,
|
|
32
|
-
zoom: 1.4,
|
|
33
|
-
});
|
|
34
|
-
},
|
|
35
|
-
stubAfter: (ctx: StubCtx) => {
|
|
36
|
-
ctx.todos(
|
|
37
|
-
"saucedemo",
|
|
38
|
-
[
|
|
39
|
-
{ id: "login-user", text: "Enter username", done: true },
|
|
40
|
-
{ id: "login-pass", text: "Enter password", done: true },
|
|
41
|
-
{ id: "login-submit", text: "Tap Login" },
|
|
42
|
-
{ id: "cart-find", text: "Find the product" },
|
|
43
|
-
{ id: "cart-add", text: "Add to cart" },
|
|
44
|
-
{ id: "cart-open", text: "Open cart" },
|
|
45
|
-
],
|
|
46
|
-
{ title: "Device showcase", index: 2 },
|
|
47
|
-
);
|
|
48
|
-
},
|
|
49
24
|
},
|
|
50
25
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineMethodBlock, checkpoint
|
|
1
|
+
import { defineMethodBlock, checkpoint } from "waygraph";
|
|
2
2
|
import type { LoginPage } from "../../../states/checkout.states.js";
|
|
3
3
|
import { LoginCreds } from "../../../states/checkout.mem-keys.js";
|
|
4
4
|
import { LoginSel } from "./login.sel.js";
|
|
@@ -23,49 +23,5 @@ export const FillUsernameBlock = defineMethodBlock<LoginPage, LoginPage>({
|
|
|
23
23
|
await page.locator(LoginSel.username).fill(username);
|
|
24
24
|
},
|
|
25
25
|
resolve: () => checkpoint("LoginPage"),
|
|
26
|
-
stubBefore: (ctx: StubCtx) => {
|
|
27
|
-
ctx.title("Signing in · mobile");
|
|
28
|
-
ctx.device("mobile");
|
|
29
|
-
ctx.portrait();
|
|
30
|
-
ctx.todos(
|
|
31
|
-
"saucedemo",
|
|
32
|
-
[
|
|
33
|
-
{ id: "login-user", text: "Enter username" },
|
|
34
|
-
{ id: "login-pass", text: "Enter password" },
|
|
35
|
-
{ id: "login-submit", text: "Tap Login" },
|
|
36
|
-
{ id: "cart-find", text: "Find the product" },
|
|
37
|
-
{ id: "cart-add", text: "Add to cart" },
|
|
38
|
-
{ id: "cart-open", text: "Open cart" },
|
|
39
|
-
],
|
|
40
|
-
{ title: "Device showcase", index: 0 },
|
|
41
|
-
);
|
|
42
|
-
ctx.zoom(1.35);
|
|
43
|
-
const fromMem = ctx.mem?.get(LoginCreds.key)?.username;
|
|
44
|
-
ctx.ring("username", {
|
|
45
|
-
selector: LoginSel.username,
|
|
46
|
-
label: "Username",
|
|
47
|
-
detail: fromMem
|
|
48
|
-
? `tone: info (blue) - mem ${fromMem}`
|
|
49
|
-
: "tone: info (blue)",
|
|
50
|
-
tone: "info",
|
|
51
|
-
todo: "login-user",
|
|
52
|
-
focus: true,
|
|
53
|
-
zoom: 1.4,
|
|
54
|
-
});
|
|
55
|
-
},
|
|
56
|
-
stubAfter: (ctx: StubCtx) => {
|
|
57
|
-
ctx.todos(
|
|
58
|
-
"saucedemo",
|
|
59
|
-
[
|
|
60
|
-
{ id: "login-user", text: "Enter username", done: true },
|
|
61
|
-
{ id: "login-pass", text: "Enter password" },
|
|
62
|
-
{ id: "login-submit", text: "Tap Login" },
|
|
63
|
-
{ id: "cart-find", text: "Find the product" },
|
|
64
|
-
{ id: "cart-add", text: "Add to cart" },
|
|
65
|
-
{ id: "cart-open", text: "Open cart" },
|
|
66
|
-
],
|
|
67
|
-
{ title: "Device showcase", index: 1 },
|
|
68
|
-
);
|
|
69
|
-
},
|
|
70
26
|
},
|
|
71
27
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { defineMethodBlock, checkpoint, Trait
|
|
2
|
-
import type {
|
|
1
|
+
import { defineMethodBlock, checkpoint, Trait } from "waygraph";
|
|
2
|
+
import type {
|
|
3
|
+
LoginPage,
|
|
4
|
+
LoginSubmitOutcome,
|
|
5
|
+
} from "../../../states/checkout.states.js";
|
|
3
6
|
import { LoginSel } from "./login.sel.js";
|
|
4
7
|
|
|
5
8
|
type LoginObserve = "success" | "failure";
|
|
@@ -14,7 +17,10 @@ type LoginObserve = "success" | "failure";
|
|
|
14
17
|
* already did that). One Block, one action: submit.
|
|
15
18
|
* Branches: good creds -> LoggedIn; bad/locked -> LoginPage with error banner.
|
|
16
19
|
*/
|
|
17
|
-
export const SubmitLoginBlock = defineMethodBlock<
|
|
20
|
+
export const SubmitLoginBlock = defineMethodBlock<
|
|
21
|
+
LoginPage,
|
|
22
|
+
LoginSubmitOutcome
|
|
23
|
+
>({
|
|
18
24
|
name: "submit-login",
|
|
19
25
|
description: "Submits the login form; stays on LoginPage when auth fails.",
|
|
20
26
|
instruction: {
|
|
@@ -26,7 +32,9 @@ export const SubmitLoginBlock = defineMethodBlock<LoginPage, LoginSubmitOutcome>
|
|
|
26
32
|
async observe(page): Promise<LoginObserve> {
|
|
27
33
|
// Race inventory nav vs error banner - locked_out / wrong password show
|
|
28
34
|
// Epic sadface immediately; do not burn the full inventory timeout.
|
|
29
|
-
const inventory = page
|
|
35
|
+
const inventory = page
|
|
36
|
+
.waitForURL(/\/inventory\.html/, { timeout: 8_000 })
|
|
37
|
+
.then(() => "success" as const);
|
|
30
38
|
const errorBanner = page
|
|
31
39
|
.locator(LoginSel.errorBanner)
|
|
32
40
|
.first()
|
|
@@ -38,82 +46,11 @@ export const SubmitLoginBlock = defineMethodBlock<LoginPage, LoginSubmitOutcome>
|
|
|
38
46
|
return "failure";
|
|
39
47
|
}
|
|
40
48
|
},
|
|
41
|
-
resolve: (observed) =>
|
|
49
|
+
resolve: (observed) =>
|
|
50
|
+
checkpoint(observed === "success" ? "LoggedIn" : "LoginPage"),
|
|
42
51
|
verify: (out) =>
|
|
43
52
|
out.__state === "LoggedIn"
|
|
44
53
|
? [Trait.url({ pathname: "/inventory.html" })]
|
|
45
54
|
: [Trait.visible(LoginSel.errorBanner)],
|
|
46
|
-
stubBefore: (ctx: StubCtx) => {
|
|
47
|
-
ctx.highlights({
|
|
48
|
-
submit: {
|
|
49
|
-
selector: LoginSel.loginButton,
|
|
50
|
-
label: "Login",
|
|
51
|
-
detail: "tone: warning (yellow)",
|
|
52
|
-
todo: "login-submit",
|
|
53
|
-
tone: "warning",
|
|
54
|
-
focus: true,
|
|
55
|
-
zoom: 1.55,
|
|
56
|
-
weight: "bold",
|
|
57
|
-
gesture: "tap",
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
},
|
|
61
|
-
stubAfter: (ctx: StubCtx) => {
|
|
62
|
-
if (ctx.out && ctx.out.__state === "LoginPage") {
|
|
63
|
-
ctx.title("Login blocked");
|
|
64
|
-
ctx.todos(
|
|
65
|
-
"saucedemo",
|
|
66
|
-
[
|
|
67
|
-
{ id: "login-user", text: "Enter username", done: true },
|
|
68
|
-
{ id: "login-pass", text: "Enter password", done: true },
|
|
69
|
-
{ id: "login-submit", text: "Click Login", done: true },
|
|
70
|
-
],
|
|
71
|
-
{ title: "Sign in", style: "checklist" },
|
|
72
|
-
);
|
|
73
|
-
ctx.ring("error", {
|
|
74
|
-
selector: LoginSel.errorBanner,
|
|
75
|
-
label: "Login error banner",
|
|
76
|
-
detail: "tone: danger (red)",
|
|
77
|
-
tone: "danger",
|
|
78
|
-
focus: true,
|
|
79
|
-
duration: true,
|
|
80
|
-
});
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
ctx.banner("Inventory · landscape");
|
|
84
|
-
ctx.landscape();
|
|
85
|
-
ctx.todos(
|
|
86
|
-
"saucedemo",
|
|
87
|
-
[
|
|
88
|
-
{ id: "login-user", text: "Enter username", done: true },
|
|
89
|
-
{ id: "login-pass", text: "Enter password", done: true },
|
|
90
|
-
{ id: "login-submit", text: "Tap Login", done: true },
|
|
91
|
-
{ id: "cart-find", text: "Find the product" },
|
|
92
|
-
{ id: "cart-add", text: "Add to cart" },
|
|
93
|
-
{ id: "cart-open", text: "Open cart" },
|
|
94
|
-
],
|
|
95
|
-
{ title: "Device showcase", index: 3 },
|
|
96
|
-
);
|
|
97
|
-
ctx.ring("shelf", {
|
|
98
|
-
selector: ".inventory_list",
|
|
99
|
-
label: "Product shelf",
|
|
100
|
-
detail: "tone: success (green) - ready to shop",
|
|
101
|
-
tone: "success",
|
|
102
|
-
focus: true,
|
|
103
|
-
zoom: 1.25,
|
|
104
|
-
duration: true,
|
|
105
|
-
});
|
|
106
|
-
},
|
|
107
|
-
stubOnError: (ctx: StubCtx) => {
|
|
108
|
-
ctx.title("Login failed");
|
|
109
|
-
ctx.ring("error", {
|
|
110
|
-
selector: LoginSel.errorBanner,
|
|
111
|
-
label: "Login error banner",
|
|
112
|
-
detail: "tone: danger (red)",
|
|
113
|
-
tone: "danger",
|
|
114
|
-
focus: true,
|
|
115
|
-
duration: true,
|
|
116
|
-
});
|
|
117
|
-
},
|
|
118
55
|
},
|
|
119
56
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { checkpoint, defineApiBlock } from "waygraph";
|
|
2
|
+
import type { Checkpoint, StubCtx } from "waygraph";
|
|
3
|
+
import { engine } from "../../config/waygraph.config.js";
|
|
4
|
+
import { loginFlow } from "./login.flow.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Proof of concept for FE+BE in one Flow: a real backend check
|
|
8
|
+
* (defineApiBlock, Playwright's own page.request - no browser, no curl
|
|
9
|
+
* shell-out) resolved into a Checkpoint exactly like a UI Block, chained
|
|
10
|
+
* right after the UI login. This is the QA-team ask: show a mixed
|
|
11
|
+
* frontend + backend journey narrated as one state machine, not two
|
|
12
|
+
* separate test files with no shared story.
|
|
13
|
+
*
|
|
14
|
+
* saucedemo.com is a static demo site with no backend of its own, so this
|
|
15
|
+
* hits a well-known public REST test API (jsonplaceholder) purely to prove
|
|
16
|
+
* the mechanism - a real project would point `call` at its own backend.
|
|
17
|
+
*/
|
|
18
|
+
export type BackendHealthy = Checkpoint<"BackendHealthy">;
|
|
19
|
+
export type BackendDown = Checkpoint<"BackendDown">;
|
|
20
|
+
|
|
21
|
+
const CheckBackendHealth = defineApiBlock<any, BackendHealthy | BackendDown>({
|
|
22
|
+
name: "check-backend-health",
|
|
23
|
+
call: ({ request }) =>
|
|
24
|
+
request.get("https://jsonplaceholder.typicode.com/todos/1"),
|
|
25
|
+
resolve: (result) =>
|
|
26
|
+
result.ok ? checkpoint("BackendHealthy") : checkpoint("BackendDown"),
|
|
27
|
+
stubBefore: (ctx: StubCtx<BackendHealthy | BackendDown>) => {
|
|
28
|
+
ctx.title("Checking backend health · real HTTP request, no browser");
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const apiDemoEpisode = engine
|
|
33
|
+
.map()
|
|
34
|
+
.start()
|
|
35
|
+
.method(CheckBackendHealth)
|
|
36
|
+
.end();
|
|
37
|
+
|
|
38
|
+
export const apiDemoFlow = engine
|
|
39
|
+
.map({ memStub: true })
|
|
40
|
+
.start()
|
|
41
|
+
.flow(loginFlow)
|
|
42
|
+
.flow(apiDemoEpisode)
|
|
43
|
+
.end();
|