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/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 = LAUNCHERS[browserName];
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 = LAUNCHERS[browserName];
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.4.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">;