@agent-scope/playwright 1.1.0 → 1.3.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 +157 -9
- package/dist/index.cjs +10 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -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,
|
|
@@ -978,6 +1115,7 @@
|
|
|
978
1115
|
};
|
|
979
1116
|
hookObj.renderers = renderersProxy;
|
|
980
1117
|
}
|
|
1118
|
+
var hasCommitted = false;
|
|
981
1119
|
var resolveFirstCommit;
|
|
982
1120
|
var firstCommit = new Promise((res) => {
|
|
983
1121
|
resolveFirstCommit = res;
|
|
@@ -985,10 +1123,20 @@
|
|
|
985
1123
|
var originalOnCommit = scopeHook.onCommitFiberRoot.bind(scopeHook);
|
|
986
1124
|
scopeHook.onCommitFiberRoot = (rendererID, root, priorityLevel) => {
|
|
987
1125
|
originalOnCommit(rendererID, root, priorityLevel);
|
|
1126
|
+
hasCommitted = true;
|
|
988
1127
|
resolveFirstCommit();
|
|
989
1128
|
};
|
|
990
|
-
window.__SCOPE_CAPTURE__ = async () => {
|
|
991
|
-
|
|
992
|
-
|
|
1129
|
+
window.__SCOPE_CAPTURE__ = async (options) => {
|
|
1130
|
+
if (!hasCommitted) {
|
|
1131
|
+
await firstCommit;
|
|
1132
|
+
}
|
|
1133
|
+
return capture({ lightweight: options?.lightweight });
|
|
1134
|
+
};
|
|
1135
|
+
window.__SCOPE_CAPTURE_JSON__ = async () => {
|
|
1136
|
+
if (!hasCommitted) {
|
|
1137
|
+
await firstCommit;
|
|
1138
|
+
}
|
|
1139
|
+
const result = await capture();
|
|
1140
|
+
return JSON.stringify(result);
|
|
993
1141
|
};
|
|
994
1142
|
})();
|
package/dist/index.cjs
CHANGED
|
@@ -31,18 +31,22 @@ async function evaluateCapture(p) {
|
|
|
31
31
|
}
|
|
32
32
|
const outcome = await p.evaluate(async () => {
|
|
33
33
|
const win = window;
|
|
34
|
-
if (typeof win.
|
|
35
|
-
|
|
36
|
-
"Scope runtime not injected. Make sure you navigated to the page AFTER the scope fixture was set up, not before."
|
|
37
|
-
);
|
|
34
|
+
if (typeof win.__SCOPE_CAPTURE_JSON__ === "function") {
|
|
35
|
+
return win.__SCOPE_CAPTURE_JSON__();
|
|
38
36
|
}
|
|
39
|
-
|
|
37
|
+
if (typeof win.__SCOPE_CAPTURE__ === "function") {
|
|
38
|
+
return win.__SCOPE_CAPTURE__();
|
|
39
|
+
}
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Scope runtime not injected. Make sure you navigated to the page AFTER the scope fixture was set up, not before."
|
|
42
|
+
);
|
|
40
43
|
}).then(
|
|
41
44
|
(val) => ({ ok: true, val }),
|
|
42
45
|
(err) => ({ ok: false, err })
|
|
43
46
|
);
|
|
44
47
|
if (outcome.ok) {
|
|
45
|
-
|
|
48
|
+
const parsed = typeof outcome.val === "string" ? JSON.parse(outcome.val) : outcome.val;
|
|
49
|
+
return { ...parsed, route: null };
|
|
46
50
|
}
|
|
47
51
|
const message = outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
|
|
48
52
|
if (CONTEXT_DESTROYED_PATTERN.test(message) && attempt < MAX_RETRIES) {
|
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__())` with retry logic\n * that catches \"Execution context was destroyed\" errors caused by navigations\n * or page reloads that race with the evaluate call.\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__?: () => Promise<unknown>;\n };\n if (typeof win.__SCOPE_CAPTURE__ !== \"function\") {\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 return win.__SCOPE_CAPTURE__();\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 // Playwright serialises the return value as JSON, so we get a plain object.\n return { ...(outcome.val 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// 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"]}
|
package/dist/index.js
CHANGED
|
@@ -29,18 +29,22 @@ async function evaluateCapture(p) {
|
|
|
29
29
|
}
|
|
30
30
|
const outcome = await p.evaluate(async () => {
|
|
31
31
|
const win = window;
|
|
32
|
-
if (typeof win.
|
|
33
|
-
|
|
34
|
-
"Scope runtime not injected. Make sure you navigated to the page AFTER the scope fixture was set up, not before."
|
|
35
|
-
);
|
|
32
|
+
if (typeof win.__SCOPE_CAPTURE_JSON__ === "function") {
|
|
33
|
+
return win.__SCOPE_CAPTURE_JSON__();
|
|
36
34
|
}
|
|
37
|
-
|
|
35
|
+
if (typeof win.__SCOPE_CAPTURE__ === "function") {
|
|
36
|
+
return win.__SCOPE_CAPTURE__();
|
|
37
|
+
}
|
|
38
|
+
throw new Error(
|
|
39
|
+
"Scope runtime not injected. Make sure you navigated to the page AFTER the scope fixture was set up, not before."
|
|
40
|
+
);
|
|
38
41
|
}).then(
|
|
39
42
|
(val) => ({ ok: true, val }),
|
|
40
43
|
(err) => ({ ok: false, err })
|
|
41
44
|
);
|
|
42
45
|
if (outcome.ok) {
|
|
43
|
-
|
|
46
|
+
const parsed = typeof outcome.val === "string" ? JSON.parse(outcome.val) : outcome.val;
|
|
47
|
+
return { ...parsed, route: null };
|
|
44
48
|
}
|
|
45
49
|
const message = outcome.err instanceof Error ? outcome.err.message : String(outcome.err);
|
|
46
50
|
if (CONTEXT_DESTROYED_PATTERN.test(message) && attempt < MAX_RETRIES) {
|
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__())` with retry logic\n * that catches \"Execution context was destroyed\" errors caused by navigations\n * or page reloads that race with the evaluate call.\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__?: () => Promise<unknown>;\n };\n if (typeof win.__SCOPE_CAPTURE__ !== \"function\") {\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 return win.__SCOPE_CAPTURE__();\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 // Playwright serialises the return value as JSON, so we get a plain object.\n return { ...(outcome.val 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// 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-scope/playwright",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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.3.0",
|
|
35
|
+
"@agent-scope/runtime": "1.3.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "*",
|