@agent-scope/playwright 1.3.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.
- package/dist/browser-bundle.iife.js +2 -2
- package/dist/index.cjs +46 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +46 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -1132,11 +1132,11 @@
|
|
|
1132
1132
|
}
|
|
1133
1133
|
return capture({ lightweight: options?.lightweight });
|
|
1134
1134
|
};
|
|
1135
|
-
window.__SCOPE_CAPTURE_JSON__ = async () => {
|
|
1135
|
+
window.__SCOPE_CAPTURE_JSON__ = async (options) => {
|
|
1136
1136
|
if (!hasCommitted) {
|
|
1137
1137
|
await firstCommit;
|
|
1138
1138
|
}
|
|
1139
|
-
const result = await capture();
|
|
1139
|
+
const result = await capture({ lightweight: options?.lightweight });
|
|
1140
1140
|
return JSON.stringify(result);
|
|
1141
1141
|
};
|
|
1142
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 {
|
|
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
|
},
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":";;;;;;;;;;;;AAUO,SAAS,WAAW,IAAA,EAA6B;AACtD,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,KAAA,MAAW,KAAA,IAAS,KAAK,QAAA,EAAU;AACjC,IAAA,KAAA,IAAS,WAAW,KAAK,CAAA;AAAA,EAC3B;AACA,EAAA,OAAO,KAAA;AACT;AAMO,SAAS,MAAM,EAAA,EAA2B;AAC/C,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AACzD;AAMO,IAAM,yBAAA,GAA4B,kCAAA;AAClC,IAAM,cAAA,GAAiB,GAAA;AACvB,IAAM,WAAA,GAAc,CAAA;AA0B3B,eAAsB,gBAAgB,CAAA,EAA8B;AAClE,EAAA,IAAI,SAAA;AAEJ,EAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,WAAA,EAAa,OAAA,EAAA,EAAW;AACvD,IAAA,IAAI,UAAU,CAAA,EAAG;AACf,MAAA,MAAM,MAAM,cAAc,CAAA;AAAA,IAC5B;AAKA,IAAA,MAAM,OAAA,GAA0B,MAAM,CAAA,CACnC,QAAA,CAAS,YAAY;AACpB,MAAA,MAAM,GAAA,GAAM,MAAA;AAKZ,MAAA,IAAI,OAAO,GAAA,CAAI,sBAAA,KAA2B,UAAA,EAAY;AACpD,QAAA,OAAO,IAAI,sBAAA,EAAuB;AAAA,MACpC;AAEA,MAAA,IAAI,OAAO,GAAA,CAAI,iBAAA,KAAsB,UAAA,EAAY;AAC/C,QAAA,OAAO,IAAI,iBAAA,EAAkB;AAAA,MAC/B;AACA,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OAGF;AAAA,IACF,CAAC,CAAA,CACA,IAAA;AAAA,MACC,CAAC,GAAA,MAAS,EAAE,EAAA,EAAI,MAAe,GAAA,EAAI,CAAA;AAAA,MACnC,CAAC,GAAA,MAAkB,EAAE,EAAA,EAAI,OAAgB,GAAA,EAAI;AAAA,KAC/C;AAEF,IAAA,IAAI,QAAQ,EAAA,EAAI;AAId,MAAA,MAAM,MAAA,GAAS,OAAO,OAAA,CAAQ,GAAA,KAAQ,QAAA,GAAW,KAAK,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,GAAI,OAAA,CAAQ,GAAA;AACnF,MAAA,OAAO,EAAE,GAAI,MAAA,EAAsC,KAAA,EAAO,IAAA,EAAK;AAAA,IACjE;AAEA,IAAA,MAAM,OAAA,GAAU,QAAQ,GAAA,YAAe,KAAA,GAAQ,QAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA;AAEvF,IAAA,IAAI,yBAAA,CAA0B,IAAA,CAAK,OAAO,CAAA,IAAK,UAAU,WAAA,EAAa;AACpE,MAAA,SAAA,GAAY,OAAA,CAAQ,GAAA;AACpB,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,OAAA,CAAQ,GAAA;AAAA,EAChB;AAGA,EAAA,MAAM,SAAA;AACR;AAgBA,eAAsB,2BAA2B,CAAA,EAA8B;AAC7E,EAAA,IAAI,SAAA;AAEJ,EAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,WAAA,EAAa,OAAA,EAAA,EAAW;AACvD,IAAA,IAAI,UAAU,CAAA,EAAG;AACf,MAAA,MAAM,MAAM,cAAc,CAAA;AAAA,IAC5B;AAEA,IAAA,MAAM,OAAA,GAA0B,MAAM,CAAA,CACnC,QAAA,CAAS,YAAY;AACpB,MAAA,MAAM,GAAA,GAAM,MAAA;AAIZ,MAAA,IAAI,OAAO,GAAA,CAAI,sBAAA,KAA2B,UAAA,EAAY;AACpD,QAAA,OAAO,GAAA,CAAI,sBAAA,CAAuB,EAAE,WAAA,EAAa,MAAM,CAAA;AAAA,MACzD;AACA,MAAA,IAAI,OAAO,GAAA,CAAI,iBAAA,KAAsB,UAAA,EAAY;AAC/C,QAAA,OAAO,GAAA,CAAI,iBAAA,CAAkB,EAAE,WAAA,EAAa,MAAM,CAAA;AAAA,MACpD;AACA,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OAGF;AAAA,IACF,CAAC,CAAA,CACA,IAAA;AAAA,MACC,CAAC,GAAA,MAAS,EAAE,EAAA,EAAI,MAAe,GAAA,EAAI,CAAA;AAAA,MACnC,CAAC,GAAA,MAAkB,EAAE,EAAA,EAAI,OAAgB,GAAA,EAAI;AAAA,KAC/C;AAEF,IAAA,IAAI,QAAQ,EAAA,EAAI;AACd,MAAA,MAAM,MAAA,GAAS,OAAO,OAAA,CAAQ,GAAA,KAAQ,QAAA,GAAW,KAAK,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,GAAI,OAAA,CAAQ,GAAA;AACnF,MAAA,OAAO,EAAE,GAAI,MAAA,EAAsC,KAAA,EAAO,IAAA,EAAK;AAAA,IACjE;AAEA,IAAA,MAAM,OAAA,GAAU,QAAQ,GAAA,YAAe,KAAA,GAAQ,QAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA;AAEvF,IAAA,IAAI,yBAAA,CAA0B,IAAA,CAAK,OAAO,CAAA,IAAK,UAAU,WAAA,EAAa;AACpE,MAAA,SAAA,GAAY,OAAA,CAAQ,GAAA;AACpB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,OAAA,CAAQ,GAAA;AAAA,EAChB;AAEA,EAAA,MAAM,SAAA;AACR;AAMO,IAAM,gBAAA,GAAmB,GAAA;AAuBhC,eAAsB,kBAAA,CACpB,CAAA,EACA,QAAA,EACA,SAAA,EACA,cAAc,KAAA,EACO;AACrB,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA;AAG9B,EAAA,IAAI,UAAA,GAAyB,MAAM,eAAA,CAAgB,CAAC,CAAA;AACpD,EAAA,IAAI,SAAA,GAAY,UAAA,CAAW,UAAA,CAAW,IAAI,CAAA;AAC1C,EAAA,IAAI,WAAA,GAAc,KAAK,GAAA,EAAI;AAE3B,EAAA,OAAO,IAAA,EAAM;AACX,IAAA,MAAM,MAAM,gBAAgB,CAAA;AAE5B,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AAGrB,IAAA,IAAI,OAAO,QAAA,EAAU;AACnB,MAAA,OAAO,UAAA;AAAA,IACT;AAKA,IAAA,MAAM,MAAA,GAAS,cAAc,MAAM,0BAAA,CAA2B,CAAC,CAAA,GAAI,MAAM,gBAAgB,CAAC,CAAA;AAC1F,IAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,MAAA,CAAO,IAAI,CAAA;AAEpC,IAAA,IAAI,UAAU,SAAA,EAAW;AAEvB,MAAA,SAAA,GAAY,KAAA;AACZ,MAAA,WAAA,GAAc,GAAA;AAId,MAAA,UAAA,GAAa,MAAA;AAAA,IACf,CAAA,MAAO;AAEL,MAAA,UAAA,GAAa,MAAA;AACb,MAAA,IAAI,GAAA,GAAM,eAAe,QAAA,EAAU;AAGjC,QAAA,IAAI,WAAA,EAAa;AACf,UAAA,OAAO,gBAAgB,CAAC,CAAA;AAAA,QAC1B;AACA,QAAA,OAAO,UAAA;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;ACzPA,IAAMA,WAAA,GAAYC,YAAA,CAAQC,iBAAA,CAAc,2PAAe,CAAC,CAAA;AAIxD,SAAS,oBAAA,GAA+B;AACtC,EAAA,MAAM,UAAA,GAAa;AAAA,IACjBC,SAAA,CAAKH,aAAW,wBAAwB,CAAA;AAAA;AAAA,IACxCG,SAAA,CAAKH,aAAW,gCAAgC;AAAA;AAAA,GAClD;AACA,EAAA,KAAA,MAAW,aAAa,UAAA,EAAY;AAClC,IAAA,IAAII,aAAA,CAAW,SAAS,CAAA,EAAG,OAAO,SAAA;AAAA,EACpC;AACA,EAAA,MAAM,IAAI,KAAA;AAAA,IACR,CAAA;AAAA;AAAA;AAAA,EAEgB,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,EAAA,EAAK,CAAC,CAAA,CAAE,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,GAC5D;AACF;AA6EO,IAAM,IAAA,GAAOC,YAAK,MAAA,CAAqB;AAAA,EAC5C,KAAA,EAAO,OAAO,EAAE,IAAA,IAAQ,GAAA,KAAQ;AAC9B,IAAA,MAAM,aAAa,oBAAA,EAAqB;AAMxC,IAAA,MAAM,IAAA,CAAK,aAAA,CAAc,EAAE,IAAA,EAAM,YAAY,CAAA;AAE7C,IAAA,MAAM,YAAA,GAAsC;AAAA;AAAA,MAE1C,MAAM,OAAA,CACJ,mBAAA,EACA,YAAA,EACqB;AACrB,QAAA,IAAI,CAAA;AACJ,QAAA,IAAI,OAAA;AAEJ,QAAA,IACE,mBAAA,KAAwB,MAAA,IACxB,OAAQ,mBAAA,CAA6B,aAAa,UAAA,EAClD;AAEA,UAAA,CAAA,GAAI,mBAAA;AACJ,UAAA,OAAA,GAAU,gBAAgB,EAAC;AAE3B,UAAA,MAAO,CAAA,CAAW,aAAA,CAAc,EAAE,IAAA,EAAM,YAAY,CAAA;AAAA,QACtD,CAAA,MAAO;AAEL,UAAA,CAAA,GAAI,IAAA;AACJ,UAAA,OAAA,GAAW,uBAAsD,EAAC;AAAA,QACpE;AAEA,QAAA,MAAM;AAAA,UACJ,aAAA,GAAgB,KAAA;AAAA,UAChB,QAAA,GAAW,GAAA;AAAA,UACX,SAAA,GAAY,IAAA;AAAA,UACZ,WAAA,GAAc;AAAA,SAChB,GAAI,OAAA;AAEJ,QAAA,IAAI,aAAA,EAAe;AACjB,UAAA,OAAO,kBAAA,CAAmB,CAAA,EAAG,QAAA,EAAU,SAAA,EAAW,WAAW,CAAA;AAAA,QAC/D;AAEA,QAAA,OAAO,gBAAgB,CAAC,CAAA;AAAA,MAC1B,CAAA;AAAA,MAEA,MAAM,WAAW,GAAA,EAAkC;AACjD,QAAA,MAAM,IAAA,CAAK,KAAK,GAAG,CAAA;AACnB,QAAA,OAAO,gBAAgB,IAAI,CAAA;AAAA,MAC7B;AAAA,KACF;AAEA,IAAA,MAAM,IAAI,YAAY,CAAA;AAAA,EACxB;AACF,CAAC;ACrHM,SAAS,UAAU,GAAA,EAA2B;AACnD,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,EACzB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,MAAM,qDAAqD,CAAA;AAAA,EACvE;AACA,EAAA,IAAI,CAACC,iBAAA,CAAa,MAAM,CAAA,EAAG;AACzB,IAAA,MAAM,IAAI,MAAM,oDAAoD,CAAA;AAAA,EACtE;AACA,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,MAAA;AAAA,IACR,UAAA,EAAY,KAAK,GAAA;AAAI,GACvB;AACF;AAkBO,SAAS,YAAA,CAAa,KAAA,EAAqB,OAAA,GAA+B,EAAC,EAAW;AAC3F,EAAA,MAAM,EAAE,WAAA,GAAc,mBAAA,EAAqB,UAAA,GAAa,iBAAgB,GAAI,OAAA;AAC5E,EAAA,MAAM,aAAA,GAAgB,KAAA,CAAM,MAAA,CAAO,IAAA,CAAK,IAAA;AACxC,EAAA,MAAM,UAAA,GAAa,KAAA,CAAM,MAAA,CAAO,MAAA,CAAO,MAAA;AAEvC,EAAA,OAAO;AAAA,IACL,CAAA,uCAAA,CAAA;AAAA,IACA,CAAA,QAAA,EAAW,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA,CAAA;AAAA,IAC3B,sBAAsB,aAAa,CAAA,CAAA;AAAA,IACnC,uBAAuB,UAAU,CAAA,CAAA;AAAA,IACjC,cAAc,UAAU,CAAA,CAAA;AAAA,IACxB,CAAA,CAAA;AAAA,IACA,CAAA,gDAAA,CAAA;AAAA,IACA,CAAA,CAAA;AAAA,IACA,SAAS,WAAW,CAAA,wBAAA,CAAA;AAAA,IACpB,CAAA,mBAAA,EAAsB,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA,GAAA,CAAA;AAAA,IACtC,CAAA,oDAAA,CAAA;AAAA,IACA,CAAA,0BAAA,CAAA;AAAA,IACA,CAAA,GAAA;AAAA,GACF,CAAE,KAAK,IAAI,CAAA;AACb;AAQA,IAAM,QAAA,GAAWL,YAAAA,CAAQC,iBAAAA,CAAc,2PAAe,CAAC,CAAA;AAWhD,SAAS,qBAAA,GAAgC;AAC9C,EAAA,MAAM,UAAA,GAAa;AAAA,IACjBC,SAAAA,CAAK,UAAU,wBAAwB,CAAA;AAAA;AAAA,IACvCA,SAAAA,CAAK,UAAU,gCAAgC;AAAA;AAAA,GACjD;AACA,EAAA,KAAA,MAAW,aAAa,UAAA,EAAY;AAClC,IAAA,IAAIC,cAAW,SAAS,CAAA,EAAG,OAAOG,eAAA,CAAa,WAAW,OAAO,CAAA;AAAA,EACnE;AACA,EAAA,MAAM,IAAI,KAAA;AAAA,IACR,CAAA;AAAA;AAAA;AAAA,EAEgB,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,EAAA,EAAK,CAAC,CAAA,CAAE,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,GAC5D;AACF","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 {
|
|
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.
|
|
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.
|
|
35
|
-
"@agent-scope/runtime": "1.
|
|
34
|
+
"@agent-scope/core": "1.4.0",
|
|
35
|
+
"@agent-scope/runtime": "1.4.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "*",
|