waygraph 0.15.36 → 0.15.37

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.
@@ -1,6 +1,7 @@
1
1
  import type { Checkpoint } from "../types.js";
2
2
  import type { Flow } from "./flow.js";
3
3
  import type { RunGraphOptions } from "./run-graph.js";
4
+ import { MemPage } from "../mem-page.js";
4
5
  import type { BrowserContext } from "@playwright/test";
5
6
  /** One branch reachable from `flow`, possibly several `.branch()` levels deep. */
6
7
  export interface BranchEntry {
@@ -13,27 +14,33 @@ export declare function branchRoutes(flow: Flow<any>): ReadonlyMap<string, Flow<
13
14
  /**
14
15
  * Every branch Flow reachable from `flow`, at any depth, deduped by object identity (the same
15
16
  * route Flow reused under two tags is only listed once). Doesn't include `flow` itself - only its
16
- * `.branch()` continuations.
17
+ * `.branch()` continuations. Static - doesn't run anything.
17
18
  */
18
19
  export declare function collectBranchFlows(flow: Flow<any>): BranchEntry[];
19
- /** Why a branch was skipped instead of run. */
20
- export type BranchSkipReason = "first Block is not nav/page - this branch needs page state a live run would have reached through its prefix, not its own blocks";
21
20
  export interface BranchRunResult {
22
21
  path: string;
23
- status: "ok" | "failed" | "skipped";
22
+ status: "ok" | "failed";
24
23
  result?: Checkpoint<string>;
25
24
  error?: string;
26
- reason?: BranchSkipReason;
25
+ }
26
+ export interface RunBranchRegressionOptions extends RunGraphOptions {
27
+ /**
28
+ * Default `true`: clone the real session (storageState + current URL) at every branch point, so
29
+ * every route runs from a genuine copy of the live state, not a fresh page. `false` runs the
30
+ * whole tree exactly once, through whichever single path the live state actually takes (same as
31
+ * calling `flow.run()` directly) - can't explore siblings, since the one real page has moved on
32
+ * once a branch's Blocks run.
33
+ */
34
+ cloneSession?: boolean;
27
35
  }
28
36
  /**
29
- * Runs every independently-enterable branch of `flow` (one whose own first Block navigates), each
30
- * with a fresh `MemPage` seeded via {@link seedMemStub} and a fresh page. Branches that assume
31
- * prior page state (first Block isn't nav/page) are reported as `"skipped"`, not silently run into
32
- * a confusing failure - see this file's own header for why that's a real, unavoidable limit, not a
33
- * gap to code around.
37
+ * Runs every path through `flow`'s `.branch()` tree - by default in a CLONED session per branch
38
+ * point (see this file's own header), so one branch finishing doesn't consume another's chance to
39
+ * run. Each explored branch gets its own `MemPage` (via {@link MemPage.clone}, seeded further with
40
+ * {@link seedMemStub} for anything a route alone requires that the shared prefix didn't already set).
34
41
  * @example
35
42
  * for (const r of await runBranchRegression(loginFlow, context)) {
36
- * console.log(r.path, r.status, r.error ?? r.reason ?? "");
43
+ * console.log(r.path, r.status, r.error ?? "");
37
44
  * }
38
45
  */
39
- export declare function runBranchRegression(flow: Flow<any>, context: BrowserContext, options?: RunGraphOptions): Promise<BranchRunResult[]>;
46
+ export declare function runBranchRegression(flow: Flow<any>, context: BrowserContext, mem?: MemPage, options?: RunBranchRegressionOptions): Promise<BranchRunResult[]>;
@@ -1,14 +1,17 @@
1
1
  import { MemPage } from "../mem-page.js";
2
2
  import { seedMemStub } from "../mem-stub.js";
3
+ function branchInfo(flow) {
4
+ return flow.__wgBranch;
5
+ }
3
6
  /** This Flow's own immediate `.branch()` routes, or `undefined` if it wasn't built by `.branch()`. */
4
7
  export function branchRoutes(flow) {
5
- const routes = flow.__wgBranchRoutes;
6
- return routes ? new Map(Object.entries(routes)) : undefined;
8
+ const info = branchInfo(flow);
9
+ return info ? new Map(Object.entries(info.routes)) : undefined;
7
10
  }
8
11
  /**
9
12
  * Every branch Flow reachable from `flow`, at any depth, deduped by object identity (the same
10
13
  * route Flow reused under two tags is only listed once). Doesn't include `flow` itself - only its
11
- * `.branch()` continuations.
14
+ * `.branch()` continuations. Static - doesn't run anything.
12
15
  */
13
16
  export function collectBranchFlows(flow) {
14
17
  const out = [];
@@ -30,43 +33,85 @@ export function collectBranchFlows(flow) {
30
33
  return out;
31
34
  }
32
35
  /**
33
- * Runs every independently-enterable branch of `flow` (one whose own first Block navigates), each
34
- * with a fresh `MemPage` seeded via {@link seedMemStub} and a fresh page. Branches that assume
35
- * prior page state (first Block isn't nav/page) are reported as `"skipped"`, not silently run into
36
- * a confusing failure - see this file's own header for why that's a real, unavoidable limit, not a
37
- * gap to code around.
36
+ * Runs every path through `flow`'s `.branch()` tree - by default in a CLONED session per branch
37
+ * point (see this file's own header), so one branch finishing doesn't consume another's chance to
38
+ * run. Each explored branch gets its own `MemPage` (via {@link MemPage.clone}, seeded further with
39
+ * {@link seedMemStub} for anything a route alone requires that the shared prefix didn't already set).
38
40
  * @example
39
41
  * for (const r of await runBranchRegression(loginFlow, context)) {
40
- * console.log(r.path, r.status, r.error ?? r.reason ?? "");
42
+ * console.log(r.path, r.status, r.error ?? "");
41
43
  * }
42
44
  */
43
- export async function runBranchRegression(flow, context, options) {
44
- const entries = collectBranchFlows(flow);
45
+ export async function runBranchRegression(flow, context, mem = new MemPage(), options) {
46
+ const cloneSession = options?.cloneSession ?? true;
45
47
  const results = [];
46
- for (const entry of entries) {
47
- const first = entry.flow.blocks()[0]?.block;
48
- const kind = first?.__waygraphKind;
49
- if (kind !== "nav" && kind !== "page") {
50
- results.push({
51
- path: entry.path,
52
- status: "skipped",
53
- reason: "first Block is not nav/page - this branch needs page state a live run would have reached through its prefix, not its own blocks",
54
- });
55
- continue;
48
+ const browser = cloneSession ? context.browser() : null;
49
+ if (cloneSession && !browser) {
50
+ throw new Error("Waygraph runBranchRegression: cloneSession needs context.browser() (a persistent context " +
51
+ "has none) - pass { cloneSession: false } to run the single live path in this context instead.");
52
+ }
53
+ /** `entryPage`, when given, is already positioned (cloned session + navigated) - reused as-is,
54
+ * never replaced by a fresh `ctx.newPage()` (that would throw away the whole point of cloning). */
55
+ async function walk(f, ctx, m, path, entryPage) {
56
+ const info = branchInfo(f);
57
+ if (!info) {
58
+ // Leaf: whatever Blocks this route itself adds - run them to completion from the (possibly
59
+ // cloned, already-positioned) session already in place.
60
+ seedMemStub(m, f);
61
+ const page = entryPage ?? (await ctx.newPage());
62
+ try {
63
+ const result = await f.run(ctx, m, { ...options, page, closeOnFinish: true });
64
+ results.push({ path, status: "ok", result: result });
65
+ }
66
+ catch (err) {
67
+ results.push({ path, status: "failed", error: err instanceof Error ? err.message : String(err) });
68
+ }
69
+ return;
56
70
  }
57
- const mem = new MemPage();
58
- seedMemStub(mem, entry.flow);
71
+ seedMemStub(m, info.before);
72
+ const page = entryPage ?? (await ctx.newPage());
73
+ let first;
59
74
  try {
60
- const result = await entry.flow.run(context, mem, { ...options, closeOnFinish: true });
61
- results.push({ path: entry.path, status: "ok", result: result });
75
+ first = (await info.before.run(ctx, m, { ...options, page, closeOnFinish: false }));
62
76
  }
63
77
  catch (err) {
64
- results.push({
65
- path: entry.path,
66
- status: "failed",
67
- error: err instanceof Error ? err.message : String(err),
68
- });
78
+ results.push({ path, status: "failed", error: err instanceof Error ? err.message : String(err) });
79
+ return;
80
+ }
81
+ if (!cloneSession) {
82
+ // Shared-session mode: only the ONE route the live state actually resolved to is reachable -
83
+ // the page has already moved past every other tag.
84
+ const route = info.routes[first.result.__state];
85
+ const tagPath = path ? `${path} > ${first.result.__state}` : first.result.__state;
86
+ if (!route) {
87
+ results.push({ path: tagPath, status: "ok", result: first.result });
88
+ return;
89
+ }
90
+ await walk(route, ctx, m, tagPath, first.page);
91
+ return;
92
+ }
93
+ const url = first.page.url();
94
+ const storageState = await ctx.storageState();
95
+ await first.page.close();
96
+ for (const [tag, route] of Object.entries(info.routes)) {
97
+ const branchPath = path ? `${path} > ${tag}` : tag;
98
+ if (!route) {
99
+ results.push({ path: branchPath, status: "ok", result: { __state: tag } });
100
+ continue;
101
+ }
102
+ const branchContext = await browser.newContext({ storageState });
103
+ try {
104
+ const branchPage = await branchContext.newPage();
105
+ // Best-effort: land back on the same page the prefix ended on before this route's own
106
+ // Blocks run - restores cookies/localStorage AND which page was loaded, not just auth state.
107
+ await branchPage.goto(url).catch(() => { });
108
+ await walk(route, branchContext, m.clone(), branchPath, branchPage);
109
+ }
110
+ finally {
111
+ await branchContext.close();
112
+ }
69
113
  }
70
114
  }
115
+ await walk(flow, context, mem, "");
71
116
  return results;
72
117
  }
@@ -134,7 +134,7 @@ function buildBranchedFlow(before, routes) {
134
134
  }
135
135
  return branchFlow.run(context, mem, { ...options, page: first.page, closeOnFinish });
136
136
  });
137
- const flow = {
137
+ return {
138
138
  resetSession: before.resetSession,
139
139
  ...(before.title !== undefined ? { title: before.title } : {}),
140
140
  ...(before.expectedFailureReason !== undefined ? { expectedFailureReason: before.expectedFailureReason } : {}),
@@ -142,6 +142,11 @@ function buildBranchedFlow(before, routes) {
142
142
  ...(before.demoPace !== undefined ? { demoPace: before.demoPace } : {}),
143
143
  ...(before.highlightStyle !== undefined ? { highlightStyle: before.highlightStyle } : {}),
144
144
  ...(before.memStub !== undefined ? { memStub: before.memStub } : {}),
145
+ // Read by branchRoutes()/branchInfo() (branch-regression.ts) - real, ORDINARY (enumerable)
146
+ // properties, not hidden ones, specifically so a `with*` wrapper's `{ ...flow, ... }` spread
147
+ // (withSessionReset, withTitle, etc.) carries them through instead of silently dropping them -
148
+ // the same class of bug branch()'s missing copyWaygraphRuntime call already was.
149
+ __wgBranch: { before, routes },
145
150
  // Static introspection (`waygraph graph`/list): the fixed prefix, plus every branch's own
146
151
  // Blocks prefixed by the tag that leads to them - can't know at analysis time which one a real
147
152
  // run takes, so this shows all of them rather than none.
@@ -157,10 +162,6 @@ function buildBranchedFlow(before, routes) {
157
162
  return buildBranchedFlow(before.modBlockVerify(block, nameOrIndex, newCheck), routes);
158
163
  },
159
164
  };
160
- // Non-enumerable so it doesn't show up on a spread/JSON.stringify of the Flow - read by
161
- // branchRoutes()/collectBranchFlows() (branch-regression.ts), not part of the public Flow shape.
162
- Object.defineProperty(flow, "__wgBranchRoutes", { value: routes, enumerable: false, configurable: true });
163
- return flow;
164
165
  }
165
166
  /**
166
167
  * Fluent builder over {@link Engine.defineFlow} - see `map()`'s own doc
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
4
  export { registerMemStub, seedMemStub, getMemStub } from "./mem-stub.js";
5
5
  export { Trait, urlMatches, textEquals, visible, disabled, enabled, frameVisible, frameTextEquals, frameContainsText, } from "./trait.js";
6
6
  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, withMemStub, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, withStubBefore, withStubAfter, withStubOnError, chainFlow, defineLayout, map, MapBuilder, branchRoutes, collectBranchFlows, runBranchRegression, } from "./engine.js";
7
- export type { BranchEntry, BranchRunResult, BranchSkipReason } from "./engine/branch-regression.js";
7
+ export type { BranchEntry, BranchRunResult, RunBranchRegressionOptions } from "./engine/branch-regression.js";
8
8
  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";
9
9
  export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, WaygraphTodosOpts, TodoDockUiOpts, BannerPos, BannerUiOpts, 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";
10
10
  export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, AssertBlock, AssertBlockOptions, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, Layout, LayoutOptions, MapBuilderOptions, } from "./engine.js";
@@ -67,4 +67,12 @@ export declare class MemPage {
67
67
  setAll<A, B, C, D>(p1: readonly [MemKey<A>, A], p2: readonly [MemKey<B>, B], p3: readonly [MemKey<C>, C], p4: readonly [MemKey<D>, D]): void;
68
68
  setAll<A, B, C, D, E>(p1: readonly [MemKey<A>, A], p2: readonly [MemKey<B>, B], p3: readonly [MemKey<C>, C], p4: readonly [MemKey<D>, D], p5: readonly [MemKey<E>, E]): void;
69
69
  setAll<A, B, C, D, E, F>(p1: readonly [MemKey<A>, A], p2: readonly [MemKey<B>, B], p3: readonly [MemKey<C>, C], p4: readonly [MemKey<D>, D], p5: readonly [MemKey<E>, E], p6: readonly [MemKey<F>, F]): void;
70
+ /**
71
+ * An independent copy of every key currently set - a later `.set()` on either the original or the
72
+ * clone never touches the other. For branch-session cloning (`runBranchRegression`): each explored
73
+ * branch gets its own mem, so one branch's writes can't bleed into a sibling that also gets tried
74
+ * from the same snapshot point. Shallow per value (same as `set()` always was - MemPage never
75
+ * owned deep-copying the values themselves, only which keys point to which).
76
+ */
77
+ clone(): MemPage;
70
78
  }
package/dist/mem-page.js CHANGED
@@ -68,4 +68,18 @@ export class MemPage {
68
68
  this.set(k, v);
69
69
  }
70
70
  }
71
+ /**
72
+ * An independent copy of every key currently set - a later `.set()` on either the original or the
73
+ * clone never touches the other. For branch-session cloning (`runBranchRegression`): each explored
74
+ * branch gets its own mem, so one branch's writes can't bleed into a sibling that also gets tried
75
+ * from the same snapshot point. Shallow per value (same as `set()` always was - MemPage never
76
+ * owned deep-copying the values themselves, only which keys point to which).
77
+ */
78
+ clone() {
79
+ const copy = new MemPage();
80
+ for (const [k, v] of this.store) {
81
+ copy.store.set(k, v);
82
+ }
83
+ return copy;
84
+ }
71
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.36",
3
+ "version": "0.15.37",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",