waygraph 0.15.52 → 0.15.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/analysis/graph.d.ts +3 -0
- package/dist/analysis/map-check.d.ts +3 -2
- package/dist/auto-session/helpers.d.ts +10 -0
- package/dist/blocks-select.d.ts +3 -0
- package/dist/blocks-select.js +2 -0
- package/dist/cli/flags.js +3 -1
- package/dist/cli/usage.js +4 -1
- package/dist/cli.js +4 -1
- package/dist/commands/agent-dive-scaffold.d.ts +0 -20
- package/dist/commands/agent-dive-scaffold.js +3 -3
- package/dist/commands/init.js +1 -1
- package/dist/commands/upgrade.d.ts +1 -1
- package/dist/commands/upgrade.js +7 -1
- package/dist/config.d.ts +55 -19
- package/dist/config.js +55 -13
- package/dist/core/block.d.ts +7 -2
- package/dist/core/block.js +7 -3
- package/dist/core/blocks/api.d.ts +1 -0
- package/dist/core/blocks/method.d.ts +2 -0
- package/dist/core/blocks/method.js +1 -0
- package/dist/core/blocks/nav.d.ts +1 -0
- package/dist/core/blocks/page.d.ts +1 -0
- package/dist/core/branch-regression.d.ts +1 -0
- package/dist/core/compose.d.ts +1 -0
- package/dist/core/engine.d.ts +2 -0
- package/dist/core/engine.js +3 -1
- package/dist/core/flow.d.ts +11 -0
- package/dist/core/flow.js +32 -8
- package/dist/core/map-builder.d.ts +34 -1
- package/dist/core/map-builder.js +42 -4
- package/dist/core/run-graph.d.ts +2 -0
- package/dist/core/run-graph.js +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/extensions.d.ts +1 -0
- package/dist/extensions.js +1 -0
- package/dist/highlights/demo-pace.d.ts +4 -1
- package/dist/highlights/demo-pace.js +28 -3
- package/dist/highlights/device.d.ts +2 -0
- package/dist/highlights/device.js +2 -0
- package/dist/highlights/types.d.ts +6 -0
- package/dist/index.d.ts +2 -2
- package/dist/pilot-overlay/action-ring.d.ts +0 -9
- package/dist/pilot-overlay/action-ring.js +1 -1
- package/dist/pilot-overlay/panel.d.ts +2 -1
- package/dist/runner/interaction-patch.js +32 -1
- package/dist/runner/main.js +5 -2
- package/dist/runner/step-mode.js +10 -4
- package/dist/session/auto-explore-run.js +6 -0
- package/dist/session/auto-explore.d.ts +2 -1
- package/dist/session/auto-session-ipc.d.ts +1 -2
- package/dist/session/auto-session-ipc.js +1 -1
- package/dist/session/block-inject.d.ts +0 -13
- package/dist/session/block-inject.js +3 -3
- package/dist/session/overlay-beacon.d.ts +7 -0
- package/dist/session/overlay-beacon.js +5 -0
- package/dist/session/pilot.d.ts +1 -2
- package/dist/session/pilot.js +1 -2
- package/dist/traverse/coverage.d.ts +4 -4
- package/dist/traverse/coverage.js +4 -1
- package/dist/traverse/lease.d.ts +1 -0
- package/dist/traverse/lease.js +1 -0
- package/dist/traverse/run.d.ts +3 -0
- package/dist/types.d.ts +1 -1
- package/examples/routed-demo/src/flows/routed-demo-map.flow.ts +1 -1
- package/examples/routed-demo/src/flows/routed-demo.flow.ts +2 -2
- package/examples/saucedemo/README.md +11 -12
- package/examples/saucedemo/config/waygraph.config.ts +17 -0
- package/examples/saucedemo/docs/CONVENTION.md +1 -2
- package/examples/saucedemo/docs/HELPERS.md +1 -1
- package/examples/saucedemo/package-lock.json +10 -3
- package/examples/saucedemo/package.json +12 -29
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-cart.block.ts +0 -49
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +22 -25
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +0 -6
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +38 -96
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +1 -26
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +1 -45
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +14 -77
- package/examples/saucedemo/src/flows/api-demo.flow.ts +17 -11
- package/examples/saucedemo/src/flows/cart-bulk.flow.ts +19 -14
- package/examples/saucedemo/src/flows/cart-continue.flow.ts +10 -8
- package/examples/saucedemo/src/flows/cart-mem.flow.ts +7 -7
- package/examples/saucedemo/src/flows/catalog-logout.flow.ts +13 -8
- package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +26 -48
- package/examples/saucedemo/src/flows/checkout.flow.ts +70 -86
- package/examples/saucedemo/src/flows/device-showcase.flow.ts +24 -25
- package/examples/saucedemo/src/flows/login.flow.ts +11 -18
- package/examples/saucedemo/src/flows/mem-stub-demo.flow.ts +9 -20
- package/examples/saucedemo/src/flows/shop.flow.ts +15 -20
- package/examples/saucedemo/src/flows/smoke.flow.ts +12 -27
- package/examples/saucedemo/src/flows/stub-on-error.flow.ts +24 -43
- package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +26 -34
- package/examples/saucedemo/src/states/checkout.mem-stubs.ts +11 -0
- package/examples/saucedemo/src/stubs/device-showcase.ts +193 -0
- package/examples/saucedemo/src/stubs/login.ts +37 -0
- package/examples/saucedemo/tests/chain-flow-episodes.spec.ts +7 -9
- package/examples/saucedemo/tests/submit-login-branch.spec.ts +18 -5
- package/package.json +12 -5
- package/templates/quickstart/README.md +4 -7
- package/templates/quickstart/config/waygraph.config.ts +17 -0
- package/templates/quickstart/docs/CONVENTION.md +1 -2
- package/templates/quickstart/docs/HELPERS.md +1 -1
- package/templates/quickstart/package.json +14 -14
- package/templates/quickstart/src/blocks/SITE-MAP.md +14 -3
- package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +0 -49
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +22 -25
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/inventory.page.block.ts +0 -6
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +38 -96
- package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-password.method.block.ts +1 -26
- package/templates/quickstart/src/blocks/saucedemo-web/methods/fill-username.method.block.ts +1 -45
- package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +14 -77
- package/templates/quickstart/src/flows/api-demo.flow.ts +43 -0
- package/templates/quickstart/src/flows/cart-bulk.flow.ts +19 -14
- package/templates/quickstart/src/flows/cart-continue.flow.ts +10 -8
- package/templates/quickstart/src/flows/cart-mem.flow.ts +7 -7
- package/templates/quickstart/src/flows/catalog-logout.flow.ts +13 -8
- package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +26 -48
- package/templates/quickstart/src/flows/checkout.flow.ts +70 -63
- package/templates/quickstart/src/flows/device-showcase.flow.ts +24 -25
- package/templates/quickstart/src/flows/login.flow.ts +11 -18
- package/templates/quickstart/src/flows/mem-stub-demo.flow.ts +12 -0
- package/templates/quickstart/src/flows/shop.flow.ts +15 -20
- package/templates/quickstart/src/flows/smoke.flow.ts +12 -27
- package/templates/quickstart/src/flows/stub-on-error.flow.ts +31 -0
- package/templates/quickstart/src/flows/viewer-blocked.flow.ts +26 -34
- package/templates/quickstart/src/states/checkout.mem-stubs.ts +11 -0
- package/templates/quickstart/src/stubs/device-showcase.ts +193 -0
- package/templates/quickstart/src/stubs/login.ts +37 -0
- package/templates/quickstart/tests/chain-flow.spec.ts +7 -10
- package/templates/scaffold/README.md +1 -1
- package/templates/scaffold/STRUCTURE.md +5 -2
- package/templates/scaffold/config/waygraph.config.ts +24 -5
- package/templates/scaffold/package.json +6 -6
- package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +36 -0
- package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +24 -0
- package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +34 -0
- package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +47 -0
- package/templates/scaffold/src/flows/README.md +35 -14
- package/templates/scaffold/src/flows/{shop.flow.ts → home/cart.flow.ts} +18 -17
- package/templates/scaffold/src/flows/{example.flow.ts → home/hello.flow.ts} +12 -13
- package/templates/scaffold/tests/example.spec.ts +5 -5
- package/templates/scaffold/tests/hoppscotch.spec.ts +3 -3
- package/templates/scaffold/tests/mail-verify.spec.ts +7 -7
- package/templates/skills/waygraph-convention.skill.md +14 -6
- package/examples/saucedemo/playwright.config.ts +0 -19
- package/examples/saucedemo/scripts/drive-auto-cli.mjs +0 -93
- package/examples/saucedemo/scripts/prove-add-to-cart-menu.mjs +0 -126
- package/examples/saucedemo/scripts/prove-auto-graph.mjs +0 -92
- package/examples/saucedemo/scripts/prove-wrong-login-runtime.mjs +0 -63
- package/templates/quickstart/playwright.config.ts +0 -14
- package/templates/quickstart/scripts/drive-auto-cli.mjs +0 -93
- package/templates/scaffold/src/flows/hoppscotch-auth.flow.ts +0 -35
- package/templates/scaffold/src/flows/hoppscotch-get.flow.ts +0 -23
- package/templates/scaffold/src/flows/hoppscotch-post.flow.ts +0 -33
- package/templates/scaffold/src/flows/mail-verify.flow.ts +0 -46
|
@@ -9,37 +9,20 @@
|
|
|
9
9
|
"test": "waygraph test",
|
|
10
10
|
"test:ui": "waygraph test ui",
|
|
11
11
|
"flows": "waygraph list",
|
|
12
|
-
"
|
|
13
|
-
"demo:step": "waygraph demo --blocks \"loginFlow then shopFlow\" --title \"Shop demo\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\"",
|
|
14
|
-
"demo:auto-next": "waygraph demo --blocks \"loginFlow then shopFlow\" --auto-next --title \"Product then cart\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\"",
|
|
15
|
-
"auto": "waygraph auto",
|
|
16
|
-
"auto:cli": "waygraph auto --cli --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
|
|
17
|
-
"prove:auto-cli": "node scripts/drive-auto-cli.mjs",
|
|
18
|
-
"traverse": "waygraph traverse --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 40",
|
|
19
|
-
"traverse:parallel": "waygraph traverse --parallel 2 --session clone --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 30",
|
|
20
|
-
"traverse:coverage": "waygraph traverse --min-edge-coverage 10% --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --max-steps 40",
|
|
12
|
+
"check": "waygraph check",
|
|
21
13
|
"graph": "waygraph graph",
|
|
22
14
|
"graph:mermaid": "waygraph graph --mermaid",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"demo:
|
|
30
|
-
"demo:
|
|
31
|
-
"demo:
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"demo:checkout": "waygraph demo --blocks checkoutFlow --auto-next --title \"Owner: Full Checkout\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\"",
|
|
35
|
-
"demo:checkout:video": "waygraph demo --blocks checkoutFlow --auto-play-video --title \"Owner: Full Checkout\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"},\\\"saucedemo.selectedItem\\\":{\\\"id\\\":\\\"sauce-labs-backpack\\\",\\\"name\\\":\\\"Sauce Labs Backpack\\\"}}\"",
|
|
36
|
-
"demo:api": "waygraph demo --blocks apiDemoFlow --auto-next --title \"FE + BE in one Flow\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
|
|
37
|
-
"demo:api:video": "waygraph demo --blocks apiDemoFlow --auto-play-video --title \"FE + BE in one Flow\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
|
|
38
|
-
"run:bulk": "waygraph run --blocks \"loginFlow then add-all-to-cart then remove-all-from-cart\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\"",
|
|
39
|
-
"chain:bulk": "waygraph run --blocks \"loginFlow then add-all-to-cart then remove-all-from-cart\" --data \"{\\\"saucedemo.credentials\\\":{\\\"username\\\":\\\"standard_user\\\",\\\"password\\\":\\\"secret_sauce\\\"}}\" --non-headless"
|
|
40
|
-
},
|
|
41
|
-
"waygraph": {
|
|
42
|
-
"baseUrl": "https://www.saucedemo.com"
|
|
15
|
+
"auto": "waygraph auto",
|
|
16
|
+
"auto:cli": "waygraph auto --cli",
|
|
17
|
+
"demo": "waygraph demo --blocks \"loginFlow then shopFlow\"",
|
|
18
|
+
"demo:auto-next": "waygraph demo --blocks \"loginFlow then shopFlow\" --auto-next",
|
|
19
|
+
"demo:checkout": "waygraph demo --blocks checkoutFlow --auto-next",
|
|
20
|
+
"demo:device": "waygraph demo --blocks deviceShowcaseFlow --auto-next",
|
|
21
|
+
"demo:bulk": "waygraph demo --blocks cartBulkFlow --auto-next",
|
|
22
|
+
"demo:api": "waygraph demo --blocks apiDemoFlow --auto-next",
|
|
23
|
+
"demo:video": "waygraph demo --blocks deviceShowcaseFlow --auto-play-video",
|
|
24
|
+
"record": "waygraph run --blocks \"loginFlow then shopFlow\" --video --no-step",
|
|
25
|
+
"traverse": "waygraph traverse --max-steps 40"
|
|
43
26
|
},
|
|
44
27
|
"dependencies": {
|
|
45
28
|
"waygraph": "file:../.."
|
|
@@ -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/examples/saucedemo/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
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { checkpoint, defineApiBlock } from "waygraph";
|
|
2
2
|
import type { Checkpoint, StubCtx } from "waygraph";
|
|
3
|
+
import { engine } from "../../config/waygraph.config.js";
|
|
3
4
|
import { loginFlow } from "./login.flow.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -17,21 +18,26 @@ import { loginFlow } from "./login.flow.js";
|
|
|
17
18
|
export type BackendHealthy = Checkpoint<"BackendHealthy">;
|
|
18
19
|
export type BackendDown = Checkpoint<"BackendDown">;
|
|
19
20
|
|
|
20
|
-
const engine = new Engine();
|
|
21
|
-
|
|
22
21
|
const CheckBackendHealth = defineApiBlock<any, BackendHealthy | BackendDown>({
|
|
23
22
|
name: "check-backend-health",
|
|
24
|
-
call: ({ request }) =>
|
|
25
|
-
|
|
23
|
+
call: ({ request }) =>
|
|
24
|
+
request.get("https://jsonplaceholder.typicode.com/todos/1"),
|
|
25
|
+
resolve: (result) =>
|
|
26
|
+
result.ok ? checkpoint("BackendHealthy") : checkpoint("BackendDown"),
|
|
26
27
|
stubBefore: (ctx: StubCtx<BackendHealthy | BackendDown>) => {
|
|
27
28
|
ctx.title("Checking backend health · real HTTP request, no browser");
|
|
28
29
|
},
|
|
29
30
|
});
|
|
30
31
|
|
|
31
|
-
export const apiDemoEpisode =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
32
|
+
export const apiDemoEpisode = engine
|
|
33
|
+
.map()
|
|
34
|
+
.start()
|
|
35
|
+
.method(CheckBackendHealth)
|
|
36
|
+
.end();
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
export const apiDemoFlow = engine
|
|
39
|
+
.map({ memStub: true })
|
|
40
|
+
.start()
|
|
41
|
+
.flow(loginFlow)
|
|
42
|
+
.flow(apiDemoEpisode)
|
|
43
|
+
.end();
|
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { engine } from "../../config/waygraph.config.js";
|
|
2
|
+
import "../states/checkout.mem-stubs.js";
|
|
2
3
|
import { loginFlow } from "./login.flow.js";
|
|
3
4
|
import { AddAllToCartBlock } from "../blocks/saucedemo-web/inventory/methods/add-all-to-cart.method.block.js";
|
|
4
5
|
import { RemoveAllFromCartBlock } from "../blocks/saucedemo-web/inventory/methods/remove-all-from-cart.method.block.js";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
/** Add every item, then remove them all (the bulk Methods). */
|
|
8
|
+
export const cartBulkEpisode = engine
|
|
9
|
+
.map()
|
|
10
|
+
.start()
|
|
11
|
+
.method(
|
|
12
|
+
AddAllToCartBlock.stubBefore((ctx) =>
|
|
13
|
+
ctx.title("Cart bulk add/remove all"),
|
|
14
|
+
),
|
|
15
|
+
)
|
|
16
|
+
.method(RemoveAllFromCartBlock)
|
|
17
|
+
.end();
|
|
7
18
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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);
|
|
19
|
+
export const cartBulkFlow = engine
|
|
20
|
+
.map({ memStub: true })
|
|
21
|
+
.start()
|
|
22
|
+
.flow(loginFlow)
|
|
23
|
+
.flow(cartBulkEpisode)
|
|
24
|
+
.end();
|