waygraph 0.15.17 → 0.15.19

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 CHANGED
@@ -298,10 +298,26 @@ code, an autonomous agent authoring Blocks) - it warns, never fails the process.
298
298
 
299
299
  **`waygraph typecheck [project]`** runs `tsc --noEmit` then the same **bad-practice** scan
300
300
  (wildcard `Checkpoint<string>`, assert blocks missing type args, multi-`.fill()` Methods,
301
- fill+click in one Method). Practices warn only unless `tsc` fails; use `--no-practices` to
302
- skip the scan. Opt out per file: `// waygraph-ignore-practices` or
303
- `// waygraph-ignore: multi-input, combined-action`. Print full agent guidance:
304
- `waygraph --skill-convention`.
301
+ fill+click in one Method, empty `verify` on a transition Block or `defineAssertBlock`).
302
+ Practices warn only unless `tsc` fails; use `--no-practices` to skip the scan. Opt out per
303
+ file: `// waygraph-ignore-practices` or `// waygraph-ignore: multi-input, combined-action,
304
+ empty-verify`. Print full agent guidance: `waygraph --skill-convention`.
305
+
306
+ **Empty verify is the one that matters most for codegen'd Blocks.** A framework can enforce
307
+ that a check *exists* and stays structurally honest (typed, wired, right file) - it cannot
308
+ author the semantic content of that check, since only someone who knows what "success" means
309
+ for this specific app can write it. That's a correct place to draw the line, but it shifts
310
+ all the risk onto whether anything catches a Block left with a placeholder `verify: []` by
311
+ the time it ships - which would otherwise pass every static check, the orphan scan, and
312
+ `tsc`, and run green in CI forever, because an empty array isn't a wrong check, it's the
313
+ *absence* of one. `waygraph check`/`typecheck` now flag exactly that: a `defineMethodBlock`/
314
+ `defineEffectBlock` whose `In`/`Out` Checkpoints actually differ (a real transition, not a
315
+ self-loop like `fill-username`) with no `verify` key or a literal `verify: []`, and the same
316
+ for `defineAssertBlock` (whose entire job is checking). Silence it deliberately, the same as
317
+ any other practice warning, with `// waygraph-ignore: empty-verify` - e.g. when a downstream
318
+ `defineAssertBlock` right after it in the `.flow.ts` is what actually confirms the transition
319
+ (see `templates/scaffold`'s own `open-message.method.block.ts`, confirmed by
320
+ `AssertEmailContentBlock` right after it, not by its own `verify`).
305
321
 
306
322
  ### Demo narration
307
323
 
@@ -412,6 +428,36 @@ engine.defineFlow([start, NavHomeBlock, AddItemBlock, AssertItemAddedBlock, Clea
412
428
  Live reference: `templates/scaffold/src/map/(app_base)/home/_methods/assert-item-added.method.block.ts`,
413
429
  wired into `shop.flow.ts`.
414
430
 
431
+ **An assert Block also names a feature *state*, not just a result or static content.** A
432
+ submit button greyed out until required fields are filled, a "Save" button re-enabling
433
+ after a successful edit, a field-level validation error appearing under one specific input
434
+ - these are conditions the UI is *in*, worth a nameable Checkpoint of their own the same way
435
+ `AssertItemAddedBlock` names "did add-item work." There's no built-in `Trait.disabled` -
436
+ write the state check as a custom Trait against the real DOM condition (`isDisabled()`, a
437
+ class, an `aria-disabled` attribute - whatever the app actually uses), the same pattern as
438
+ `removeButtonVisibleForSelectedItem` above:
439
+
440
+ ```typescript
441
+ const submitGatedUntilFieldsComplete: Trait = {
442
+ name: "submit-gated-until-fields-complete",
443
+ async check(page) {
444
+ return page.locator(CheckoutSel.submitButton).isDisabled();
445
+ },
446
+ };
447
+
448
+ export const AssertSubmitGatedBlock = defineAssertBlock<CheckoutInfoPage>({
449
+ name: "assert-submit-gated",
450
+ checkpoint: "CheckoutInfoPage",
451
+ verify: [submitGatedUntilFieldsComplete],
452
+ });
453
+
454
+ // wired right after the fields that gate it, before the ones that clear the gate:
455
+ engine.defineFlow([start, NavCheckoutInfoBlock, AssertSubmitGatedBlock, FillFirstNameBlock, ...])
456
+ ```
457
+
458
+ Same rule as any other assert Block: give it an explicit type argument when it sits
459
+ mid-chain (see above), and keep the selector in a `*Sel` object, never inlined in the Trait.
460
+
415
461
  **`methods/` folder:** on-page work lives next to the route, not free-floating.
416
462
 
417
463
  **`*Sel`:** DOM selectors only (static strings + `(id) => ...` for item-no-1 style). Mem
@@ -748,6 +794,22 @@ actually shows. Doesn't crash or corrupt anything if you never need it - it's a
748
794
  gap (confirmed by reading the exact code path, `src/auto-session.ts`'s `currentMenu()`), fixed
749
795
  outright rather than left as a caveat.
750
796
 
797
+ **Coverage gaps `waygraph check`/`graph` can't see, because there's no Block to scan yet.**
798
+ Static tools only find gaps in Blocks that already exist - a real link or button on the live
799
+ page with *no Block written for it at all* has nothing in source to catch it. Every live
800
+ `auto`/`browser`/`pilot` session watches the real page instead and warns (once per element,
801
+ via `console.warn`) on a same-origin `<a href>` no `defineNavBlock` covers, or a visible
802
+ `button`/`[role=button]`/submit input no Method/NavClick Block selector matches:
803
+
804
+ ```text
805
+ [waygraph] unmapped nav link on this page: /settings - no NavBlock covers this URL; add defineNavBlock
806
+ [waygraph] unmapped button on this page: #save-draft - no Method/NavClick Block selector matches; consider defineMethodBlock or defineNavClickBlock
807
+ ```
808
+
809
+ Read them back with `auto console <sessionId>` - the same channel real console/network errors
810
+ already come through, not a separate command (`src/coverage-gap.ts`, wired into every session
811
+ in `src/auto-session.ts`).
812
+
751
813
  **On-page indicator.** A headful (`--non-headless`) session looks like an ordinary browser
752
814
  tab otherwise - no sign anything is driving it, which matters once a human might be watching
753
815
  or co-driving the same visible session. Every session injects a small badge (bottom-right
@@ -952,25 +1014,22 @@ really a Nav/Page/Assert/Method from this package's own factories.
952
1014
  |------|---------|
953
1015
  | Watch Sauce Demo step-through (temp dir only) | `npx waygraph try demo` |
954
1016
  | Interactive explore (Add/Remove/Open details from live page) | `cd examples/saucedemo && npm i && npx waygraph auto` |
955
- | Scaffold a new **offline** project (green `npm test` on local fixture `:4177`) | `npx create-waygraph my-app` **or** `npx waygraph init my-app` |
1017
+ | Scaffold a new **offline** project (green `npm test` on local fixture `:4177`) | `npx waygraph init my-app` |
956
1018
  | Install coding-agent defs for diving an app into Blocks | `npx waygraph agent-dive --loop claude` (also `opencode` / `cursor` / `vscode`) |
957
1019
  | Print a packaged agent skill to stdout | `npx waygraph --skill` (list) / `--skill-pilot` / `--skill-pilot-blind` / `--skill-convention` |
958
1020
  | Add waygraph to an existing repo | `npm install waygraph @playwright/test` |
959
1021
 
960
- **Scaffold is not hidden inside waygraph alone** - the offline starter also ships as
961
- [`create-waygraph`](https://github.com/deviate-dv8/create-waygraph) on npm (same template
962
- as `waygraph init` since 0.7.5). Use whichever entry you already have:
963
- `npx create-waygraph my-app` when reading the GitHub Pages docs, or `npx waygraph init my-app`
964
- when the CLI is already installed. After either:
965
-
966
1022
  ```bash
1023
+ npx waygraph init my-app
967
1024
  cd my-app && npm install && npx playwright install chromium && npm test
968
1025
  ```
969
1026
 
970
1027
  `try demo` is different: live saucedemo.com, step-through **Sign In -> Shop & Checkout ->
971
1028
  blocked Viewer login**, then headless tests. Permanent full example:
972
- [`examples/saucedemo`](./examples/saucedemo). Offline empty scaffold: `init` /
973
- `create-waygraph`.
1029
+ [`examples/saucedemo`](./examples/saucedemo). Offline empty scaffold: `init`.
1030
+
1031
+ **`create-waygraph` (npm) is deprecated** - it was the identical `init` template shipped as
1032
+ a second package for no real benefit. Use `npx waygraph init my-app` instead.
974
1033
 
975
1034
  ```bash
976
1035
  # From this checkout, after npm run build:
@@ -1044,8 +1103,8 @@ suite - it forwards to the local `node_modules/.bin/playwright` (falling back to
1044
1103
  `npx playwright` if the project hasn't installed it locally yet), so `waygraph test` behaves
1045
1104
  exactly like `playwright test`, and anything after it (`--grep`, a spec path, `--headed`, …)
1046
1105
  passes straight through. `waygraph test ui` (or `waygraph test --ui`) launches Playwright's
1047
- interactive UI Mode the same way. Scaffolded projects (`waygraph init`/`create-waygraph`)
1048
- wire this up as `npm test` / `npm run test:ui`, and ship `trace: "retain-on-failure"` plus
1106
+ interactive UI Mode the same way. Scaffolded projects (`waygraph init`) wire this up as
1107
+ `npm test` / `npm run test:ui`, and ship `trace: "retain-on-failure"` plus
1049
1108
  the `html` reporter in `playwright.config.ts`, so a failing `waygraph test` already has a
1050
1109
  trace waiting - `waygraph test report` opens the HTML report, and
1051
1110
  `waygraph test show-trace <trace.zip>` opens one trace directly (both forward to Playwright's
@@ -1084,6 +1143,8 @@ Flows are files (0.10.5+):
1084
1143
  | Scope either to one element | `waygraph auto dom <sessionId> --selector ".inventory_list"` |
1085
1144
  | Detached session with a real visible browser | `waygraph auto --cli --detach --non-headless` |
1086
1145
  | Read the session's Checkpoint/Block history | `waygraph auto trace <sessionId>` |
1146
+ | Read real console/network errors + coverage-gap warnings | `waygraph auto console <sessionId>` |
1147
+ | Read localStorage/sessionStorage + service workers | `waygraph auto storage <sessionId>` |
1087
1148
  | Whole-project nav-escape + inline-selector + orphan sweep | `waygraph check [project]` |
1088
1149
  | `tsc --noEmit` + bad-practice warnings | `waygraph typecheck [project]` (`--no-practices` to skip scan) |
1089
1150
  | Waygraph Map enforcement (verbatim url-vs-folder, exits 1 on a violation) | `waygraph map [project]` |
@@ -1273,7 +1334,7 @@ src/
1273
1334
  index.ts public barrel
1274
1335
  examples/saucedemo/ live Sauce Demo (Page + methods/ + Sel)
1275
1336
  templates/quickstart/ try demo / init template (mirrors sauce)
1276
- templates/scaffold/ offline init/create-waygraph scaffold template
1337
+ templates/scaffold/ offline waygraph init scaffold template
1277
1338
  templates/agents/ waygraph agent-dive coding-agent definitions
1278
1339
  docs/ GitHub Pages static HTML + docs/proposals/ RFCs
1279
1340
  openspec/ spec-driven planning: specs/ (current), changes/ (in-flight),
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { Checkpoint, Instruction, Block, DefinedBlock, Start, WaygraphInstanceOption } from "./types.js";
2
2
  export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
- export { Trait, urlMatches, textEquals, visible, frameVisible, frameTextEquals, frameContainsText } from "./trait.js";
4
+ export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
5
5
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
6
6
  export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
7
7
  export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, WaygraphTodosOpts, TodoDockUiOpts, TodoDockUiResolved, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, DevicePreset, DeviceViewport, DeviceState, DeviceOrientation, } from "./highlights.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
- export { Trait, urlMatches, textEquals, visible, frameVisible, frameTextEquals, frameContainsText } from "./trait.js";
3
+ export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
4
4
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
5
5
  export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, resolveTodoDockUi, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
6
6
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
@@ -1,4 +1,4 @@
1
- export type PracticeKind = "wildcard-checkpoint" | "assert-no-type-arg" | "multi-input" | "combined-action";
1
+ export type PracticeKind = "wildcard-checkpoint" | "assert-no-type-arg" | "multi-input" | "combined-action" | "empty-verify";
2
2
  export interface PracticeWarning {
3
3
  kind: PracticeKind;
4
4
  file: string;
@@ -9,6 +9,19 @@ import { relative } from "node:path";
9
9
  const WILDCARD_CHECKPOINT = /\bdefine(?:Method|Effect|Page|Nav|MemNav|NavClick|Action)Block\s*<[^>]*\bCheckpoint\s*<\s*string\s*>/;
10
10
  /** defineAssertBlock({ … }) with no explicit Out type — defaults to wildcard. */
11
11
  const ASSERT_NO_TYPE_ARG = /\bdefineAssertBlock\s*\(\s*\{/;
12
+ /** defineAssertBlock, any form - used to scope the empty-verify check below. */
13
+ const IS_ASSERT_BLOCK = /\bdefineAssertBlock\s*[<(]/;
14
+ /**
15
+ * define(Method|Effect)Block<In, Out> with two BARE identifier type args
16
+ * (never a nested generic like Checkpoint<string> - that's WILDCARD_CHECKPOINT's
17
+ * own warning) - lets a real transition (In !== Out) be told apart from a
18
+ * self-loop (In === Out, e.g. fill-username) using only source text.
19
+ */
20
+ const METHOD_EFFECT_GENERIC = /\bdefine(?:Method|Effect)Block\s*<\s*([A-Za-z_$][\w$]*)\s*,\s*([A-Za-z_$][\w$]*)\s*>/;
21
+ /** `verify: []` - a literal empty array, the codegen-stub shape this check exists for. */
22
+ const EMPTY_VERIFY_ARRAY = /\bverify\s*:\s*\[\s*\]/;
23
+ /** Any `verify:` key at all, empty or not. */
24
+ const HAS_VERIFY_KEY = /\bverify\s*:/;
12
25
  const SCAN_GLOB = /\.(block|flow)\.ts$/;
13
26
  const METHOD_OR_EFFECT_FILE = /\.(method|effect)\.block\.ts$/;
14
27
  const METHOD_OR_EFFECT_DEFINE = /\bdefine(?:Method|Effect)Block\b/;
@@ -80,6 +93,27 @@ export function collectPracticeWarnings(projectDir, walkDir, opts) {
80
93
  detail: "Method/Effect act() mixes .fill() and .click() — fill and submit must be separate Blocks (see examples/saucedemo login split)",
81
94
  });
82
95
  }
96
+ const genericMatch = METHOD_EFFECT_GENERIC.exec(src);
97
+ const isTransition = !!genericMatch && genericMatch[1] !== genericMatch[2];
98
+ if (isTransition &&
99
+ !isIgnored("empty-verify", ignore) &&
100
+ (!HAS_VERIFY_KEY.test(src) || EMPTY_VERIFY_ARRAY.test(src))) {
101
+ warnings.push({
102
+ kind: "empty-verify",
103
+ file: rel,
104
+ detail: `transition Block (${genericMatch[1]} → ${genericMatch[2]}) has no verify checks — resolve() moving ` +
105
+ "to a new Checkpoint is never confirmed by anything; add a Trait (or defineAssertBlock right after it), " +
106
+ "or opt out explicitly with // waygraph-ignore: empty-verify if that's intentional",
107
+ });
108
+ }
109
+ }
110
+ if (!isIgnored("empty-verify", ignore) && IS_ASSERT_BLOCK.test(src) && EMPTY_VERIFY_ARRAY.test(src)) {
111
+ warnings.push({
112
+ kind: "empty-verify",
113
+ file: rel,
114
+ detail: "defineAssertBlock's verify array is empty — an assert Block whose only job is checking will pass " +
115
+ "trivially forever; fill in real Traits or remove the stub",
116
+ });
83
117
  }
84
118
  }
85
119
  return warnings;
@@ -94,6 +128,8 @@ export function practiceKindLabel(kind) {
94
128
  return "multiple inputs in one Block";
95
129
  case "combined-action":
96
130
  return "fill and click in one Block";
131
+ case "empty-verify":
132
+ return "empty verify — transition unconfirmed";
97
133
  default: {
98
134
  const _exhaustive = kind;
99
135
  return _exhaustive;
package/dist/trait.d.ts CHANGED
@@ -40,6 +40,20 @@ export declare function textEquals(selector: string, expected: string): Trait;
40
40
  * "something matching is visible", not "the selector is unambiguous".
41
41
  */
42
42
  export declare function visible(selector: string): Trait;
43
+ /**
44
+ * Passes when at least one element matching `selector` is currently disabled -
45
+ * a submit button greyed out until required fields are filled, a "Save" not
46
+ * yet re-enabled after a pending edit. A real, nameable feature *state*, not
47
+ * just static content or an action's result. Uses `.first()` for the same
48
+ * reason `visible` does. A point-in-time read via Playwright's own
49
+ * `isDisabled()` - no wait built in, so pair it with `Trait.visible` (or rely
50
+ * on `verify`'s own natural timing right after `resolve`) if the element
51
+ * itself might not be on the page yet.
52
+ * @example Trait.disabled(CheckoutSel.submitButton)
53
+ */
54
+ export declare function disabled(selector: string): Trait;
55
+ /** The complement of {@link disabled} - passes once `selector` is enabled. */
56
+ export declare function enabled(selector: string): Trait;
43
57
  /**
44
58
  * `visible`, scoped to a frame (e.g. a mail catcher's message-preview iframe)
45
59
  * instead of the top-level page. `page.locator(selector)` alone never reaches
@@ -73,6 +87,8 @@ export declare const Trait: {
73
87
  url: typeof urlMatches;
74
88
  text: typeof textEquals;
75
89
  visible: typeof visible;
90
+ disabled: typeof disabled;
91
+ enabled: typeof enabled;
76
92
  frameVisible: typeof frameVisible;
77
93
  frameText: typeof frameTextEquals;
78
94
  frameContains: typeof frameContainsText;
package/dist/trait.js CHANGED
@@ -65,6 +65,34 @@ export function visible(selector) {
65
65
  },
66
66
  };
67
67
  }
68
+ /**
69
+ * Passes when at least one element matching `selector` is currently disabled -
70
+ * a submit button greyed out until required fields are filled, a "Save" not
71
+ * yet re-enabled after a pending edit. A real, nameable feature *state*, not
72
+ * just static content or an action's result. Uses `.first()` for the same
73
+ * reason `visible` does. A point-in-time read via Playwright's own
74
+ * `isDisabled()` - no wait built in, so pair it with `Trait.visible` (or rely
75
+ * on `verify`'s own natural timing right after `resolve`) if the element
76
+ * itself might not be on the page yet.
77
+ * @example Trait.disabled(CheckoutSel.submitButton)
78
+ */
79
+ export function disabled(selector) {
80
+ return {
81
+ name: `disabled(${selector})`,
82
+ async check(page) {
83
+ return page.locator(selector).first().isDisabled();
84
+ },
85
+ };
86
+ }
87
+ /** The complement of {@link disabled} - passes once `selector` is enabled. */
88
+ export function enabled(selector) {
89
+ return {
90
+ name: `enabled(${selector})`,
91
+ async check(page) {
92
+ return page.locator(selector).first().isEnabled();
93
+ },
94
+ };
95
+ }
68
96
  /**
69
97
  * `visible`, scoped to a frame (e.g. a mail catcher's message-preview iframe)
70
98
  * instead of the top-level page. `page.locator(selector)` alone never reaches
@@ -121,6 +149,8 @@ export const Trait = {
121
149
  url: urlMatches,
122
150
  text: textEquals,
123
151
  visible,
152
+ disabled,
153
+ enabled,
124
154
  frameVisible,
125
155
  frameText: frameTextEquals,
126
156
  frameContains: frameContainsText,
File without changes
@@ -0,0 +1,8 @@
1
+ {
2
+ "sessionId": "9f0287e6",
3
+ "socketPath": "/tmp/waygraph-auto/9f0287e6.sock",
4
+ "pid": 2653914,
5
+ "projectDir": "/home/dan/Desktop/Projects/waygraph/examples/saucedemo",
6
+ "startedAt": "2026-09-23T01:33:06.508Z",
7
+ "headless": false
8
+ }
File without changes
@@ -16,29 +16,424 @@
16
16
  "typescript": "^5.6.0"
17
17
  }
18
18
  },
19
- "../..": {
20
- "version": "0.12.47",
19
+ "node_modules/@esbuild/aix-ppc64": {
20
+ "version": "0.28.2",
21
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
22
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
23
+ "cpu": [
24
+ "ppc64"
25
+ ],
21
26
  "license": "MIT",
22
- "dependencies": {
23
- "tsx": "^4.0.0"
24
- },
25
- "bin": {
26
- "waygraph": "bin/waygraph"
27
- },
28
- "devDependencies": {
29
- "@playwright/test": "^1.63.0",
30
- "@types/node": "^26.0.0",
31
- "typescript": "^5.6.0"
32
- },
33
- "peerDependencies": {
34
- "@playwright/test": "^1.40.0"
27
+ "optional": true,
28
+ "os": [
29
+ "aix"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ }
34
+ },
35
+ "node_modules/@esbuild/android-arm": {
36
+ "version": "0.28.2",
37
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
38
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
39
+ "cpu": [
40
+ "arm"
41
+ ],
42
+ "license": "MIT",
43
+ "optional": true,
44
+ "os": [
45
+ "android"
46
+ ],
47
+ "engines": {
48
+ "node": ">=18"
49
+ }
50
+ },
51
+ "node_modules/@esbuild/android-arm64": {
52
+ "version": "0.28.2",
53
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
54
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
55
+ "cpu": [
56
+ "arm64"
57
+ ],
58
+ "license": "MIT",
59
+ "optional": true,
60
+ "os": [
61
+ "android"
62
+ ],
63
+ "engines": {
64
+ "node": ">=18"
65
+ }
66
+ },
67
+ "node_modules/@esbuild/android-x64": {
68
+ "version": "0.28.2",
69
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
70
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
71
+ "cpu": [
72
+ "x64"
73
+ ],
74
+ "license": "MIT",
75
+ "optional": true,
76
+ "os": [
77
+ "android"
78
+ ],
79
+ "engines": {
80
+ "node": ">=18"
81
+ }
82
+ },
83
+ "node_modules/@esbuild/darwin-arm64": {
84
+ "version": "0.28.2",
85
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
86
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
87
+ "cpu": [
88
+ "arm64"
89
+ ],
90
+ "license": "MIT",
91
+ "optional": true,
92
+ "os": [
93
+ "darwin"
94
+ ],
95
+ "engines": {
96
+ "node": ">=18"
97
+ }
98
+ },
99
+ "node_modules/@esbuild/darwin-x64": {
100
+ "version": "0.28.2",
101
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
102
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
103
+ "cpu": [
104
+ "x64"
105
+ ],
106
+ "license": "MIT",
107
+ "optional": true,
108
+ "os": [
109
+ "darwin"
110
+ ],
111
+ "engines": {
112
+ "node": ">=18"
113
+ }
114
+ },
115
+ "node_modules/@esbuild/freebsd-arm64": {
116
+ "version": "0.28.2",
117
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
118
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
119
+ "cpu": [
120
+ "arm64"
121
+ ],
122
+ "license": "MIT",
123
+ "optional": true,
124
+ "os": [
125
+ "freebsd"
126
+ ],
127
+ "engines": {
128
+ "node": ">=18"
129
+ }
130
+ },
131
+ "node_modules/@esbuild/freebsd-x64": {
132
+ "version": "0.28.2",
133
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
134
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
135
+ "cpu": [
136
+ "x64"
137
+ ],
138
+ "license": "MIT",
139
+ "optional": true,
140
+ "os": [
141
+ "freebsd"
142
+ ],
143
+ "engines": {
144
+ "node": ">=18"
145
+ }
146
+ },
147
+ "node_modules/@esbuild/linux-arm": {
148
+ "version": "0.28.2",
149
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
150
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
151
+ "cpu": [
152
+ "arm"
153
+ ],
154
+ "license": "MIT",
155
+ "optional": true,
156
+ "os": [
157
+ "linux"
158
+ ],
159
+ "engines": {
160
+ "node": ">=18"
161
+ }
162
+ },
163
+ "node_modules/@esbuild/linux-arm64": {
164
+ "version": "0.28.2",
165
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
166
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
167
+ "cpu": [
168
+ "arm64"
169
+ ],
170
+ "license": "MIT",
171
+ "optional": true,
172
+ "os": [
173
+ "linux"
174
+ ],
175
+ "engines": {
176
+ "node": ">=18"
177
+ }
178
+ },
179
+ "node_modules/@esbuild/linux-ia32": {
180
+ "version": "0.28.2",
181
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
182
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
183
+ "cpu": [
184
+ "ia32"
185
+ ],
186
+ "license": "MIT",
187
+ "optional": true,
188
+ "os": [
189
+ "linux"
190
+ ],
191
+ "engines": {
192
+ "node": ">=18"
193
+ }
194
+ },
195
+ "node_modules/@esbuild/linux-loong64": {
196
+ "version": "0.28.2",
197
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
198
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
199
+ "cpu": [
200
+ "loong64"
201
+ ],
202
+ "license": "MIT",
203
+ "optional": true,
204
+ "os": [
205
+ "linux"
206
+ ],
207
+ "engines": {
208
+ "node": ">=18"
209
+ }
210
+ },
211
+ "node_modules/@esbuild/linux-mips64el": {
212
+ "version": "0.28.2",
213
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
214
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
215
+ "cpu": [
216
+ "mips64el"
217
+ ],
218
+ "license": "MIT",
219
+ "optional": true,
220
+ "os": [
221
+ "linux"
222
+ ],
223
+ "engines": {
224
+ "node": ">=18"
225
+ }
226
+ },
227
+ "node_modules/@esbuild/linux-ppc64": {
228
+ "version": "0.28.2",
229
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
230
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
231
+ "cpu": [
232
+ "ppc64"
233
+ ],
234
+ "license": "MIT",
235
+ "optional": true,
236
+ "os": [
237
+ "linux"
238
+ ],
239
+ "engines": {
240
+ "node": ">=18"
241
+ }
242
+ },
243
+ "node_modules/@esbuild/linux-riscv64": {
244
+ "version": "0.28.2",
245
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
246
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
247
+ "cpu": [
248
+ "riscv64"
249
+ ],
250
+ "license": "MIT",
251
+ "optional": true,
252
+ "os": [
253
+ "linux"
254
+ ],
255
+ "engines": {
256
+ "node": ">=18"
257
+ }
258
+ },
259
+ "node_modules/@esbuild/linux-s390x": {
260
+ "version": "0.28.2",
261
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
262
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
263
+ "cpu": [
264
+ "s390x"
265
+ ],
266
+ "license": "MIT",
267
+ "optional": true,
268
+ "os": [
269
+ "linux"
270
+ ],
271
+ "engines": {
272
+ "node": ">=18"
273
+ }
274
+ },
275
+ "node_modules/@esbuild/linux-x64": {
276
+ "version": "0.28.2",
277
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
278
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
279
+ "cpu": [
280
+ "x64"
281
+ ],
282
+ "license": "MIT",
283
+ "optional": true,
284
+ "os": [
285
+ "linux"
286
+ ],
287
+ "engines": {
288
+ "node": ">=18"
289
+ }
290
+ },
291
+ "node_modules/@esbuild/netbsd-arm64": {
292
+ "version": "0.28.2",
293
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
294
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
295
+ "cpu": [
296
+ "arm64"
297
+ ],
298
+ "license": "MIT",
299
+ "optional": true,
300
+ "os": [
301
+ "netbsd"
302
+ ],
303
+ "engines": {
304
+ "node": ">=18"
305
+ }
306
+ },
307
+ "node_modules/@esbuild/netbsd-x64": {
308
+ "version": "0.28.2",
309
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
310
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
311
+ "cpu": [
312
+ "x64"
313
+ ],
314
+ "license": "MIT",
315
+ "optional": true,
316
+ "os": [
317
+ "netbsd"
318
+ ],
319
+ "engines": {
320
+ "node": ">=18"
321
+ }
322
+ },
323
+ "node_modules/@esbuild/openbsd-arm64": {
324
+ "version": "0.28.2",
325
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
326
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
327
+ "cpu": [
328
+ "arm64"
329
+ ],
330
+ "license": "MIT",
331
+ "optional": true,
332
+ "os": [
333
+ "openbsd"
334
+ ],
335
+ "engines": {
336
+ "node": ">=18"
337
+ }
338
+ },
339
+ "node_modules/@esbuild/openbsd-x64": {
340
+ "version": "0.28.2",
341
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
342
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
343
+ "cpu": [
344
+ "x64"
345
+ ],
346
+ "license": "MIT",
347
+ "optional": true,
348
+ "os": [
349
+ "openbsd"
350
+ ],
351
+ "engines": {
352
+ "node": ">=18"
353
+ }
354
+ },
355
+ "node_modules/@esbuild/openharmony-arm64": {
356
+ "version": "0.28.2",
357
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
358
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
359
+ "cpu": [
360
+ "arm64"
361
+ ],
362
+ "license": "MIT",
363
+ "optional": true,
364
+ "os": [
365
+ "openharmony"
366
+ ],
367
+ "engines": {
368
+ "node": ">=18"
369
+ }
370
+ },
371
+ "node_modules/@esbuild/sunos-x64": {
372
+ "version": "0.28.2",
373
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
374
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
375
+ "cpu": [
376
+ "x64"
377
+ ],
378
+ "license": "MIT",
379
+ "optional": true,
380
+ "os": [
381
+ "sunos"
382
+ ],
383
+ "engines": {
384
+ "node": ">=18"
385
+ }
386
+ },
387
+ "node_modules/@esbuild/win32-arm64": {
388
+ "version": "0.28.2",
389
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
390
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
391
+ "cpu": [
392
+ "arm64"
393
+ ],
394
+ "license": "MIT",
395
+ "optional": true,
396
+ "os": [
397
+ "win32"
398
+ ],
399
+ "engines": {
400
+ "node": ">=18"
401
+ }
402
+ },
403
+ "node_modules/@esbuild/win32-ia32": {
404
+ "version": "0.28.2",
405
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
406
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
407
+ "cpu": [
408
+ "ia32"
409
+ ],
410
+ "license": "MIT",
411
+ "optional": true,
412
+ "os": [
413
+ "win32"
414
+ ],
415
+ "engines": {
416
+ "node": ">=18"
417
+ }
418
+ },
419
+ "node_modules/@esbuild/win32-x64": {
420
+ "version": "0.28.2",
421
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
422
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
423
+ "cpu": [
424
+ "x64"
425
+ ],
426
+ "license": "MIT",
427
+ "optional": true,
428
+ "os": [
429
+ "win32"
430
+ ],
431
+ "engines": {
432
+ "node": ">=18"
35
433
  }
36
434
  },
37
435
  "node_modules/@playwright/test": {
38
436
  "version": "1.63.0",
39
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
40
- "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
41
- "dev": true,
42
437
  "license": "Apache-2.0",
43
438
  "dependencies": {
44
439
  "playwright": "1.63.0"
@@ -52,19 +447,69 @@
52
447
  },
53
448
  "node_modules/@types/node": {
54
449
  "version": "26.6.1",
55
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.1.tgz",
56
- "integrity": "sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA==",
57
450
  "dev": true,
58
451
  "license": "MIT",
59
452
  "dependencies": {
60
453
  "undici-types": "~8.9.0"
61
454
  }
62
455
  },
456
+ "node_modules/esbuild": {
457
+ "version": "0.28.2",
458
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
459
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
460
+ "hasInstallScript": true,
461
+ "license": "MIT",
462
+ "bin": {
463
+ "esbuild": "bin/esbuild"
464
+ },
465
+ "engines": {
466
+ "node": ">=18"
467
+ },
468
+ "optionalDependencies": {
469
+ "@esbuild/aix-ppc64": "0.28.2",
470
+ "@esbuild/android-arm": "0.28.2",
471
+ "@esbuild/android-arm64": "0.28.2",
472
+ "@esbuild/android-x64": "0.28.2",
473
+ "@esbuild/darwin-arm64": "0.28.2",
474
+ "@esbuild/darwin-x64": "0.28.2",
475
+ "@esbuild/freebsd-arm64": "0.28.2",
476
+ "@esbuild/freebsd-x64": "0.28.2",
477
+ "@esbuild/linux-arm": "0.28.2",
478
+ "@esbuild/linux-arm64": "0.28.2",
479
+ "@esbuild/linux-ia32": "0.28.2",
480
+ "@esbuild/linux-loong64": "0.28.2",
481
+ "@esbuild/linux-mips64el": "0.28.2",
482
+ "@esbuild/linux-ppc64": "0.28.2",
483
+ "@esbuild/linux-riscv64": "0.28.2",
484
+ "@esbuild/linux-s390x": "0.28.2",
485
+ "@esbuild/linux-x64": "0.28.2",
486
+ "@esbuild/netbsd-arm64": "0.28.2",
487
+ "@esbuild/netbsd-x64": "0.28.2",
488
+ "@esbuild/openbsd-arm64": "0.28.2",
489
+ "@esbuild/openbsd-x64": "0.28.2",
490
+ "@esbuild/openharmony-arm64": "0.28.2",
491
+ "@esbuild/sunos-x64": "0.28.2",
492
+ "@esbuild/win32-arm64": "0.28.2",
493
+ "@esbuild/win32-ia32": "0.28.2",
494
+ "@esbuild/win32-x64": "0.28.2"
495
+ }
496
+ },
497
+ "node_modules/fsevents": {
498
+ "version": "2.3.3",
499
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
500
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
501
+ "hasInstallScript": true,
502
+ "license": "MIT",
503
+ "optional": true,
504
+ "os": [
505
+ "darwin"
506
+ ],
507
+ "engines": {
508
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
509
+ }
510
+ },
63
511
  "node_modules/playwright": {
64
512
  "version": "1.63.0",
65
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
66
- "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
67
- "dev": true,
68
513
  "license": "Apache-2.0",
69
514
  "dependencies": {
70
515
  "playwright-core": "1.63.0"
@@ -78,9 +523,6 @@
78
523
  },
79
524
  "node_modules/playwright-core": {
80
525
  "version": "1.63.0",
81
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
82
- "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
83
- "dev": true,
84
526
  "license": "Apache-2.0",
85
527
  "bin": {
86
528
  "playwright-core": "cli.js"
@@ -89,10 +531,26 @@
89
531
  "node": ">=20"
90
532
  }
91
533
  },
534
+ "node_modules/tsx": {
535
+ "version": "4.23.15",
536
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
537
+ "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
538
+ "license": "MIT",
539
+ "dependencies": {
540
+ "esbuild": "~0.28.0"
541
+ },
542
+ "bin": {
543
+ "tsx": "dist/cli.mjs"
544
+ },
545
+ "engines": {
546
+ "node": ">=18.0.0"
547
+ },
548
+ "optionalDependencies": {
549
+ "fsevents": "~2.3.3"
550
+ }
551
+ },
92
552
  "node_modules/typescript": {
93
553
  "version": "5.9.3",
94
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
95
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
96
554
  "dev": true,
97
555
  "license": "Apache-2.0",
98
556
  "bin": {
@@ -105,14 +563,22 @@
105
563
  },
106
564
  "node_modules/undici-types": {
107
565
  "version": "8.9.0",
108
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
109
- "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
110
566
  "dev": true,
111
567
  "license": "MIT"
112
568
  },
113
569
  "node_modules/waygraph": {
114
- "resolved": "../..",
115
- "link": true
570
+ "version": "0.15.17",
571
+ "resolved": "file:../..",
572
+ "license": "MIT",
573
+ "dependencies": {
574
+ "tsx": "^4.0.0"
575
+ },
576
+ "bin": {
577
+ "waygraph": "bin/waygraph"
578
+ },
579
+ "peerDependencies": {
580
+ "@playwright/test": "^1.40.0"
581
+ }
116
582
  }
117
583
  }
118
584
  }
@@ -5,7 +5,7 @@ import { dirname, join } from "node:path";
5
5
 
6
6
  const projectDir = join(dirname(fileURLToPath(import.meta.url)), "..");
7
7
 
8
- test("waygraph auto discovers the decomposed saucedemo graph with zero skipped Blocks", async () => {
8
+ test("waygraph auto discovers the decomposed saucedemo graph, skipping only the FFCompose auth step", async () => {
9
9
  const graph = await discoverGraph(projectDir);
10
10
 
11
11
  expect(graph.nodes.map((n) => n.checkpoint).sort()).toEqual(
@@ -20,8 +20,12 @@ test("waygraph auto discovers the decomposed saucedemo graph with zero skipped B
20
20
  "OrderComplete",
21
21
  ].sort(),
22
22
  );
23
- // +2 vs pre-remove: remove-from-cart branches * → ItemInCart | LoggedIn
24
- expect(graph.edges.length).toBe(15);
23
+ // 24 vs the earlier 15: checkout-info split into fill-first-name/
24
+ // fill-last-name/fill-postal-code (+2 net) plus the atomic login Blocks
25
+ // (fill-username/fill-password/submit-login's 2 branches) that static
26
+ // discovery still sees even though checkoutFlow itself now drives them
27
+ // through the ff-owner-auth composition below.
28
+ expect(graph.edges.length).toBe(24);
25
29
  const removeEdges = graph.edges.filter((e) => e.block === "remove-from-cart");
26
30
  expect(removeEdges).toEqual(
27
31
  expect.arrayContaining([
@@ -32,7 +36,15 @@ test("waygraph auto discovers the decomposed saucedemo graph with zero skipped B
32
36
 
33
37
  const loginFail = graph.edges.find((e) => e.block === "submit-login" && e.to === "LoginPage");
34
38
  expect(loginFail).toMatchObject({ from: "LoginPage", to: "LoginPage", kind: "action" });
35
- expect(graph.skipped.length).toBe(0);
39
+
40
+ // ff-owner-auth (fastForwardComposeBlock) collapses nav-login + fill-username
41
+ // + fill-password + submit-login into one opaque demo/run step - discoverGraph
42
+ // only understands plain defineMethodBlock/defineEffectBlock<In, Out> generic
43
+ // calls, so this one Block is expected to be skipped by static discovery even
44
+ // though it's a real, used step (see src/blocks/saucedemo-web/ff-owner-auth.block.ts).
45
+ expect(graph.skipped).toEqual([
46
+ expect.objectContaining({ block: "ff-owner-auth" }),
47
+ ]);
36
48
 
37
49
  const logout = graph.edges.find((e) => e.block === "submit-logout");
38
50
  expect(logout).toMatchObject({ from: "LoggedIn", to: "LoginPage", kind: "action" });
@@ -29,10 +29,15 @@ test("each episode's title survives chaining, and blocks() reports the flattened
29
29
 
30
30
  expect(blocks.map((b) => b.name)).toEqual([
31
31
  "nav-login",
32
+ "fill-username",
33
+ "fill-password",
32
34
  "submit-login",
33
35
  "add-to-cart",
34
36
  "nav-cart",
35
37
  "nav-checkout-info",
38
+ "fill-first-name",
39
+ "fill-last-name",
40
+ "fill-postal-code",
36
41
  "submit-checkout-info",
37
42
  "finish-order",
38
43
  ]);
@@ -1,7 +1,7 @@
1
1
  import { test, expect } from "@playwright/test";
2
2
  import { MemPage, checkpoint, Trait } from "waygraph";
3
3
  import { checkoutFlow } from "../src/flows/checkout.flow.js";
4
- import { SubmitLoginBlock } from "../src/blocks/saucedemo-web/methods/submit-login.method.block.js";
4
+ import { FfOwnerAuthBlock } from "../src/blocks/saucedemo-web/ff-owner-auth.block.js";
5
5
  import { LoginCreds, SelectedItem } from "../src/states/checkout.mem-keys.js";
6
6
 
7
7
  // checkoutFlow.withBlockVerify/modBlockVerify - a spec that only imports the
@@ -31,9 +31,12 @@ test("withBlockVerify: replacing login's verify with an extra real check still p
31
31
  mem.set(LoginCreds({ username: "standard_user", password: "secret_sauce" }));
32
32
  mem.set(SelectedItem({ id: "sauce-labs-backpack", name: "Sauce Labs Backpack" }));
33
33
 
34
- // Addressed by the actual SubmitLoginBlock reference - the preferred
35
- // form: no typo risk, refactor-safe, jump-to-definition works.
36
- const patched = checkoutFlow.withBlockVerify(SubmitLoginBlock, [
34
+ // Addressed by the actual FfOwnerAuthBlock reference - the preferred
35
+ // form: no typo risk, refactor-safe, jump-to-definition works. checkoutFlow
36
+ // drives login through the ff-owner-auth FFCompose step (collapsed
37
+ // nav-login + fill-username + fill-password + submit-login), not a bare
38
+ // SubmitLoginBlock reference, so that's the Block whose verify this patches.
39
+ const patched = checkoutFlow.withBlockVerify(FfOwnerAuthBlock, [
37
40
  Trait.url({ pathname: "/inventory.html" }),
38
41
  Trait.visible(".inventory_list"),
39
42
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.17",
3
+ "version": "0.15.19",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -1,3 +1,4 @@
1
+ // waygraph-ignore: empty-verify (confirmed by AssertEmailContentBlock right after it in mail-verify.flow.ts)
1
2
  import { defineMethodBlock, checkpoint } from "waygraph";
2
3
  import type { MailpitInbox, MailpitMessageOpen } from "../../../../states/demo.states.js";
3
4
  import { ExpectedRecipient } from "../../../../states/demo.mem-keys.js";
@@ -22,6 +22,11 @@ against a live site, use `--skill-pilot-blind`.
22
22
  inline selector literals in `Trait.visible` / `Trait.text`.
23
23
  4. **Assert-only = `defineAssertBlock`.** Self-loop, no hand-written `act`/`resolve`. Pass
24
24
  an explicit type argument when it sits between two specifically-typed Blocks in a Flow.
25
+ Not just static content or an action's result - it also names a *feature state* worth a
26
+ Checkpoint of its own (a submit button greyed out until required fields are filled, a
27
+ field error under one input, a stale "Save" re-enabling). `Trait.disabled(selector)` /
28
+ `Trait.enabled(selector)` cover that directly; write a custom Trait only for a state
29
+ condition neither one expresses.
25
30
  5. **`requires` = externally supplied.** A key an earlier Block in the same chain produces
26
31
  is never listed under `requires` - preflight checks the whole chain up front.
27
32
  6. **Keep Checkpoints typed — never `Checkpoint<string>`.** Block helpers take concrete
@@ -32,6 +37,16 @@ against a live site, use `--skill-pilot-blind`.
32
37
  7. **One input/action per Method Block.** No block that `.fill()`s two fields or `.fill()` + `.click()` in one
33
38
  `act()` — split like saucedemo login (`fill-username`, `fill-password`, `submit-login`). Opt out per file:
34
39
  `// waygraph-ignore: multi-input` or `// waygraph-ignore-practices`.
40
+ 8. **A transition never ships with an empty `verify`.** A framework can enforce that a check
41
+ *exists*; it can't author the semantic content of one - only whoever knows what "success"
42
+ means for this app can write it. That's the right line to draw, but it means an unfilled
43
+ `verify: []` placeholder (codegen's honest way of not guessing) is indistinguishable from
44
+ a Block that was never given real verification at all, and it passes `check`/`typecheck`/
45
+ the orphan scan/`tsc` silently unless something is actually looking for it. Something is:
46
+ `defineMethodBlock`/`defineEffectBlock` where `In`/`Out` genuinely differ (a transition,
47
+ not a self-loop) needs a non-empty `verify`, same for `defineAssertBlock`. Opt out only
48
+ when a *different* Block right after it in the `.flow.ts` is what actually confirms the
49
+ transition (e.g. an `AssertXBlock`) - `// waygraph-ignore: empty-verify`.
35
50
 
36
51
  ## Block kinds (file convention)
37
52
 
@@ -105,6 +120,22 @@ npm run typecheck # when the consumer has it
105
120
  are still runnable in Pilot's live menu - orphan status only blocks
106
121
  `auto --blocks <From> <To>` pathfinding.
107
122
 
123
+ **Static tools above only find gaps in Blocks that already exist.** A real link/button on
124
+ the live page with *no Block written for it at all* has nothing in source for `check`/
125
+ `graph` to scan - the only way to find that is to actually run the app. Every live
126
+ `auto`/`browser`/`pilot` session watches the real page and warns (once per element) on
127
+ anything with no matching Block:
128
+
129
+ ```text
130
+ [waygraph] unmapped nav link on this page: /settings - no NavBlock covers this URL; add defineNavBlock
131
+ [waygraph] unmapped button on this page: #save-draft - no Method/NavClick Block selector matches; consider defineMethodBlock or defineNavClickBlock
132
+ ```
133
+
134
+ Read them back with `auto console <sessionId>` (same channel as real console/network
135
+ errors) - see `--skill-pilot`'s "Coverage gaps on the live page" section for the full
136
+ detection rules (same-origin `<a href>` only, visible clickables only, one warning per
137
+ element per session).
138
+
108
139
  ## Related skills (print via CLI)
109
140
 
110
141
  ```bash