waygraph 0.12.46 → 0.12.48
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/cli.js +97 -4
- package/examples/saucedemo/package-lock.json +1 -1
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-cart.block.ts +3 -2
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +3 -2
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +11 -9
- package/examples/saucedemo/src/blocks/saucedemo-web/nav-login.block.ts +11 -5
- package/package.json +1 -1
- package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +6 -1
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +12 -2
- package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +26 -9
- package/templates/quickstart/src/blocks/saucedemo-web/nav-login.block.ts +11 -5
package/dist/cli.js
CHANGED
|
@@ -1724,6 +1724,23 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
|
|
|
1724
1724
|
// rings stay visible ~2s before the error panel (PIA stubOnError RFC).
|
|
1725
1725
|
const defaultHoldMs = opts && opts.defaultHoldMs != null ? opts.defaultHoldMs : null;
|
|
1726
1726
|
const pace = opts && opts.pace !== undefined ? opts.pace : undefined;
|
|
1727
|
+
let dock = opts && opts.todoDock;
|
|
1728
|
+
const advanceTodos = !(opts && opts.advanceTodos === false);
|
|
1729
|
+
// Push stubAfter/before dock before the first ring so Method after-panels
|
|
1730
|
+
// show the updated checklist immediately (not only after the ring cycle).
|
|
1731
|
+
if (dock) {
|
|
1732
|
+
await page
|
|
1733
|
+
.evaluate((d) => {
|
|
1734
|
+
if (window.__wgSyncTodos) window.__wgSyncTodos({ sync: "set", dock: d });
|
|
1735
|
+
}, dock)
|
|
1736
|
+
.catch(() => {});
|
|
1737
|
+
} else if (opts && opts.todoSync === "clear") {
|
|
1738
|
+
await page
|
|
1739
|
+
.evaluate(() => {
|
|
1740
|
+
if (window.__wgSyncTodos) window.__wgSyncTodos({ sync: "clear" });
|
|
1741
|
+
})
|
|
1742
|
+
.catch(() => {});
|
|
1743
|
+
}
|
|
1727
1744
|
for (let i = 0; i < list.length; i++) {
|
|
1728
1745
|
const h = list[i];
|
|
1729
1746
|
try {
|
|
@@ -1735,7 +1752,18 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
|
|
|
1735
1752
|
size: h.size,
|
|
1736
1753
|
weight: h.weight,
|
|
1737
1754
|
});
|
|
1738
|
-
|
|
1755
|
+
// Do NOT advanceTodoDock by ring index after stubAfter already set
|
|
1756
|
+
// progress - that reset current back to 0 and the next block looked
|
|
1757
|
+
// blank. Mid-act Method advance still uses syncTodoDockAdvance.
|
|
1758
|
+
if (dock && advanceTodos) {
|
|
1759
|
+
dock = advanceTodoDock(dock, i);
|
|
1760
|
+
if (opts && opts.todoDockRef) opts.todoDockRef.current = dock;
|
|
1761
|
+
await page
|
|
1762
|
+
.evaluate((d) => {
|
|
1763
|
+
if (window.__wgSyncTodos) window.__wgSyncTodos({ sync: "set", dock: d });
|
|
1764
|
+
}, dock)
|
|
1765
|
+
.catch(() => {});
|
|
1766
|
+
} else if (!dock && ((opts && opts.todos && opts.todos.length) || (h.todos && h.todos.length))) {
|
|
1739
1767
|
const rows =
|
|
1740
1768
|
opts && opts.todos && opts.todos.length
|
|
1741
1769
|
? opts.todos
|
|
@@ -1743,7 +1771,7 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
|
|
|
1743
1771
|
await page
|
|
1744
1772
|
.evaluate((todos) => {
|
|
1745
1773
|
if (window.__wgSyncTodos) {
|
|
1746
|
-
window.__wgSyncTodos(todos);
|
|
1774
|
+
window.__wgSyncTodos({ sync: "set", list: todos });
|
|
1747
1775
|
return;
|
|
1748
1776
|
}
|
|
1749
1777
|
// Fallback for pages that never got installOverlay sync helper.
|
|
@@ -1836,7 +1864,17 @@ async function renderAfterStep(page, info) {
|
|
|
1836
1864
|
await cycleHighlightRings(page, highlights, gatesFast, {
|
|
1837
1865
|
pace: info.pace,
|
|
1838
1866
|
stepLabel: miniStepLabel(info),
|
|
1867
|
+
todoDock: info.todoDock,
|
|
1868
|
+
todos: info.todos,
|
|
1869
|
+
todoSync: info.todoSync,
|
|
1870
|
+
todoDockRef: info.todoDockRef,
|
|
1871
|
+
// stubAfter already authored progress - do not re-index by ring 0..n
|
|
1872
|
+
advanceTodos: false,
|
|
1839
1873
|
});
|
|
1874
|
+
// After-ring advances may have moved the dock - surface latest for caller.
|
|
1875
|
+
if (info.todoDockRef && info.todoDockRef.current) {
|
|
1876
|
+
info.todoDock = info.todoDockRef.current;
|
|
1877
|
+
}
|
|
1840
1878
|
const afterPayload = { ...info, stepLabel: miniStepLabel(info) };
|
|
1841
1879
|
await page
|
|
1842
1880
|
.evaluate((info) => {
|
|
@@ -2246,6 +2284,46 @@ async function matchStubForLocator(page, locator, stubs) {
|
|
|
2246
2284
|
return null;
|
|
2247
2285
|
}
|
|
2248
2286
|
|
|
2287
|
+
/** 0-based stub index for Method fill/click todo advance; -1 if none. */
|
|
2288
|
+
async function matchStubIndexForLocator(page, locator, stubs) {
|
|
2289
|
+
if (!stubs || stubs.length === 0) return -1;
|
|
2290
|
+
const box = await locator.boundingBox().catch(() => null);
|
|
2291
|
+
if (!box) return -1;
|
|
2292
|
+
for (let i = 0; i < stubs.length; i++) {
|
|
2293
|
+
const s = stubs[i];
|
|
2294
|
+
try {
|
|
2295
|
+
const b = await page.locator(s.selector).first().boundingBox();
|
|
2296
|
+
if (
|
|
2297
|
+
b &&
|
|
2298
|
+
Math.abs(b.x - box.x) < 4 &&
|
|
2299
|
+
Math.abs(b.y - box.y) < 4 &&
|
|
2300
|
+
Math.abs(b.width - box.width) < 8
|
|
2301
|
+
) {
|
|
2302
|
+
return i;
|
|
2303
|
+
}
|
|
2304
|
+
} catch {
|
|
2305
|
+
/* skip */
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
return -1;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/** Advance sequential todo dock to stubIndex and push to the page (Method act). */
|
|
2312
|
+
async function syncTodoDockAdvance(page, todoDockRef, stubIndex) {
|
|
2313
|
+
if (!todoDockRef || stubIndex < 0) return;
|
|
2314
|
+
const prev = todoDockRef.current;
|
|
2315
|
+
if (!prev) return;
|
|
2316
|
+
const advanced = advanceTodoDock(prev, stubIndex);
|
|
2317
|
+
todoDockRef.current = advanced;
|
|
2318
|
+
await page
|
|
2319
|
+
.evaluate((dock) => {
|
|
2320
|
+
if (window.__wgSyncTodos) {
|
|
2321
|
+
window.__wgSyncTodos({ sync: "set", dock });
|
|
2322
|
+
}
|
|
2323
|
+
}, advanced)
|
|
2324
|
+
.catch(() => {});
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2249
2327
|
async function captionForLocator(page, locator, stubs, fallback) {
|
|
2250
2328
|
const matched = await matchStubForLocator(page, locator, stubs);
|
|
2251
2329
|
if (matched) {
|
|
@@ -2619,7 +2697,7 @@ async function wasJustNarrated(page) {
|
|
|
2619
2697
|
.catch(() => false);
|
|
2620
2698
|
}
|
|
2621
2699
|
|
|
2622
|
-
function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef) {
|
|
2700
|
+
function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef, todoDockRef) {
|
|
2623
2701
|
// Playwright's own slowMo ALREADY pauses after every single low-level
|
|
2624
2702
|
// action it dispatches - and pressSequentially() fires one such action
|
|
2625
2703
|
// PER CHARACTER. Also giving pressSequentially its own fixed delay
|
|
@@ -2675,6 +2753,8 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
2675
2753
|
: "writing from mem";
|
|
2676
2754
|
const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
|
|
2677
2755
|
const cap = await captionForLocator(page, this, stubs, fallback);
|
|
2756
|
+
const stubIdx = await matchStubIndexForLocator(page, this, stubs);
|
|
2757
|
+
await syncTodoDockAdvance(page, todoDockRef, stubIdx);
|
|
2678
2758
|
await moveCursorTo(page, box, cursorMs(500));
|
|
2679
2759
|
await showRing(page, box, cap.label, cap.tone, {
|
|
2680
2760
|
size: cap.size,
|
|
@@ -2749,6 +2829,8 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
2749
2829
|
}
|
|
2750
2830
|
const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
|
|
2751
2831
|
const cap = await captionForLocator(page, this, stubs, fallback);
|
|
2832
|
+
const stubIdx = await matchStubIndexForLocator(page, this, stubs);
|
|
2833
|
+
await syncTodoDockAdvance(page, todoDockRef, stubIdx);
|
|
2752
2834
|
clickTone = cap.tone;
|
|
2753
2835
|
clickPoint = await moveCursorTo(page, box, cursorMs(600));
|
|
2754
2836
|
await showRing(page, box, cap.label, cap.tone, {
|
|
@@ -2887,7 +2969,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
2887
2969
|
const stepperMode = process.env.WAYGRAPH_STEPPER === "full" ? "full" : "carousel";
|
|
2888
2970
|
const pacing = { gatesFast: demoFast, gatesSlow: false, skipTheater: false, demoPace: "normal" };
|
|
2889
2971
|
const stubBeforeRef = { current: [] };
|
|
2890
|
-
|
|
2972
|
+
/** Live todo dock for Method fill/click advance (same carry as lastTodoDock). */
|
|
2973
|
+
const todoDockRef = { current: undefined };
|
|
2974
|
+
instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef, todoDockRef);
|
|
2891
2975
|
// Force the panel checkbox from this process's flags/env at run start.
|
|
2892
2976
|
// installOverlay only seeds localStorage when the key is null (so mid-run
|
|
2893
2977
|
// checkbox clicks survive navigations). Without this, a prior --autoplay
|
|
@@ -3108,6 +3192,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3108
3192
|
todoPos: stubBeforePhase.todoPos,
|
|
3109
3193
|
});
|
|
3110
3194
|
lastTodoDock = appliedBefore.dock;
|
|
3195
|
+
todoDockRef.current = lastTodoDock;
|
|
3111
3196
|
const stubBeforeTodos =
|
|
3112
3197
|
(lastTodoDock && lastTodoDock.groups[0] && lastTodoDock.groups[0].items) ||
|
|
3113
3198
|
stubBeforePhase.todos ||
|
|
@@ -3265,6 +3350,10 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3265
3350
|
})
|
|
3266
3351
|
.catch(() => {});
|
|
3267
3352
|
result = stepOutcome.result;
|
|
3353
|
+
// Mid-act Method fill/click advances live on todoDockRef - fold that
|
|
3354
|
+
// back into lastTodoDock before stubAfter / next-block keep, or the
|
|
3355
|
+
// next step reverts to the pre-act (blank / index-0) checklist.
|
|
3356
|
+
if (todoDockRef.current) lastTodoDock = todoDockRef.current;
|
|
3268
3357
|
// withExpectedFailure last block that SUCCEEDS on the intentional fail
|
|
3269
3358
|
// branch (e.g. submit-login -> LoginPage + error banner). Still show
|
|
3270
3359
|
// stubOnError rings + amber "expected outcome" panel - branching no
|
|
@@ -3341,6 +3430,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3341
3430
|
todoPos: afterPhase.todoPos,
|
|
3342
3431
|
});
|
|
3343
3432
|
lastTodoDock = appliedAfter.dock;
|
|
3433
|
+
todoDockRef.current = lastTodoDock;
|
|
3344
3434
|
stubAfterTodos =
|
|
3345
3435
|
(lastTodoDock && lastTodoDock.groups[0] && lastTodoDock.groups[0].items) ||
|
|
3346
3436
|
afterPhase.todos ||
|
|
@@ -3392,6 +3482,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3392
3482
|
todoDock: lastTodoDock,
|
|
3393
3483
|
todoSync: appliedAfter.sync,
|
|
3394
3484
|
todoId: lastTodoDock && lastTodoDock.id,
|
|
3485
|
+
todoDockRef,
|
|
3395
3486
|
forceCollapsed:
|
|
3396
3487
|
!!process.env.WAYGRAPH_VIDEO ||
|
|
3397
3488
|
process.env.WAYGRAPH_MINI === "1" ||
|
|
@@ -3399,6 +3490,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3399
3490
|
? true
|
|
3400
3491
|
: undefined,
|
|
3401
3492
|
});
|
|
3493
|
+
// Ring cycle / stubAfter may have advanced the dock - persist for next block.
|
|
3494
|
+
if (todoDockRef.current) lastTodoDock = todoDockRef.current;
|
|
3402
3495
|
await gate();
|
|
3403
3496
|
}
|
|
3404
3497
|
await teardownOverlay(page);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineNavClickBlock, Trait } from "waygraph";
|
|
2
|
+
import type { StubCtx } from "waygraph";
|
|
2
3
|
import type { CartPage } from "../../../states/checkout.states.js";
|
|
3
4
|
import { InventorySel } from "../inventory/methods/inventory-items.js";
|
|
4
5
|
|
|
@@ -15,7 +16,7 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
|
|
|
15
16
|
checkpoint: "CartPage",
|
|
16
17
|
click: ".shopping_cart_link",
|
|
17
18
|
verify: [Trait.url({ pathname: "/cart.html" })],
|
|
18
|
-
stubBefore: (ctx) => {
|
|
19
|
+
stubBefore: (ctx: StubCtx) => {
|
|
19
20
|
ctx.title("Open cart");
|
|
20
21
|
ctx.todoId("saucedemo-cart");
|
|
21
22
|
ctx.todos([
|
|
@@ -40,7 +41,7 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
|
|
|
40
41
|
},
|
|
41
42
|
});
|
|
42
43
|
},
|
|
43
|
-
stubAfter: (ctx) => {
|
|
44
|
+
stubAfter: (ctx: StubCtx) => {
|
|
44
45
|
ctx.banner("Your cart");
|
|
45
46
|
ctx.zoomOut(false);
|
|
46
47
|
ctx.ring("items", {
|
package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineEffectBlock, checkpoint, type Checkpoint, type WaygraphInstanceOption } from "waygraph";
|
|
2
|
+
import type { StubCtx } from "waygraph";
|
|
2
3
|
import type { ItemInCart } from "../../../../states/checkout.states.js";
|
|
3
4
|
import { SelectedItem } from "../../../../states/checkout.mem-keys.js";
|
|
4
5
|
import { collectAddableItems, InventorySel } from "./inventory-items.js";
|
|
@@ -34,7 +35,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
|
34
35
|
},
|
|
35
36
|
],
|
|
36
37
|
// Demo: zoom product -> add (stay zoomed), then badge after act.
|
|
37
|
-
stubBefore: (ctx) => {
|
|
38
|
+
stubBefore: (ctx: StubCtx) => {
|
|
38
39
|
ctx.title("Pick a product");
|
|
39
40
|
ctx.todoId("saucedemo-cart");
|
|
40
41
|
ctx.todos([
|
|
@@ -66,7 +67,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
|
66
67
|
},
|
|
67
68
|
});
|
|
68
69
|
},
|
|
69
|
-
stubAfter: (ctx) => {
|
|
70
|
+
stubAfter: (ctx: StubCtx) => {
|
|
70
71
|
ctx.banner("In the cart");
|
|
71
72
|
ctx.todoId("saucedemo-cart");
|
|
72
73
|
ctx.todos([
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineMethodBlock, checkpoint, Trait } from "waygraph";
|
|
1
|
+
import { defineMethodBlock, checkpoint, Trait, type StubCtx } 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
|
|
|
@@ -9,8 +9,10 @@ type LoginObserve = "success" | "failure";
|
|
|
9
9
|
* Helper: defineMethodBlock
|
|
10
10
|
* Route: saucedemo-web/methods/ (page URL /)
|
|
11
11
|
*
|
|
12
|
-
* Fills credentials and submits - never page.goto (nav-login owns that).
|
|
12
|
+
* Fills credentials and submits - never page.goto (nav-login Page hub owns that).
|
|
13
13
|
* Branches: good creds -> LoggedIn; bad/locked -> LoginPage with error banner.
|
|
14
|
+
* stubBefore/stubAfter drive the floating todo dock (Method fill/click advances
|
|
15
|
+
* todoIndex mid-act; stubAfter marks complete / fail).
|
|
14
16
|
*/
|
|
15
17
|
export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOutcome>({
|
|
16
18
|
name: "submit-login",
|
|
@@ -47,7 +49,7 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
47
49
|
: [Trait.visible('[data-test="error"]')],
|
|
48
50
|
// Open lifecycle: banner title, todos, Screen Studio camera zoom,
|
|
49
51
|
// sequential highlight queue (appear / dwell / fade), focus + color.
|
|
50
|
-
stubBefore: (ctx) => {
|
|
52
|
+
stubBefore: (ctx: StubCtx) => {
|
|
51
53
|
ctx.title("Signing in");
|
|
52
54
|
ctx.todoId("saucedemo-login");
|
|
53
55
|
ctx.todos([
|
|
@@ -82,18 +84,18 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
82
84
|
},
|
|
83
85
|
});
|
|
84
86
|
},
|
|
85
|
-
stubAfter: (ctx) => {
|
|
87
|
+
stubAfter: (ctx: StubCtx) => {
|
|
86
88
|
// Fail branch lands on LoginPage with Epic sadface - do not ring the
|
|
87
89
|
// inventory shelf. Success (LoggedIn) hands off to shopFlow.
|
|
88
90
|
if (ctx.out && ctx.out.__state === "LoginPage") {
|
|
89
91
|
ctx.title("Login blocked");
|
|
90
92
|
ctx.todoId("saucedemo-login");
|
|
91
93
|
ctx.todos([
|
|
92
|
-
{ id: "login-user", text: "Enter username" },
|
|
93
|
-
{ id: "login-pass", text: "Enter password" },
|
|
94
|
-
{ id: "login-submit", text: "Click Login" },
|
|
94
|
+
{ id: "login-user", text: "Enter username", done: true },
|
|
95
|
+
{ id: "login-pass", text: "Enter password", done: true },
|
|
96
|
+
{ id: "login-submit", text: "Click Login", done: true },
|
|
95
97
|
]);
|
|
96
|
-
ctx.
|
|
98
|
+
ctx.todoStyle("checklist");
|
|
97
99
|
ctx.ring("error", {
|
|
98
100
|
selector: '[data-test="error"]',
|
|
99
101
|
label: "Login error banner",
|
|
@@ -123,7 +125,7 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
123
125
|
duration: true,
|
|
124
126
|
});
|
|
125
127
|
},
|
|
126
|
-
stubOnError: (ctx) => {
|
|
128
|
+
stubOnError: (ctx: StubCtx) => {
|
|
127
129
|
ctx.title("Login failed");
|
|
128
130
|
ctx.ring("error", {
|
|
129
131
|
selector: '[data-test="error"]',
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { definePageBlock, Trait } from "waygraph";
|
|
2
2
|
import type { LoginPage } from "../../states/checkout.states.js";
|
|
3
|
+
import { SubmitLoginActionBlock } from "./methods/submit-login.method.block.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Kind:
|
|
6
|
-
* Helper:
|
|
6
|
+
* Kind: Page
|
|
7
|
+
* Helper: definePageBlock
|
|
7
8
|
* Route: saucedemo-web/ (= /)
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* Login screen hub. Deep-link via url; method submit-login hangs off this
|
|
11
|
+
* page (stubBefore/stubAfter todos live on the Method). Name stays
|
|
12
|
+
* `nav-login` so existing flows / chain specs keep working.
|
|
10
13
|
*/
|
|
11
|
-
export const NavLoginBlock =
|
|
14
|
+
export const NavLoginBlock = definePageBlock<LoginPage>({
|
|
12
15
|
name: "nav-login",
|
|
13
16
|
description: "Goes to the Swag Labs login page.",
|
|
14
17
|
checkpoint: "LoginPage",
|
|
15
18
|
url: "/",
|
|
16
19
|
verify: [Trait.visible("#login-button")],
|
|
20
|
+
methods: {
|
|
21
|
+
submitLogin: () => SubmitLoginActionBlock,
|
|
22
|
+
},
|
|
17
23
|
});
|
package/package.json
CHANGED
|
@@ -17,7 +17,12 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
|
|
|
17
17
|
verify: [Trait.url({ pathname: "/cart.html" })],
|
|
18
18
|
stubBefore: (ctx) => {
|
|
19
19
|
ctx.title("Open cart");
|
|
20
|
-
ctx.
|
|
20
|
+
ctx.todoId("saucedemo-cart");
|
|
21
|
+
ctx.todos([
|
|
22
|
+
{ id: "cart-find", text: "Find the product" },
|
|
23
|
+
{ id: "cart-add", text: "Add to cart" },
|
|
24
|
+
{ id: "cart-open", text: "Open cart" },
|
|
25
|
+
]);
|
|
21
26
|
ctx.todoIndex(2);
|
|
22
27
|
ctx.zoom(1.4);
|
|
23
28
|
ctx.zoomOut(false);
|
package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts
CHANGED
|
@@ -36,7 +36,12 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
|
36
36
|
// Demo: zoom product -> add (stay zoomed), then badge after act.
|
|
37
37
|
stubBefore: (ctx) => {
|
|
38
38
|
ctx.title("Pick a product");
|
|
39
|
-
ctx.
|
|
39
|
+
ctx.todoId("saucedemo-cart");
|
|
40
|
+
ctx.todos([
|
|
41
|
+
{ id: "cart-find", text: "Find the product" },
|
|
42
|
+
{ id: "cart-add", text: "Add to cart" },
|
|
43
|
+
{ id: "cart-open", text: "Open cart" },
|
|
44
|
+
]);
|
|
40
45
|
ctx.todoIndex(0);
|
|
41
46
|
ctx.zoom(1.35);
|
|
42
47
|
ctx.zoomOut(false);
|
|
@@ -63,7 +68,12 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
|
|
|
63
68
|
},
|
|
64
69
|
stubAfter: (ctx) => {
|
|
65
70
|
ctx.banner("In the cart");
|
|
66
|
-
ctx.
|
|
71
|
+
ctx.todoId("saucedemo-cart");
|
|
72
|
+
ctx.todos([
|
|
73
|
+
{ id: "cart-find", text: "Find the product" },
|
|
74
|
+
{ id: "cart-add", text: "Add to cart" },
|
|
75
|
+
{ id: "cart-open", text: "Open cart" },
|
|
76
|
+
]);
|
|
67
77
|
ctx.todoIndex(1);
|
|
68
78
|
ctx.zoomOut(false);
|
|
69
79
|
ctx.ring("badge", {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineMethodBlock, checkpoint, Trait } from "waygraph";
|
|
1
|
+
import { defineMethodBlock, checkpoint, Trait, type StubCtx } 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
|
|
|
@@ -9,8 +9,10 @@ type LoginObserve = "success" | "failure";
|
|
|
9
9
|
* Helper: defineMethodBlock
|
|
10
10
|
* Route: saucedemo-web/methods/ (page URL /)
|
|
11
11
|
*
|
|
12
|
-
* Fills credentials and submits - never page.goto (nav-login owns that).
|
|
12
|
+
* Fills credentials and submits - never page.goto (nav-login Page hub owns that).
|
|
13
13
|
* Branches: good creds -> LoggedIn; bad/locked -> LoginPage with error banner.
|
|
14
|
+
* stubBefore/stubAfter drive the floating todo dock (Method fill/click advances
|
|
15
|
+
* todoIndex mid-act; stubAfter marks complete / fail).
|
|
14
16
|
*/
|
|
15
17
|
export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOutcome>({
|
|
16
18
|
name: "submit-login",
|
|
@@ -47,9 +49,14 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
47
49
|
: [Trait.visible('[data-test="error"]')],
|
|
48
50
|
// Open lifecycle: banner title, todos, Screen Studio camera zoom,
|
|
49
51
|
// sequential highlight queue (appear / dwell / fade), focus + color.
|
|
50
|
-
stubBefore: (ctx) => {
|
|
52
|
+
stubBefore: (ctx: StubCtx) => {
|
|
51
53
|
ctx.title("Signing in");
|
|
52
|
-
ctx.
|
|
54
|
+
ctx.todoId("saucedemo-login");
|
|
55
|
+
ctx.todos([
|
|
56
|
+
{ id: "login-user", text: "Enter username" },
|
|
57
|
+
{ id: "login-pass", text: "Enter password" },
|
|
58
|
+
{ id: "login-submit", text: "Click Login" },
|
|
59
|
+
]);
|
|
53
60
|
ctx.todoIndex(0);
|
|
54
61
|
ctx.zoom(1.35);
|
|
55
62
|
ctx.highlights({
|
|
@@ -77,13 +84,18 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
77
84
|
},
|
|
78
85
|
});
|
|
79
86
|
},
|
|
80
|
-
stubAfter: (ctx) => {
|
|
87
|
+
stubAfter: (ctx: StubCtx) => {
|
|
81
88
|
// Fail branch lands on LoginPage with Epic sadface - do not ring the
|
|
82
89
|
// inventory shelf. Success (LoggedIn) hands off to shopFlow.
|
|
83
90
|
if (ctx.out && ctx.out.__state === "LoginPage") {
|
|
84
91
|
ctx.title("Login blocked");
|
|
85
|
-
ctx.
|
|
86
|
-
ctx.
|
|
92
|
+
ctx.todoId("saucedemo-login");
|
|
93
|
+
ctx.todos([
|
|
94
|
+
{ id: "login-user", text: "Enter username", done: true },
|
|
95
|
+
{ id: "login-pass", text: "Enter password", done: true },
|
|
96
|
+
{ id: "login-submit", text: "Click Login", done: true },
|
|
97
|
+
]);
|
|
98
|
+
ctx.todoStyle("checklist");
|
|
87
99
|
ctx.ring("error", {
|
|
88
100
|
selector: '[data-test="error"]',
|
|
89
101
|
label: "Login error banner",
|
|
@@ -95,7 +107,12 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
95
107
|
return;
|
|
96
108
|
}
|
|
97
109
|
ctx.banner("Inventory");
|
|
98
|
-
ctx.
|
|
110
|
+
ctx.todoId("saucedemo-login");
|
|
111
|
+
ctx.todos([
|
|
112
|
+
{ id: "login-user", text: "Enter username" },
|
|
113
|
+
{ id: "login-pass", text: "Enter password" },
|
|
114
|
+
{ id: "login-submit", text: "Click Login" },
|
|
115
|
+
]);
|
|
99
116
|
ctx.todoIndex(2);
|
|
100
117
|
// Landed on inventory - hand off to shopFlow for product -> cart zoom.
|
|
101
118
|
ctx.ring("shelf", {
|
|
@@ -108,7 +125,7 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
|
|
|
108
125
|
duration: true,
|
|
109
126
|
});
|
|
110
127
|
},
|
|
111
|
-
stubOnError: (ctx) => {
|
|
128
|
+
stubOnError: (ctx: StubCtx) => {
|
|
112
129
|
ctx.title("Login failed");
|
|
113
130
|
ctx.ring("error", {
|
|
114
131
|
selector: '[data-test="error"]',
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { definePageBlock, Trait } from "waygraph";
|
|
2
2
|
import type { LoginPage } from "../../states/checkout.states.js";
|
|
3
|
+
import { SubmitLoginActionBlock } from "./methods/submit-login.method.block.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Kind:
|
|
6
|
-
* Helper:
|
|
6
|
+
* Kind: Page
|
|
7
|
+
* Helper: definePageBlock
|
|
7
8
|
* Route: saucedemo-web/ (= /)
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* Login screen hub. Deep-link via url; method submit-login hangs off this
|
|
11
|
+
* page (stubBefore/stubAfter todos live on the Method). Name stays
|
|
12
|
+
* `nav-login` so existing flows / chain specs keep working.
|
|
10
13
|
*/
|
|
11
|
-
export const NavLoginBlock =
|
|
14
|
+
export const NavLoginBlock = definePageBlock<LoginPage>({
|
|
12
15
|
name: "nav-login",
|
|
13
16
|
description: "Goes to the Swag Labs login page.",
|
|
14
17
|
checkpoint: "LoginPage",
|
|
15
18
|
url: "/",
|
|
16
19
|
verify: [Trait.visible("#login-button")],
|
|
20
|
+
methods: {
|
|
21
|
+
submitLogin: () => SubmitLoginActionBlock,
|
|
22
|
+
},
|
|
17
23
|
});
|