waygraph 0.12.42 → 0.12.44

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
@@ -292,15 +292,15 @@ export declare function withBlockPace<In extends Checkpoint<string>, Out extends
292
292
  * {@link withTitle}.
293
293
  *
294
294
  * By itself this changes nothing about how the Flow runs - it still throws
295
- * exactly as it would otherwise. It's a hint a step-through overlay
296
- * (waygraph's `chain --step`) reads when that throw happens, to show an
297
- * informational "this is the point" panel instead of one indistinguishable
298
- * from a genuine break - the distinction a demo author needs so a viewer
299
- * (or the author, mid-build) can tell "the app just did what it's supposed
300
- * to" apart from "something in this demo is actually broken."
295
+ * Marks a Flow whose last step is an intentional product failure (demo overlay).
296
+ * Step mode shows stubOnError rings + an amber "expected outcome" panel when:
297
+ * - the last block **throws**, or
298
+ * - the last block **succeeds** on the fail branch (e.g. LoginPage + error banner
299
+ * after submit-login branched instead of throwing).
300
+ * Headless `run()` is unchanged - branching success returns the checkpoint.
301
301
  * @example const viewerBlockedFlow = withExpectedFailure(
302
- * engine.defineFlow([start, NavLoginBlock, blockedLoginCheck, end]),
303
- * "locked_out_user can never reach /inventory.html - this is the block working as intended.",
302
+ * engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end]),
303
+ * "locked_out_user stays on LoginPage with the error banner - working as intended.",
304
304
  * );
305
305
  */
306
306
  export declare function withExpectedFailure<Out extends Checkpoint<string>>(flow: Flow<Out>, reason: string): Flow<Out>;
package/dist/engine.js CHANGED
@@ -405,15 +405,15 @@ export function withBlockPace(block, pace) {
405
405
  * {@link withTitle}.
406
406
  *
407
407
  * By itself this changes nothing about how the Flow runs - it still throws
408
- * exactly as it would otherwise. It's a hint a step-through overlay
409
- * (waygraph's `chain --step`) reads when that throw happens, to show an
410
- * informational "this is the point" panel instead of one indistinguishable
411
- * from a genuine break - the distinction a demo author needs so a viewer
412
- * (or the author, mid-build) can tell "the app just did what it's supposed
413
- * to" apart from "something in this demo is actually broken."
408
+ * Marks a Flow whose last step is an intentional product failure (demo overlay).
409
+ * Step mode shows stubOnError rings + an amber "expected outcome" panel when:
410
+ * - the last block **throws**, or
411
+ * - the last block **succeeds** on the fail branch (e.g. LoginPage + error banner
412
+ * after submit-login branched instead of throwing).
413
+ * Headless `run()` is unchanged - branching success returns the checkpoint.
414
414
  * @example const viewerBlockedFlow = withExpectedFailure(
415
- * engine.defineFlow([start, NavLoginBlock, blockedLoginCheck, end]),
416
- * "locked_out_user can never reach /inventory.html - this is the block working as intended.",
415
+ * engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end]),
416
+ * "locked_out_user stays on LoginPage with the error banner - working as intended.",
417
417
  * );
418
418
  */
419
419
  export function withExpectedFailure(flow, reason) {
@@ -54,6 +54,11 @@ export type StubPhaseFixtures = {
54
54
  zoomOut?: boolean;
55
55
  /** Top demo banner title (the purple "waygraph demo" card). */
56
56
  title?: string;
57
+ /**
58
+ * Floating checklist dock side. `left` | `right`.
59
+ * Authored control (also click / WAYGRAPH_TODO_POS / --todo-left|right).
60
+ */
61
+ todoPos?: "left" | "right";
57
62
  };
58
63
  /** One named highlight slot on a block (selector + caption). */
59
64
  export interface WaygraphHighlightStub extends FixtureDurationFields {
@@ -161,6 +166,11 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
161
166
  title(text: string): void;
162
167
  /** Alias for {@link StubCtx.title}. */
163
168
  banner(text: string): void;
169
+ /**
170
+ * Floating checklist dock side (`left` | `right`). Authors control this in
171
+ * code; click / env / --todo-left|right are fallbacks.
172
+ */
173
+ todoPos(side: "left" | "right"): void;
164
174
  /** Batch-set rings + episode fixtures. */
165
175
  set(partial: {
166
176
  highlights?: HighlightStubPhase;
@@ -182,6 +192,7 @@ export type StubPhaseResult = {
182
192
  zoom?: number;
183
193
  zoomOut?: boolean;
184
194
  title?: string;
195
+ todoPos?: "left" | "right";
185
196
  };
186
197
  /**
187
198
  * One yap/slide in a multi-step demo caption sequence.
@@ -322,6 +333,8 @@ export type ResolvedHighlight = WaygraphHighlightStub;
322
333
  * Normalize checklist rows; apply todoIndex for sequential done/current.
323
334
  */
324
335
  export declare function normalizeTodos(todos: readonly WaygraphTodoInput[] | undefined, todoIndex?: number): WaygraphTodoItem[];
336
+ /** Normalize checklist dock side. */
337
+ export declare function normalizeTodoPos(raw: unknown): "left" | "right" | undefined;
325
338
  /**
326
339
  * Run an open stub lifecycle (preferred) or resolve a static slot map.
327
340
  * Returns rings + episode fixtures (todos / todoIndex / zoom).
@@ -383,6 +383,17 @@ function mergeSlot(base, patch) {
383
383
  ...fx,
384
384
  };
385
385
  }
386
+ /** Normalize checklist dock side. */
387
+ export function normalizeTodoPos(raw) {
388
+ const s = String(raw ?? "")
389
+ .trim()
390
+ .toLowerCase();
391
+ if (s === "left" || s === "l")
392
+ return "left";
393
+ if (s === "right" || s === "r")
394
+ return "right";
395
+ return undefined;
396
+ }
386
397
  function createStubCtx(bag, opts) {
387
398
  return {
388
399
  out: opts.out,
@@ -412,6 +423,11 @@ function createStubCtx(bag, opts) {
412
423
  banner(text) {
413
424
  bag.title = String(text ?? "");
414
425
  },
426
+ todoPos(side) {
427
+ const p = normalizeTodoPos(side);
428
+ if (p)
429
+ bag.todoPos = p;
430
+ },
415
431
  set(partial) {
416
432
  if (partial.highlights)
417
433
  bag.highlights = { ...partial.highlights };
@@ -426,6 +442,9 @@ function createStubCtx(bag, opts) {
426
442
  bag.zoomOut = !!partial.zoomOut;
427
443
  if (partial.title !== undefined)
428
444
  bag.title = String(partial.title ?? "");
445
+ const tp = normalizeTodoPos(partial.todoPos);
446
+ if (tp)
447
+ bag.todoPos = tp;
429
448
  },
430
449
  };
431
450
  }
@@ -565,6 +584,7 @@ export async function runStubPhase(block, phase, opts) {
565
584
  ...(bag.zoom !== undefined ? { zoom: bag.zoom } : {}),
566
585
  ...(bag.zoomOut !== undefined ? { zoomOut: bag.zoomOut } : {}),
567
586
  ...(bag.title !== undefined && bag.title !== "" ? { title: bag.title } : {}),
587
+ ...(bag.todoPos !== undefined ? { todoPos: bag.todoPos } : {}),
568
588
  };
569
589
  }
570
590
  /**
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ 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
5
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
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, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
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, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
7
7
  export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, } from "./highlights.js";
8
8
  export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
9
9
  export type { ActionPage } from "./types.js";
@@ -16,3 +16,5 @@ export { runTraverse } from "./traverse-run.js";
16
16
  export type { TraverseOptions, TraverseSessionMode, TraverseWorkerResult, } from "./traverse-run.js";
17
17
  export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport, } from "./traverse-coverage.js";
18
18
  export type { TraverseCoverageReport } from "./traverse-coverage.js";
19
+ export { WG_UI_SEL, WG_READY_PANEL_SEL, WG_READY_AUTO_SEL, WG_READY_BANNER_SEL, readWgOverlayBeacon, readWgOverlayBeaconInPage, assertWgOverlayReady, } from "./overlay-beacon.js";
20
+ export type { WgModalKind, WgBeaconRow } from "./overlay-beacon.js";
package/dist/index.js CHANGED
@@ -2,9 +2,10 @@ 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
4
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
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, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
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, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
6
6
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
7
7
  export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
8
8
  export { EdgeLeaseCoordinator } from "./traverse-lease.js";
9
9
  export { runTraverse } from "./traverse-run.js";
10
10
  export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, normalizeHitKey, parseMinEdgeCoverage, writeCoverageReport, } from "./traverse-coverage.js";
11
+ export { WG_UI_SEL, WG_READY_PANEL_SEL, WG_READY_AUTO_SEL, WG_READY_BANNER_SEL, readWgOverlayBeacon, readWgOverlayBeaconInPage, assertWgOverlayReady, } from "./overlay-beacon.js";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Blind-agent / Playwright checks for waygraph overlay modals.
3
+ *
4
+ * Every live modal root must carry:
5
+ * data-wg-ui="1"
6
+ * data-wg-modal="panel|banner|auto-panel"
7
+ * data-wg-ready="1" when painted and readable
8
+ *
9
+ * Agents: do not claim the stepper works unless
10
+ * `assertWgOverlayReady(page)` (or WAYGRAPH_PROVE_EXIT=1) passes.
11
+ */
12
+ import type { Page } from "@playwright/test";
13
+ export declare const WG_UI_SEL = "[data-wg-ui=\"1\"]";
14
+ export declare const WG_READY_PANEL_SEL = "[data-wg-modal=\"panel\"][data-wg-ready=\"1\"]";
15
+ export declare const WG_READY_AUTO_SEL = "[data-wg-modal=\"auto-panel\"][data-wg-ready=\"1\"]";
16
+ export declare const WG_READY_BANNER_SEL = "[data-wg-modal=\"banner\"][data-wg-ready=\"1\"]";
17
+ export type WgModalKind = "panel" | "banner" | "auto-panel";
18
+ export type WgBeaconRow = {
19
+ id: string | null;
20
+ modal: string | null;
21
+ ready: boolean;
22
+ phase: string | null;
23
+ step: string | null;
24
+ block: string | null;
25
+ collapsed: boolean;
26
+ opacity: string;
27
+ textLen: number;
28
+ w: number;
29
+ h: number;
30
+ visible: boolean;
31
+ };
32
+ /** Browser-side beacon reader (also installed as window.__wgOverlayBeacon). */
33
+ export declare function readWgOverlayBeaconInPage(): WgBeaconRow[];
34
+ export declare function readWgOverlayBeacon(page: Page): Promise<WgBeaconRow[]>;
35
+ /**
36
+ * Hard gate: a blank / missing / opacity-0 modal fails.
37
+ * Use after demo/auto paints its first overlay.
38
+ */
39
+ export declare function assertWgOverlayReady(page: Page, opts?: {
40
+ modal?: WgModalKind;
41
+ minTextLen?: number;
42
+ /** When true, collapsed mini chrome is allowed (still needs visible box + text). */
43
+ allowCollapsed?: boolean;
44
+ }): Promise<WgBeaconRow>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Blind-agent / Playwright checks for waygraph overlay modals.
3
+ *
4
+ * Every live modal root must carry:
5
+ * data-wg-ui="1"
6
+ * data-wg-modal="panel|banner|auto-panel"
7
+ * data-wg-ready="1" when painted and readable
8
+ *
9
+ * Agents: do not claim the stepper works unless
10
+ * `assertWgOverlayReady(page)` (or WAYGRAPH_PROVE_EXIT=1) passes.
11
+ */
12
+ export const WG_UI_SEL = '[data-wg-ui="1"]';
13
+ export const WG_READY_PANEL_SEL = '[data-wg-modal="panel"][data-wg-ready="1"]';
14
+ export const WG_READY_AUTO_SEL = '[data-wg-modal="auto-panel"][data-wg-ready="1"]';
15
+ export const WG_READY_BANNER_SEL = '[data-wg-modal="banner"][data-wg-ready="1"]';
16
+ /** Browser-side beacon reader (also installed as window.__wgOverlayBeacon). */
17
+ export function readWgOverlayBeaconInPage() {
18
+ const w = globalThis;
19
+ if (typeof w.__wgOverlayBeacon === "function")
20
+ return w.__wgOverlayBeacon();
21
+ return Array.from(document.querySelectorAll(WG_UI_SEL)).map((el) => {
22
+ const r = el.getBoundingClientRect();
23
+ const st = getComputedStyle(el);
24
+ return {
25
+ id: el.id || null,
26
+ modal: el.getAttribute("data-wg-modal"),
27
+ ready: el.getAttribute("data-wg-ready") === "1",
28
+ phase: el.getAttribute("data-wg-phase"),
29
+ step: el.getAttribute("data-wg-step"),
30
+ block: el.getAttribute("data-wg-block"),
31
+ collapsed: el.getAttribute("data-wg-collapsed") === "1" || el.classList.contains("wg-collapsed"),
32
+ opacity: st.opacity,
33
+ textLen: (el.textContent || "").trim().length,
34
+ w: Math.round(r.width),
35
+ h: Math.round(r.height),
36
+ visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
37
+ };
38
+ });
39
+ }
40
+ export async function readWgOverlayBeacon(page) {
41
+ return page.evaluate(() => {
42
+ const w = window;
43
+ if (typeof w.__wgOverlayBeacon === "function") {
44
+ return w.__wgOverlayBeacon();
45
+ }
46
+ return Array.from(document.querySelectorAll('[data-wg-ui="1"]')).map((el) => {
47
+ const r = el.getBoundingClientRect();
48
+ const st = getComputedStyle(el);
49
+ return {
50
+ id: el.id || null,
51
+ modal: el.getAttribute("data-wg-modal"),
52
+ ready: el.getAttribute("data-wg-ready") === "1",
53
+ phase: el.getAttribute("data-wg-phase"),
54
+ step: el.getAttribute("data-wg-step"),
55
+ block: el.getAttribute("data-wg-block"),
56
+ collapsed: el.getAttribute("data-wg-collapsed") === "1" || el.classList.contains("wg-collapsed"),
57
+ opacity: st.opacity,
58
+ textLen: (el.textContent || "").trim().length,
59
+ w: Math.round(r.width),
60
+ h: Math.round(r.height),
61
+ visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
62
+ };
63
+ });
64
+ });
65
+ }
66
+ /**
67
+ * Hard gate: a blank / missing / opacity-0 modal fails.
68
+ * Use after demo/auto paints its first overlay.
69
+ */
70
+ export async function assertWgOverlayReady(page, opts) {
71
+ const modal = opts?.modal ?? "panel";
72
+ const minText = opts?.minTextLen ?? 12;
73
+ const rows = await readWgOverlayBeacon(page);
74
+ const hit = rows.find((r) => r.modal === modal &&
75
+ r.ready &&
76
+ r.visible &&
77
+ r.textLen >= minText &&
78
+ (opts?.allowCollapsed || !r.collapsed || r.textLen >= minText));
79
+ if (!hit) {
80
+ throw new Error(`waygraph overlay NOT ready (modal=${modal}, minTextLen=${minText}). ` +
81
+ `beacons=${JSON.stringify(rows)}. ` +
82
+ `Do not claim stepper/modal works.`);
83
+ }
84
+ return hit;
85
+ }
@@ -18,5 +18,20 @@ declare global {
18
18
  __wgHideRing?: () => void;
19
19
  __wgPendingNavClickLabel?: string;
20
20
  __wgWirePanelChrome?: (panel: HTMLElement, storageKey: string, chromeTitle?: string) => void;
21
+ __wgStampModal?: (el: Element, kind: string, meta?: Record<string, unknown>) => void;
22
+ __wgOverlayBeacon?: () => Array<{
23
+ id: string | null;
24
+ modal: string | null;
25
+ ready: boolean;
26
+ phase: string | null;
27
+ step: string | null;
28
+ block: string | null;
29
+ collapsed: boolean;
30
+ opacity: string;
31
+ textLen: number;
32
+ w: number;
33
+ h: number;
34
+ visible: boolean;
35
+ }>;
21
36
  }
22
37
  }
@@ -1,7 +1,9 @@
1
1
  const RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
2
2
  "border:2.5px solid #7C3AED;border-radius:10px;box-shadow:0 0 0 4px rgba(124,58,237,.16);" +
3
- "transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
3
+ // Opacity only - tone color must snap (no gray/purple/yellow morph).
4
+ "transition:opacity .3s ease;}" +
4
5
  // Automation = gray; semantic tones for authored highlights.
6
+ "#wg-ring[data-tone=planned]{border-color:#7C3AED;box-shadow:0 0 0 4px rgba(124,58,237,.16);}" +
5
7
  "#wg-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}" +
6
8
  "#wg-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}" +
7
9
  "#wg-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
@@ -9,7 +11,8 @@ const RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none
9
11
  "#wg-ring[data-tone=success]{border-color:#22C55E;box-shadow:0 0 0 4px rgba(34,197,94,.22);}" +
10
12
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
11
13
  "white-space:nowrap;padding:4px 9px;border-radius:7px;background:#7C3AED;color:#fff;" +
12
- "font:600 12px/1.2 system-ui,sans-serif;transition:opacity .3s ease,background .15s,color .15s;}" +
14
+ "font:600 12px/1.2 system-ui,sans-serif;transition:opacity .3s ease;}" +
15
+ "#wg-ring-label[data-tone=planned]{background:#7C3AED;color:#fff;}" +
13
16
  "#wg-ring-label[data-tone=auto]{background:#6B7280;color:#fff;}" +
14
17
  "#wg-ring-label[data-tone=info]{background:#2563EB;color:#fff;}" +
15
18
  "#wg-ring-label[data-tone=warning]{background:#EAB308;color:#1c1917;}" +
@@ -146,6 +149,17 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
146
149
  banner.innerHTML =
147
150
  "<span class='wg-banner-tag'>waygraph auto</span><span>" + title + "</span>";
148
151
  document.documentElement.appendChild(banner);
152
+ banner.setAttribute("data-wg-ui", "1");
153
+ banner.setAttribute("data-wg-modal", "banner");
154
+ banner.setAttribute("data-wg-ready", "1");
155
+ }
156
+ else {
157
+ const existing = document.getElementById("wg-banner");
158
+ if (existing) {
159
+ existing.setAttribute("data-wg-ui", "1");
160
+ existing.setAttribute("data-wg-modal", "banner");
161
+ existing.setAttribute("data-wg-ready", "1");
162
+ }
149
163
  }
150
164
  if (showBanner && !document.title.startsWith("[waygraph] ")) {
151
165
  document.title = "[waygraph] " + document.title;
@@ -17,7 +17,7 @@
17
17
  }
18
18
  },
19
19
  "../..": {
20
- "version": "0.12.24",
20
+ "version": "0.12.43",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "tsx": "^4.0.0"
@@ -78,6 +78,22 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
78
78
  });
79
79
  },
80
80
  stubAfter: (ctx) => {
81
+ // Fail branch lands on LoginPage with Epic sadface - do not ring the
82
+ // inventory shelf. Success (LoggedIn) hands off to shopFlow.
83
+ if (ctx.out && ctx.out.__state === "LoginPage") {
84
+ ctx.title("Login blocked");
85
+ ctx.todos(["Enter username", "Enter password", "Click Login"]);
86
+ ctx.todoIndex(2);
87
+ ctx.ring("error", {
88
+ selector: '[data-test="error"]',
89
+ label: "Login error banner",
90
+ tone: "danger",
91
+ focus: true,
92
+ color: "#f87171",
93
+ duration: true,
94
+ });
95
+ return;
96
+ }
81
97
  ctx.banner("Inventory");
82
98
  ctx.todos(["Enter username", "Enter password", "Click Login"]);
83
99
  ctx.todoIndex(2);
@@ -1,4 +1,4 @@
1
- import { Engine, start, end, withTitle, withSessionReset } from "waygraph";
1
+ import { Engine, start, end, withTitle, withSessionReset, withExpectedFailure } from "waygraph";
2
2
  import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
3
3
  import { SubmitLoginActionBlock } from "../blocks/saucedemo-web/methods/submit-login.method.block.js";
4
4
 
@@ -16,10 +16,14 @@ const engine = new Engine();
16
16
  // genuinely fresh cookies/storage, or the previous user's session would
17
17
  // still be live and this login attempt wouldn't mean anything.
18
18
  //
19
- // submit-login now branches: observe sees no /inventory.html navigation,
20
- // resolve -> LoginPage, verify confirms the error banner - the flow completes
21
- // at LoginPage (not a thrown verify on a fake LoggedIn resolve).
22
- export const viewerBlockedFlow = withTitle(
23
- withSessionReset(engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end])),
24
- "Viewer: Blocked Login Attempt",
19
+ // submit-login branches: observe sees no /inventory.html navigation,
20
+ // resolve -> LoginPage, verify confirms the error banner. Demo still wraps
21
+ // withExpectedFailure so step 9/9 shows stubOnError rings + amber
22
+ // "expected outcome" panel (branching no longer throws).
23
+ export const viewerBlockedFlow = withExpectedFailure(
24
+ withTitle(
25
+ withSessionReset(engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end])),
26
+ "Viewer: Blocked Login Attempt",
27
+ ),
28
+ "locked_out_user stays on LoginPage with the error banner - this is the product working as intended.",
25
29
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.42",
3
+ "version": "0.12.44",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -78,6 +78,22 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
78
78
  });
79
79
  },
80
80
  stubAfter: (ctx) => {
81
+ // Fail branch lands on LoginPage with Epic sadface - do not ring the
82
+ // inventory shelf. Success (LoggedIn) hands off to shopFlow.
83
+ if (ctx.out && ctx.out.__state === "LoginPage") {
84
+ ctx.title("Login blocked");
85
+ ctx.todos(["Enter username", "Enter password", "Click Login"]);
86
+ ctx.todoIndex(2);
87
+ ctx.ring("error", {
88
+ selector: '[data-test="error"]',
89
+ label: "Login error banner",
90
+ tone: "danger",
91
+ focus: true,
92
+ color: "#f87171",
93
+ duration: true,
94
+ });
95
+ return;
96
+ }
81
97
  ctx.banner("Inventory");
82
98
  ctx.todos(["Enter username", "Enter password", "Click Login"]);
83
99
  ctx.todoIndex(2);
@@ -1,4 +1,4 @@
1
- import { Engine, start, end, withTitle, withSessionReset } from "waygraph";
1
+ import { Engine, start, end, withTitle, withSessionReset, withExpectedFailure } from "waygraph";
2
2
  import { NavLoginBlock } from "../blocks/saucedemo-web/nav-login.block.js";
3
3
  import { SubmitLoginActionBlock } from "../blocks/saucedemo-web/methods/submit-login.method.block.js";
4
4
 
@@ -16,10 +16,14 @@ const engine = new Engine();
16
16
  // genuinely fresh cookies/storage, or the previous user's session would
17
17
  // still be live and this login attempt wouldn't mean anything.
18
18
  //
19
- // submit-login now branches: observe sees no /inventory.html navigation,
20
- // resolve -> LoginPage, verify confirms the error banner - the flow completes
21
- // at LoginPage (not a thrown verify on a fake LoggedIn resolve).
22
- export const viewerBlockedFlow = withTitle(
23
- withSessionReset(engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end])),
24
- "Viewer: Blocked Login Attempt",
19
+ // submit-login branches: observe sees no /inventory.html navigation,
20
+ // resolve -> LoginPage, verify confirms the error banner. Demo still wraps
21
+ // withExpectedFailure so step 9/9 shows stubOnError rings + amber
22
+ // "expected outcome" panel (branching no longer throws).
23
+ export const viewerBlockedFlow = withExpectedFailure(
24
+ withTitle(
25
+ withSessionReset(engine.defineFlow([start, NavLoginBlock, SubmitLoginActionBlock, end])),
26
+ "Viewer: Blocked Login Attempt",
27
+ ),
28
+ "locked_out_user stays on LoginPage with the error banner - this is the product working as intended.",
25
29
  );