framewatch-mcp-server 0.1.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/LICENSE +21 -0
- package/README.md +537 -0
- package/dist/constants.d.ts +172 -0
- package/dist/constants.js +168 -0
- package/dist/constants.js.map +1 -0
- package/dist/engine/browser.d.ts +56 -0
- package/dist/engine/browser.js +142 -0
- package/dist/engine/browser.js.map +1 -0
- package/dist/engine/differ.d.ts +88 -0
- package/dist/engine/differ.js +373 -0
- package/dist/engine/differ.js.map +1 -0
- package/dist/engine/interaction.d.ts +76 -0
- package/dist/engine/interaction.js +254 -0
- package/dist/engine/interaction.js.map +1 -0
- package/dist/engine/layers/console.d.ts +63 -0
- package/dist/engine/layers/console.js +118 -0
- package/dist/engine/layers/console.js.map +1 -0
- package/dist/engine/layers/dom.d.ts +53 -0
- package/dist/engine/layers/dom.js +282 -0
- package/dist/engine/layers/dom.js.map +1 -0
- package/dist/engine/layers/index.d.ts +95 -0
- package/dist/engine/layers/index.js +184 -0
- package/dist/engine/layers/index.js.map +1 -0
- package/dist/engine/layers/network.d.ts +62 -0
- package/dist/engine/layers/network.js +169 -0
- package/dist/engine/layers/network.js.map +1 -0
- package/dist/engine/layers/performance.d.ts +55 -0
- package/dist/engine/layers/performance.js +215 -0
- package/dist/engine/layers/performance.js.map +1 -0
- package/dist/engine/layers/probe.d.ts +50 -0
- package/dist/engine/layers/probe.js +39 -0
- package/dist/engine/layers/probe.js.map +1 -0
- package/dist/engine/layers/session.d.ts +46 -0
- package/dist/engine/layers/session.js +131 -0
- package/dist/engine/layers/session.js.map +1 -0
- package/dist/engine/recorder.d.ts +61 -0
- package/dist/engine/recorder.js +256 -0
- package/dist/engine/recorder.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/accessibility.d.ts +140 -0
- package/dist/tools/accessibility.js +357 -0
- package/dist/tools/accessibility.js.map +1 -0
- package/dist/tools/capture.d.ts +279 -0
- package/dist/tools/capture.js +275 -0
- package/dist/tools/capture.js.map +1 -0
- package/dist/tools/compare.d.ts +86 -0
- package/dist/tools/compare.js +247 -0
- package/dist/tools/compare.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +25 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/interact.d.ts +160 -0
- package/dist/tools/interact.js +203 -0
- package/dist/tools/interact.js.map +1 -0
- package/dist/tools/responsive.d.ts +89 -0
- package/dist/tools/responsive.js +197 -0
- package/dist/tools/responsive.js.map +1 -0
- package/dist/tools/screenshot.d.ts +76 -0
- package/dist/tools/screenshot.js +117 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/server.d.ts +89 -0
- package/dist/tools/server.js +201 -0
- package/dist/tools/server.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/bounded-log.d.ts +41 -0
- package/dist/utils/bounded-log.js +78 -0
- package/dist/utils/bounded-log.js.map +1 -0
- package/dist/utils/format.d.ts +56 -0
- package/dist/utils/format.js +130 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/image.d.ts +44 -0
- package/dist/utils/image.js +81 -0
- package/dist/utils/image.js.map +1 -0
- package/dist/utils/server-process.d.ts +84 -0
- package/dist/utils/server-process.js +251 -0
- package/dist/utils/server-process.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, SWIPE_STEPS, SWIPE_STEP_DELAY_MS } from "../constants.js";
|
|
3
|
+
/**
|
|
4
|
+
* Interaction executor.
|
|
5
|
+
*
|
|
6
|
+
* Turns one declarative interaction step (see CLAUDE.md "Interactions as JSON
|
|
7
|
+
* script") into real input on a Playwright page. Every step is validated
|
|
8
|
+
* before anything touches the page, and every Playwright failure is reduced to
|
|
9
|
+
* a single actionable line naming the step that failed — a recording is a bad
|
|
10
|
+
* place to surface a 30-line call log.
|
|
11
|
+
*/
|
|
12
|
+
/** Actions `framewatch_capture` can replay during a recording. */
|
|
13
|
+
export const CAPTURE_ACTIONS = ["click", "tap", "type", "scroll", "swipe", "wait", "navigate"];
|
|
14
|
+
/** Actions `framewatch_interact` can perform as a one-off. */
|
|
15
|
+
export const INTERACT_ACTIONS = ["click", "tap", "type", "scroll", "swipe", "navigate", "select", "hover"];
|
|
16
|
+
/** Steps that need a touch-capable browser context (`hasTouch: true`). */
|
|
17
|
+
const TOUCH_ACTIONS = new Set(["tap", "swipe"]);
|
|
18
|
+
/** True when any step needs a touch-enabled browser context. */
|
|
19
|
+
export function needsTouch(interactions) {
|
|
20
|
+
return interactions.some((i) => TOUCH_ACTIONS.has(i.action));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check that a step carries the fields its action needs. Returns a message
|
|
24
|
+
* naming what is missing, or null when the step is executable. Used both by
|
|
25
|
+
* the tools (at input-validation time, before a browser is even launched) and
|
|
26
|
+
* by `executeInteraction` itself.
|
|
27
|
+
*/
|
|
28
|
+
export function validateInteraction(interaction) {
|
|
29
|
+
const { action, selector, value, x, y, delta_x, delta_y } = interaction;
|
|
30
|
+
const hasPoint = typeof x === "number" && typeof y === "number";
|
|
31
|
+
const hasPartialPoint = typeof x === "number" || typeof y === "number";
|
|
32
|
+
switch (action) {
|
|
33
|
+
case "click":
|
|
34
|
+
case "tap":
|
|
35
|
+
case "hover":
|
|
36
|
+
if (selector)
|
|
37
|
+
return null;
|
|
38
|
+
if (hasPoint)
|
|
39
|
+
return null;
|
|
40
|
+
return hasPartialPoint
|
|
41
|
+
? `${action} needs both \`x\` and \`y\` (or a \`selector\`)`
|
|
42
|
+
: `${action} needs a \`selector\`, or \`x\` and \`y\``;
|
|
43
|
+
case "type":
|
|
44
|
+
return typeof value === "string" ? null : "type needs a `value` (the text to type)";
|
|
45
|
+
case "select":
|
|
46
|
+
if (!selector)
|
|
47
|
+
return "select needs a `selector` naming the <select> element";
|
|
48
|
+
return typeof value === "string" ? null : "select needs a `value` (the option to choose)";
|
|
49
|
+
case "scroll":
|
|
50
|
+
return typeof delta_x === "number" || typeof delta_y === "number"
|
|
51
|
+
? null
|
|
52
|
+
: "scroll needs `delta_y` (and/or `delta_x`) — the distance to scroll";
|
|
53
|
+
case "swipe":
|
|
54
|
+
if (!hasPoint)
|
|
55
|
+
return "swipe needs `x` and `y` — where the finger goes down";
|
|
56
|
+
return typeof delta_x === "number" || typeof delta_y === "number"
|
|
57
|
+
? null
|
|
58
|
+
: "swipe needs `delta_x` and/or `delta_y` — how far the finger travels";
|
|
59
|
+
case "navigate":
|
|
60
|
+
return typeof value === "string" && value.length > 0 ? null : "navigate needs a `value` (the URL to go to)";
|
|
61
|
+
case "wait":
|
|
62
|
+
return null;
|
|
63
|
+
default:
|
|
64
|
+
return `unknown action "${String(action)}"`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Longest `value` printed in a step description before it is elided. */
|
|
68
|
+
const DESCRIBE_VALUE_MAX = 60;
|
|
69
|
+
/**
|
|
70
|
+
* One-line, human-readable summary of a step, used in error messages and in
|
|
71
|
+
* the capture summary. Long values (a pasted token, a password) are elided
|
|
72
|
+
* rather than echoed in full.
|
|
73
|
+
*/
|
|
74
|
+
export function describeInteraction(interaction) {
|
|
75
|
+
const { action, selector, value, x, y, delta_x = 0, delta_y = 0, delay_ms } = interaction;
|
|
76
|
+
const target = selector ? ` "${selector}"` : typeof x === "number" && typeof y === "number" ? ` at ${x},${y}` : "";
|
|
77
|
+
switch (action) {
|
|
78
|
+
case "click":
|
|
79
|
+
case "tap":
|
|
80
|
+
case "hover":
|
|
81
|
+
return `${action}${target}`;
|
|
82
|
+
case "type":
|
|
83
|
+
return `type "${elide(value)}"${selector ? ` into "${selector}"` : ""}`;
|
|
84
|
+
case "select":
|
|
85
|
+
return `select "${elide(value)}"${selector ? ` in "${selector}"` : ""}`;
|
|
86
|
+
case "scroll":
|
|
87
|
+
return `scroll${selector ? ` "${selector}"` : ""} by ${delta_x},${delta_y}`;
|
|
88
|
+
case "swipe":
|
|
89
|
+
return `swipe${target} by ${delta_x},${delta_y}`;
|
|
90
|
+
case "navigate":
|
|
91
|
+
return `navigate to ${elide(value)}`;
|
|
92
|
+
case "wait":
|
|
93
|
+
return `wait ${delay_ms ?? 0}ms`;
|
|
94
|
+
default:
|
|
95
|
+
return String(action);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function elide(value) {
|
|
99
|
+
if (value === undefined)
|
|
100
|
+
return "";
|
|
101
|
+
return value.length > DESCRIBE_VALUE_MAX ? `${value.slice(0, DESCRIBE_VALUE_MAX)}…` : value;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Perform one interaction on `page`.
|
|
105
|
+
*
|
|
106
|
+
* Waits `delay_ms` first (that is what `delay_ms` means: "wait before this
|
|
107
|
+
* action"), so a `wait` step is simply a step with no action of its own.
|
|
108
|
+
* Throws a single-line Error naming the step on any failure; the page is never
|
|
109
|
+
* touched when the step is invalid.
|
|
110
|
+
*/
|
|
111
|
+
export async function executeInteraction(page, interaction, options = {}) {
|
|
112
|
+
const problem = validateInteraction(interaction);
|
|
113
|
+
if (problem)
|
|
114
|
+
throw new Error(problem);
|
|
115
|
+
const delay = interaction.delay_ms;
|
|
116
|
+
if (typeof delay === "number" && delay > 0)
|
|
117
|
+
await sleep(delay);
|
|
118
|
+
const timeout = options.timeout_ms ?? SELECTOR_TIMEOUT_MS;
|
|
119
|
+
try {
|
|
120
|
+
await perform(page, interaction, timeout);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
throw new Error(describeFailure(interaction, error), { cause: error });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function perform(page, interaction, timeout) {
|
|
127
|
+
const { action, selector, value, x, y, delta_x = 0, delta_y = 0 } = interaction;
|
|
128
|
+
switch (action) {
|
|
129
|
+
case "click":
|
|
130
|
+
if (selector)
|
|
131
|
+
await page.locator(selector).first().click({ timeout });
|
|
132
|
+
else
|
|
133
|
+
await page.mouse.click(x, y);
|
|
134
|
+
return;
|
|
135
|
+
case "hover":
|
|
136
|
+
if (selector)
|
|
137
|
+
await page.locator(selector).first().hover({ timeout });
|
|
138
|
+
else
|
|
139
|
+
await page.mouse.move(x, y);
|
|
140
|
+
return;
|
|
141
|
+
case "tap":
|
|
142
|
+
if (selector)
|
|
143
|
+
await page.locator(selector).first().tap({ timeout });
|
|
144
|
+
else
|
|
145
|
+
await page.touchscreen.tap(x, y);
|
|
146
|
+
return;
|
|
147
|
+
case "type":
|
|
148
|
+
// `fill` clears the field first and dispatches a single input event, so
|
|
149
|
+
// replaying a script twice does not append to what is already there.
|
|
150
|
+
if (selector)
|
|
151
|
+
await page.locator(selector).first().fill(value, { timeout });
|
|
152
|
+
else
|
|
153
|
+
await page.keyboard.type(value);
|
|
154
|
+
return;
|
|
155
|
+
case "select":
|
|
156
|
+
await page.locator(selector).first().selectOption(value, { timeout });
|
|
157
|
+
return;
|
|
158
|
+
case "scroll":
|
|
159
|
+
// With a selector, put the pointer over that element first so the wheel
|
|
160
|
+
// scrolls it rather than the page behind it.
|
|
161
|
+
if (selector)
|
|
162
|
+
await page.locator(selector).first().hover({ timeout });
|
|
163
|
+
await page.mouse.wheel(delta_x, delta_y);
|
|
164
|
+
return;
|
|
165
|
+
case "swipe":
|
|
166
|
+
await swipe(page, x, y, delta_x, delta_y);
|
|
167
|
+
return;
|
|
168
|
+
case "navigate":
|
|
169
|
+
// Playwright only resolves relative URLs against a context `baseURL`;
|
|
170
|
+
// resolve against the current page instead, which is what a script
|
|
171
|
+
// author means by "navigate to /login".
|
|
172
|
+
await page.goto(resolveUrl(value, page.url()), { waitUntil: "commit", timeout: NAVIGATION_TIMEOUT_MS });
|
|
173
|
+
return;
|
|
174
|
+
case "wait":
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* A real finger drag: touchStart, several touchMoves, touchEnd.
|
|
180
|
+
*
|
|
181
|
+
* Playwright's Touchscreen can only tap, so the drag goes through CDP (we are
|
|
182
|
+
* Chromium-only). Intermediate moves matter — a carousel or pull-to-refresh
|
|
183
|
+
* needs the travel and its velocity, not a teleport — and each one is spaced
|
|
184
|
+
* about a frame apart so the page's own velocity maths sees a plausible gesture.
|
|
185
|
+
*/
|
|
186
|
+
async function swipe(page, x, y, deltaX, deltaY) {
|
|
187
|
+
const cdp = await page.context().newCDPSession(page);
|
|
188
|
+
try {
|
|
189
|
+
await cdp.send("Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [{ x, y }] });
|
|
190
|
+
for (let step = 1; step <= SWIPE_STEPS; step++) {
|
|
191
|
+
await sleep(SWIPE_STEP_DELAY_MS);
|
|
192
|
+
await cdp.send("Input.dispatchTouchEvent", {
|
|
193
|
+
// Interpolate from the origin (not from the previous point) so the last
|
|
194
|
+
// move lands exactly on x + deltaX, y + deltaY.
|
|
195
|
+
type: "touchMove",
|
|
196
|
+
touchPoints: [{ x: x + (deltaX * step) / SWIPE_STEPS, y: y + (deltaY * step) / SWIPE_STEPS }],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
await cdp.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] });
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
await cdp.detach().catch(() => { });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function resolveUrl(value, base) {
|
|
206
|
+
try {
|
|
207
|
+
return new URL(value, base).href;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Reduce a Playwright error to one actionable line naming the step. Touch
|
|
215
|
+
* failures get their own wording: the fix is a context option the caller
|
|
216
|
+
* controls, not anything about the page.
|
|
217
|
+
*/
|
|
218
|
+
function describeFailure(interaction, error) {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
const firstLine = message.split("\n")[0];
|
|
221
|
+
const step = describeInteraction(interaction);
|
|
222
|
+
if (/hasTouch|does not support tap/i.test(message)) {
|
|
223
|
+
return `${step} failed: this page is not touch-enabled. Touch is turned on automatically for tap/swipe steps — if you see this, the page was opened without it.`;
|
|
224
|
+
}
|
|
225
|
+
return `${step} failed: ${firstLine}`;
|
|
226
|
+
}
|
|
227
|
+
function sleep(ms) {
|
|
228
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The shared zod fields every interaction step accepts. Both tools build their
|
|
232
|
+
* own object around these (the action enums differ, and `framewatch_capture`
|
|
233
|
+
* adds `delay_ms` where `framewatch_interact` adds `wait_ms`).
|
|
234
|
+
*/
|
|
235
|
+
export const interactionFieldShape = {
|
|
236
|
+
selector: z.string().optional().describe("CSS selector for click/tap/type/select/hover targets, or the scroll container"),
|
|
237
|
+
value: z.string().optional().describe("Text to type, option value to select, or URL to navigate to"),
|
|
238
|
+
x: z.number().optional().describe("X coordinate for click/tap/swipe (used when no selector is given)"),
|
|
239
|
+
y: z.number().optional().describe("Y coordinate for click/tap/swipe (used when no selector is given)"),
|
|
240
|
+
delta_x: z.number().optional().describe("Horizontal distance for scroll/swipe"),
|
|
241
|
+
delta_y: z.number().optional().describe("Vertical distance for scroll/swipe"),
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* zod `superRefine` hook that rejects a step missing the fields its action
|
|
245
|
+
* needs, so an unusable script is reported as invalid input instead of failing
|
|
246
|
+
* half way through a recording.
|
|
247
|
+
*/
|
|
248
|
+
export function refineInteraction(value, ctx) {
|
|
249
|
+
const problem = validateInteraction(value);
|
|
250
|
+
if (problem !== null) {
|
|
251
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem, path: ["action"] });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
//# sourceMappingURL=interaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interaction.js","sourceRoot":"","sources":["../../src/engine/interaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { Page } from \"playwright\";\nimport { NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, SWIPE_STEPS, SWIPE_STEP_DELAY_MS } from \"../constants.js\";\n\n/**\n * Interaction executor.\n *\n * Turns one declarative interaction step (see CLAUDE.md \"Interactions as JSON\n * script\") into real input on a Playwright page. Every step is validated\n * before anything touches the page, and every Playwright failure is reduced to\n * a single actionable line naming the step that failed — a recording is a bad\n * place to surface a 30-line call log.\n */\n\n/** Actions `framewatch_capture` can replay during a recording. */\nexport const CAPTURE_ACTIONS = [\"click\", \"tap\", \"type\", \"scroll\", \"swipe\", \"wait\", \"navigate\"] as const;\n\n/** Actions `framewatch_interact` can perform as a one-off. */\nexport const INTERACT_ACTIONS = [\"click\", \"tap\", \"type\", \"scroll\", \"swipe\", \"navigate\", \"select\", \"hover\"] as const;\n\nexport type InteractionAction = (typeof CAPTURE_ACTIONS)[number] | (typeof INTERACT_ACTIONS)[number];\n\nexport interface Interaction {\n action: InteractionAction;\n /** CSS selector for click/tap/type/select/hover, or the scroll container. */\n selector?: string;\n /** Text to type, option value to select, or URL to navigate to. */\n value?: string;\n x?: number;\n y?: number;\n delta_x?: number;\n delta_y?: number;\n /** Wait this long *before* performing the action. */\n delay_ms?: number;\n}\n\nexport interface ExecuteOptions {\n /** Timeout for selector-based actions. Default SELECTOR_TIMEOUT_MS. */\n timeout_ms?: number;\n}\n\n/** Steps that need a touch-capable browser context (`hasTouch: true`). */\nconst TOUCH_ACTIONS = new Set<InteractionAction>([\"tap\", \"swipe\"]);\n\n/** True when any step needs a touch-enabled browser context. */\nexport function needsTouch(interactions: readonly Interaction[]): boolean {\n return interactions.some((i) => TOUCH_ACTIONS.has(i.action));\n}\n\n/**\n * Check that a step carries the fields its action needs. Returns a message\n * naming what is missing, or null when the step is executable. Used both by\n * the tools (at input-validation time, before a browser is even launched) and\n * by `executeInteraction` itself.\n */\nexport function validateInteraction(interaction: Interaction): string | null {\n const { action, selector, value, x, y, delta_x, delta_y } = interaction;\n const hasPoint = typeof x === \"number\" && typeof y === \"number\";\n const hasPartialPoint = typeof x === \"number\" || typeof y === \"number\";\n\n switch (action) {\n case \"click\":\n case \"tap\":\n case \"hover\":\n if (selector) return null;\n if (hasPoint) return null;\n return hasPartialPoint\n ? `${action} needs both \\`x\\` and \\`y\\` (or a \\`selector\\`)`\n : `${action} needs a \\`selector\\`, or \\`x\\` and \\`y\\``;\n\n case \"type\":\n return typeof value === \"string\" ? null : \"type needs a `value` (the text to type)\";\n\n case \"select\":\n if (!selector) return \"select needs a `selector` naming the <select> element\";\n return typeof value === \"string\" ? null : \"select needs a `value` (the option to choose)\";\n\n case \"scroll\":\n return typeof delta_x === \"number\" || typeof delta_y === \"number\"\n ? null\n : \"scroll needs `delta_y` (and/or `delta_x`) — the distance to scroll\";\n\n case \"swipe\":\n if (!hasPoint) return \"swipe needs `x` and `y` — where the finger goes down\";\n return typeof delta_x === \"number\" || typeof delta_y === \"number\"\n ? null\n : \"swipe needs `delta_x` and/or `delta_y` — how far the finger travels\";\n\n case \"navigate\":\n return typeof value === \"string\" && value.length > 0 ? null : \"navigate needs a `value` (the URL to go to)\";\n\n case \"wait\":\n return null;\n\n default:\n return `unknown action \"${String(action)}\"`;\n }\n}\n\n/** Longest `value` printed in a step description before it is elided. */\nconst DESCRIBE_VALUE_MAX = 60;\n\n/**\n * One-line, human-readable summary of a step, used in error messages and in\n * the capture summary. Long values (a pasted token, a password) are elided\n * rather than echoed in full.\n */\nexport function describeInteraction(interaction: Interaction): string {\n const { action, selector, value, x, y, delta_x = 0, delta_y = 0, delay_ms } = interaction;\n const target = selector ? ` \"${selector}\"` : typeof x === \"number\" && typeof y === \"number\" ? ` at ${x},${y}` : \"\";\n\n switch (action) {\n case \"click\":\n case \"tap\":\n case \"hover\":\n return `${action}${target}`;\n case \"type\":\n return `type \"${elide(value)}\"${selector ? ` into \"${selector}\"` : \"\"}`;\n case \"select\":\n return `select \"${elide(value)}\"${selector ? ` in \"${selector}\"` : \"\"}`;\n case \"scroll\":\n return `scroll${selector ? ` \"${selector}\"` : \"\"} by ${delta_x},${delta_y}`;\n case \"swipe\":\n return `swipe${target} by ${delta_x},${delta_y}`;\n case \"navigate\":\n return `navigate to ${elide(value)}`;\n case \"wait\":\n return `wait ${delay_ms ?? 0}ms`;\n default:\n return String(action);\n }\n}\n\nfunction elide(value: string | undefined): string {\n if (value === undefined) return \"\";\n return value.length > DESCRIBE_VALUE_MAX ? `${value.slice(0, DESCRIBE_VALUE_MAX)}…` : value;\n}\n\n/**\n * Perform one interaction on `page`.\n *\n * Waits `delay_ms` first (that is what `delay_ms` means: \"wait before this\n * action\"), so a `wait` step is simply a step with no action of its own.\n * Throws a single-line Error naming the step on any failure; the page is never\n * touched when the step is invalid.\n */\nexport async function executeInteraction(page: Page, interaction: Interaction, options: ExecuteOptions = {}): Promise<void> {\n const problem = validateInteraction(interaction);\n if (problem) throw new Error(problem);\n\n const delay = interaction.delay_ms;\n if (typeof delay === \"number\" && delay > 0) await sleep(delay);\n\n const timeout = options.timeout_ms ?? SELECTOR_TIMEOUT_MS;\n try {\n await perform(page, interaction, timeout);\n } catch (error) {\n throw new Error(describeFailure(interaction, error), { cause: error });\n }\n}\n\nasync function perform(page: Page, interaction: Interaction, timeout: number): Promise<void> {\n const { action, selector, value, x, y, delta_x = 0, delta_y = 0 } = interaction;\n\n switch (action) {\n case \"click\":\n if (selector) await page.locator(selector).first().click({ timeout });\n else await page.mouse.click(x!, y!);\n return;\n\n case \"hover\":\n if (selector) await page.locator(selector).first().hover({ timeout });\n else await page.mouse.move(x!, y!);\n return;\n\n case \"tap\":\n if (selector) await page.locator(selector).first().tap({ timeout });\n else await page.touchscreen.tap(x!, y!);\n return;\n\n case \"type\":\n // `fill` clears the field first and dispatches a single input event, so\n // replaying a script twice does not append to what is already there.\n if (selector) await page.locator(selector).first().fill(value!, { timeout });\n else await page.keyboard.type(value!);\n return;\n\n case \"select\":\n await page.locator(selector!).first().selectOption(value!, { timeout });\n return;\n\n case \"scroll\":\n // With a selector, put the pointer over that element first so the wheel\n // scrolls it rather than the page behind it.\n if (selector) await page.locator(selector).first().hover({ timeout });\n await page.mouse.wheel(delta_x, delta_y);\n return;\n\n case \"swipe\":\n await swipe(page, x!, y!, delta_x, delta_y);\n return;\n\n case \"navigate\":\n // Playwright only resolves relative URLs against a context `baseURL`;\n // resolve against the current page instead, which is what a script\n // author means by \"navigate to /login\".\n await page.goto(resolveUrl(value!, page.url()), { waitUntil: \"commit\", timeout: NAVIGATION_TIMEOUT_MS });\n return;\n\n case \"wait\":\n return;\n }\n}\n\n/**\n * A real finger drag: touchStart, several touchMoves, touchEnd.\n *\n * Playwright's Touchscreen can only tap, so the drag goes through CDP (we are\n * Chromium-only). Intermediate moves matter — a carousel or pull-to-refresh\n * needs the travel and its velocity, not a teleport — and each one is spaced\n * about a frame apart so the page's own velocity maths sees a plausible gesture.\n */\nasync function swipe(page: Page, x: number, y: number, deltaX: number, deltaY: number): Promise<void> {\n const cdp = await page.context().newCDPSession(page);\n try {\n await cdp.send(\"Input.dispatchTouchEvent\", { type: \"touchStart\", touchPoints: [{ x, y }] });\n for (let step = 1; step <= SWIPE_STEPS; step++) {\n await sleep(SWIPE_STEP_DELAY_MS);\n await cdp.send(\"Input.dispatchTouchEvent\", {\n // Interpolate from the origin (not from the previous point) so the last\n // move lands exactly on x + deltaX, y + deltaY.\n type: \"touchMove\",\n touchPoints: [{ x: x + (deltaX * step) / SWIPE_STEPS, y: y + (deltaY * step) / SWIPE_STEPS }],\n });\n }\n await cdp.send(\"Input.dispatchTouchEvent\", { type: \"touchEnd\", touchPoints: [] });\n } finally {\n await cdp.detach().catch(() => {});\n }\n}\n\nfunction resolveUrl(value: string, base: string): string {\n try {\n return new URL(value, base).href;\n } catch {\n return value;\n }\n}\n\n/**\n * Reduce a Playwright error to one actionable line naming the step. Touch\n * failures get their own wording: the fix is a context option the caller\n * controls, not anything about the page.\n */\nfunction describeFailure(interaction: Interaction, error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n const firstLine = message.split(\"\\n\")[0];\n const step = describeInteraction(interaction);\n\n if (/hasTouch|does not support tap/i.test(message)) {\n return `${step} failed: this page is not touch-enabled. Touch is turned on automatically for tap/swipe steps — if you see this, the page was opened without it.`;\n }\n return `${step} failed: ${firstLine}`;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * The shared zod fields every interaction step accepts. Both tools build their\n * own object around these (the action enums differ, and `framewatch_capture`\n * adds `delay_ms` where `framewatch_interact` adds `wait_ms`).\n */\nexport const interactionFieldShape = {\n selector: z.string().optional().describe(\"CSS selector for click/tap/type/select/hover targets, or the scroll container\"),\n value: z.string().optional().describe(\"Text to type, option value to select, or URL to navigate to\"),\n x: z.number().optional().describe(\"X coordinate for click/tap/swipe (used when no selector is given)\"),\n y: z.number().optional().describe(\"Y coordinate for click/tap/swipe (used when no selector is given)\"),\n delta_x: z.number().optional().describe(\"Horizontal distance for scroll/swipe\"),\n delta_y: z.number().optional().describe(\"Vertical distance for scroll/swipe\"),\n};\n\n/**\n * zod `superRefine` hook that rejects a step missing the fields its action\n * needs, so an unusable script is reported as invalid input instead of failing\n * half way through a recording.\n */\nexport function refineInteraction(value: Interaction, ctx: z.RefinementCtx): void {\n const problem = validateInteraction(value);\n if (problem !== null) {\n ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem, path: [\"action\"] });\n }\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
import type { ConsoleEntry } from "../../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Console layer.
|
|
5
|
+
*
|
|
6
|
+
* Collects everything the page says while a capture runs: `console.*` calls,
|
|
7
|
+
* uncaught exceptions, unhandled promise rejections, and the tab crashing.
|
|
8
|
+
* Entries are stamped with absolute time and rebased onto the recording clock
|
|
9
|
+
* by `entries(origin)`, because collection starts *before* the navigation —
|
|
10
|
+
* an error thrown during page load is usually the most valuable thing here,
|
|
11
|
+
* and it happens before frame 0 exists.
|
|
12
|
+
*
|
|
13
|
+
* Nothing in this layer touches the page. It is all Playwright events, so a
|
|
14
|
+
* page that has frozen, navigated away or died still yields whatever it said
|
|
15
|
+
* before it went.
|
|
16
|
+
*/
|
|
17
|
+
export type ConsoleLevel = ConsoleEntry["level"];
|
|
18
|
+
/** One collected message, stamped with absolute (epoch) time and already normalised. */
|
|
19
|
+
export interface ConsoleRecord {
|
|
20
|
+
at: number;
|
|
21
|
+
level: ConsoleLevel;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Map Chromium's console message types onto the four levels FrameWatch
|
|
26
|
+
* reports. `assert` is an error because that is what a failed assertion is;
|
|
27
|
+
* `count`/`timeEnd` are instrumentation, so they sit with `info`; everything
|
|
28
|
+
* else that is not clearly a warning or an error is a plain log.
|
|
29
|
+
*/
|
|
30
|
+
export declare function toConsoleLevel(type: string): ConsoleLevel;
|
|
31
|
+
/**
|
|
32
|
+
* Reduce a message to one printable line. Console output is rendered one entry
|
|
33
|
+
* per line, so an embedded newline (a stack trace, a pretty-printed object)
|
|
34
|
+
* would otherwise be indistinguishable from the next entry.
|
|
35
|
+
*/
|
|
36
|
+
export declare function normaliseText(text: string, maxLength?: number): string;
|
|
37
|
+
/**
|
|
38
|
+
* Describe an uncaught error the way a developer reads a stack trace: the
|
|
39
|
+
* message, then where it came from. Playwright hands `pageerror` a real Error
|
|
40
|
+
* whose `stack` is "Name: message\n at fn (url:line:col)"; the top frame is
|
|
41
|
+
* the actionable part and the rest is noise in a one-line entry.
|
|
42
|
+
*/
|
|
43
|
+
export declare function describePageError(error: Error): string;
|
|
44
|
+
export declare class ConsoleCollector {
|
|
45
|
+
#private;
|
|
46
|
+
constructor(page: Page, limit?: number);
|
|
47
|
+
/** Start listening. Idempotent. */
|
|
48
|
+
attach(): this;
|
|
49
|
+
/** Stop listening. Collected entries are kept. */
|
|
50
|
+
detach(): void;
|
|
51
|
+
/** Entries refused or evicted by the cap. */
|
|
52
|
+
get dropped(): number;
|
|
53
|
+
/** Forget everything collected so far. See `BoundedLog.clear`. */
|
|
54
|
+
clear(): void;
|
|
55
|
+
/**
|
|
56
|
+
* Collected entries, oldest first, with timestamps rebased onto the
|
|
57
|
+
* recording clock (`origin` is the recording's start in epoch ms). Entries
|
|
58
|
+
* from before the recording started keep their negative timestamp: they
|
|
59
|
+
* happened during page load, and pretending otherwise would put them out of
|
|
60
|
+
* order with the rest.
|
|
61
|
+
*/
|
|
62
|
+
entries(origin: number): ConsoleEntry[];
|
|
63
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { MAX_CONSOLE_ENTRIES, MAX_CONSOLE_TEXT_LENGTH } from "../../constants.js";
|
|
2
|
+
import { BoundedLog } from "../../utils/bounded-log.js";
|
|
3
|
+
/**
|
|
4
|
+
* Map Chromium's console message types onto the four levels FrameWatch
|
|
5
|
+
* reports. `assert` is an error because that is what a failed assertion is;
|
|
6
|
+
* `count`/`timeEnd` are instrumentation, so they sit with `info`; everything
|
|
7
|
+
* else that is not clearly a warning or an error is a plain log.
|
|
8
|
+
*/
|
|
9
|
+
export function toConsoleLevel(type) {
|
|
10
|
+
switch (type) {
|
|
11
|
+
case "error":
|
|
12
|
+
case "assert":
|
|
13
|
+
return "error";
|
|
14
|
+
case "warning":
|
|
15
|
+
return "warn";
|
|
16
|
+
case "info":
|
|
17
|
+
case "count":
|
|
18
|
+
case "timeEnd":
|
|
19
|
+
return "info";
|
|
20
|
+
default:
|
|
21
|
+
return "log";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Reduce a message to one printable line. Console output is rendered one entry
|
|
26
|
+
* per line, so an embedded newline (a stack trace, a pretty-printed object)
|
|
27
|
+
* would otherwise be indistinguishable from the next entry.
|
|
28
|
+
*/
|
|
29
|
+
export function normaliseText(text, maxLength = MAX_CONSOLE_TEXT_LENGTH) {
|
|
30
|
+
const flat = text.replace(/\s*\n\s*/g, " ↵ ").trim();
|
|
31
|
+
return flat.length > maxLength ? `${flat.slice(0, maxLength)}… (${flat.length} chars)` : flat;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Describe an uncaught error the way a developer reads a stack trace: the
|
|
35
|
+
* message, then where it came from. Playwright hands `pageerror` a real Error
|
|
36
|
+
* whose `stack` is "Name: message\n at fn (url:line:col)"; the top frame is
|
|
37
|
+
* the actionable part and the rest is noise in a one-line entry.
|
|
38
|
+
*/
|
|
39
|
+
export function describePageError(error) {
|
|
40
|
+
const head = error.message ? `${error.name}: ${error.message}` : error.name || "Error";
|
|
41
|
+
const frame = (error.stack ?? "")
|
|
42
|
+
.split("\n")
|
|
43
|
+
.slice(1)
|
|
44
|
+
.map((line) => line.trim())
|
|
45
|
+
.find((line) => line.startsWith("at "));
|
|
46
|
+
return frame ? `${head} (${frame})` : head;
|
|
47
|
+
}
|
|
48
|
+
export class ConsoleCollector {
|
|
49
|
+
#page;
|
|
50
|
+
#log;
|
|
51
|
+
#attached = false;
|
|
52
|
+
#onConsole = (message) => {
|
|
53
|
+
this.#record(toConsoleLevel(message.type()), message.text());
|
|
54
|
+
};
|
|
55
|
+
#onPageError = (error) => {
|
|
56
|
+
this.#record("error", describePageError(error));
|
|
57
|
+
};
|
|
58
|
+
#onCrash = () => {
|
|
59
|
+
// Not a console message, but it is the explanation for every frame after
|
|
60
|
+
// it, and there is nowhere better for the page to tell us it has died.
|
|
61
|
+
this.#record("error", "Page crashed (the browser tab stopped responding)");
|
|
62
|
+
};
|
|
63
|
+
constructor(page, limit = MAX_CONSOLE_ENTRIES) {
|
|
64
|
+
this.#page = page;
|
|
65
|
+
this.#log = new BoundedLog(limit, (entry) => entry.level === "error");
|
|
66
|
+
}
|
|
67
|
+
/** Start listening. Idempotent. */
|
|
68
|
+
attach() {
|
|
69
|
+
if (this.#attached)
|
|
70
|
+
return this;
|
|
71
|
+
this.#attached = true;
|
|
72
|
+
this.#page.on("console", this.#onConsole);
|
|
73
|
+
this.#page.on("pageerror", this.#onPageError);
|
|
74
|
+
this.#page.on("crash", this.#onCrash);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
/** Stop listening. Collected entries are kept. */
|
|
78
|
+
detach() {
|
|
79
|
+
if (!this.#attached)
|
|
80
|
+
return;
|
|
81
|
+
this.#attached = false;
|
|
82
|
+
this.#page.off("console", this.#onConsole);
|
|
83
|
+
this.#page.off("pageerror", this.#onPageError);
|
|
84
|
+
this.#page.off("crash", this.#onCrash);
|
|
85
|
+
}
|
|
86
|
+
/** Entries refused or evicted by the cap. */
|
|
87
|
+
get dropped() {
|
|
88
|
+
return this.#log.dropped;
|
|
89
|
+
}
|
|
90
|
+
/** Forget everything collected so far. See `BoundedLog.clear`. */
|
|
91
|
+
clear() {
|
|
92
|
+
this.#log.clear();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Collected entries, oldest first, with timestamps rebased onto the
|
|
96
|
+
* recording clock (`origin` is the recording's start in epoch ms). Entries
|
|
97
|
+
* from before the recording started keep their negative timestamp: they
|
|
98
|
+
* happened during page load, and pretending otherwise would put them out of
|
|
99
|
+
* order with the rest.
|
|
100
|
+
*/
|
|
101
|
+
entries(origin) {
|
|
102
|
+
return this.#log.items.map((record) => ({
|
|
103
|
+
level: record.level,
|
|
104
|
+
text: record.text,
|
|
105
|
+
timestamp_ms: Math.round(record.at - origin),
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Text is normalised here rather than on the way out: `console.log` of a
|
|
110
|
+
* megabyte-long string is one call, and holding a hundred of those until the
|
|
111
|
+
* recording ends is a real amount of memory to keep for text that will be
|
|
112
|
+
* elided anyway.
|
|
113
|
+
*/
|
|
114
|
+
#record(level, text) {
|
|
115
|
+
this.#log.add({ at: Date.now(), level, text: normaliseText(text) });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=console.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console.js","sourceRoot":"","sources":["../../../src/engine/layers/console.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ConsoleMessage, Page } from \"playwright\";\nimport { MAX_CONSOLE_ENTRIES, MAX_CONSOLE_TEXT_LENGTH } from \"../../constants.js\";\nimport type { ConsoleEntry } from \"../../types.js\";\nimport { BoundedLog } from \"../../utils/bounded-log.js\";\n\n/**\n * Console layer.\n *\n * Collects everything the page says while a capture runs: `console.*` calls,\n * uncaught exceptions, unhandled promise rejections, and the tab crashing.\n * Entries are stamped with absolute time and rebased onto the recording clock\n * by `entries(origin)`, because collection starts *before* the navigation —\n * an error thrown during page load is usually the most valuable thing here,\n * and it happens before frame 0 exists.\n *\n * Nothing in this layer touches the page. It is all Playwright events, so a\n * page that has frozen, navigated away or died still yields whatever it said\n * before it went.\n */\n\nexport type ConsoleLevel = ConsoleEntry[\"level\"];\n\n/** One collected message, stamped with absolute (epoch) time and already normalised. */\nexport interface ConsoleRecord {\n at: number;\n level: ConsoleLevel;\n text: string;\n}\n\n/**\n * Map Chromium's console message types onto the four levels FrameWatch\n * reports. `assert` is an error because that is what a failed assertion is;\n * `count`/`timeEnd` are instrumentation, so they sit with `info`; everything\n * else that is not clearly a warning or an error is a plain log.\n */\nexport function toConsoleLevel(type: string): ConsoleLevel {\n switch (type) {\n case \"error\":\n case \"assert\":\n return \"error\";\n case \"warning\":\n return \"warn\";\n case \"info\":\n case \"count\":\n case \"timeEnd\":\n return \"info\";\n default:\n return \"log\";\n }\n}\n\n/**\n * Reduce a message to one printable line. Console output is rendered one entry\n * per line, so an embedded newline (a stack trace, a pretty-printed object)\n * would otherwise be indistinguishable from the next entry.\n */\nexport function normaliseText(text: string, maxLength: number = MAX_CONSOLE_TEXT_LENGTH): string {\n const flat = text.replace(/\\s*\\n\\s*/g, \" ↵ \").trim();\n return flat.length > maxLength ? `${flat.slice(0, maxLength)}… (${flat.length} chars)` : flat;\n}\n\n/**\n * Describe an uncaught error the way a developer reads a stack trace: the\n * message, then where it came from. Playwright hands `pageerror` a real Error\n * whose `stack` is \"Name: message\\n at fn (url:line:col)\"; the top frame is\n * the actionable part and the rest is noise in a one-line entry.\n */\nexport function describePageError(error: Error): string {\n const head = error.message ? `${error.name}: ${error.message}` : error.name || \"Error\";\n const frame = (error.stack ?? \"\")\n .split(\"\\n\")\n .slice(1)\n .map((line) => line.trim())\n .find((line) => line.startsWith(\"at \"));\n return frame ? `${head} (${frame})` : head;\n}\n\nexport class ConsoleCollector {\n readonly #page: Page;\n readonly #log: BoundedLog<ConsoleRecord>;\n #attached = false;\n\n readonly #onConsole = (message: ConsoleMessage): void => {\n this.#record(toConsoleLevel(message.type()), message.text());\n };\n\n readonly #onPageError = (error: Error): void => {\n this.#record(\"error\", describePageError(error));\n };\n\n readonly #onCrash = (): void => {\n // Not a console message, but it is the explanation for every frame after\n // it, and there is nowhere better for the page to tell us it has died.\n this.#record(\"error\", \"Page crashed (the browser tab stopped responding)\");\n };\n\n constructor(page: Page, limit: number = MAX_CONSOLE_ENTRIES) {\n this.#page = page;\n this.#log = new BoundedLog<ConsoleRecord>(limit, (entry) => entry.level === \"error\");\n }\n\n /** Start listening. Idempotent. */\n attach(): this {\n if (this.#attached) return this;\n this.#attached = true;\n this.#page.on(\"console\", this.#onConsole);\n this.#page.on(\"pageerror\", this.#onPageError);\n this.#page.on(\"crash\", this.#onCrash);\n return this;\n }\n\n /** Stop listening. Collected entries are kept. */\n detach(): void {\n if (!this.#attached) return;\n this.#attached = false;\n this.#page.off(\"console\", this.#onConsole);\n this.#page.off(\"pageerror\", this.#onPageError);\n this.#page.off(\"crash\", this.#onCrash);\n }\n\n /** Entries refused or evicted by the cap. */\n get dropped(): number {\n return this.#log.dropped;\n }\n\n /** Forget everything collected so far. See `BoundedLog.clear`. */\n clear(): void {\n this.#log.clear();\n }\n\n /**\n * Collected entries, oldest first, with timestamps rebased onto the\n * recording clock (`origin` is the recording's start in epoch ms). Entries\n * from before the recording started keep their negative timestamp: they\n * happened during page load, and pretending otherwise would put them out of\n * order with the rest.\n */\n entries(origin: number): ConsoleEntry[] {\n return this.#log.items.map((record) => ({\n level: record.level,\n text: record.text,\n timestamp_ms: Math.round(record.at - origin),\n }));\n }\n\n /**\n * Text is normalised here rather than on the way out: `console.log` of a\n * megabyte-long string is one call, and holding a hundred of those until the\n * recording ends is a real amount of memory to keep for text that will be\n * elided anyway.\n */\n #record(level: ConsoleLevel, text: string): void {\n this.#log.add({ at: Date.now(), level, text: normaliseText(text) });\n }\n}\n"]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
import { type InstallOptions } from "./probe.js";
|
|
3
|
+
/** What happened to a node. */
|
|
4
|
+
export type DomOp = "+" | "-" | "~" | "t";
|
|
5
|
+
/** One mutation, as pushed by the page. Timestamps are absolute (epoch ms). */
|
|
6
|
+
export interface RawDomRecord {
|
|
7
|
+
/** Absolute time the mutation was observed. */
|
|
8
|
+
t: number;
|
|
9
|
+
op: DomOp;
|
|
10
|
+
/** Short descriptor of the node — `div#id`, `span.class`, `p`. */
|
|
11
|
+
target: string;
|
|
12
|
+
/** Descriptor of the parent, for adds and removes. */
|
|
13
|
+
parent?: string;
|
|
14
|
+
/** Attribute name, for `~`. */
|
|
15
|
+
detail?: string;
|
|
16
|
+
}
|
|
17
|
+
/** One mutation, rebased onto the recording clock. */
|
|
18
|
+
export interface DomRecord {
|
|
19
|
+
timestamp_ms: number;
|
|
20
|
+
op: DomOp;
|
|
21
|
+
target: string;
|
|
22
|
+
parent?: string;
|
|
23
|
+
detail?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare class DomCollector {
|
|
26
|
+
#private;
|
|
27
|
+
constructor(page: Page, limit?: number);
|
|
28
|
+
/**
|
|
29
|
+
* Install the probe. Call before the page navigates, or pass
|
|
30
|
+
* `{ runNow: true }` to also watch the document that is already loaded.
|
|
31
|
+
*/
|
|
32
|
+
attach(options?: InstallOptions): Promise<this>;
|
|
33
|
+
/** Mutations the cap refused. */
|
|
34
|
+
get dropped(): number;
|
|
35
|
+
/** Forget everything collected so far. See `BoundedLog.clear`. */
|
|
36
|
+
clear(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Collected mutations in time order, rebased onto the recording clock
|
|
39
|
+
* (`origin` is the recording's start in epoch ms). Mutations from before the
|
|
40
|
+
* recording started keep their negative timestamp — they are the page
|
|
41
|
+
* building itself, and belong on the first card.
|
|
42
|
+
*/
|
|
43
|
+
records(origin: number): DomRecord[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Turn one card's mutations into a few readable lines.
|
|
47
|
+
*
|
|
48
|
+
* Identical mutations are collapsed with a count, in first-seen order: an
|
|
49
|
+
* animation driven by an inline style produces one `~ #logo [style] ×24` line
|
|
50
|
+
* rather than 24 identical ones, and the order still tells the story of what
|
|
51
|
+
* happened first. Returns undefined when there is nothing to say.
|
|
52
|
+
*/
|
|
53
|
+
export declare function renderDomChanges(records: DomRecord[], maxLines?: number): string | undefined;
|