libretto 0.5.0 → 0.5.2
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/README.md +109 -35
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/execution.js +199 -86
- package/dist/cli/commands/init.js +34 -29
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +434 -174
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +20 -4
- package/dist/cli/framework/simple-cli.js +45 -25
- package/dist/cli/router.js +14 -21
- package/dist/cli/workers/run-integration-runtime.js +24 -5
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +7 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +17 -69
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +47 -3
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +36 -14
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +4 -5
- package/dist/shared/workflow/workflow.js +3 -5
- package/package.json +6 -2
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +15 -15
- package/scripts/prepare-release.sh +97 -0
- package/scripts/skills-libretto.mjs +103 -0
- package/scripts/summarize-evals.mjs +135 -0
- package/scripts/sync-skills.mjs +12 -0
- package/skills/libretto/SKILL.md +132 -54
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +210 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/execution.ts +233 -102
- package/src/cli/commands/init.ts +37 -33
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +449 -197
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +39 -4
- package/src/cli/framework/simple-cli.ts +144 -77
- package/src/cli/router.ts +13 -21
- package/src/cli/workers/run-integration-runtime.ts +36 -9
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +73 -66
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +27 -82
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +65 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +180 -149
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +19 -25
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
ensureGhostCursor,
|
|
7
|
+
moveGhostCursor,
|
|
8
|
+
moveGhostCursorWithDistance,
|
|
9
|
+
ghostClick,
|
|
10
|
+
getGhostCursorPosition,
|
|
11
11
|
} from "../visualization/ghost-cursor.js";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
+
__librettoInstrumented: true;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
const LOCATOR_ACTIONS = [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
99
|
+
await clearHighlights(page);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
function wrapLocatorActions(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
locator: Locator,
|
|
104
|
+
page: Page,
|
|
105
|
+
opts: Required<
|
|
106
|
+
Pick<InstrumentationOptions, "visualize" | "highlightBeforeActionMs">
|
|
107
|
+
> &
|
|
108
|
+
InstrumentationOptions,
|
|
104
109
|
): void {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
172
|
-
|
|
284
|
+
page: Page,
|
|
285
|
+
options?: InstrumentationOptions,
|
|
173
286
|
): Promise<void> {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
253
|
-
|
|
382
|
+
page: Page,
|
|
383
|
+
options?: InstrumentationOptions,
|
|
254
384
|
): Promise<InstrumentedPage> {
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
265
|
-
|
|
394
|
+
context: BrowserContext,
|
|
395
|
+
options?: InstrumentationOptions,
|
|
266
396
|
): Promise<void> {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
}
|