libretto 0.5.0 → 0.5.1

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.
Files changed (116) hide show
  1. package/README.md +106 -36
  2. package/dist/cli/cli.js +22 -97
  3. package/dist/cli/commands/browser.js +86 -59
  4. package/dist/cli/commands/execution.js +199 -86
  5. package/dist/cli/commands/init.js +30 -8
  6. package/dist/cli/commands/logs.js +4 -5
  7. package/dist/cli/commands/shared.js +30 -29
  8. package/dist/cli/commands/snapshot.js +26 -39
  9. package/dist/cli/core/ai-config.js +9 -2
  10. package/dist/cli/core/api-snapshot-analyzer.js +15 -5
  11. package/dist/cli/core/browser.js +132 -29
  12. package/dist/cli/core/context.js +4 -1
  13. package/dist/cli/core/session-telemetry.js +5 -2
  14. package/dist/cli/core/session.js +21 -8
  15. package/dist/cli/core/snapshot-analyzer.js +14 -31
  16. package/dist/cli/core/snapshot-api-config.js +2 -6
  17. package/dist/cli/core/telemetry.js +10 -2
  18. package/dist/cli/framework/simple-cli.js +45 -25
  19. package/dist/cli/router.js +14 -21
  20. package/dist/cli/workers/run-integration-runtime.js +24 -5
  21. package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
  22. package/dist/cli/workers/run-integration-worker.js +1 -4
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.js +7 -10
  25. package/dist/runtime/download/download.js +5 -1
  26. package/dist/runtime/extract/extract.js +11 -2
  27. package/dist/runtime/network/network.js +8 -1
  28. package/dist/runtime/recovery/agent.js +6 -2
  29. package/dist/runtime/recovery/errors.js +3 -1
  30. package/dist/runtime/recovery/recovery.js +3 -1
  31. package/dist/shared/condense-dom/condense-dom.js +6 -13
  32. package/dist/shared/config/config.d.ts +1 -9
  33. package/dist/shared/config/config.js +0 -18
  34. package/dist/shared/config/index.d.ts +2 -1
  35. package/dist/shared/config/index.js +0 -10
  36. package/dist/shared/debug/pause.js +9 -3
  37. package/dist/shared/instrumentation/instrument.js +101 -5
  38. package/dist/shared/llm/ai-sdk-adapter.js +3 -1
  39. package/dist/shared/llm/client.js +3 -1
  40. package/dist/shared/logger/index.js +4 -1
  41. package/dist/shared/run/api.js +3 -1
  42. package/dist/shared/run/browser.js +7 -2
  43. package/dist/shared/state/session-state.d.ts +2 -1
  44. package/dist/shared/state/session-state.js +5 -2
  45. package/dist/shared/visualization/ghost-cursor.js +19 -10
  46. package/dist/shared/visualization/highlight.js +9 -6
  47. package/dist/shared/workflow/workflow.d.ts +4 -5
  48. package/dist/shared/workflow/workflow.js +3 -5
  49. package/package.json +6 -2
  50. package/scripts/check-skills-sync.mjs +25 -0
  51. package/scripts/compare-eval-summary.mjs +47 -0
  52. package/scripts/postinstall.mjs +15 -15
  53. package/scripts/prepare-release.sh +97 -0
  54. package/scripts/skills-libretto.mjs +103 -0
  55. package/scripts/summarize-evals.mjs +135 -0
  56. package/scripts/sync-skills.mjs +12 -0
  57. package/skills/libretto/SKILL.md +113 -49
  58. package/skills/libretto/references/code-generation-rules.md +208 -0
  59. package/skills/libretto/references/configuration-file-reference.md +53 -0
  60. package/skills/libretto/references/site-security-review.md +143 -0
  61. package/src/cli/cli.ts +23 -110
  62. package/src/cli/commands/browser.ts +94 -70
  63. package/src/cli/commands/execution.ts +233 -102
  64. package/src/cli/commands/init.ts +32 -9
  65. package/src/cli/commands/logs.ts +7 -7
  66. package/src/cli/commands/shared.ts +36 -37
  67. package/src/cli/commands/snapshot.ts +44 -59
  68. package/src/cli/core/ai-config.ts +12 -3
  69. package/src/cli/core/api-snapshot-analyzer.ts +17 -6
  70. package/src/cli/core/browser.ts +178 -41
  71. package/src/cli/core/context.ts +7 -2
  72. package/src/cli/core/session-telemetry.ts +19 -8
  73. package/src/cli/core/session.ts +21 -7
  74. package/src/cli/core/snapshot-analyzer.ts +26 -46
  75. package/src/cli/core/snapshot-api-config.ts +170 -175
  76. package/src/cli/core/telemetry.ts +16 -3
  77. package/src/cli/framework/simple-cli.ts +144 -77
  78. package/src/cli/router.ts +13 -21
  79. package/src/cli/workers/run-integration-runtime.ts +36 -9
  80. package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
  81. package/src/cli/workers/run-integration-worker.ts +1 -4
  82. package/src/index.ts +73 -66
  83. package/src/runtime/download/download.ts +62 -58
  84. package/src/runtime/download/index.ts +5 -5
  85. package/src/runtime/extract/extract.ts +71 -61
  86. package/src/runtime/network/index.ts +3 -3
  87. package/src/runtime/network/network.ts +99 -93
  88. package/src/runtime/recovery/agent.ts +217 -212
  89. package/src/runtime/recovery/errors.ts +107 -104
  90. package/src/runtime/recovery/index.ts +3 -3
  91. package/src/runtime/recovery/recovery.ts +38 -35
  92. package/src/shared/condense-dom/condense-dom.ts +15 -18
  93. package/src/shared/config/config.ts +0 -19
  94. package/src/shared/config/index.ts +0 -5
  95. package/src/shared/debug/pause.ts +57 -51
  96. package/src/shared/instrumentation/errors.ts +64 -62
  97. package/src/shared/instrumentation/index.ts +5 -5
  98. package/src/shared/instrumentation/instrument.ts +339 -209
  99. package/src/shared/llm/ai-sdk-adapter.ts +58 -55
  100. package/src/shared/llm/client.ts +181 -174
  101. package/src/shared/llm/types.ts +39 -39
  102. package/src/shared/logger/index.ts +11 -4
  103. package/src/shared/logger/logger.ts +312 -306
  104. package/src/shared/logger/sinks.ts +118 -114
  105. package/src/shared/paths/paths.ts +50 -49
  106. package/src/shared/paths/repo-root.ts +17 -17
  107. package/src/shared/run/api.ts +5 -1
  108. package/src/shared/run/browser.ts +12 -3
  109. package/src/shared/state/index.ts +9 -9
  110. package/src/shared/state/session-state.ts +46 -43
  111. package/src/shared/visualization/ghost-cursor.ts +161 -148
  112. package/src/shared/visualization/highlight.ts +89 -86
  113. package/src/shared/visualization/index.ts +13 -13
  114. package/src/shared/workflow/workflow.ts +19 -25
  115. package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
  116. package/skills/libretto/references/user-action-log.md +0 -31
@@ -1,247 +1,377 @@
1
- import type { Page, Locator, BrowserContext } from "playwright";
1
+ import type { Page, Locator, FrameLocator, BrowserContext } from "playwright";
2
2
  import type { MinimalLogger } from "../logger/logger.js";
3
3
  import type { GhostCursorOptions } from "../visualization/ghost-cursor.js";
4
4
  import type { HighlightOptions } from "../visualization/highlight.js";
5
5
  import {
6
- ensureGhostCursor,
7
- moveGhostCursor,
8
- moveGhostCursorWithDistance,
9
- ghostClick,
10
- getGhostCursorPosition,
6
+ ensureGhostCursor,
7
+ moveGhostCursor,
8
+ moveGhostCursorWithDistance,
9
+ ghostClick,
10
+ getGhostCursorPosition,
11
11
  } from "../visualization/ghost-cursor.js";
12
12
  import {
13
- ensureHighlightLayer,
14
- showHighlight,
15
- clearHighlights,
13
+ ensureHighlightLayer,
14
+ showHighlight,
15
+ clearHighlights,
16
16
  } from "../visualization/highlight.js";
17
17
  import { enrichTimeoutError } from "./errors.js";
18
18
 
19
19
  export type InstrumentationOptions = {
20
- visualize?: boolean;
21
- logger?: MinimalLogger;
22
- highlightBeforeActionMs?: number;
23
- ghostCursor?: GhostCursorOptions;
24
- highlight?: HighlightOptions;
20
+ visualize?: boolean;
21
+ logger?: MinimalLogger;
22
+ highlightBeforeActionMs?: number;
23
+ ghostCursor?: GhostCursorOptions;
24
+ highlight?: HighlightOptions;
25
25
  };
26
26
 
27
27
  export type InstrumentedPage = Page & {
28
- __librettoInstrumented: true;
28
+ __librettoInstrumented: true;
29
29
  };
30
30
 
31
31
  const LOCATOR_ACTIONS = [
32
- "click",
33
- "dblclick",
34
- "hover",
35
- "fill",
36
- "type",
37
- "press",
38
- "check",
39
- "uncheck",
40
- "selectOption",
41
- "focus",
32
+ "click",
33
+ "dblclick",
34
+ "hover",
35
+ "fill",
36
+ "type",
37
+ "press",
38
+ "check",
39
+ "uncheck",
40
+ "selectOption",
41
+ "focus",
42
42
  ] as const;
43
43
 
44
44
  const NAV_ACTIONS = ["goto", "reload", "goBack", "goForward"] as const;
45
45
 
46
46
  const POINTER_ACTIONS = new Set<string>(["click", "dblclick", "hover"]);
47
47
 
48
+ const instrumentedTargets = new WeakSet<object>();
49
+
48
50
  // Per-page serialization queue so overlapping visualization actions don't glitch
49
51
  const pageQueues = new WeakMap<Page, Promise<void>>();
50
52
 
51
53
  function enqueue(page: Page, fn: () => Promise<void>): Promise<void> {
52
- const prev = pageQueues.get(page) ?? Promise.resolve();
53
- const next = prev.then(fn, fn);
54
- pageQueues.set(page, next);
55
- return next;
54
+ const prev = pageQueues.get(page) ?? Promise.resolve();
55
+ const next = prev.then(fn, fn);
56
+ pageQueues.set(page, next);
57
+ return next;
56
58
  }
57
59
 
58
60
  async function visualizeBeforeAction(
59
- page: Page,
60
- box: { x: number; y: number; width: number; height: number } | null,
61
- actionName: string,
62
- highlightMs: number,
61
+ page: Page,
62
+ box: { x: number; y: number; width: number; height: number } | null,
63
+ actionName: string,
64
+ highlightMs: number,
63
65
  ): Promise<void> {
64
- if (!box) return;
65
-
66
- // Re-ensure overlays in case DOM was replaced (e.g. page.setContent()).
67
- await ensureGhostCursor(page);
68
- await ensureHighlightLayer(page);
69
-
70
- const centerX = box.x + box.width / 2;
71
- const centerY = box.y + box.height / 2;
72
-
73
- // Show highlight on the target element
74
- await showHighlight(page, {
75
- box,
76
- durationMs: highlightMs + 200, // keep visible a bit past the cursor arrival
77
- });
78
-
79
- // Move ghost cursor to target
80
- const cursorPos = await getGhostCursorPosition(page);
81
- if (cursorPos) {
82
- await moveGhostCursorWithDistance(page, cursorPos, {
83
- x: centerX,
84
- y: centerY,
85
- });
86
- } else {
87
- await moveGhostCursor(page, { x: centerX, y: centerY, durationMs: 200 });
88
- }
89
-
90
- // For click actions, show click feedback
91
- if (actionName === "click" || actionName === "dblclick") {
92
- await ghostClick(page, { x: centerX, y: centerY });
93
- }
66
+ if (!box) return;
67
+
68
+ // Re-ensure overlays in case DOM was replaced (e.g. page.setContent()).
69
+ await ensureGhostCursor(page);
70
+ await ensureHighlightLayer(page);
71
+
72
+ const centerX = box.x + box.width / 2;
73
+ const centerY = box.y + box.height / 2;
74
+
75
+ // Show highlight on the target element
76
+ await showHighlight(page, {
77
+ box,
78
+ durationMs: highlightMs + 200, // keep visible a bit past the cursor arrival
79
+ });
80
+
81
+ // Move ghost cursor to target
82
+ const cursorPos = await getGhostCursorPosition(page);
83
+ if (cursorPos) {
84
+ await moveGhostCursorWithDistance(page, cursorPos, {
85
+ x: centerX,
86
+ y: centerY,
87
+ });
88
+ } else {
89
+ await moveGhostCursor(page, { x: centerX, y: centerY, durationMs: 200 });
90
+ }
91
+
92
+ // For click actions, show click feedback
93
+ if (actionName === "click" || actionName === "dblclick") {
94
+ await ghostClick(page, { x: centerX, y: centerY });
95
+ }
94
96
  }
95
97
 
96
98
  async function visualizeAfterAction(page: Page): Promise<void> {
97
- await clearHighlights(page);
99
+ await clearHighlights(page);
98
100
  }
99
101
 
100
102
  function wrapLocatorActions(
101
- locator: Locator,
102
- page: Page,
103
- opts: Required<Pick<InstrumentationOptions, "visualize" | "highlightBeforeActionMs">> & InstrumentationOptions,
103
+ locator: Locator,
104
+ page: Page,
105
+ opts: Required<
106
+ Pick<InstrumentationOptions, "visualize" | "highlightBeforeActionMs">
107
+ > &
108
+ InstrumentationOptions,
104
109
  ): void {
105
- for (const method of LOCATOR_ACTIONS) {
106
- if (typeof (locator as any)[method] !== "function") continue;
107
- const orig = (locator as any)[method].bind(locator);
108
-
109
- (locator as any)[method] = async (...args: any[]) => {
110
- if (opts.visualize) {
111
- await enqueue(page, async () => {
112
- try {
113
- const box = await locator.boundingBox();
114
- await visualizeBeforeAction(
115
- page,
116
- box,
117
- method,
118
- opts.highlightBeforeActionMs,
119
- );
120
- } catch {
121
- // Best-effort visualization
122
- }
123
- });
124
- }
125
-
126
- try {
127
- const result = await orig(...args);
128
- if (opts.visualize) {
129
- enqueue(page, () => visualizeAfterAction(page));
130
- }
131
- return result;
132
- } catch (err: any) {
133
- if (opts.visualize) {
134
- enqueue(page, () => visualizeAfterAction(page));
135
- }
136
- // Enrich timeout errors for pointer actions
137
- if (POINTER_ACTIONS.has(method) && isTimeoutError(err)) {
138
- await enrichTimeoutError(err, locator, page);
139
- }
140
- throw err;
141
- }
142
- };
143
- }
110
+ for (const method of LOCATOR_ACTIONS) {
111
+ if (typeof (locator as any)[method] !== "function") continue;
112
+ const orig = (locator as any)[method].bind(locator);
113
+
114
+ (locator as any)[method] = async (...args: any[]) => {
115
+ if (opts.visualize) {
116
+ await enqueue(page, async () => {
117
+ try {
118
+ const box = await locator.boundingBox();
119
+ await visualizeBeforeAction(
120
+ page,
121
+ box,
122
+ method,
123
+ opts.highlightBeforeActionMs,
124
+ );
125
+ } catch {
126
+ // Best-effort visualization
127
+ }
128
+ });
129
+ }
130
+
131
+ try {
132
+ const result = await orig(...args);
133
+ if (opts.visualize) {
134
+ enqueue(page, () => visualizeAfterAction(page));
135
+ }
136
+ return result;
137
+ } catch (err: any) {
138
+ if (opts.visualize) {
139
+ enqueue(page, () => visualizeAfterAction(page));
140
+ }
141
+ // Enrich timeout errors for pointer actions
142
+ if (POINTER_ACTIONS.has(method) && isTimeoutError(err)) {
143
+ await enrichTimeoutError(err, locator, page);
144
+ }
145
+ throw err;
146
+ }
147
+ };
148
+ }
149
+ }
150
+
151
+ const LOCATOR_FACTORY_METHODS = [
152
+ "locator",
153
+ "getByRole",
154
+ "getByText",
155
+ "getByLabel",
156
+ "getByPlaceholder",
157
+ "getByAltText",
158
+ "getByTitle",
159
+ "getByTestId",
160
+ "filter",
161
+ "and",
162
+ "or",
163
+ "first",
164
+ "last",
165
+ "nth",
166
+ ] as const;
167
+
168
+ const FRAME_LOCATOR_FACTORY_METHODS = [
169
+ "locator",
170
+ "getByRole",
171
+ "getByText",
172
+ "getByLabel",
173
+ "getByPlaceholder",
174
+ "getByAltText",
175
+ "getByTitle",
176
+ "getByTestId",
177
+ "owner",
178
+ "first",
179
+ "last",
180
+ "nth",
181
+ ] as const;
182
+
183
+ type InstrumentationRuntimeOptions = Required<
184
+ Pick<InstrumentationOptions, "visualize" | "highlightBeforeActionMs">
185
+ > &
186
+ InstrumentationOptions;
187
+
188
+ function instrumentLocator(
189
+ locator: Locator,
190
+ page: Page,
191
+ opts: InstrumentationRuntimeOptions,
192
+ ): Locator {
193
+ const target = locator as object;
194
+ if (instrumentedTargets.has(target)) {
195
+ return locator;
196
+ }
197
+ instrumentedTargets.add(target);
198
+
199
+ wrapLocatorActions(locator, page, opts);
200
+
201
+ for (const method of LOCATOR_FACTORY_METHODS) {
202
+ if (typeof (locator as any)[method] !== "function") continue;
203
+ const orig = (locator as any)[method].bind(locator);
204
+ (locator as any)[method] = (...args: any[]) => {
205
+ const nextLocator = orig(...args);
206
+ return instrumentLocator(nextLocator, page, opts);
207
+ };
208
+ }
209
+
210
+ if (typeof (locator as any).contentFrame === "function") {
211
+ const origContentFrame = (locator as any).contentFrame.bind(locator);
212
+ (locator as any).contentFrame = (...args: any[]) => {
213
+ const frameLocator = origContentFrame(...args);
214
+ return instrumentFrameLocator(frameLocator, page, opts);
215
+ };
216
+ }
217
+
218
+ return locator;
219
+ }
220
+
221
+ function instrumentFrameLocator(
222
+ frameLocator: FrameLocator,
223
+ page: Page,
224
+ opts: InstrumentationRuntimeOptions,
225
+ ): FrameLocator {
226
+ const target = frameLocator as object;
227
+ if (instrumentedTargets.has(target)) {
228
+ return frameLocator;
229
+ }
230
+ instrumentedTargets.add(target);
231
+
232
+ for (const method of FRAME_LOCATOR_FACTORY_METHODS) {
233
+ if (typeof (frameLocator as any)[method] !== "function") continue;
234
+ const orig = (frameLocator as any)[method].bind(frameLocator);
235
+ (frameLocator as any)[method] = (...args: any[]) => {
236
+ const result = orig(...args);
237
+ if (method === "first" || method === "last" || method === "nth") {
238
+ return instrumentFrameLocator(result, page, opts);
239
+ }
240
+ return instrumentLocator(result, page, opts);
241
+ };
242
+ }
243
+
244
+ if (typeof (frameLocator as any).frameLocator === "function") {
245
+ const origFrameLocator = (frameLocator as any).frameLocator.bind(
246
+ frameLocator,
247
+ );
248
+ (frameLocator as any).frameLocator = (...args: any[]) => {
249
+ const nestedFrameLocator = origFrameLocator(...args);
250
+ return instrumentFrameLocator(nestedFrameLocator, page, opts);
251
+ };
252
+ }
253
+
254
+ return frameLocator;
144
255
  }
145
256
 
146
257
  function isTimeoutError(err: any): boolean {
147
- if (!err || typeof err.message !== "string") return false;
148
- return (
149
- err.message.includes("Timeout") ||
150
- err.message.includes("timeout") ||
151
- err.name === "TimeoutError"
152
- );
258
+ if (!err || typeof err.message !== "string") return false;
259
+ return (
260
+ err.message.includes("Timeout") ||
261
+ err.message.includes("timeout") ||
262
+ err.name === "TimeoutError"
263
+ );
153
264
  }
154
265
 
155
- const LOCATOR_FACTORIES = [
156
- "locator",
157
- "getByRole",
158
- "getByText",
159
- "getByLabel",
160
- "getByPlaceholder",
161
- "getByAltText",
162
- "getByTitle",
163
- "getByTestId",
266
+ const PAGE_LOCATOR_FACTORIES = [
267
+ "locator",
268
+ "getByRole",
269
+ "getByText",
270
+ "getByLabel",
271
+ "getByPlaceholder",
272
+ "getByAltText",
273
+ "getByTitle",
274
+ "getByTestId",
164
275
  ] as const;
165
276
 
277
+ const PAGE_FRAME_LOCATOR_FACTORIES = ["frameLocator"] as const;
278
+
166
279
  /**
167
280
  * In-place patching of a Page object to add visualization and error enrichment.
168
281
  * Modifies the page directly (does not return a new object).
169
282
  */
170
283
  export async function installInstrumentation(
171
- page: Page,
172
- options?: InstrumentationOptions,
284
+ page: Page,
285
+ options?: InstrumentationOptions,
173
286
  ): Promise<void> {
174
- if ((page as any).__librettoInstrumented) return;
175
- (page as any).__librettoInstrumented = true;
176
-
177
- const visualize = options?.visualize ?? false;
178
- const highlightBeforeActionMs = options?.highlightBeforeActionMs ?? 350;
179
- const mergedOpts = { ...options, visualize, highlightBeforeActionMs };
180
-
181
- // Install overlay layers if visualization is on
182
- if (visualize) {
183
- await ensureGhostCursor(page, options?.ghostCursor);
184
- await ensureHighlightLayer(page, options?.highlight);
185
- }
186
-
187
- // Wrap page-level locator actions (page.click, page.fill, etc.)
188
- for (const method of LOCATOR_ACTIONS) {
189
- if (typeof (page as any)[method] !== "function") continue;
190
- const orig = (page as any)[method].bind(page);
191
- (page as any)[method] = async (...args: any[]) => {
192
- // For page-level actions, the first arg is typically the selector
193
- if (visualize && typeof args[0] === "string") {
194
- await enqueue(page, async () => {
195
- try {
196
- const loc = page.locator(args[0]);
197
- const box = await loc.boundingBox();
198
- await visualizeBeforeAction(page, box, method, highlightBeforeActionMs);
199
- } catch {
200
- // Best-effort
201
- }
202
- });
203
- }
204
-
205
- try {
206
- const result = await orig(...args);
207
- if (visualize) {
208
- enqueue(page, () => visualizeAfterAction(page));
209
- }
210
- return result;
211
- } catch (err: any) {
212
- if (visualize) {
213
- enqueue(page, () => visualizeAfterAction(page));
214
- }
215
- if (POINTER_ACTIONS.has(method) && isTimeoutError(err) && typeof args[0] === "string") {
216
- await enrichTimeoutError(err, page.locator(args[0]), page);
217
- }
218
- throw err;
219
- }
220
- };
221
- }
222
-
223
- // Wrap navigation actions (no visualization, just logging)
224
- for (const method of NAV_ACTIONS) {
225
- if (typeof (page as any)[method] !== "function") continue;
226
- const orig = (page as any)[method].bind(page);
227
- (page as any)[method] = async (...args: any[]) => {
228
- options?.logger?.info(`instrumentation:${method}`, {
229
- url: typeof args[0] === "string" ? args[0] : page.url(),
230
- });
231
- return orig(...args);
232
- };
233
- }
234
-
235
- // Wrap locator factories to instrument returned locators
236
- for (const factory of LOCATOR_FACTORIES) {
237
- if (typeof (page as any)[factory] !== "function") continue;
238
- const origFactory = (page as any)[factory].bind(page);
239
- (page as any)[factory] = (...factoryArgs: any[]) => {
240
- const locator = origFactory(...factoryArgs);
241
- wrapLocatorActions(locator, page, mergedOpts);
242
- return locator;
243
- };
244
- }
287
+ if ((page as any).__librettoInstrumented) return;
288
+ (page as any).__librettoInstrumented = true;
289
+
290
+ const visualize = options?.visualize ?? false;
291
+ const highlightBeforeActionMs = options?.highlightBeforeActionMs ?? 350;
292
+ const mergedOpts = { ...options, visualize, highlightBeforeActionMs };
293
+
294
+ // Install overlay layers if visualization is on
295
+ if (visualize) {
296
+ await ensureGhostCursor(page, options?.ghostCursor);
297
+ await ensureHighlightLayer(page, options?.highlight);
298
+ }
299
+
300
+ // Wrap page-level locator actions (page.click, page.fill, etc.)
301
+ for (const method of LOCATOR_ACTIONS) {
302
+ if (typeof (page as any)[method] !== "function") continue;
303
+ const orig = (page as any)[method].bind(page);
304
+ (page as any)[method] = async (...args: any[]) => {
305
+ // For page-level actions, the first arg is typically the selector
306
+ if (visualize && typeof args[0] === "string") {
307
+ await enqueue(page, async () => {
308
+ try {
309
+ const loc = page.locator(args[0]);
310
+ const box = await loc.boundingBox();
311
+ await visualizeBeforeAction(
312
+ page,
313
+ box,
314
+ method,
315
+ highlightBeforeActionMs,
316
+ );
317
+ } catch {
318
+ // Best-effort
319
+ }
320
+ });
321
+ }
322
+
323
+ try {
324
+ const result = await orig(...args);
325
+ if (visualize) {
326
+ enqueue(page, () => visualizeAfterAction(page));
327
+ }
328
+ return result;
329
+ } catch (err: any) {
330
+ if (visualize) {
331
+ enqueue(page, () => visualizeAfterAction(page));
332
+ }
333
+ if (
334
+ POINTER_ACTIONS.has(method) &&
335
+ isTimeoutError(err) &&
336
+ typeof args[0] === "string"
337
+ ) {
338
+ await enrichTimeoutError(err, page.locator(args[0]), page);
339
+ }
340
+ throw err;
341
+ }
342
+ };
343
+ }
344
+
345
+ // Wrap navigation actions (no visualization, just logging)
346
+ for (const method of NAV_ACTIONS) {
347
+ if (typeof (page as any)[method] !== "function") continue;
348
+ const orig = (page as any)[method].bind(page);
349
+ (page as any)[method] = async (...args: any[]) => {
350
+ options?.logger?.info(`instrumentation:${method}`, {
351
+ url: typeof args[0] === "string" ? args[0] : page.url(),
352
+ });
353
+ return orig(...args);
354
+ };
355
+ }
356
+
357
+ // Wrap locator factories to instrument returned locators
358
+ for (const factory of PAGE_LOCATOR_FACTORIES) {
359
+ if (typeof (page as any)[factory] !== "function") continue;
360
+ const origFactory = (page as any)[factory].bind(page);
361
+ (page as any)[factory] = (...factoryArgs: any[]) => {
362
+ const locator = origFactory(...factoryArgs);
363
+ return instrumentLocator(locator, page, mergedOpts);
364
+ };
365
+ }
366
+
367
+ for (const factory of PAGE_FRAME_LOCATOR_FACTORIES) {
368
+ if (typeof (page as any)[factory] !== "function") continue;
369
+ const origFactory = (page as any)[factory].bind(page);
370
+ (page as any)[factory] = (...factoryArgs: any[]) => {
371
+ const frameLocator = origFactory(...factoryArgs);
372
+ return instrumentFrameLocator(frameLocator, page, mergedOpts);
373
+ };
374
+ }
245
375
  }
246
376
 
247
377
  /**
@@ -249,11 +379,11 @@ export async function installInstrumentation(
249
379
  * The original page is not modified.
250
380
  */
251
381
  export async function instrumentPage(
252
- page: Page,
253
- options?: InstrumentationOptions,
382
+ page: Page,
383
+ options?: InstrumentationOptions,
254
384
  ): Promise<InstrumentedPage> {
255
- await installInstrumentation(page, options);
256
- return page as InstrumentedPage;
385
+ await installInstrumentation(page, options);
386
+ return page as InstrumentedPage;
257
387
  }
258
388
 
259
389
  /**
@@ -261,16 +391,16 @@ export async function instrumentPage(
261
391
  * Useful when connecting to an existing browser via CDP.
262
392
  */
263
393
  export async function instrumentContext(
264
- context: BrowserContext,
265
- options?: InstrumentationOptions,
394
+ context: BrowserContext,
395
+ options?: InstrumentationOptions,
266
396
  ): Promise<void> {
267
- // Instrument all existing pages
268
- for (const page of context.pages()) {
269
- await installInstrumentation(page, options);
270
- }
271
-
272
- // Auto-instrument new pages
273
- context.on("page", async (page) => {
274
- await installInstrumentation(page, options);
275
- });
397
+ // Instrument all existing pages
398
+ for (const page of context.pages()) {
399
+ await installInstrumentation(page, options);
400
+ }
401
+
402
+ // Auto-instrument new pages
403
+ context.on("page", async (page) => {
404
+ await installInstrumentation(page, options);
405
+ });
276
406
  }