@agent-scope/playwright 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -848,6 +848,126 @@
848
848
  children
849
849
  };
850
850
  }
851
+ var HookLayout2 = 4;
852
+ function isLightweightEffectNode(node) {
853
+ const ms = node.memoizedState;
854
+ if (ms === null || typeof ms !== "object") return false;
855
+ const obj = ms;
856
+ return typeof obj.create === "function" && "deps" in obj && typeof obj.tag === "number";
857
+ }
858
+ function isLightweightRefNode(node) {
859
+ if (node.queue != null) return false;
860
+ const ms = node.memoizedState;
861
+ if (ms === null || typeof ms !== "object" || Array.isArray(ms)) return false;
862
+ const keys = Object.keys(ms);
863
+ return keys.length === 1 && keys[0] === "current";
864
+ }
865
+ function isLightweightMemoTuple(node) {
866
+ if (node.queue != null) return false;
867
+ const ms = node.memoizedState;
868
+ if (!Array.isArray(ms) || ms.length !== 2) return false;
869
+ return ms[1] === null || Array.isArray(ms[1]);
870
+ }
871
+ function isLightweightStateOrReducer(node) {
872
+ return node.queue != null && typeof node.queue === "object" && typeof node.queue.dispatch === "function";
873
+ }
874
+ function isLightweightReducer(node) {
875
+ if (!isLightweightStateOrReducer(node)) return false;
876
+ const q = node.queue;
877
+ if (typeof q.reducer === "function") return true;
878
+ const lrr = q.lastRenderedReducer;
879
+ if (typeof lrr !== "function") return false;
880
+ const name = lrr.name ?? "";
881
+ return name !== "basicStateReducer" && name !== "";
882
+ }
883
+ function classifyHookType(node) {
884
+ if (isLightweightEffectNode(node)) {
885
+ const ms = node.memoizedState;
886
+ return ms.tag & HookLayout2 ? "useLayoutEffect" : "useEffect";
887
+ }
888
+ if (isLightweightRefNode(node)) return "useRef";
889
+ if (isLightweightMemoTuple(node)) {
890
+ const [val] = node.memoizedState;
891
+ return typeof val === "function" ? "useCallback" : "useMemo";
892
+ }
893
+ if (isLightweightStateOrReducer(node)) {
894
+ return isLightweightReducer(node) ? "useReducer" : "useState";
895
+ }
896
+ return "custom";
897
+ }
898
+ function countAndClassifyHooks(fiber) {
899
+ const hookTypes = [];
900
+ let node = fiber.memoizedState ?? null;
901
+ if (node === null || typeof node !== "object" || !("next" in node)) {
902
+ return { hookCount: 0, hookTypes: [] };
903
+ }
904
+ while (node !== null) {
905
+ hookTypes.push(classifyHookType(node));
906
+ node = node.next ?? null;
907
+ }
908
+ return { hookCount: hookTypes.length, hookTypes };
909
+ }
910
+ function walkFiberLightweightInner(fiber, includeHost, visited, depth) {
911
+ if (fiber === null || fiber === void 0) return null;
912
+ if (visited.has(fiber)) return null;
913
+ if (shouldSkip(fiber, includeHost)) return null;
914
+ visited.add(fiber);
915
+ const id = typeof fiber._debugID === "number" ? fiber._debugID : nextId();
916
+ const { hookCount, hookTypes } = countAndClassifyHooks(fiber);
917
+ const children = collectLightweightChildren(fiber, includeHost, visited, depth + 1);
918
+ const node = {
919
+ id,
920
+ name: extractName(fiber),
921
+ type: classifyType(fiber),
922
+ hookCount,
923
+ hookTypes,
924
+ childCount: children.length,
925
+ depth,
926
+ children
927
+ };
928
+ return node;
929
+ }
930
+ function collectLightweightChildren(fiber, includeHost, visited, childDepth) {
931
+ const nodes = [];
932
+ let current = fiber.child ?? null;
933
+ while (current !== null) {
934
+ if (visited.has(current)) {
935
+ current = current.sibling ?? null;
936
+ continue;
937
+ }
938
+ if (shouldSkip(current, includeHost)) {
939
+ const promoted = collectLightweightChildren(current, includeHost, visited, childDepth);
940
+ nodes.push(...promoted);
941
+ } else {
942
+ const node = walkFiberLightweightInner(current, includeHost, visited, childDepth);
943
+ if (node !== null) {
944
+ nodes.push(node);
945
+ }
946
+ }
947
+ current = current.sibling ?? null;
948
+ }
949
+ return nodes;
950
+ }
951
+ function walkFiberRootLightweight(fiberRoot, options = {}) {
952
+ if (!fiberRoot) return null;
953
+ const hostRootFiber = fiberRoot.current ?? null;
954
+ if (!hostRootFiber) return null;
955
+ const includeHost = options.includeHostElements ?? false;
956
+ const visited = /* @__PURE__ */ new Set();
957
+ const children = collectLightweightChildren(hostRootFiber, includeHost, visited, 0);
958
+ if (children.length === 0) return null;
959
+ if (children.length === 1) return children[0] ?? null;
960
+ return {
961
+ id: nextId(),
962
+ name: "Root",
963
+ type: "function",
964
+ hookCount: 0,
965
+ hookTypes: [],
966
+ childCount: children.length,
967
+ depth: 0,
968
+ children
969
+ };
970
+ }
851
971
 
852
972
  // ../runtime/src/suspense-detector.ts
853
973
  var SuspenseComponent2 = 13;
@@ -939,18 +1059,35 @@
939
1059
  const walkOptions = {
940
1060
  includeHostElements: options.includeHostElements ?? false
941
1061
  };
942
- const tree = walkFiberRoot(fiberRoot, walkOptions);
943
- if (!tree) {
944
- throw new Error(
945
- "capture(): Fiber tree is empty. Make sure React has rendered at least one component."
946
- );
947
- }
948
1062
  const hostRootFiber = fiberRoot.current ?? null;
949
1063
  const rootChild = hostRootFiber?.child ?? null;
950
1064
  const errors = detectErrors(rootChild);
951
1065
  const suspenseBoundaries = detectSuspenseBoundaries(rootChild);
952
1066
  const capturedIn = Date.now() - startTime;
953
1067
  const consoleEntries = getConsoleEntries();
1068
+ if (options.lightweight) {
1069
+ const tree2 = walkFiberRootLightweight(fiberRoot, walkOptions);
1070
+ if (!tree2) {
1071
+ throw new Error(
1072
+ "capture(): Fiber tree is empty. Make sure React has rendered at least one component."
1073
+ );
1074
+ }
1075
+ return {
1076
+ url,
1077
+ timestamp: startTime,
1078
+ capturedIn,
1079
+ tree: tree2,
1080
+ consoleEntries,
1081
+ errors,
1082
+ suspenseBoundaries
1083
+ };
1084
+ }
1085
+ const tree = walkFiberRoot(fiberRoot, walkOptions);
1086
+ if (!tree) {
1087
+ throw new Error(
1088
+ "capture(): Fiber tree is empty. Make sure React has rendered at least one component."
1089
+ );
1090
+ }
954
1091
  return {
955
1092
  url,
956
1093
  timestamp: startTime,
@@ -989,15 +1126,17 @@
989
1126
  hasCommitted = true;
990
1127
  resolveFirstCommit();
991
1128
  };
992
- window.__SCOPE_CAPTURE__ = async () => {
1129
+ window.__SCOPE_CAPTURE__ = async (options) => {
993
1130
  if (!hasCommitted) {
994
1131
  await firstCommit;
995
1132
  }
996
- return capture();
1133
+ return capture({ lightweight: options?.lightweight });
997
1134
  };
998
- window.__SCOPE_CAPTURE_JSON__ = async () => {
999
- await firstCommit;
1000
- const result = await capture();
1135
+ window.__SCOPE_CAPTURE_JSON__ = async (options) => {
1136
+ if (!hasCommitted) {
1137
+ await firstCommit;
1138
+ }
1139
+ const result = await capture({ lightweight: options?.lightweight });
1001
1140
  return JSON.stringify(result);
1002
1141
  };
1003
1142
  })();
package/dist/index.cjs CHANGED
@@ -57,8 +57,42 @@ async function evaluateCapture(p) {
57
57
  }
58
58
  throw lastError;
59
59
  }
60
+ async function evaluateLightweightCapture(p) {
61
+ let lastError;
62
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
63
+ if (attempt > 0) {
64
+ await sleep(RETRY_DELAY_MS);
65
+ }
66
+ const outcome = await p.evaluate(async () => {
67
+ const win = window;
68
+ if (typeof win.__SCOPE_CAPTURE_JSON__ === "function") {
69
+ return win.__SCOPE_CAPTURE_JSON__({ lightweight: true });
70
+ }
71
+ if (typeof win.__SCOPE_CAPTURE__ === "function") {
72
+ return win.__SCOPE_CAPTURE__({ lightweight: true });
73
+ }
74
+ throw new Error(
75
+ "Scope runtime not injected. Make sure you navigated to the page AFTER the scope fixture was set up, not before."
76
+ );
77
+ }).then(
78
+ (val) => ({ ok: true, val }),
79
+ (err) => ({ ok: false, err })
80
+ );
81
+ if (outcome.ok) {
82
+ const parsed = typeof outcome.val === "string" ? JSON.parse(outcome.val) : outcome.val;
83
+ return { ...parsed, route: null };
84
+ }
85
+ const message = outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
86
+ if (CONTEXT_DESTROYED_PATTERN.test(message) && attempt < MAX_RETRIES) {
87
+ lastError = outcome.err;
88
+ continue;
89
+ }
90
+ throw outcome.err;
91
+ }
92
+ throw lastError;
93
+ }
60
94
  var POLL_INTERVAL_MS = 300;
61
- async function captureUntilStable(p, stableMs, timeoutMs) {
95
+ async function captureUntilStable(p, stableMs, timeoutMs, lightweight = false) {
62
96
  const deadline = Date.now() + timeoutMs;
63
97
  let lastReport = await evaluateCapture(p);
64
98
  let lastCount = countNodes(lastReport.tree);
@@ -69,7 +103,7 @@ async function captureUntilStable(p, stableMs, timeoutMs) {
69
103
  if (now >= deadline) {
70
104
  return lastReport;
71
105
  }
72
- const report = await evaluateCapture(p);
106
+ const report = lightweight ? await evaluateLightweightCapture(p) : await evaluateCapture(p);
73
107
  const count = countNodes(report.tree);
74
108
  if (count !== lastCount) {
75
109
  lastCount = count;
@@ -78,6 +112,9 @@ async function captureUntilStable(p, stableMs, timeoutMs) {
78
112
  } else {
79
113
  lastReport = report;
80
114
  if (now - stableSince >= stableMs) {
115
+ if (lightweight) {
116
+ return evaluateCapture(p);
117
+ }
81
118
  return lastReport;
82
119
  }
83
120
  }
@@ -118,9 +155,14 @@ var test = test$1.test.extend({
118
155
  p = page;
119
156
  options = targetPageOrOptions ?? {};
120
157
  }
121
- const { waitForStable = false, stableMs = 1e3, timeoutMs = 15e3 } = options;
158
+ const {
159
+ waitForStable = false,
160
+ stableMs = 1e3,
161
+ timeoutMs = 15e3,
162
+ lightweight = false
163
+ } = options;
122
164
  if (waitForStable) {
123
- return captureUntilStable(p, stableMs, timeoutMs);
165
+ return captureUntilStable(p, stableMs, timeoutMs, lightweight);
124
166
  }
125
167
  return evaluateCapture(p);
126
168
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/capture-utils.ts","../src/fixture.ts","../src/index.ts"],"names":["__dirname","dirname","fileURLToPath","join","existsSync","base","isPageReport","readFileSync"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ComponentNode, PageReport } from \"@agent-scope/core\";\nimport type { Page } from \"@playwright/test\";\n\n// ---------------------------------------------------------------------------\n// Node counting (Node.js side — not in-browser)\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively counts the total number of `ComponentNode` instances in a tree.\n */\nexport function countNodes(node: ComponentNode): number {\n let count = 1;\n for (const child of node.children) {\n count += countNodes(child);\n }\n return count;\n}\n\n// ---------------------------------------------------------------------------\n// Timing helpers\n// ---------------------------------------------------------------------------\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ---------------------------------------------------------------------------\n// Context-destroyed retry constants\n// ---------------------------------------------------------------------------\n\nexport const CONTEXT_DESTROYED_PATTERN = /execution context was destroyed/i;\nexport const RETRY_DELAY_MS = 500;\nexport const MAX_RETRIES = 3;\n\n// ---------------------------------------------------------------------------\n// Internal result types for safe promise handling\n// ---------------------------------------------------------------------------\n\ntype EvaluateResult = { ok: true; val: unknown } | { ok: false; err: unknown };\n\n// ---------------------------------------------------------------------------\n// Retry wrapper for context-destroyed errors\n// ---------------------------------------------------------------------------\n\n/**\n * Calls `page.evaluate(() => window.__SCOPE_CAPTURE_JSON__())` with retry\n * logic that catches \"Execution context was destroyed\" errors caused by\n * navigations or page reloads that race with the evaluate call.\n *\n * Prefers `__SCOPE_CAPTURE_JSON__` (returns a pre-serialized JSON string from\n * the browser, bypassing Playwright's CDP structured-clone limit) and falls\n * back to `__SCOPE_CAPTURE__` for older runtime versions that don't expose the\n * JSON variant.\n *\n * Always active — not gated on `waitForStable`.\n * Retries up to {@link MAX_RETRIES} times, waiting {@link RETRY_DELAY_MS} ms between\n * attempts.\n */\nexport async function evaluateCapture(p: Page): Promise<PageReport> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await sleep(RETRY_DELAY_MS);\n }\n\n // Use .then(ok, err) to attach the rejection handler synchronously,\n // preventing \"PromiseRejectionHandledWarning\" in test environments\n // that use fake timers.\n const outcome: EvaluateResult = await p\n .evaluate(async () => {\n const win = window as Window & {\n __SCOPE_CAPTURE_JSON__?: () => Promise<string>;\n __SCOPE_CAPTURE__?: () => Promise<unknown>;\n };\n // Prefer JSON serialization to avoid CDP structured-clone limits.\n if (typeof win.__SCOPE_CAPTURE_JSON__ === \"function\") {\n return win.__SCOPE_CAPTURE_JSON__();\n }\n // Fallback for older runtime versions without the JSON variant.\n if (typeof win.__SCOPE_CAPTURE__ === \"function\") {\n return win.__SCOPE_CAPTURE__();\n }\n throw new Error(\n \"Scope runtime not injected. \" +\n \"Make sure you navigated to the page AFTER the scope fixture was set up, \" +\n \"not before.\",\n );\n })\n .then(\n (val) => ({ ok: true as const, val }),\n (err: unknown) => ({ ok: false as const, err }),\n );\n\n if (outcome.ok) {\n // If the result is a string, it came from __SCOPE_CAPTURE_JSON__ —\n // parse it on the Node side. Otherwise it's a plain object from the\n // legacy __SCOPE_CAPTURE__ path (Playwright serialised it via CDP).\n const parsed = typeof outcome.val === \"string\" ? JSON.parse(outcome.val) : outcome.val;\n return { ...(parsed as Omit<PageReport, \"route\">), route: null };\n }\n\n const message = outcome.err instanceof Error ? outcome.err.message : String(outcome.err);\n\n if (CONTEXT_DESTROYED_PATTERN.test(message) && attempt < MAX_RETRIES) {\n lastError = outcome.err;\n continue;\n }\n\n // Not a retriable error, or we've exhausted retries — rethrow.\n throw outcome.err;\n }\n\n // Only reachable after MAX_RETRIES consecutive context-destroyed failures.\n throw lastError;\n}\n\n// ---------------------------------------------------------------------------\n// waitForStable polling\n// ---------------------------------------------------------------------------\n\nexport const POLL_INTERVAL_MS = 300;\nexport const DEFAULT_STABLE_MS = 1000;\nexport const DEFAULT_TIMEOUT_MS = 15000;\n\n/**\n * Polls `evaluateCapture` every {@link POLL_INTERVAL_MS} ms until the\n * component-node count in the returned tree has been stable for `stableMs`\n * milliseconds, or `timeoutMs` has elapsed.\n *\n * When the timeout is reached the last successful capture is returned instead\n * of throwing, so tests stay resilient against perpetually-updating SPAs.\n */\nexport async function captureUntilStable(\n p: Page,\n stableMs: number,\n timeoutMs: number,\n): Promise<PageReport> {\n const deadline = Date.now() + timeoutMs;\n\n let lastReport: PageReport = await evaluateCapture(p);\n let lastCount = countNodes(lastReport.tree);\n let stableSince = Date.now();\n\n while (true) {\n await sleep(POLL_INTERVAL_MS);\n\n const now = Date.now();\n\n // Timeout: return the last good capture instead of throwing.\n if (now >= deadline) {\n return lastReport;\n }\n\n const report = await evaluateCapture(p);\n const count = countNodes(report.tree);\n\n if (count !== lastCount) {\n // Tree is still growing/shrinking — reset the stable clock.\n lastCount = count;\n stableSince = now;\n lastReport = report;\n } else {\n // Count unchanged — check if we've been stable long enough.\n lastReport = report;\n if (now - stableSince >= stableMs) {\n return lastReport;\n }\n }\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { PageReport } from \"@agent-scope/core\";\nimport type { Page } from \"@playwright/test\";\nimport { test as base } from \"@playwright/test\";\nimport { captureUntilStable, evaluateCapture } from \"./capture-utils.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Locate the pre-built browser IIFE bundle.\n// Works from both src/ (during Playwright TS transpilation) and dist/ (installed).\nfunction getBrowserBundlePath(): string {\n const candidates = [\n join(__dirname, \"browser-bundle.iife.js\"), // when running from dist/\n join(__dirname, \"../dist/browser-bundle.iife.js\"), // when running from src/\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) return candidate;\n }\n throw new Error(\n `@agent-scope/playwright: browser bundle not found.\\n` +\n `Run \\`bun run build\\` in packages/playwright first.\\n` +\n `Searched:\\n${candidates.map((c) => ` ${c}`).join(\"\\n\")}`,\n );\n}\n\n/**\n * Options for {@link ScopeFixture.scope.capture}.\n */\nexport interface CaptureOptions {\n /**\n * When `true`, capture polls `__SCOPE_CAPTURE__()` until the component count\n * in the returned tree is stable for `stableMs` milliseconds.\n *\n * Useful when the page performs async data loading that causes React to\n * mount additional components after the initial render.\n *\n * @default false\n */\n waitForStable?: boolean;\n /**\n * How long (in milliseconds) the component count must remain unchanged\n * before the capture is considered stable.\n *\n * Only used when `waitForStable` is `true`.\n *\n * @default 1000\n */\n stableMs?: number;\n /**\n * Maximum time (in milliseconds) to spend polling for a stable capture.\n * When this timeout is reached the last successful capture is returned\n * instead of throwing, so tests remain resilient against perpetually\n * updating SPAs.\n *\n * Only used when `waitForStable` is `true`.\n *\n * @default 15000\n */\n timeoutMs?: number;\n}\n\nexport interface ScopeFixture {\n scope: {\n /**\n * Capture the React component tree from the current page.\n * The init script must already be injected (happens automatically when using\n * this fixture — navigate the page AFTER the test starts).\n *\n * The browser bundle waits for React's first commit internally, so it is\n * safe to call immediately after page.goto().\n *\n * @param options - Optional capture options (waitForStable, stableMs, timeoutMs).\n */\n capture(options?: CaptureOptions): Promise<PageReport>;\n /**\n * Capture the React component tree from `targetPage`.\n *\n * @param targetPage - An alternative Playwright `Page` to capture from.\n * @param options - Optional capture options (waitForStable, stableMs, timeoutMs).\n */\n capture(targetPage: Page, options?: CaptureOptions): Promise<PageReport>;\n /**\n * Navigate to `url` then capture.\n * Uses the fixture's default `page` — the init script is injected automatically.\n */\n captureUrl(url: string): Promise<PageReport>;\n };\n}\n\nexport const test = base.extend<ScopeFixture>({\n scope: async ({ page }, use) => {\n const bundlePath = getBrowserBundlePath();\n\n // Register the init script on the default page.\n // addInitScript() applies to ALL future navigations on this page.\n // Tests must call page.goto() AFTER the fixture has started (which is always\n // true since fixtures run before test bodies).\n await page.addInitScript({ path: bundlePath });\n\n const scopeFixture: ScopeFixture[\"scope\"] = {\n // Overload implementation: first arg may be a Page or CaptureOptions.\n async capture(\n targetPageOrOptions?: Page | CaptureOptions,\n maybeOptions?: CaptureOptions,\n ): Promise<PageReport> {\n let p: Page;\n let options: CaptureOptions;\n\n if (\n targetPageOrOptions !== undefined &&\n typeof (targetPageOrOptions as Page).evaluate === \"function\"\n ) {\n // Called as capture(page, options?)\n p = targetPageOrOptions as Page;\n options = maybeOptions ?? {};\n // If a different page object is passed, inject the bundle there too.\n await (p as Page).addInitScript({ path: bundlePath });\n } else {\n // Called as capture(options?)\n p = page;\n options = (targetPageOrOptions as CaptureOptions | undefined) ?? {};\n }\n\n const { waitForStable = false, stableMs = 1000, timeoutMs = 15000 } = options;\n\n if (waitForStable) {\n return captureUntilStable(p, stableMs, timeoutMs);\n }\n\n return evaluateCapture(p);\n },\n\n async captureUrl(url: string): Promise<PageReport> {\n await page.goto(url);\n return evaluateCapture(page);\n },\n };\n\n await use(scopeFixture);\n },\n});\n\nexport { expect } from \"@playwright/test\";\n","/**\n * @agent-scope/playwright\n *\n * Playwright integration for Scope.\n * Provides fixtures, helpers, and test generators that consume\n * @agent-scope/core PageReport captures.\n */\n\nexport type { CaptureOptions, ScopeFixture } from \"./fixture.js\";\n// Fixture re-exports\nexport { expect, test } from \"./fixture.js\";\n\nimport type { PageReport } from \"@agent-scope/core\";\nimport { isPageReport } from \"@agent-scope/core\";\nimport type { ScopeRuntime } from \"@agent-scope/runtime\";\n\nexport type { PageReport };\nexport type { ScopeRuntime };\n\n// --- Playwright fixture types ---\n\n/** Options for the Scope Playwright fixture */\nexport interface ScopeFixtureOptions {\n /** Base URL of the app under test */\n baseURL: string;\n /** Timeout (ms) to wait for a capture to complete */\n captureTimeout?: number;\n}\n\n/** A captured page report ready for assertion or snapshot */\nexport interface CaptureTrace {\n readonly report: PageReport;\n readonly capturedAt: number;\n}\n\n// --- Trace loading ---\n\n/**\n * Load a Scope `PageReport` from a raw JSON string.\n * Throws when the payload is not a valid `PageReport`.\n */\nexport function loadTrace(raw: string): CaptureTrace {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\"@agent-scope/playwright: failed to parse trace JSON\");\n }\n if (!isPageReport(parsed)) {\n throw new Error(\"@agent-scope/playwright: invalid PageReport format\");\n }\n return {\n report: parsed,\n capturedAt: Date.now(),\n };\n}\n\n// --- Test generation ---\n\n/** Options for generating a Playwright test from a trace */\nexport interface GenerateTestOptions {\n /** Human-readable test description */\n description?: string;\n /** Target file path for the generated test */\n outputPath?: string;\n}\n\n/**\n * Generate a Playwright test skeleton from a capture trace.\n * Returns the test source as a string.\n *\n * Full implementation in Phase 1.\n */\nexport function generateTest(trace: CaptureTrace, options: GenerateTestOptions = {}): string {\n const { description = \"Scope replay test\", outputPath = \"scope.spec.ts\" } = options;\n const componentName = trace.report.tree.name;\n const errorCount = trace.report.errors.length;\n\n return [\n `// Generated by @agent-scope/playwright`,\n `// URL: ${trace.report.url}`,\n `// Root component: ${componentName}`,\n `// Errors captured: ${errorCount}`,\n `// Output: ${outputPath}`,\n ``,\n `import { test, expect } from \"@playwright/test\";`,\n ``,\n `test(\"${description}\", async ({ page }) => {`,\n ` await page.goto(\"${trace.report.url}\");`,\n ` // TODO: replay captured component tree from trace`,\n ` expect(true).toBe(true);`,\n `});`,\n ].join(\"\\n\");\n}\n\n// --- Browser entry bundle ---\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst _dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Returns the pre-built browser IIFE bundle as a string.\n * Inject via `page.addInitScript({ content: getBrowserEntryScript() })`.\n *\n * The bundle:\n * - Installs the DevTools hook (with Vite react-refresh compatibility)\n * - Awaits the first React commit before resolving captures\n * - Exposes `window.__SCOPE_CAPTURE__(): Promise<PageReport>`\n */\nexport function getBrowserEntryScript(): string {\n const candidates = [\n join(_dirname, \"browser-bundle.iife.js\"), // when running from dist/\n join(_dirname, \"../dist/browser-bundle.iife.js\"), // when running from src/\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) return readFileSync(candidate, \"utf-8\");\n }\n throw new Error(\n `@agent-scope/playwright: browser bundle not found.\\n` +\n `Run \\`bun run build\\` in packages/playwright first.\\n` +\n `Searched:\\n${candidates.map((c) => ` ${c}`).join(\"\\n\")}`,\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/capture-utils.ts","../src/fixture.ts","../src/index.ts"],"names":["__dirname","dirname","fileURLToPath","join","existsSync","base","isPageReport","readFileSync"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ComponentNode, PageReport } from \"@agent-scope/core\";\nimport type { Page } from \"@playwright/test\";\n\n// ---------------------------------------------------------------------------\n// Node counting (Node.js side — not in-browser)\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively counts the total number of `ComponentNode` instances in a tree.\n */\nexport function countNodes(node: ComponentNode): number {\n let count = 1;\n for (const child of node.children) {\n count += countNodes(child);\n }\n return count;\n}\n\n// ---------------------------------------------------------------------------\n// Timing helpers\n// ---------------------------------------------------------------------------\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ---------------------------------------------------------------------------\n// Context-destroyed retry constants\n// ---------------------------------------------------------------------------\n\nexport const CONTEXT_DESTROYED_PATTERN = /execution context was destroyed/i;\nexport const RETRY_DELAY_MS = 500;\nexport const MAX_RETRIES = 3;\n\n// ---------------------------------------------------------------------------\n// Internal result types for safe promise handling\n// ---------------------------------------------------------------------------\n\ntype EvaluateResult = { ok: true; val: unknown } | { ok: false; err: unknown };\n\n// ---------------------------------------------------------------------------\n// Retry wrapper for context-destroyed errors\n// ---------------------------------------------------------------------------\n\n/**\n * Calls `page.evaluate(() => window.__SCOPE_CAPTURE_JSON__())` with retry\n * logic that catches \"Execution context was destroyed\" errors caused by\n * navigations or page reloads that race with the evaluate call.\n *\n * Prefers `__SCOPE_CAPTURE_JSON__` (returns a pre-serialized JSON string from\n * the browser, bypassing Playwright's CDP structured-clone limit) and falls\n * back to `__SCOPE_CAPTURE__` for older runtime versions that don't expose the\n * JSON variant.\n *\n * Always active — not gated on `waitForStable`.\n * Retries up to {@link MAX_RETRIES} times, waiting {@link RETRY_DELAY_MS} ms between\n * attempts.\n */\nexport async function evaluateCapture(p: Page): Promise<PageReport> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await sleep(RETRY_DELAY_MS);\n }\n\n // Use .then(ok, err) to attach the rejection handler synchronously,\n // preventing \"PromiseRejectionHandledWarning\" in test environments\n // that use fake timers.\n const outcome: EvaluateResult = await p\n .evaluate(async () => {\n const win = window as Window & {\n __SCOPE_CAPTURE_JSON__?: () => Promise<string>;\n __SCOPE_CAPTURE__?: () => Promise<unknown>;\n };\n // Prefer JSON serialization to avoid CDP structured-clone limits.\n if (typeof win.__SCOPE_CAPTURE_JSON__ === \"function\") {\n return win.__SCOPE_CAPTURE_JSON__();\n }\n // Fallback for older runtime versions without the JSON variant.\n if (typeof win.__SCOPE_CAPTURE__ === \"function\") {\n return win.__SCOPE_CAPTURE__();\n }\n throw new Error(\n \"Scope runtime not injected. \" +\n \"Make sure you navigated to the page AFTER the scope fixture was set up, \" +\n \"not before.\",\n );\n })\n .then(\n (val) => ({ ok: true as const, val }),\n (err: unknown) => ({ ok: false as const, err }),\n );\n\n if (outcome.ok) {\n // If the result is a string, it came from __SCOPE_CAPTURE_JSON__ —\n // parse it on the Node side. Otherwise it's a plain object from the\n // legacy __SCOPE_CAPTURE__ path (Playwright serialised it via CDP).\n const parsed = typeof outcome.val === \"string\" ? JSON.parse(outcome.val) : outcome.val;\n return { ...(parsed as Omit<PageReport, \"route\">), route: null };\n }\n\n const message = outcome.err instanceof Error ? outcome.err.message : String(outcome.err);\n\n if (CONTEXT_DESTROYED_PATTERN.test(message) && attempt < MAX_RETRIES) {\n lastError = outcome.err;\n continue;\n }\n\n // Not a retriable error, or we've exhausted retries — rethrow.\n throw outcome.err;\n }\n\n // Only reachable after MAX_RETRIES consecutive context-destroyed failures.\n throw lastError;\n}\n\n// ---------------------------------------------------------------------------\n// Lightweight capture (for stability polling)\n// ---------------------------------------------------------------------------\n\n/**\n * Calls `page.evaluate(() => window.__SCOPE_CAPTURE_JSON__({ lightweight: true }))`.\n *\n * Returns a minimal tree (structure only, no props/state/hooks) suitable for\n * node-count comparisons during stability polling. This reduces payload size\n * and browser-side serialization cost on each poll tick.\n *\n * Falls back to a full capture if the runtime doesn't support the lightweight\n * option (older versions will ignore it and return a full capture).\n */\nexport async function evaluateLightweightCapture(p: Page): Promise<PageReport> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await sleep(RETRY_DELAY_MS);\n }\n\n const outcome: EvaluateResult = await p\n .evaluate(async () => {\n const win = window as Window & {\n __SCOPE_CAPTURE_JSON__?: (options?: { lightweight?: boolean }) => Promise<string>;\n __SCOPE_CAPTURE__?: (options?: { lightweight?: boolean }) => Promise<unknown>;\n };\n if (typeof win.__SCOPE_CAPTURE_JSON__ === \"function\") {\n return win.__SCOPE_CAPTURE_JSON__({ lightweight: true });\n }\n if (typeof win.__SCOPE_CAPTURE__ === \"function\") {\n return win.__SCOPE_CAPTURE__({ lightweight: true });\n }\n throw new Error(\n \"Scope runtime not injected. \" +\n \"Make sure you navigated to the page AFTER the scope fixture was set up, \" +\n \"not before.\",\n );\n })\n .then(\n (val) => ({ ok: true as const, val }),\n (err: unknown) => ({ ok: false as const, err }),\n );\n\n if (outcome.ok) {\n const parsed = typeof outcome.val === \"string\" ? JSON.parse(outcome.val) : outcome.val;\n return { ...(parsed as Omit<PageReport, \"route\">), route: null };\n }\n\n const message = outcome.err instanceof Error ? outcome.err.message : String(outcome.err);\n\n if (CONTEXT_DESTROYED_PATTERN.test(message) && attempt < MAX_RETRIES) {\n lastError = outcome.err;\n continue;\n }\n\n throw outcome.err;\n }\n\n throw lastError;\n}\n\n// ---------------------------------------------------------------------------\n// waitForStable polling\n// ---------------------------------------------------------------------------\n\nexport const POLL_INTERVAL_MS = 300;\nexport const DEFAULT_STABLE_MS = 1000;\nexport const DEFAULT_TIMEOUT_MS = 15000;\n\n/**\n * Polls `evaluateCapture` every {@link POLL_INTERVAL_MS} ms until the\n * component-node count in the returned tree has been stable for `stableMs`\n * milliseconds, or `timeoutMs` has elapsed.\n *\n * When the timeout is reached the last successful capture is returned instead\n * of throwing, so tests stay resilient against perpetually-updating SPAs.\n *\n * @param p - The Playwright `Page` to capture from.\n * @param stableMs - How long the node count must remain unchanged before\n * the capture is considered stable.\n * @param timeoutMs - Maximum time to spend polling before returning the last\n * successful capture.\n * @param lightweight - When `true`, stability polling uses lightweight captures\n * (minimal tree data) to reduce payload size during each\n * poll tick. A single full capture is performed once\n * stability is confirmed, so the returned `PageReport`\n * always contains complete data.\n */\nexport async function captureUntilStable(\n p: Page,\n stableMs: number,\n timeoutMs: number,\n lightweight = false,\n): Promise<PageReport> {\n const deadline = Date.now() + timeoutMs;\n\n // Initial capture — always full so we have a valid baseline to return on timeout.\n let lastReport: PageReport = await evaluateCapture(p);\n let lastCount = countNodes(lastReport.tree);\n let stableSince = Date.now();\n\n while (true) {\n await sleep(POLL_INTERVAL_MS);\n\n const now = Date.now();\n\n // Timeout: return the last good capture instead of throwing.\n if (now >= deadline) {\n return lastReport;\n }\n\n // During polling, prefer lightweight captures when requested — they\n // contain only tree structure (enough for node counting) and cost less\n // to serialize and transfer over CDP.\n const report = lightweight ? await evaluateLightweightCapture(p) : await evaluateCapture(p);\n const count = countNodes(report.tree);\n\n if (count !== lastCount) {\n // Tree is still growing/shrinking — reset the stable clock.\n lastCount = count;\n stableSince = now;\n // Keep a full-ish report for timeout fallback. When lightweight, the\n // last lightweight report is sufficient — we'll do a full capture on\n // stability anyway.\n lastReport = report;\n } else {\n // Count unchanged — check if we've been stable long enough.\n lastReport = report;\n if (now - stableSince >= stableMs) {\n // Stability confirmed. If we were polling with lightweight captures,\n // perform one final full capture to return complete data.\n if (lightweight) {\n return evaluateCapture(p);\n }\n return lastReport;\n }\n }\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { PageReport } from \"@agent-scope/core\";\nimport type { Page } from \"@playwright/test\";\nimport { test as base } from \"@playwright/test\";\nimport { captureUntilStable, evaluateCapture } from \"./capture-utils.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Locate the pre-built browser IIFE bundle.\n// Works from both src/ (during Playwright TS transpilation) and dist/ (installed).\nfunction getBrowserBundlePath(): string {\n const candidates = [\n join(__dirname, \"browser-bundle.iife.js\"), // when running from dist/\n join(__dirname, \"../dist/browser-bundle.iife.js\"), // when running from src/\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) return candidate;\n }\n throw new Error(\n `@agent-scope/playwright: browser bundle not found.\\n` +\n `Run \\`bun run build\\` in packages/playwright first.\\n` +\n `Searched:\\n${candidates.map((c) => ` ${c}`).join(\"\\n\")}`,\n );\n}\n\n/**\n * Options for {@link ScopeFixture.scope.capture}.\n */\nexport interface CaptureOptions {\n /**\n * When `true`, capture polls `__SCOPE_CAPTURE__()` until the component count\n * in the returned tree is stable for `stableMs` milliseconds.\n *\n * Useful when the page performs async data loading that causes React to\n * mount additional components after the initial render.\n *\n * @default false\n */\n waitForStable?: boolean;\n /**\n * How long (in milliseconds) the component count must remain unchanged\n * before the capture is considered stable.\n *\n * Only used when `waitForStable` is `true`.\n *\n * @default 1000\n */\n stableMs?: number;\n /**\n * Maximum time (in milliseconds) to spend polling for a stable capture.\n * When this timeout is reached the last successful capture is returned\n * instead of throwing, so tests remain resilient against perpetually\n * updating SPAs.\n *\n * Only used when `waitForStable` is `true`.\n *\n * @default 15000\n */\n timeoutMs?: number;\n /**\n * When `true`, stability polling uses lightweight captures (minimal tree\n * data — structure only, no props/state/hooks) to reduce payload size and\n * serialization cost on each poll tick. Once stability is confirmed, a\n * single full capture is performed and returned.\n *\n * Only used when `waitForStable` is `true`.\n *\n * @default false\n */\n lightweight?: boolean;\n}\n\nexport interface ScopeFixture {\n scope: {\n /**\n * Capture the React component tree from the current page.\n * The init script must already be injected (happens automatically when using\n * this fixture — navigate the page AFTER the test starts).\n *\n * The browser bundle waits for React's first commit internally, so it is\n * safe to call immediately after page.goto().\n *\n * @param options - Optional capture options (waitForStable, stableMs, timeoutMs, lightweight).\n */\n capture(options?: CaptureOptions): Promise<PageReport>;\n /**\n * Capture the React component tree from `targetPage`.\n *\n * @param targetPage - An alternative Playwright `Page` to capture from.\n * @param options - Optional capture options (waitForStable, stableMs, timeoutMs, lightweight).\n */\n capture(targetPage: Page, options?: CaptureOptions): Promise<PageReport>;\n /**\n * Navigate to `url` then capture.\n * Uses the fixture's default `page` — the init script is injected automatically.\n */\n captureUrl(url: string): Promise<PageReport>;\n };\n}\n\nexport const test = base.extend<ScopeFixture>({\n scope: async ({ page }, use) => {\n const bundlePath = getBrowserBundlePath();\n\n // Register the init script on the default page.\n // addInitScript() applies to ALL future navigations on this page.\n // Tests must call page.goto() AFTER the fixture has started (which is always\n // true since fixtures run before test bodies).\n await page.addInitScript({ path: bundlePath });\n\n const scopeFixture: ScopeFixture[\"scope\"] = {\n // Overload implementation: first arg may be a Page or CaptureOptions.\n async capture(\n targetPageOrOptions?: Page | CaptureOptions,\n maybeOptions?: CaptureOptions,\n ): Promise<PageReport> {\n let p: Page;\n let options: CaptureOptions;\n\n if (\n targetPageOrOptions !== undefined &&\n typeof (targetPageOrOptions as Page).evaluate === \"function\"\n ) {\n // Called as capture(page, options?)\n p = targetPageOrOptions as Page;\n options = maybeOptions ?? {};\n // If a different page object is passed, inject the bundle there too.\n await (p as Page).addInitScript({ path: bundlePath });\n } else {\n // Called as capture(options?)\n p = page;\n options = (targetPageOrOptions as CaptureOptions | undefined) ?? {};\n }\n\n const {\n waitForStable = false,\n stableMs = 1000,\n timeoutMs = 15000,\n lightweight = false,\n } = options;\n\n if (waitForStable) {\n return captureUntilStable(p, stableMs, timeoutMs, lightweight);\n }\n\n return evaluateCapture(p);\n },\n\n async captureUrl(url: string): Promise<PageReport> {\n await page.goto(url);\n return evaluateCapture(page);\n },\n };\n\n await use(scopeFixture);\n },\n});\n\nexport { expect } from \"@playwright/test\";\n","/**\n * @agent-scope/playwright\n *\n * Playwright integration for Scope.\n * Provides fixtures, helpers, and test generators that consume\n * @agent-scope/core PageReport captures.\n */\n\nexport type { CaptureOptions, ScopeFixture } from \"./fixture.js\";\n// Fixture re-exports\nexport { expect, test } from \"./fixture.js\";\n\nimport type { PageReport } from \"@agent-scope/core\";\nimport { isPageReport } from \"@agent-scope/core\";\nimport type { ScopeRuntime } from \"@agent-scope/runtime\";\n\nexport type { PageReport };\nexport type { ScopeRuntime };\n\n// --- Playwright fixture types ---\n\n/** Options for the Scope Playwright fixture */\nexport interface ScopeFixtureOptions {\n /** Base URL of the app under test */\n baseURL: string;\n /** Timeout (ms) to wait for a capture to complete */\n captureTimeout?: number;\n}\n\n/** A captured page report ready for assertion or snapshot */\nexport interface CaptureTrace {\n readonly report: PageReport;\n readonly capturedAt: number;\n}\n\n// --- Trace loading ---\n\n/**\n * Load a Scope `PageReport` from a raw JSON string.\n * Throws when the payload is not a valid `PageReport`.\n */\nexport function loadTrace(raw: string): CaptureTrace {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\"@agent-scope/playwright: failed to parse trace JSON\");\n }\n if (!isPageReport(parsed)) {\n throw new Error(\"@agent-scope/playwright: invalid PageReport format\");\n }\n return {\n report: parsed,\n capturedAt: Date.now(),\n };\n}\n\n// --- Test generation ---\n\n/** Options for generating a Playwright test from a trace */\nexport interface GenerateTestOptions {\n /** Human-readable test description */\n description?: string;\n /** Target file path for the generated test */\n outputPath?: string;\n}\n\n/**\n * Generate a Playwright test skeleton from a capture trace.\n * Returns the test source as a string.\n *\n * Full implementation in Phase 1.\n */\nexport function generateTest(trace: CaptureTrace, options: GenerateTestOptions = {}): string {\n const { description = \"Scope replay test\", outputPath = \"scope.spec.ts\" } = options;\n const componentName = trace.report.tree.name;\n const errorCount = trace.report.errors.length;\n\n return [\n `// Generated by @agent-scope/playwright`,\n `// URL: ${trace.report.url}`,\n `// Root component: ${componentName}`,\n `// Errors captured: ${errorCount}`,\n `// Output: ${outputPath}`,\n ``,\n `import { test, expect } from \"@playwright/test\";`,\n ``,\n `test(\"${description}\", async ({ page }) => {`,\n ` await page.goto(\"${trace.report.url}\");`,\n ` // TODO: replay captured component tree from trace`,\n ` expect(true).toBe(true);`,\n `});`,\n ].join(\"\\n\");\n}\n\n// --- Browser entry bundle ---\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst _dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Returns the pre-built browser IIFE bundle as a string.\n * Inject via `page.addInitScript({ content: getBrowserEntryScript() })`.\n *\n * The bundle:\n * - Installs the DevTools hook (with Vite react-refresh compatibility)\n * - Awaits the first React commit before resolving captures\n * - Exposes `window.__SCOPE_CAPTURE__(): Promise<PageReport>`\n */\nexport function getBrowserEntryScript(): string {\n const candidates = [\n join(_dirname, \"browser-bundle.iife.js\"), // when running from dist/\n join(_dirname, \"../dist/browser-bundle.iife.js\"), // when running from src/\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) return readFileSync(candidate, \"utf-8\");\n }\n throw new Error(\n `@agent-scope/playwright: browser bundle not found.\\n` +\n `Run \\`bun run build\\` in packages/playwright first.\\n` +\n `Searched:\\n${candidates.map((c) => ` ${c}`).join(\"\\n\")}`,\n );\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -39,6 +39,17 @@ interface CaptureOptions {
39
39
  * @default 15000
40
40
  */
41
41
  timeoutMs?: number;
42
+ /**
43
+ * When `true`, stability polling uses lightweight captures (minimal tree
44
+ * data — structure only, no props/state/hooks) to reduce payload size and
45
+ * serialization cost on each poll tick. Once stability is confirmed, a
46
+ * single full capture is performed and returned.
47
+ *
48
+ * Only used when `waitForStable` is `true`.
49
+ *
50
+ * @default false
51
+ */
52
+ lightweight?: boolean;
42
53
  }
43
54
  interface ScopeFixture {
44
55
  scope: {
@@ -50,14 +61,14 @@ interface ScopeFixture {
50
61
  * The browser bundle waits for React's first commit internally, so it is
51
62
  * safe to call immediately after page.goto().
52
63
  *
53
- * @param options - Optional capture options (waitForStable, stableMs, timeoutMs).
64
+ * @param options - Optional capture options (waitForStable, stableMs, timeoutMs, lightweight).
54
65
  */
55
66
  capture(options?: CaptureOptions): Promise<PageReport>;
56
67
  /**
57
68
  * Capture the React component tree from `targetPage`.
58
69
  *
59
70
  * @param targetPage - An alternative Playwright `Page` to capture from.
60
- * @param options - Optional capture options (waitForStable, stableMs, timeoutMs).
71
+ * @param options - Optional capture options (waitForStable, stableMs, timeoutMs, lightweight).
61
72
  */
62
73
  capture(targetPage: Page, options?: CaptureOptions): Promise<PageReport>;
63
74
  /**
package/dist/index.d.ts CHANGED
@@ -39,6 +39,17 @@ interface CaptureOptions {
39
39
  * @default 15000
40
40
  */
41
41
  timeoutMs?: number;
42
+ /**
43
+ * When `true`, stability polling uses lightweight captures (minimal tree
44
+ * data — structure only, no props/state/hooks) to reduce payload size and
45
+ * serialization cost on each poll tick. Once stability is confirmed, a
46
+ * single full capture is performed and returned.
47
+ *
48
+ * Only used when `waitForStable` is `true`.
49
+ *
50
+ * @default false
51
+ */
52
+ lightweight?: boolean;
42
53
  }
43
54
  interface ScopeFixture {
44
55
  scope: {
@@ -50,14 +61,14 @@ interface ScopeFixture {
50
61
  * The browser bundle waits for React's first commit internally, so it is
51
62
  * safe to call immediately after page.goto().
52
63
  *
53
- * @param options - Optional capture options (waitForStable, stableMs, timeoutMs).
64
+ * @param options - Optional capture options (waitForStable, stableMs, timeoutMs, lightweight).
54
65
  */
55
66
  capture(options?: CaptureOptions): Promise<PageReport>;
56
67
  /**
57
68
  * Capture the React component tree from `targetPage`.
58
69
  *
59
70
  * @param targetPage - An alternative Playwright `Page` to capture from.
60
- * @param options - Optional capture options (waitForStable, stableMs, timeoutMs).
71
+ * @param options - Optional capture options (waitForStable, stableMs, timeoutMs, lightweight).
61
72
  */
62
73
  capture(targetPage: Page, options?: CaptureOptions): Promise<PageReport>;
63
74
  /**
package/dist/index.js CHANGED
@@ -55,8 +55,42 @@ async function evaluateCapture(p) {
55
55
  }
56
56
  throw lastError;
57
57
  }
58
+ async function evaluateLightweightCapture(p) {
59
+ let lastError;
60
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
61
+ if (attempt > 0) {
62
+ await sleep(RETRY_DELAY_MS);
63
+ }
64
+ const outcome = await p.evaluate(async () => {
65
+ const win = window;
66
+ if (typeof win.__SCOPE_CAPTURE_JSON__ === "function") {
67
+ return win.__SCOPE_CAPTURE_JSON__({ lightweight: true });
68
+ }
69
+ if (typeof win.__SCOPE_CAPTURE__ === "function") {
70
+ return win.__SCOPE_CAPTURE__({ lightweight: true });
71
+ }
72
+ throw new Error(
73
+ "Scope runtime not injected. Make sure you navigated to the page AFTER the scope fixture was set up, not before."
74
+ );
75
+ }).then(
76
+ (val) => ({ ok: true, val }),
77
+ (err) => ({ ok: false, err })
78
+ );
79
+ if (outcome.ok) {
80
+ const parsed = typeof outcome.val === "string" ? JSON.parse(outcome.val) : outcome.val;
81
+ return { ...parsed, route: null };
82
+ }
83
+ const message = outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
84
+ if (CONTEXT_DESTROYED_PATTERN.test(message) && attempt < MAX_RETRIES) {
85
+ lastError = outcome.err;
86
+ continue;
87
+ }
88
+ throw outcome.err;
89
+ }
90
+ throw lastError;
91
+ }
58
92
  var POLL_INTERVAL_MS = 300;
59
- async function captureUntilStable(p, stableMs, timeoutMs) {
93
+ async function captureUntilStable(p, stableMs, timeoutMs, lightweight = false) {
60
94
  const deadline = Date.now() + timeoutMs;
61
95
  let lastReport = await evaluateCapture(p);
62
96
  let lastCount = countNodes(lastReport.tree);
@@ -67,7 +101,7 @@ async function captureUntilStable(p, stableMs, timeoutMs) {
67
101
  if (now >= deadline) {
68
102
  return lastReport;
69
103
  }
70
- const report = await evaluateCapture(p);
104
+ const report = lightweight ? await evaluateLightweightCapture(p) : await evaluateCapture(p);
71
105
  const count = countNodes(report.tree);
72
106
  if (count !== lastCount) {
73
107
  lastCount = count;
@@ -76,6 +110,9 @@ async function captureUntilStable(p, stableMs, timeoutMs) {
76
110
  } else {
77
111
  lastReport = report;
78
112
  if (now - stableSince >= stableMs) {
113
+ if (lightweight) {
114
+ return evaluateCapture(p);
115
+ }
79
116
  return lastReport;
80
117
  }
81
118
  }
@@ -116,9 +153,14 @@ var test = test$1.extend({
116
153
  p = page;
117
154
  options = targetPageOrOptions ?? {};
118
155
  }
119
- const { waitForStable = false, stableMs = 1e3, timeoutMs = 15e3 } = options;
156
+ const {
157
+ waitForStable = false,
158
+ stableMs = 1e3,
159
+ timeoutMs = 15e3,
160
+ lightweight = false
161
+ } = options;
120
162
  if (waitForStable) {
121
- return captureUntilStable(p, stableMs, timeoutMs);
163
+ return captureUntilStable(p, stableMs, timeoutMs, lightweight);
122
164
  }
123
165
  return evaluateCapture(p);
124
166
  },
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/capture-utils.ts","../src/fixture.ts","../src/index.ts"],"names":["__dirname","base","dirname","fileURLToPath","join","existsSync"],"mappings":"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","file":"index.js","sourcesContent":["import type { ComponentNode, PageReport } from \"@agent-scope/core\";\nimport type { Page } from \"@playwright/test\";\n\n// ---------------------------------------------------------------------------\n// Node counting (Node.js side — not in-browser)\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively counts the total number of `ComponentNode` instances in a tree.\n */\nexport function countNodes(node: ComponentNode): number {\n let count = 1;\n for (const child of node.children) {\n count += countNodes(child);\n }\n return count;\n}\n\n// ---------------------------------------------------------------------------\n// Timing helpers\n// ---------------------------------------------------------------------------\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ---------------------------------------------------------------------------\n// Context-destroyed retry constants\n// ---------------------------------------------------------------------------\n\nexport const CONTEXT_DESTROYED_PATTERN = /execution context was destroyed/i;\nexport const RETRY_DELAY_MS = 500;\nexport const MAX_RETRIES = 3;\n\n// ---------------------------------------------------------------------------\n// Internal result types for safe promise handling\n// ---------------------------------------------------------------------------\n\ntype EvaluateResult = { ok: true; val: unknown } | { ok: false; err: unknown };\n\n// ---------------------------------------------------------------------------\n// Retry wrapper for context-destroyed errors\n// ---------------------------------------------------------------------------\n\n/**\n * Calls `page.evaluate(() => window.__SCOPE_CAPTURE_JSON__())` with retry\n * logic that catches \"Execution context was destroyed\" errors caused by\n * navigations or page reloads that race with the evaluate call.\n *\n * Prefers `__SCOPE_CAPTURE_JSON__` (returns a pre-serialized JSON string from\n * the browser, bypassing Playwright's CDP structured-clone limit) and falls\n * back to `__SCOPE_CAPTURE__` for older runtime versions that don't expose the\n * JSON variant.\n *\n * Always active — not gated on `waitForStable`.\n * Retries up to {@link MAX_RETRIES} times, waiting {@link RETRY_DELAY_MS} ms between\n * attempts.\n */\nexport async function evaluateCapture(p: Page): Promise<PageReport> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await sleep(RETRY_DELAY_MS);\n }\n\n // Use .then(ok, err) to attach the rejection handler synchronously,\n // preventing \"PromiseRejectionHandledWarning\" in test environments\n // that use fake timers.\n const outcome: EvaluateResult = await p\n .evaluate(async () => {\n const win = window as Window & {\n __SCOPE_CAPTURE_JSON__?: () => Promise<string>;\n __SCOPE_CAPTURE__?: () => Promise<unknown>;\n };\n // Prefer JSON serialization to avoid CDP structured-clone limits.\n if (typeof win.__SCOPE_CAPTURE_JSON__ === \"function\") {\n return win.__SCOPE_CAPTURE_JSON__();\n }\n // Fallback for older runtime versions without the JSON variant.\n if (typeof win.__SCOPE_CAPTURE__ === \"function\") {\n return win.__SCOPE_CAPTURE__();\n }\n throw new Error(\n \"Scope runtime not injected. \" +\n \"Make sure you navigated to the page AFTER the scope fixture was set up, \" +\n \"not before.\",\n );\n })\n .then(\n (val) => ({ ok: true as const, val }),\n (err: unknown) => ({ ok: false as const, err }),\n );\n\n if (outcome.ok) {\n // If the result is a string, it came from __SCOPE_CAPTURE_JSON__ —\n // parse it on the Node side. Otherwise it's a plain object from the\n // legacy __SCOPE_CAPTURE__ path (Playwright serialised it via CDP).\n const parsed = typeof outcome.val === \"string\" ? JSON.parse(outcome.val) : outcome.val;\n return { ...(parsed as Omit<PageReport, \"route\">), route: null };\n }\n\n const message = outcome.err instanceof Error ? outcome.err.message : String(outcome.err);\n\n if (CONTEXT_DESTROYED_PATTERN.test(message) && attempt < MAX_RETRIES) {\n lastError = outcome.err;\n continue;\n }\n\n // Not a retriable error, or we've exhausted retries — rethrow.\n throw outcome.err;\n }\n\n // Only reachable after MAX_RETRIES consecutive context-destroyed failures.\n throw lastError;\n}\n\n// ---------------------------------------------------------------------------\n// waitForStable polling\n// ---------------------------------------------------------------------------\n\nexport const POLL_INTERVAL_MS = 300;\nexport const DEFAULT_STABLE_MS = 1000;\nexport const DEFAULT_TIMEOUT_MS = 15000;\n\n/**\n * Polls `evaluateCapture` every {@link POLL_INTERVAL_MS} ms until the\n * component-node count in the returned tree has been stable for `stableMs`\n * milliseconds, or `timeoutMs` has elapsed.\n *\n * When the timeout is reached the last successful capture is returned instead\n * of throwing, so tests stay resilient against perpetually-updating SPAs.\n */\nexport async function captureUntilStable(\n p: Page,\n stableMs: number,\n timeoutMs: number,\n): Promise<PageReport> {\n const deadline = Date.now() + timeoutMs;\n\n let lastReport: PageReport = await evaluateCapture(p);\n let lastCount = countNodes(lastReport.tree);\n let stableSince = Date.now();\n\n while (true) {\n await sleep(POLL_INTERVAL_MS);\n\n const now = Date.now();\n\n // Timeout: return the last good capture instead of throwing.\n if (now >= deadline) {\n return lastReport;\n }\n\n const report = await evaluateCapture(p);\n const count = countNodes(report.tree);\n\n if (count !== lastCount) {\n // Tree is still growing/shrinking — reset the stable clock.\n lastCount = count;\n stableSince = now;\n lastReport = report;\n } else {\n // Count unchanged — check if we've been stable long enough.\n lastReport = report;\n if (now - stableSince >= stableMs) {\n return lastReport;\n }\n }\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { PageReport } from \"@agent-scope/core\";\nimport type { Page } from \"@playwright/test\";\nimport { test as base } from \"@playwright/test\";\nimport { captureUntilStable, evaluateCapture } from \"./capture-utils.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Locate the pre-built browser IIFE bundle.\n// Works from both src/ (during Playwright TS transpilation) and dist/ (installed).\nfunction getBrowserBundlePath(): string {\n const candidates = [\n join(__dirname, \"browser-bundle.iife.js\"), // when running from dist/\n join(__dirname, \"../dist/browser-bundle.iife.js\"), // when running from src/\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) return candidate;\n }\n throw new Error(\n `@agent-scope/playwright: browser bundle not found.\\n` +\n `Run \\`bun run build\\` in packages/playwright first.\\n` +\n `Searched:\\n${candidates.map((c) => ` ${c}`).join(\"\\n\")}`,\n );\n}\n\n/**\n * Options for {@link ScopeFixture.scope.capture}.\n */\nexport interface CaptureOptions {\n /**\n * When `true`, capture polls `__SCOPE_CAPTURE__()` until the component count\n * in the returned tree is stable for `stableMs` milliseconds.\n *\n * Useful when the page performs async data loading that causes React to\n * mount additional components after the initial render.\n *\n * @default false\n */\n waitForStable?: boolean;\n /**\n * How long (in milliseconds) the component count must remain unchanged\n * before the capture is considered stable.\n *\n * Only used when `waitForStable` is `true`.\n *\n * @default 1000\n */\n stableMs?: number;\n /**\n * Maximum time (in milliseconds) to spend polling for a stable capture.\n * When this timeout is reached the last successful capture is returned\n * instead of throwing, so tests remain resilient against perpetually\n * updating SPAs.\n *\n * Only used when `waitForStable` is `true`.\n *\n * @default 15000\n */\n timeoutMs?: number;\n}\n\nexport interface ScopeFixture {\n scope: {\n /**\n * Capture the React component tree from the current page.\n * The init script must already be injected (happens automatically when using\n * this fixture — navigate the page AFTER the test starts).\n *\n * The browser bundle waits for React's first commit internally, so it is\n * safe to call immediately after page.goto().\n *\n * @param options - Optional capture options (waitForStable, stableMs, timeoutMs).\n */\n capture(options?: CaptureOptions): Promise<PageReport>;\n /**\n * Capture the React component tree from `targetPage`.\n *\n * @param targetPage - An alternative Playwright `Page` to capture from.\n * @param options - Optional capture options (waitForStable, stableMs, timeoutMs).\n */\n capture(targetPage: Page, options?: CaptureOptions): Promise<PageReport>;\n /**\n * Navigate to `url` then capture.\n * Uses the fixture's default `page` — the init script is injected automatically.\n */\n captureUrl(url: string): Promise<PageReport>;\n };\n}\n\nexport const test = base.extend<ScopeFixture>({\n scope: async ({ page }, use) => {\n const bundlePath = getBrowserBundlePath();\n\n // Register the init script on the default page.\n // addInitScript() applies to ALL future navigations on this page.\n // Tests must call page.goto() AFTER the fixture has started (which is always\n // true since fixtures run before test bodies).\n await page.addInitScript({ path: bundlePath });\n\n const scopeFixture: ScopeFixture[\"scope\"] = {\n // Overload implementation: first arg may be a Page or CaptureOptions.\n async capture(\n targetPageOrOptions?: Page | CaptureOptions,\n maybeOptions?: CaptureOptions,\n ): Promise<PageReport> {\n let p: Page;\n let options: CaptureOptions;\n\n if (\n targetPageOrOptions !== undefined &&\n typeof (targetPageOrOptions as Page).evaluate === \"function\"\n ) {\n // Called as capture(page, options?)\n p = targetPageOrOptions as Page;\n options = maybeOptions ?? {};\n // If a different page object is passed, inject the bundle there too.\n await (p as Page).addInitScript({ path: bundlePath });\n } else {\n // Called as capture(options?)\n p = page;\n options = (targetPageOrOptions as CaptureOptions | undefined) ?? {};\n }\n\n const { waitForStable = false, stableMs = 1000, timeoutMs = 15000 } = options;\n\n if (waitForStable) {\n return captureUntilStable(p, stableMs, timeoutMs);\n }\n\n return evaluateCapture(p);\n },\n\n async captureUrl(url: string): Promise<PageReport> {\n await page.goto(url);\n return evaluateCapture(page);\n },\n };\n\n await use(scopeFixture);\n },\n});\n\nexport { expect } from \"@playwright/test\";\n","/**\n * @agent-scope/playwright\n *\n * Playwright integration for Scope.\n * Provides fixtures, helpers, and test generators that consume\n * @agent-scope/core PageReport captures.\n */\n\nexport type { CaptureOptions, ScopeFixture } from \"./fixture.js\";\n// Fixture re-exports\nexport { expect, test } from \"./fixture.js\";\n\nimport type { PageReport } from \"@agent-scope/core\";\nimport { isPageReport } from \"@agent-scope/core\";\nimport type { ScopeRuntime } from \"@agent-scope/runtime\";\n\nexport type { PageReport };\nexport type { ScopeRuntime };\n\n// --- Playwright fixture types ---\n\n/** Options for the Scope Playwright fixture */\nexport interface ScopeFixtureOptions {\n /** Base URL of the app under test */\n baseURL: string;\n /** Timeout (ms) to wait for a capture to complete */\n captureTimeout?: number;\n}\n\n/** A captured page report ready for assertion or snapshot */\nexport interface CaptureTrace {\n readonly report: PageReport;\n readonly capturedAt: number;\n}\n\n// --- Trace loading ---\n\n/**\n * Load a Scope `PageReport` from a raw JSON string.\n * Throws when the payload is not a valid `PageReport`.\n */\nexport function loadTrace(raw: string): CaptureTrace {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\"@agent-scope/playwright: failed to parse trace JSON\");\n }\n if (!isPageReport(parsed)) {\n throw new Error(\"@agent-scope/playwright: invalid PageReport format\");\n }\n return {\n report: parsed,\n capturedAt: Date.now(),\n };\n}\n\n// --- Test generation ---\n\n/** Options for generating a Playwright test from a trace */\nexport interface GenerateTestOptions {\n /** Human-readable test description */\n description?: string;\n /** Target file path for the generated test */\n outputPath?: string;\n}\n\n/**\n * Generate a Playwright test skeleton from a capture trace.\n * Returns the test source as a string.\n *\n * Full implementation in Phase 1.\n */\nexport function generateTest(trace: CaptureTrace, options: GenerateTestOptions = {}): string {\n const { description = \"Scope replay test\", outputPath = \"scope.spec.ts\" } = options;\n const componentName = trace.report.tree.name;\n const errorCount = trace.report.errors.length;\n\n return [\n `// Generated by @agent-scope/playwright`,\n `// URL: ${trace.report.url}`,\n `// Root component: ${componentName}`,\n `// Errors captured: ${errorCount}`,\n `// Output: ${outputPath}`,\n ``,\n `import { test, expect } from \"@playwright/test\";`,\n ``,\n `test(\"${description}\", async ({ page }) => {`,\n ` await page.goto(\"${trace.report.url}\");`,\n ` // TODO: replay captured component tree from trace`,\n ` expect(true).toBe(true);`,\n `});`,\n ].join(\"\\n\");\n}\n\n// --- Browser entry bundle ---\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst _dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Returns the pre-built browser IIFE bundle as a string.\n * Inject via `page.addInitScript({ content: getBrowserEntryScript() })`.\n *\n * The bundle:\n * - Installs the DevTools hook (with Vite react-refresh compatibility)\n * - Awaits the first React commit before resolving captures\n * - Exposes `window.__SCOPE_CAPTURE__(): Promise<PageReport>`\n */\nexport function getBrowserEntryScript(): string {\n const candidates = [\n join(_dirname, \"browser-bundle.iife.js\"), // when running from dist/\n join(_dirname, \"../dist/browser-bundle.iife.js\"), // when running from src/\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) return readFileSync(candidate, \"utf-8\");\n }\n throw new Error(\n `@agent-scope/playwright: browser bundle not found.\\n` +\n `Run \\`bun run build\\` in packages/playwright first.\\n` +\n `Searched:\\n${candidates.map((c) => ` ${c}`).join(\"\\n\")}`,\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/capture-utils.ts","../src/fixture.ts","../src/index.ts"],"names":["__dirname","base","dirname","fileURLToPath","join","existsSync"],"mappings":"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","file":"index.js","sourcesContent":["import type { ComponentNode, PageReport } from \"@agent-scope/core\";\nimport type { Page } from \"@playwright/test\";\n\n// ---------------------------------------------------------------------------\n// Node counting (Node.js side — not in-browser)\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively counts the total number of `ComponentNode` instances in a tree.\n */\nexport function countNodes(node: ComponentNode): number {\n let count = 1;\n for (const child of node.children) {\n count += countNodes(child);\n }\n return count;\n}\n\n// ---------------------------------------------------------------------------\n// Timing helpers\n// ---------------------------------------------------------------------------\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ---------------------------------------------------------------------------\n// Context-destroyed retry constants\n// ---------------------------------------------------------------------------\n\nexport const CONTEXT_DESTROYED_PATTERN = /execution context was destroyed/i;\nexport const RETRY_DELAY_MS = 500;\nexport const MAX_RETRIES = 3;\n\n// ---------------------------------------------------------------------------\n// Internal result types for safe promise handling\n// ---------------------------------------------------------------------------\n\ntype EvaluateResult = { ok: true; val: unknown } | { ok: false; err: unknown };\n\n// ---------------------------------------------------------------------------\n// Retry wrapper for context-destroyed errors\n// ---------------------------------------------------------------------------\n\n/**\n * Calls `page.evaluate(() => window.__SCOPE_CAPTURE_JSON__())` with retry\n * logic that catches \"Execution context was destroyed\" errors caused by\n * navigations or page reloads that race with the evaluate call.\n *\n * Prefers `__SCOPE_CAPTURE_JSON__` (returns a pre-serialized JSON string from\n * the browser, bypassing Playwright's CDP structured-clone limit) and falls\n * back to `__SCOPE_CAPTURE__` for older runtime versions that don't expose the\n * JSON variant.\n *\n * Always active — not gated on `waitForStable`.\n * Retries up to {@link MAX_RETRIES} times, waiting {@link RETRY_DELAY_MS} ms between\n * attempts.\n */\nexport async function evaluateCapture(p: Page): Promise<PageReport> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await sleep(RETRY_DELAY_MS);\n }\n\n // Use .then(ok, err) to attach the rejection handler synchronously,\n // preventing \"PromiseRejectionHandledWarning\" in test environments\n // that use fake timers.\n const outcome: EvaluateResult = await p\n .evaluate(async () => {\n const win = window as Window & {\n __SCOPE_CAPTURE_JSON__?: () => Promise<string>;\n __SCOPE_CAPTURE__?: () => Promise<unknown>;\n };\n // Prefer JSON serialization to avoid CDP structured-clone limits.\n if (typeof win.__SCOPE_CAPTURE_JSON__ === \"function\") {\n return win.__SCOPE_CAPTURE_JSON__();\n }\n // Fallback for older runtime versions without the JSON variant.\n if (typeof win.__SCOPE_CAPTURE__ === \"function\") {\n return win.__SCOPE_CAPTURE__();\n }\n throw new Error(\n \"Scope runtime not injected. \" +\n \"Make sure you navigated to the page AFTER the scope fixture was set up, \" +\n \"not before.\",\n );\n })\n .then(\n (val) => ({ ok: true as const, val }),\n (err: unknown) => ({ ok: false as const, err }),\n );\n\n if (outcome.ok) {\n // If the result is a string, it came from __SCOPE_CAPTURE_JSON__ —\n // parse it on the Node side. Otherwise it's a plain object from the\n // legacy __SCOPE_CAPTURE__ path (Playwright serialised it via CDP).\n const parsed = typeof outcome.val === \"string\" ? JSON.parse(outcome.val) : outcome.val;\n return { ...(parsed as Omit<PageReport, \"route\">), route: null };\n }\n\n const message = outcome.err instanceof Error ? outcome.err.message : String(outcome.err);\n\n if (CONTEXT_DESTROYED_PATTERN.test(message) && attempt < MAX_RETRIES) {\n lastError = outcome.err;\n continue;\n }\n\n // Not a retriable error, or we've exhausted retries — rethrow.\n throw outcome.err;\n }\n\n // Only reachable after MAX_RETRIES consecutive context-destroyed failures.\n throw lastError;\n}\n\n// ---------------------------------------------------------------------------\n// Lightweight capture (for stability polling)\n// ---------------------------------------------------------------------------\n\n/**\n * Calls `page.evaluate(() => window.__SCOPE_CAPTURE_JSON__({ lightweight: true }))`.\n *\n * Returns a minimal tree (structure only, no props/state/hooks) suitable for\n * node-count comparisons during stability polling. This reduces payload size\n * and browser-side serialization cost on each poll tick.\n *\n * Falls back to a full capture if the runtime doesn't support the lightweight\n * option (older versions will ignore it and return a full capture).\n */\nexport async function evaluateLightweightCapture(p: Page): Promise<PageReport> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n await sleep(RETRY_DELAY_MS);\n }\n\n const outcome: EvaluateResult = await p\n .evaluate(async () => {\n const win = window as Window & {\n __SCOPE_CAPTURE_JSON__?: (options?: { lightweight?: boolean }) => Promise<string>;\n __SCOPE_CAPTURE__?: (options?: { lightweight?: boolean }) => Promise<unknown>;\n };\n if (typeof win.__SCOPE_CAPTURE_JSON__ === \"function\") {\n return win.__SCOPE_CAPTURE_JSON__({ lightweight: true });\n }\n if (typeof win.__SCOPE_CAPTURE__ === \"function\") {\n return win.__SCOPE_CAPTURE__({ lightweight: true });\n }\n throw new Error(\n \"Scope runtime not injected. \" +\n \"Make sure you navigated to the page AFTER the scope fixture was set up, \" +\n \"not before.\",\n );\n })\n .then(\n (val) => ({ ok: true as const, val }),\n (err: unknown) => ({ ok: false as const, err }),\n );\n\n if (outcome.ok) {\n const parsed = typeof outcome.val === \"string\" ? JSON.parse(outcome.val) : outcome.val;\n return { ...(parsed as Omit<PageReport, \"route\">), route: null };\n }\n\n const message = outcome.err instanceof Error ? outcome.err.message : String(outcome.err);\n\n if (CONTEXT_DESTROYED_PATTERN.test(message) && attempt < MAX_RETRIES) {\n lastError = outcome.err;\n continue;\n }\n\n throw outcome.err;\n }\n\n throw lastError;\n}\n\n// ---------------------------------------------------------------------------\n// waitForStable polling\n// ---------------------------------------------------------------------------\n\nexport const POLL_INTERVAL_MS = 300;\nexport const DEFAULT_STABLE_MS = 1000;\nexport const DEFAULT_TIMEOUT_MS = 15000;\n\n/**\n * Polls `evaluateCapture` every {@link POLL_INTERVAL_MS} ms until the\n * component-node count in the returned tree has been stable for `stableMs`\n * milliseconds, or `timeoutMs` has elapsed.\n *\n * When the timeout is reached the last successful capture is returned instead\n * of throwing, so tests stay resilient against perpetually-updating SPAs.\n *\n * @param p - The Playwright `Page` to capture from.\n * @param stableMs - How long the node count must remain unchanged before\n * the capture is considered stable.\n * @param timeoutMs - Maximum time to spend polling before returning the last\n * successful capture.\n * @param lightweight - When `true`, stability polling uses lightweight captures\n * (minimal tree data) to reduce payload size during each\n * poll tick. A single full capture is performed once\n * stability is confirmed, so the returned `PageReport`\n * always contains complete data.\n */\nexport async function captureUntilStable(\n p: Page,\n stableMs: number,\n timeoutMs: number,\n lightweight = false,\n): Promise<PageReport> {\n const deadline = Date.now() + timeoutMs;\n\n // Initial capture — always full so we have a valid baseline to return on timeout.\n let lastReport: PageReport = await evaluateCapture(p);\n let lastCount = countNodes(lastReport.tree);\n let stableSince = Date.now();\n\n while (true) {\n await sleep(POLL_INTERVAL_MS);\n\n const now = Date.now();\n\n // Timeout: return the last good capture instead of throwing.\n if (now >= deadline) {\n return lastReport;\n }\n\n // During polling, prefer lightweight captures when requested — they\n // contain only tree structure (enough for node counting) and cost less\n // to serialize and transfer over CDP.\n const report = lightweight ? await evaluateLightweightCapture(p) : await evaluateCapture(p);\n const count = countNodes(report.tree);\n\n if (count !== lastCount) {\n // Tree is still growing/shrinking — reset the stable clock.\n lastCount = count;\n stableSince = now;\n // Keep a full-ish report for timeout fallback. When lightweight, the\n // last lightweight report is sufficient — we'll do a full capture on\n // stability anyway.\n lastReport = report;\n } else {\n // Count unchanged — check if we've been stable long enough.\n lastReport = report;\n if (now - stableSince >= stableMs) {\n // Stability confirmed. If we were polling with lightweight captures,\n // perform one final full capture to return complete data.\n if (lightweight) {\n return evaluateCapture(p);\n }\n return lastReport;\n }\n }\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { PageReport } from \"@agent-scope/core\";\nimport type { Page } from \"@playwright/test\";\nimport { test as base } from \"@playwright/test\";\nimport { captureUntilStable, evaluateCapture } from \"./capture-utils.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n// Locate the pre-built browser IIFE bundle.\n// Works from both src/ (during Playwright TS transpilation) and dist/ (installed).\nfunction getBrowserBundlePath(): string {\n const candidates = [\n join(__dirname, \"browser-bundle.iife.js\"), // when running from dist/\n join(__dirname, \"../dist/browser-bundle.iife.js\"), // when running from src/\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) return candidate;\n }\n throw new Error(\n `@agent-scope/playwright: browser bundle not found.\\n` +\n `Run \\`bun run build\\` in packages/playwright first.\\n` +\n `Searched:\\n${candidates.map((c) => ` ${c}`).join(\"\\n\")}`,\n );\n}\n\n/**\n * Options for {@link ScopeFixture.scope.capture}.\n */\nexport interface CaptureOptions {\n /**\n * When `true`, capture polls `__SCOPE_CAPTURE__()` until the component count\n * in the returned tree is stable for `stableMs` milliseconds.\n *\n * Useful when the page performs async data loading that causes React to\n * mount additional components after the initial render.\n *\n * @default false\n */\n waitForStable?: boolean;\n /**\n * How long (in milliseconds) the component count must remain unchanged\n * before the capture is considered stable.\n *\n * Only used when `waitForStable` is `true`.\n *\n * @default 1000\n */\n stableMs?: number;\n /**\n * Maximum time (in milliseconds) to spend polling for a stable capture.\n * When this timeout is reached the last successful capture is returned\n * instead of throwing, so tests remain resilient against perpetually\n * updating SPAs.\n *\n * Only used when `waitForStable` is `true`.\n *\n * @default 15000\n */\n timeoutMs?: number;\n /**\n * When `true`, stability polling uses lightweight captures (minimal tree\n * data — structure only, no props/state/hooks) to reduce payload size and\n * serialization cost on each poll tick. Once stability is confirmed, a\n * single full capture is performed and returned.\n *\n * Only used when `waitForStable` is `true`.\n *\n * @default false\n */\n lightweight?: boolean;\n}\n\nexport interface ScopeFixture {\n scope: {\n /**\n * Capture the React component tree from the current page.\n * The init script must already be injected (happens automatically when using\n * this fixture — navigate the page AFTER the test starts).\n *\n * The browser bundle waits for React's first commit internally, so it is\n * safe to call immediately after page.goto().\n *\n * @param options - Optional capture options (waitForStable, stableMs, timeoutMs, lightweight).\n */\n capture(options?: CaptureOptions): Promise<PageReport>;\n /**\n * Capture the React component tree from `targetPage`.\n *\n * @param targetPage - An alternative Playwright `Page` to capture from.\n * @param options - Optional capture options (waitForStable, stableMs, timeoutMs, lightweight).\n */\n capture(targetPage: Page, options?: CaptureOptions): Promise<PageReport>;\n /**\n * Navigate to `url` then capture.\n * Uses the fixture's default `page` — the init script is injected automatically.\n */\n captureUrl(url: string): Promise<PageReport>;\n };\n}\n\nexport const test = base.extend<ScopeFixture>({\n scope: async ({ page }, use) => {\n const bundlePath = getBrowserBundlePath();\n\n // Register the init script on the default page.\n // addInitScript() applies to ALL future navigations on this page.\n // Tests must call page.goto() AFTER the fixture has started (which is always\n // true since fixtures run before test bodies).\n await page.addInitScript({ path: bundlePath });\n\n const scopeFixture: ScopeFixture[\"scope\"] = {\n // Overload implementation: first arg may be a Page or CaptureOptions.\n async capture(\n targetPageOrOptions?: Page | CaptureOptions,\n maybeOptions?: CaptureOptions,\n ): Promise<PageReport> {\n let p: Page;\n let options: CaptureOptions;\n\n if (\n targetPageOrOptions !== undefined &&\n typeof (targetPageOrOptions as Page).evaluate === \"function\"\n ) {\n // Called as capture(page, options?)\n p = targetPageOrOptions as Page;\n options = maybeOptions ?? {};\n // If a different page object is passed, inject the bundle there too.\n await (p as Page).addInitScript({ path: bundlePath });\n } else {\n // Called as capture(options?)\n p = page;\n options = (targetPageOrOptions as CaptureOptions | undefined) ?? {};\n }\n\n const {\n waitForStable = false,\n stableMs = 1000,\n timeoutMs = 15000,\n lightweight = false,\n } = options;\n\n if (waitForStable) {\n return captureUntilStable(p, stableMs, timeoutMs, lightweight);\n }\n\n return evaluateCapture(p);\n },\n\n async captureUrl(url: string): Promise<PageReport> {\n await page.goto(url);\n return evaluateCapture(page);\n },\n };\n\n await use(scopeFixture);\n },\n});\n\nexport { expect } from \"@playwright/test\";\n","/**\n * @agent-scope/playwright\n *\n * Playwright integration for Scope.\n * Provides fixtures, helpers, and test generators that consume\n * @agent-scope/core PageReport captures.\n */\n\nexport type { CaptureOptions, ScopeFixture } from \"./fixture.js\";\n// Fixture re-exports\nexport { expect, test } from \"./fixture.js\";\n\nimport type { PageReport } from \"@agent-scope/core\";\nimport { isPageReport } from \"@agent-scope/core\";\nimport type { ScopeRuntime } from \"@agent-scope/runtime\";\n\nexport type { PageReport };\nexport type { ScopeRuntime };\n\n// --- Playwright fixture types ---\n\n/** Options for the Scope Playwright fixture */\nexport interface ScopeFixtureOptions {\n /** Base URL of the app under test */\n baseURL: string;\n /** Timeout (ms) to wait for a capture to complete */\n captureTimeout?: number;\n}\n\n/** A captured page report ready for assertion or snapshot */\nexport interface CaptureTrace {\n readonly report: PageReport;\n readonly capturedAt: number;\n}\n\n// --- Trace loading ---\n\n/**\n * Load a Scope `PageReport` from a raw JSON string.\n * Throws when the payload is not a valid `PageReport`.\n */\nexport function loadTrace(raw: string): CaptureTrace {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\"@agent-scope/playwright: failed to parse trace JSON\");\n }\n if (!isPageReport(parsed)) {\n throw new Error(\"@agent-scope/playwright: invalid PageReport format\");\n }\n return {\n report: parsed,\n capturedAt: Date.now(),\n };\n}\n\n// --- Test generation ---\n\n/** Options for generating a Playwright test from a trace */\nexport interface GenerateTestOptions {\n /** Human-readable test description */\n description?: string;\n /** Target file path for the generated test */\n outputPath?: string;\n}\n\n/**\n * Generate a Playwright test skeleton from a capture trace.\n * Returns the test source as a string.\n *\n * Full implementation in Phase 1.\n */\nexport function generateTest(trace: CaptureTrace, options: GenerateTestOptions = {}): string {\n const { description = \"Scope replay test\", outputPath = \"scope.spec.ts\" } = options;\n const componentName = trace.report.tree.name;\n const errorCount = trace.report.errors.length;\n\n return [\n `// Generated by @agent-scope/playwright`,\n `// URL: ${trace.report.url}`,\n `// Root component: ${componentName}`,\n `// Errors captured: ${errorCount}`,\n `// Output: ${outputPath}`,\n ``,\n `import { test, expect } from \"@playwright/test\";`,\n ``,\n `test(\"${description}\", async ({ page }) => {`,\n ` await page.goto(\"${trace.report.url}\");`,\n ` // TODO: replay captured component tree from trace`,\n ` expect(true).toBe(true);`,\n `});`,\n ].join(\"\\n\");\n}\n\n// --- Browser entry bundle ---\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst _dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * Returns the pre-built browser IIFE bundle as a string.\n * Inject via `page.addInitScript({ content: getBrowserEntryScript() })`.\n *\n * The bundle:\n * - Installs the DevTools hook (with Vite react-refresh compatibility)\n * - Awaits the first React commit before resolving captures\n * - Exposes `window.__SCOPE_CAPTURE__(): Promise<PageReport>`\n */\nexport function getBrowserEntryScript(): string {\n const candidates = [\n join(_dirname, \"browser-bundle.iife.js\"), // when running from dist/\n join(_dirname, \"../dist/browser-bundle.iife.js\"), // when running from src/\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) return readFileSync(candidate, \"utf-8\");\n }\n throw new Error(\n `@agent-scope/playwright: browser bundle not found.\\n` +\n `Run \\`bun run build\\` in packages/playwright first.\\n` +\n `Searched:\\n${candidates.map((c) => ` ${c}`).join(\"\\n\")}`,\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-scope/playwright",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Playwright integration for Scope — replay traces and generate tests",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@playwright/test": "^1.58.2",
34
- "@agent-scope/core": "1.2.0",
35
- "@agent-scope/runtime": "1.2.0"
34
+ "@agent-scope/core": "1.4.0",
35
+ "@agent-scope/runtime": "1.4.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "*",