waygraph 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/cli.js +601 -71
- package/dist/engine.d.ts +82 -3
- package/dist/engine.js +86 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/types.d.ts +23 -0
- package/package.json +3 -2
- package/templates/quickstart/src/blocks/actions/add-to-cart.action.block.ts +18 -0
- package/templates/quickstart/src/blocks/actions/checkout-info.action.block.ts +19 -0
- package/templates/quickstart/src/blocks/actions/finish-order.action.block.ts +18 -0
- package/templates/quickstart/src/blocks/actions/submit-login.action.block.ts +22 -0
- package/templates/quickstart/src/blocks/nav-login.block.ts +15 -0
- package/templates/quickstart/src/flows/quickstart.flow.ts +19 -0
- package/templates/quickstart/src/states/quickstart.states.ts +11 -0
package/dist/engine.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { BrowserContext, Page, Locator } from "@playwright/test";
|
|
2
|
-
import type { Block, Checkpoint, Instruction, DefinedBlock } from "./types.js";
|
|
1
|
+
import type { BrowserContext, BrowserType, Page, Locator } from "@playwright/test";
|
|
2
|
+
import type { Block, Checkpoint, Instruction, DefinedBlock, ActionPage, WaygraphHighlight } from "./types.js";
|
|
3
3
|
import { MemPage } from "./mem-page.js";
|
|
4
|
+
import type { MemKey } from "./mem-page.js";
|
|
4
5
|
import type { Trait } from "./trait.js";
|
|
5
6
|
/**
|
|
6
7
|
* Checks `block.requires` against `mem` and throws, naming every missing key at
|
|
@@ -169,6 +170,16 @@ export interface Flow<Out extends Checkpoint<string>> {
|
|
|
169
170
|
* something `run()` itself uses.
|
|
170
171
|
*/
|
|
171
172
|
readonly title?: string;
|
|
173
|
+
/**
|
|
174
|
+
* Set once this Flow has been wrapped in {@link withExpectedFailure} - a
|
|
175
|
+
* display hint for a step-through overlay: this Flow is DESIGNED to throw
|
|
176
|
+
* (e.g. a blocked user's login attempt genuinely failing), not a bug in
|
|
177
|
+
* the demo itself. `run()` never reads this - the Flow still throws
|
|
178
|
+
* exactly as it would otherwise; only the overlay's error panel reads it,
|
|
179
|
+
* to show an informational "this is the point" panel instead of an
|
|
180
|
+
* alarming one indistinguishable from a real break.
|
|
181
|
+
*/
|
|
182
|
+
readonly expectedFailureReason?: string;
|
|
172
183
|
/**
|
|
173
184
|
* This Flow's constituent Blocks, in order between `start`/`end`, as plain
|
|
174
185
|
* data - for introspection/visualization tools, without running anything.
|
|
@@ -236,6 +247,25 @@ export declare function withSessionReset<Out extends Checkpoint<string>>(flow: F
|
|
|
236
247
|
* @example const ticket123 = withTitle(loginFlow, "Ticket #123 - Sales rep signs in");
|
|
237
248
|
*/
|
|
238
249
|
export declare function withTitle<Out extends Checkpoint<string>>(flow: Flow<Out>, title: string): Flow<Out>;
|
|
250
|
+
/**
|
|
251
|
+
* Marks a Flow as one that's SUPPOSED to throw - e.g. a locked-out user's
|
|
252
|
+
* login attempt genuinely failing, demonstrating the app's own real
|
|
253
|
+
* behavior rather than a bug in the demo. Non-destructive, same pattern as
|
|
254
|
+
* {@link withTitle}.
|
|
255
|
+
*
|
|
256
|
+
* By itself this changes nothing about how the Flow runs - it still throws
|
|
257
|
+
* exactly as it would otherwise. It's a hint a step-through overlay
|
|
258
|
+
* (waygraph's `chain --step`) reads when that throw happens, to show an
|
|
259
|
+
* informational "this is the point" panel instead of one indistinguishable
|
|
260
|
+
* from a genuine break - the distinction a demo author needs so a viewer
|
|
261
|
+
* (or the author, mid-build) can tell "the app just did what it's supposed
|
|
262
|
+
* to" apart from "something in this demo is actually broken."
|
|
263
|
+
* @example const viewerBlockedFlow = withExpectedFailure(
|
|
264
|
+
* engine.defineFlow([start, NavLoginBlock, blockedLoginCheck, end]),
|
|
265
|
+
* "locked_out_user can never reach /inventory.html - this is the block working as intended.",
|
|
266
|
+
* );
|
|
267
|
+
*/
|
|
268
|
+
export declare function withExpectedFailure<Out extends Checkpoint<string>>(flow: Flow<Out>, reason: string): Flow<Out>;
|
|
239
269
|
/**
|
|
240
270
|
* Runs several complete, independent Flows one after another, reusing the
|
|
241
271
|
* same page/context/mem in order - each remains its own validated
|
|
@@ -313,6 +343,18 @@ export interface EngineConfig {
|
|
|
313
343
|
browserName?: "chromium" | "firefox" | "webkit";
|
|
314
344
|
/** Milliseconds Playwright pauses before each operation - for watching a non-headless run with your own eyes, not for real runs. Default `0`. */
|
|
315
345
|
slowMo?: number;
|
|
346
|
+
/**
|
|
347
|
+
* Overrides which `BrowserType` actually launches, per browser name -
|
|
348
|
+
* default is the real `@playwright/test` export for whichever one
|
|
349
|
+
* `browserName` picks. Accepts anything shaped like Playwright's own
|
|
350
|
+
* `chromium`/`firefox`/`webkit` (same `.launch()` signature) - a
|
|
351
|
+
* stealth-patched or otherwise customized launcher (e.g. `playwright-extra`
|
|
352
|
+
* plus a stealth plugin) drops in here unmodified. Puppeteer is a
|
|
353
|
+
* different `Page`/`BrowserContext` shape entirely and isn't supported by
|
|
354
|
+
* this seam.
|
|
355
|
+
* @example new Engine({ browsers: { chromium: stealthChromium } })
|
|
356
|
+
*/
|
|
357
|
+
browsers?: Partial<Record<"chromium" | "firefox" | "webkit", BrowserType>>;
|
|
316
358
|
}
|
|
317
359
|
type S = Checkpoint<"__start__">;
|
|
318
360
|
/**
|
|
@@ -385,11 +427,48 @@ export declare function branch<In extends Checkpoint<string>, Out extends Checkp
|
|
|
385
427
|
export declare function defineBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(base: {
|
|
386
428
|
name: string;
|
|
387
429
|
description?: Block<In, Out>["description"];
|
|
388
|
-
instruction: Instruction<In, Out, any
|
|
430
|
+
instruction: Omit<Instruction<In, Out, any>, "act"> & {
|
|
431
|
+
act(page: ActionPage, input: In, mem: MemPage): Promise<void>;
|
|
432
|
+
};
|
|
389
433
|
next?: Block<In, Out>["next"];
|
|
390
434
|
requires?: Block<In, Out>["requires"];
|
|
391
435
|
routes?: Block<In, Out>["routes"];
|
|
392
436
|
}): DefinedBlock<In, Out>;
|
|
437
|
+
/**
|
|
438
|
+
* A Block whose only possible action is navigating to a URL - built via
|
|
439
|
+
* {@link defineNavBlock}, never with a hand-authored `act()`. Extends `Block`
|
|
440
|
+
* (same shape `DefinedBlock`/`ComposedBlock` already use), so it drops into
|
|
441
|
+
* `defineFlow([...])`, `connect()`, `composeBlock()` exactly like any other
|
|
442
|
+
* Block - no special-casing needed anywhere that only expects a `Block`.
|
|
443
|
+
* See `openspec/changes/nav-block-and-check/` for why this exists.
|
|
444
|
+
*/
|
|
445
|
+
export type NavBlock<Out extends Checkpoint<string>> = DefinedBlock<Checkpoint<string>, Out>;
|
|
446
|
+
/**
|
|
447
|
+
* Builds a {@link NavBlock}. `url` accepts a plain string or a function
|
|
448
|
+
* `(mem) => string` for parameterized routes (e.g. `/dashboard/requests/:id`
|
|
449
|
+
* with a real id read from mem). `checkpoint` is the tag this NavBlock
|
|
450
|
+
* resolves to once navigation completes - a NavBlock never branches on
|
|
451
|
+
* observed evidence the way a regular Block can, so there's no `resolve`
|
|
452
|
+
* logic to author; it always just declares "arrived."
|
|
453
|
+
*
|
|
454
|
+
* The generated `act()` is always exactly `page.goto(url)` - never anything
|
|
455
|
+
* else, never author-supplied. Callers get a real `Page` here internally
|
|
456
|
+
* (the one legitimate place navigation belongs); `defineBlock`'s own
|
|
457
|
+
* `ActionPage` narrowing is what discourages navigation everywhere else,
|
|
458
|
+
* and never applies to this generated function since no author ever writes
|
|
459
|
+
* it by hand.
|
|
460
|
+
* @example defineNavBlock({ name: "nav-web-login", checkpoint: "LoginForm", url: "/login" })
|
|
461
|
+
* @example defineNavBlock({ name: "nav-web-request-detail", checkpoint: "RequestDetail", url: (mem) => `/dashboard/requests/${mem.get(RequestId.key)}` })
|
|
462
|
+
*/
|
|
463
|
+
export declare function defineNavBlock<Out extends Checkpoint<string>>(options: {
|
|
464
|
+
name: string;
|
|
465
|
+
description?: string;
|
|
466
|
+
checkpoint: Out["__state"];
|
|
467
|
+
url: string | ((mem: MemPage) => string);
|
|
468
|
+
requires?: readonly MemKey<any>[];
|
|
469
|
+
verify?: Trait[] | ((out: Out) => Trait[]);
|
|
470
|
+
highlights?: readonly WaygraphHighlight[] | ((out: Out) => readonly WaygraphHighlight[]);
|
|
471
|
+
}): NavBlock<Out>;
|
|
393
472
|
/**
|
|
394
473
|
* Returns a new Block that runs exactly like `block` - same act/observe/resolve,
|
|
395
474
|
* same routing - but with `verify` replaced. For giving one flow stricter (or
|
package/dist/engine.js
CHANGED
|
@@ -3,6 +3,20 @@ import { connect, checkpoint } from "./types.js";
|
|
|
3
3
|
import { MemPage } from "./mem-page.js";
|
|
4
4
|
import { runVerify } from "./trait.js";
|
|
5
5
|
const LAUNCHERS = { chromium, firefox, webkit };
|
|
6
|
+
/**
|
|
7
|
+
* Resolves which `BrowserType` actually launches for a `mem`-only run (no
|
|
8
|
+
* caller-supplied context) - `config.browsers[browserName]` if given,
|
|
9
|
+
* otherwise the real `@playwright/test` export. Waygraph is deliberately
|
|
10
|
+
* "just an opinionated Playwright" - it doesn't own browser automation
|
|
11
|
+
* itself, so it shouldn't force every consumer onto the vanilla launcher.
|
|
12
|
+
* A stealth-patched or otherwise customized launcher (e.g. `playwright-extra`
|
|
13
|
+
* plus a stealth plugin) is a drop-in replacement for `chromium`/`firefox`/
|
|
14
|
+
* `webkit` - same `.launch()` shape - so accepting one here costs nothing for
|
|
15
|
+
* a consumer who never sets it.
|
|
16
|
+
*/
|
|
17
|
+
function resolveLauncher(browserName, config) {
|
|
18
|
+
return config?.browsers?.[browserName] ?? LAUNCHERS[browserName];
|
|
19
|
+
}
|
|
6
20
|
/**
|
|
7
21
|
* Checks `block.requires` against `mem` and throws, naming every missing key at
|
|
8
22
|
* once, if any are unset. `connect()` unions each Block's `requires` into the
|
|
@@ -218,7 +232,7 @@ function buildFlow(middle, engineConfig) {
|
|
|
218
232
|
// run(context, mem) already has via an existing context.
|
|
219
233
|
preflight(mem, chain);
|
|
220
234
|
const browserName = config.browserName ?? "chromium";
|
|
221
|
-
const launch =
|
|
235
|
+
const launch = resolveLauncher(browserName, config);
|
|
222
236
|
// If CHROME_PATH/CHROMIUM_PATH is set in the environment (e.g. a system
|
|
223
237
|
// or Flatpak Chromium), launch that instead of Playwright's own bundled
|
|
224
238
|
// binary - only for chromium (the env var names a Chromium build, not a
|
|
@@ -327,6 +341,32 @@ export function withTitle(flow, title) {
|
|
|
327
341
|
modBlockVerify: (block, nameOrIndex, newCheck) => withTitle(flow.modBlockVerify(block, nameOrIndex, newCheck), title),
|
|
328
342
|
};
|
|
329
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Marks a Flow as one that's SUPPOSED to throw - e.g. a locked-out user's
|
|
346
|
+
* login attempt genuinely failing, demonstrating the app's own real
|
|
347
|
+
* behavior rather than a bug in the demo. Non-destructive, same pattern as
|
|
348
|
+
* {@link withTitle}.
|
|
349
|
+
*
|
|
350
|
+
* By itself this changes nothing about how the Flow runs - it still throws
|
|
351
|
+
* exactly as it would otherwise. It's a hint a step-through overlay
|
|
352
|
+
* (waygraph's `chain --step`) reads when that throw happens, to show an
|
|
353
|
+
* informational "this is the point" panel instead of one indistinguishable
|
|
354
|
+
* from a genuine break - the distinction a demo author needs so a viewer
|
|
355
|
+
* (or the author, mid-build) can tell "the app just did what it's supposed
|
|
356
|
+
* to" apart from "something in this demo is actually broken."
|
|
357
|
+
* @example const viewerBlockedFlow = withExpectedFailure(
|
|
358
|
+
* engine.defineFlow([start, NavLoginBlock, blockedLoginCheck, end]),
|
|
359
|
+
* "locked_out_user can never reach /inventory.html - this is the block working as intended.",
|
|
360
|
+
* );
|
|
361
|
+
*/
|
|
362
|
+
export function withExpectedFailure(flow, reason) {
|
|
363
|
+
return {
|
|
364
|
+
...flow,
|
|
365
|
+
expectedFailureReason: reason,
|
|
366
|
+
withBlockVerify: (block, verify) => withExpectedFailure(flow.withBlockVerify(block, verify), reason),
|
|
367
|
+
modBlockVerify: (block, nameOrIndex, newCheck) => withExpectedFailure(flow.modBlockVerify(block, nameOrIndex, newCheck), reason),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
330
370
|
function locateInChain(flows, block) {
|
|
331
371
|
if (typeof block === "number") {
|
|
332
372
|
let remaining = block;
|
|
@@ -391,7 +431,7 @@ export function chainFlow(...flows) {
|
|
|
391
431
|
// the point of sequencing them (a fresh browser is itself a full
|
|
392
432
|
// session reset, silently making withSessionReset a no-op).
|
|
393
433
|
const browserName = config?.browserName ?? "chromium";
|
|
394
|
-
const launch =
|
|
434
|
+
const launch = resolveLauncher(browserName, config);
|
|
395
435
|
const executablePath = browserName === "chromium" ? process.env.CHROME_PATH || process.env.CHROMIUM_PATH : undefined;
|
|
396
436
|
const browser = await launch.launch({
|
|
397
437
|
headless: config?.headless ?? true,
|
|
@@ -568,6 +608,50 @@ export function defineBlock(base) {
|
|
|
568
608
|
modVerifyAll: (patches) => modVerifyAll(plain, patches),
|
|
569
609
|
};
|
|
570
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* Builds a {@link NavBlock}. `url` accepts a plain string or a function
|
|
613
|
+
* `(mem) => string` for parameterized routes (e.g. `/dashboard/requests/:id`
|
|
614
|
+
* with a real id read from mem). `checkpoint` is the tag this NavBlock
|
|
615
|
+
* resolves to once navigation completes - a NavBlock never branches on
|
|
616
|
+
* observed evidence the way a regular Block can, so there's no `resolve`
|
|
617
|
+
* logic to author; it always just declares "arrived."
|
|
618
|
+
*
|
|
619
|
+
* The generated `act()` is always exactly `page.goto(url)` - never anything
|
|
620
|
+
* else, never author-supplied. Callers get a real `Page` here internally
|
|
621
|
+
* (the one legitimate place navigation belongs); `defineBlock`'s own
|
|
622
|
+
* `ActionPage` narrowing is what discourages navigation everywhere else,
|
|
623
|
+
* and never applies to this generated function since no author ever writes
|
|
624
|
+
* it by hand.
|
|
625
|
+
* @example defineNavBlock({ name: "nav-web-login", checkpoint: "LoginForm", url: "/login" })
|
|
626
|
+
* @example defineNavBlock({ name: "nav-web-request-detail", checkpoint: "RequestDetail", url: (mem) => `/dashboard/requests/${mem.get(RequestId.key)}` })
|
|
627
|
+
*/
|
|
628
|
+
export function defineNavBlock(options) {
|
|
629
|
+
const built = defineBlock({
|
|
630
|
+
name: options.name,
|
|
631
|
+
...(options.description ? { description: options.description } : {}),
|
|
632
|
+
...(options.requires ? { requires: options.requires } : {}),
|
|
633
|
+
instruction: {
|
|
634
|
+
async act(page, _input, mem) {
|
|
635
|
+
const url = typeof options.url === "function" ? options.url(mem) : options.url;
|
|
636
|
+
await page.goto(url);
|
|
637
|
+
},
|
|
638
|
+
resolve: () => checkpoint(options.checkpoint),
|
|
639
|
+
...(options.verify ? { verify: options.verify } : {}),
|
|
640
|
+
...(options.highlights ? { highlights: options.highlights } : {}),
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
// Non-enumerable so it never shows up in Object.keys/JSON/autocomplete -
|
|
644
|
+
// purely a runtime marker `waygraph check` (or anything else walking a
|
|
645
|
+
// project's Blocks) can test for to tell a NavBlock apart from a regular
|
|
646
|
+
// one, since the TypeScript type alone (NavBlock = DefinedBlock) doesn't
|
|
647
|
+
// survive to runtime.
|
|
648
|
+
Object.defineProperty(built, "__waygraphKind", {
|
|
649
|
+
value: "nav",
|
|
650
|
+
enumerable: false,
|
|
651
|
+
configurable: false,
|
|
652
|
+
});
|
|
653
|
+
return built;
|
|
654
|
+
}
|
|
571
655
|
/**
|
|
572
656
|
* Returns a new Block that runs exactly like `block` - same act/observe/resolve,
|
|
573
657
|
* same routing - but with `verify` replaced. For giving one flow stricter (or
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export type { Checkpoint, Instruction, Block, DefinedBlock, Start } from "./type
|
|
|
2
2
|
export { connect, checkpoint } from "./types.js";
|
|
3
3
|
export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
|
|
4
4
|
export { Trait, urlMatches, textEquals, visible } from "./trait.js";
|
|
5
|
-
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, composeBlock, preflight, withSessionReset, withTitle, chainFlow, } from "./engine.js";
|
|
6
|
-
export type { Flow, EngineConfig, ComposedBlock, RunGraphOptions, BlockInfo } from "./engine.js";
|
|
5
|
+
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, chainFlow, } from "./engine.js";
|
|
6
|
+
export type { Flow, EngineConfig, ComposedBlock, RunGraphOptions, BlockInfo, NavBlock } from "./engine.js";
|
|
7
|
+
export type { ActionPage } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { connect, checkpoint } from "./types.js";
|
|
2
2
|
export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
|
|
3
3
|
export { Trait, urlMatches, textEquals, visible } from "./trait.js";
|
|
4
|
-
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, composeBlock, preflight, withSessionReset, withTitle, chainFlow, } from "./engine.js";
|
|
4
|
+
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, composeBlock, preflight, withSessionReset, withTitle, withExpectedFailure, chainFlow, } from "./engine.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,29 @@ export declare function checkpoint<Tag extends string>(tag: Tag): Checkpoint<Tag
|
|
|
23
23
|
* @example export const LoginBlock: Block<Start, LoggedIn> = { ... }
|
|
24
24
|
*/
|
|
25
25
|
export type Start = Checkpoint<"__start__">;
|
|
26
|
+
/**
|
|
27
|
+
* The `page` a regular Block's `act()` is typed against via `defineBlock` -
|
|
28
|
+
* structurally identical to Playwright's own `Page` (the same real object is
|
|
29
|
+
* passed at runtime; this is a type-only narrowing), except `goto`, `reload`,
|
|
30
|
+
* `goBack`, and `goForward` are re-declared under `@deprecated`. Nothing is
|
|
31
|
+
* blocked - every method is still fully present and callable, and this never
|
|
32
|
+
* causes a build to fail - but a TypeScript-aware editor shows the call
|
|
33
|
+
* struck through with a hover warning the instant it's typed, pointing at
|
|
34
|
+
* `defineNavBlock` instead. `NavBlock`'s own generated `act()` (the one place
|
|
35
|
+
* navigation actually belongs) uses the real `Page`, not this type - that
|
|
36
|
+
* distinction lives entirely inside `defineNavBlock` and never reaches a
|
|
37
|
+
* NavBlock's own author, who never writes its `act()` by hand.
|
|
38
|
+
*/
|
|
39
|
+
export type ActionPage = Omit<Page, "goto" | "reload" | "goBack" | "goForward"> & {
|
|
40
|
+
/** @deprecated Navigation doesn't belong in a regular Block's act() - use defineNavBlock instead. */
|
|
41
|
+
goto: Page["goto"];
|
|
42
|
+
/** @deprecated Navigation doesn't belong in a regular Block's act() - use defineNavBlock instead. */
|
|
43
|
+
reload: Page["reload"];
|
|
44
|
+
/** @deprecated Navigation doesn't belong in a regular Block's act() - use defineNavBlock instead. */
|
|
45
|
+
goBack: Page["goBack"];
|
|
46
|
+
/** @deprecated Navigation doesn't belong in a regular Block's act() - use defineNavBlock instead. */
|
|
47
|
+
goForward: Page["goForward"];
|
|
48
|
+
};
|
|
26
49
|
/**
|
|
27
50
|
* The four-phase pipeline every Block runs. Most Blocks only need `act` + a
|
|
28
51
|
* one-line `resolve` - add `observe` only once a Block genuinely branches, add
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waygraph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
-
"bin"
|
|
19
|
+
"bin",
|
|
20
|
+
"templates"
|
|
20
21
|
],
|
|
21
22
|
"repository": {
|
|
22
23
|
"type": "git",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineBlock, checkpoint, Trait } from "waygraph";
|
|
2
|
+
import type { LoggedIn, ItemInCart } from "../../states/quickstart.states.js";
|
|
3
|
+
|
|
4
|
+
export const AddToCartBlock = defineBlock<LoggedIn, ItemInCart>({
|
|
5
|
+
name: "add-to-cart",
|
|
6
|
+
description: "Adds the Sauce Labs Backpack to the cart and confirms the cart badge shows 1 item.",
|
|
7
|
+
instruction: {
|
|
8
|
+
async act(page) {
|
|
9
|
+
await page.locator("#add-to-cart-sauce-labs-backpack").click();
|
|
10
|
+
},
|
|
11
|
+
resolve: () => checkpoint("ItemInCart"),
|
|
12
|
+
verify: [Trait.text(".shopping_cart_badge", "1")],
|
|
13
|
+
// Author-declared highlights - independent of verify's own pass/fail
|
|
14
|
+
// check. "Remember this" moments a step-through overlay can call out
|
|
15
|
+
// even when there's nothing to assert, just something worth pointing at.
|
|
16
|
+
highlights: () => [{ selector: ".shopping_cart_badge", label: "remember this: cart now has 1 item" }],
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineBlock, checkpoint, Trait } from "waygraph";
|
|
2
|
+
import type { ItemInCart, CheckoutInfoFilled } from "../../states/quickstart.states.js";
|
|
3
|
+
|
|
4
|
+
export const CheckoutInfoBlock = defineBlock<ItemInCart, CheckoutInfoFilled>({
|
|
5
|
+
name: "checkout-info",
|
|
6
|
+
description: "Goes to the cart, starts checkout, and fills in the shipping info form.",
|
|
7
|
+
instruction: {
|
|
8
|
+
async act(page) {
|
|
9
|
+
await page.locator(".shopping_cart_link").click();
|
|
10
|
+
await page.locator("#checkout").click();
|
|
11
|
+
await page.locator("#first-name").fill("Ada");
|
|
12
|
+
await page.locator("#last-name").fill("Lovelace");
|
|
13
|
+
await page.locator("#postal-code").fill("1000");
|
|
14
|
+
await page.locator("#continue").click();
|
|
15
|
+
},
|
|
16
|
+
resolve: () => checkpoint("CheckoutInfoFilled"),
|
|
17
|
+
verify: [Trait.url({ pathname: "/checkout-step-two.html" })],
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineBlock, checkpoint, Trait, narrate } from "waygraph";
|
|
2
|
+
import type { CheckoutInfoFilled, OrderComplete } from "../../states/quickstart.states.js";
|
|
3
|
+
|
|
4
|
+
export const FinishOrderBlock = defineBlock<CheckoutInfoFilled, OrderComplete>({
|
|
5
|
+
name: "finish-order",
|
|
6
|
+
description: "Places the order and confirms the 'Thank you for your order!' completion page.",
|
|
7
|
+
instruction: {
|
|
8
|
+
async act(page) {
|
|
9
|
+
// narrate() captions an explicit action for a step-through overlay,
|
|
10
|
+
// instead of it guessing a generic label from the element's own text.
|
|
11
|
+
const finishButton = page.locator("#finish");
|
|
12
|
+
await narrate(finishButton, "Placing the order", () => finishButton.click());
|
|
13
|
+
},
|
|
14
|
+
resolve: () => checkpoint("OrderComplete"),
|
|
15
|
+
verify: [Trait.text(".complete-header", "Thank you for your order!")],
|
|
16
|
+
highlights: () => [{ selector: ".complete-header", label: "remember this: order confirmed" }],
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineBlock, checkpoint, Trait } from "waygraph";
|
|
2
|
+
import type { LoginPage, LoggedIn } from "../../states/quickstart.states.js";
|
|
3
|
+
|
|
4
|
+
// Credentials hardcoded on purpose, to keep this first look at waygraph
|
|
5
|
+
// self-contained - no setup step, nothing to type before you can watch it
|
|
6
|
+
// run. A real project reaches for a MemKey/keyGroup instead (see waygraph's
|
|
7
|
+
// own README, or projects_waygraph/saucedemo in this same workspace) so
|
|
8
|
+
// different scenarios (an Owner vs. a blocked Viewer, say) can hand a Flow
|
|
9
|
+
// different credentials at run time without editing the Block itself.
|
|
10
|
+
export const SubmitLoginActionBlock = defineBlock<LoginPage, LoggedIn>({
|
|
11
|
+
name: "submit-login",
|
|
12
|
+
description: "Logs a standard user into Swag Labs and confirms the inventory page loads.",
|
|
13
|
+
instruction: {
|
|
14
|
+
async act(page) {
|
|
15
|
+
await page.locator("#user-name").fill("standard_user");
|
|
16
|
+
await page.locator("#password").fill("secret_sauce");
|
|
17
|
+
await page.locator("#login-button").click();
|
|
18
|
+
},
|
|
19
|
+
resolve: () => checkpoint("LoggedIn"),
|
|
20
|
+
verify: [Trait.url({ pathname: "/inventory.html" })],
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineNavBlock, Trait } from "waygraph";
|
|
2
|
+
import type { LoginPage } from "../states/quickstart.states.js";
|
|
3
|
+
|
|
4
|
+
// Navigation lives in its own Block, built by defineNavBlock - its act() is
|
|
5
|
+
// ALWAYS exactly page.goto(url), generated for you, never hand-written. A
|
|
6
|
+
// regular defineBlock() Block that calls page.goto/reload/goBack/goForward
|
|
7
|
+
// instead gets a struck-through @deprecated warning in your editor, pointing
|
|
8
|
+
// back here - see waygraph's ActionPage type and `waygraph check`.
|
|
9
|
+
export const NavLoginBlock = defineNavBlock<LoginPage>({
|
|
10
|
+
name: "nav-login",
|
|
11
|
+
description: "Goes to the Swag Labs login page.",
|
|
12
|
+
checkpoint: "LoginPage",
|
|
13
|
+
url: "/",
|
|
14
|
+
verify: [Trait.visible("#login-button")],
|
|
15
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Engine, start, end, withTitle } from "waygraph";
|
|
2
|
+
import { NavLoginBlock } from "../blocks/nav-login.block.js";
|
|
3
|
+
import { SubmitLoginActionBlock } from "../blocks/actions/submit-login.action.block.js";
|
|
4
|
+
import { AddToCartBlock } from "../blocks/actions/add-to-cart.action.block.js";
|
|
5
|
+
import { CheckoutInfoBlock } from "../blocks/actions/checkout-info.action.block.js";
|
|
6
|
+
import { FinishOrderBlock } from "../blocks/actions/finish-order.action.block.js";
|
|
7
|
+
|
|
8
|
+
const engine = new Engine();
|
|
9
|
+
|
|
10
|
+
// This is it - a whole real waygraph Flow, five Blocks wide, generated by
|
|
11
|
+
// `waygraph try` but otherwise no different from a Flow you'd hand-write:
|
|
12
|
+
// nav-* Blocks (defineNavBlock) navigate, action-* Blocks (defineBlock) do
|
|
13
|
+
// everything else, connect() (here, defineFlow's own array form) chains
|
|
14
|
+
// them into one typed graph, each Block's own Trait verify confirms it
|
|
15
|
+
// actually landed where it says it did.
|
|
16
|
+
export const quickstartFlow = withTitle(
|
|
17
|
+
engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, AddToCartBlock, CheckoutInfoBlock, FinishOrderBlock, end]),
|
|
18
|
+
"waygraph quickstart - swag labs demo",
|
|
19
|
+
);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Checkpoint } from "waygraph";
|
|
2
|
+
|
|
3
|
+
// One Checkpoint per real state the target app can be in - the whole shape
|
|
4
|
+
// of this quickstart's Flow, readable in one place before you ever open a
|
|
5
|
+
// Block file. `Start` is waygraph's own exported type, not redeclared here.
|
|
6
|
+
|
|
7
|
+
export type LoginPage = Checkpoint<"LoginPage">;
|
|
8
|
+
export type LoggedIn = Checkpoint<"LoggedIn">;
|
|
9
|
+
export type ItemInCart = Checkpoint<"ItemInCart">;
|
|
10
|
+
export type CheckoutInfoFilled = Checkpoint<"CheckoutInfoFilled">;
|
|
11
|
+
export type OrderComplete = Checkpoint<"OrderComplete">;
|