framewatch-mcp-server 0.1.0 → 0.2.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.
Files changed (133) hide show
  1. package/README.md +895 -15
  2. package/dist/constants.d.ts +274 -0
  3. package/dist/constants.js +279 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/engine/browser.d.ts +20 -4
  6. package/dist/engine/browser.js +26 -9
  7. package/dist/engine/browser.js.map +1 -1
  8. package/dist/engine/clicks.d.ts +221 -0
  9. package/dist/engine/clicks.js +801 -0
  10. package/dist/engine/clicks.js.map +1 -0
  11. package/dist/engine/forms.d.ts +137 -0
  12. package/dist/engine/forms.js +474 -0
  13. package/dist/engine/forms.js.map +1 -0
  14. package/dist/engine/hmr.d.ts +41 -0
  15. package/dist/engine/hmr.js +91 -0
  16. package/dist/engine/hmr.js.map +1 -0
  17. package/dist/engine/inspect.d.ts +31 -0
  18. package/dist/engine/inspect.js +383 -0
  19. package/dist/engine/inspect.js.map +1 -0
  20. package/dist/engine/interaction.d.ts +12 -7
  21. package/dist/engine/interaction.js +110 -18
  22. package/dist/engine/interaction.js.map +1 -1
  23. package/dist/engine/links.d.ts +134 -0
  24. package/dist/engine/links.js +384 -0
  25. package/dist/engine/links.js.map +1 -0
  26. package/dist/engine/mocks.d.ts +53 -0
  27. package/dist/engine/mocks.js +148 -0
  28. package/dist/engine/mocks.js.map +1 -0
  29. package/dist/engine/rtl.d.ts +129 -0
  30. package/dist/engine/rtl.js +540 -0
  31. package/dist/engine/rtl.js.map +1 -0
  32. package/dist/engine/seo.d.ts +189 -0
  33. package/dist/engine/seo.js +398 -0
  34. package/dist/engine/seo.js.map +1 -0
  35. package/dist/engine/snapshot.d.ts +29 -0
  36. package/dist/engine/snapshot.js +10 -0
  37. package/dist/engine/snapshot.js.map +1 -0
  38. package/dist/engine/vue.d.ts +54 -0
  39. package/dist/engine/vue.js +419 -0
  40. package/dist/engine/vue.js.map +1 -0
  41. package/dist/index.js +45 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/tools/accessibility.d.ts +4 -0
  44. package/dist/tools/accessibility.js +9 -2
  45. package/dist/tools/accessibility.js.map +1 -1
  46. package/dist/tools/api-mock.d.ts +405 -0
  47. package/dist/tools/api-mock.js +186 -0
  48. package/dist/tools/api-mock.js.map +1 -0
  49. package/dist/tools/capture.d.ts +90 -26
  50. package/dist/tools/capture.js +109 -58
  51. package/dist/tools/capture.js.map +1 -1
  52. package/dist/tools/compare.d.ts +4 -0
  53. package/dist/tools/compare.js +16 -5
  54. package/dist/tools/compare.js.map +1 -1
  55. package/dist/tools/dead-clicks.d.ts +128 -0
  56. package/dist/tools/dead-clicks.js +570 -0
  57. package/dist/tools/dead-clicks.js.map +1 -0
  58. package/dist/tools/form-test.d.ts +112 -0
  59. package/dist/tools/form-test.js +477 -0
  60. package/dist/tools/form-test.js.map +1 -0
  61. package/dist/tools/index.d.ts +17 -1
  62. package/dist/tools/index.js +45 -1
  63. package/dist/tools/index.js.map +1 -1
  64. package/dist/tools/inspect.d.ts +78 -0
  65. package/dist/tools/inspect.js +136 -0
  66. package/dist/tools/inspect.js.map +1 -0
  67. package/dist/tools/interact.d.ts +37 -18
  68. package/dist/tools/interact.js +113 -13
  69. package/dist/tools/interact.js.map +1 -1
  70. package/dist/tools/links.d.ts +129 -0
  71. package/dist/tools/links.js +640 -0
  72. package/dist/tools/links.js.map +1 -0
  73. package/dist/tools/responsive.d.ts +10 -6
  74. package/dist/tools/responsive.js +21 -4
  75. package/dist/tools/responsive.js.map +1 -1
  76. package/dist/tools/rtl.d.ts +241 -0
  77. package/dist/tools/rtl.js +410 -0
  78. package/dist/tools/rtl.js.map +1 -0
  79. package/dist/tools/save-auth.d.ts +263 -0
  80. package/dist/tools/save-auth.js +253 -0
  81. package/dist/tools/save-auth.js.map +1 -0
  82. package/dist/tools/screenshot.d.ts +4 -0
  83. package/dist/tools/screenshot.js +15 -4
  84. package/dist/tools/screenshot.js.map +1 -1
  85. package/dist/tools/seo.d.ts +113 -0
  86. package/dist/tools/seo.js +281 -0
  87. package/dist/tools/seo.js.map +1 -0
  88. package/dist/tools/snapshot.d.ts +122 -0
  89. package/dist/tools/snapshot.js +183 -0
  90. package/dist/tools/snapshot.js.map +1 -0
  91. package/dist/tools/wait-for.d.ts +107 -0
  92. package/dist/tools/wait-for.js +167 -0
  93. package/dist/tools/wait-for.js.map +1 -0
  94. package/dist/utils/arabic-text.d.ts +14 -0
  95. package/dist/utils/arabic-text.js +193 -0
  96. package/dist/utils/arabic-text.js.map +1 -0
  97. package/dist/utils/budget.d.ts +41 -0
  98. package/dist/utils/budget.js +182 -0
  99. package/dist/utils/budget.js.map +1 -0
  100. package/dist/utils/format.d.ts +11 -1
  101. package/dist/utils/format.js +27 -4
  102. package/dist/utils/format.js.map +1 -1
  103. package/dist/utils/highlight.d.ts +69 -0
  104. package/dist/utils/highlight.js +181 -0
  105. package/dist/utils/highlight.js.map +1 -0
  106. package/dist/utils/link-rules.d.ts +100 -0
  107. package/dist/utils/link-rules.js +284 -0
  108. package/dist/utils/link-rules.js.map +1 -0
  109. package/dist/utils/mock-rules.d.ts +144 -0
  110. package/dist/utils/mock-rules.js +224 -0
  111. package/dist/utils/mock-rules.js.map +1 -0
  112. package/dist/utils/rtl-rules.d.ts +142 -0
  113. package/dist/utils/rtl-rules.js +296 -0
  114. package/dist/utils/rtl-rules.js.map +1 -0
  115. package/dist/utils/seo-rules.d.ts +129 -0
  116. package/dist/utils/seo-rules.js +726 -0
  117. package/dist/utils/seo-rules.js.map +1 -0
  118. package/dist/utils/snapshot-rules.d.ts +33 -0
  119. package/dist/utils/snapshot-rules.js +111 -0
  120. package/dist/utils/snapshot-rules.js.map +1 -0
  121. package/dist/utils/storage-state.d.ts +76 -0
  122. package/dist/utils/storage-state.js +195 -0
  123. package/dist/utils/storage-state.js.map +1 -0
  124. package/dist/utils/style-rules.d.ts +107 -0
  125. package/dist/utils/style-rules.js +223 -0
  126. package/dist/utils/style-rules.js.map +1 -0
  127. package/dist/utils/test-data.d.ts +75 -0
  128. package/dist/utils/test-data.js +294 -0
  129. package/dist/utils/test-data.js.map +1 -0
  130. package/dist/utils/vue-rules.d.ts +72 -0
  131. package/dist/utils/vue-rules.js +108 -0
  132. package/dist/utils/vue-rules.js.map +1 -0
  133. package/package.json +6 -4
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, SWIPE_STEPS, SWIPE_STEP_DELAY_MS } from "../constants.js";
3
+ import { classifyNavigate } from "../utils/vue-rules.js";
4
+ import { detectVue, routerNavigate } from "./vue.js";
3
5
  /**
4
6
  * Interaction executor.
5
7
  *
@@ -9,10 +11,31 @@ import { NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, SWIPE_STEPS, SWIPE_STEP_DEL
9
11
  * a single actionable line naming the step that failed — a recording is a bad
10
12
  * place to surface a 30-line call log.
11
13
  */
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"];
14
+ /** Actions a replayable script (`framewatch_capture`, `framewatch_save_auth`) can perform. */
15
+ export const CAPTURE_ACTIONS = [
16
+ "click",
17
+ "tap",
18
+ "type",
19
+ "key",
20
+ "scroll",
21
+ "swipe",
22
+ "hover",
23
+ "select",
24
+ "wait",
25
+ "navigate",
26
+ ];
27
+ /** Actions `framewatch_interact` can perform as a one-off — everything but `wait`, which needs a script to sit in. */
28
+ export const INTERACT_ACTIONS = [
29
+ "click",
30
+ "tap",
31
+ "type",
32
+ "key",
33
+ "scroll",
34
+ "swipe",
35
+ "navigate",
36
+ "select",
37
+ "hover",
38
+ ];
16
39
  /** Steps that need a touch-capable browser context (`hasTouch: true`). */
17
40
  const TOUCH_ACTIONS = new Set(["tap", "swipe"]);
18
41
  /** True when any step needs a touch-enabled browser context. */
@@ -41,7 +64,13 @@ export function validateInteraction(interaction) {
41
64
  ? `${action} needs both \`x\` and \`y\` (or a \`selector\`)`
42
65
  : `${action} needs a \`selector\`, or \`x\` and \`y\``;
43
66
  case "type":
44
- return typeof value === "string" ? null : "type needs a `value` (the text to type)";
67
+ return typeof value === "string"
68
+ ? null
69
+ : "type needs a `value` (the text to type; `\n` presses Enter and `\t` presses Tab)";
70
+ case "key":
71
+ return typeof value === "string" && value.length > 0
72
+ ? null
73
+ : "key needs a `value` (the key to press, e.g. `Enter`, `Escape`, `ArrowDown` or `Control+a`)";
45
74
  case "select":
46
75
  if (!selector)
47
76
  return "select needs a `selector` naming the <select> element";
@@ -83,6 +112,8 @@ export function describeInteraction(interaction) {
83
112
  return `type "${elide(value)}"${selector ? ` into "${selector}"` : ""}`;
84
113
  case "select":
85
114
  return `select "${elide(value)}"${selector ? ` in "${selector}"` : ""}`;
115
+ case "key":
116
+ return `press "${elide(value)}"${selector ? ` in "${selector}"` : ""}`;
86
117
  case "scroll":
87
118
  return `scroll${selector ? ` "${selector}"` : ""} by ${delta_x},${delta_y}`;
88
119
  case "swipe":
@@ -117,7 +148,7 @@ export async function executeInteraction(page, interaction, options = {}) {
117
148
  await sleep(delay);
118
149
  const timeout = options.timeout_ms ?? SELECTOR_TIMEOUT_MS;
119
150
  try {
120
- await perform(page, interaction, timeout);
151
+ return (await perform(page, interaction, timeout)) ?? {};
121
152
  }
122
153
  catch (error) {
123
154
  throw new Error(describeFailure(interaction, error), { cause: error });
@@ -145,12 +176,14 @@ async function perform(page, interaction, timeout) {
145
176
  await page.touchscreen.tap(x, y);
146
177
  return;
147
178
  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.
179
+ await typeValue(page, selector, value, timeout);
180
+ return;
181
+ case "key":
182
+ // A key goes wherever focus is; `selector` is the way to say where that
183
+ // should be without spending a separate click step on it.
150
184
  if (selector)
151
- await page.locator(selector).first().fill(value, { timeout });
152
- else
153
- await page.keyboard.type(value);
185
+ await page.locator(selector).first().focus({ timeout });
186
+ await page.keyboard.press(value);
154
187
  return;
155
188
  case "select":
156
189
  await page.locator(selector).first().selectOption(value, { timeout });
@@ -165,16 +198,68 @@ async function perform(page, interaction, timeout) {
165
198
  case "swipe":
166
199
  await swipe(page, x, y, delta_x, delta_y);
167
200
  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 });
201
+ case "navigate": {
202
+ // A path on a page with vue-router goes through the router — same
203
+ // document, state kept, no reload which is what a person clicking a
204
+ // link in the app gets. Anything else, or a path the router does not
205
+ // know, is a full load. Playwright only resolves relative URLs against
206
+ // a context `baseURL`; resolve against the current page instead, which
207
+ // is what a script author means by "navigate to /login".
208
+ const vue = await detectVue(page);
209
+ const plan = classifyNavigate(value, page.url(), vue?.router === true);
210
+ if (plan.via === "router") {
211
+ const outcome = await routerNavigate(page, plan.target);
212
+ if (outcome.ok)
213
+ return { note: " (vue-router)" };
214
+ await page.goto(resolveUrl(value, page.url()), { waitUntil: "commit", timeout: NAVIGATION_TIMEOUT_MS });
215
+ return { note: ` (full load — vue-router: ${outcome.error})` };
216
+ }
217
+ await page.goto(plan.target, { waitUntil: "commit", timeout: NAVIGATION_TIMEOUT_MS });
173
218
  return;
219
+ }
174
220
  case "wait":
175
221
  return;
176
222
  }
177
223
  }
224
+ /** Characters a `type` value may carry inline to mean a key press. */
225
+ const TYPE_KEYS = { "\n": "Enter", "\t": "Tab" };
226
+ /**
227
+ * Type `value`, pressing Enter for each `\n` in it and Tab for each `\t`.
228
+ *
229
+ * Typing a newline is what ends most forms, and splitting "duo\n" into a type
230
+ * step and a key step is a step the caller should not have to think about.
231
+ * The first text run still goes in with `fill` — it clears the field, so
232
+ * replaying a script twice does not append to what is already there — and
233
+ * everything after a key press is typed on top of wherever focus ended up,
234
+ * which is what a person continuing to type would produce.
235
+ */
236
+ async function typeValue(page, selector, value, timeout) {
237
+ // Keep the separators: split(/([\n\t])/) yields text, key, text, key, …
238
+ const segments = value.split(/([\n\t])/).filter((segment) => segment !== "");
239
+ // An empty value still means "empty the field", which is `fill("")`.
240
+ if (segments.length === 0)
241
+ segments.push("");
242
+ let focused = false;
243
+ for (const segment of segments) {
244
+ const key = TYPE_KEYS[segment];
245
+ if (key === undefined) {
246
+ // The first run replaces the field's contents; later runs are typed at
247
+ // the caret, since a key press may well have moved focus elsewhere.
248
+ if (selector && !focused)
249
+ await page.locator(selector).first().fill(segment, { timeout });
250
+ else
251
+ await page.keyboard.type(segment);
252
+ focused = true;
253
+ continue;
254
+ }
255
+ // A value that opens with a key ("\n" alone, meaning "press Enter in this
256
+ // field") still has to land on the element the caller named.
257
+ if (selector && !focused)
258
+ await page.locator(selector).first().focus({ timeout });
259
+ focused = true;
260
+ await page.keyboard.press(key);
261
+ }
262
+ }
178
263
  /**
179
264
  * A real finger drag: touchStart, several touchMoves, touchEnd.
180
265
  *
@@ -233,8 +318,15 @@ function sleep(ms) {
233
318
  * adds `delay_ms` where `framewatch_interact` adds `wait_ms`).
234
319
  */
235
320
  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"),
321
+ selector: z
322
+ .string()
323
+ .optional()
324
+ .describe("CSS selector for click/tap/type/select/hover targets, the field to focus first for `key`, or the scroll container"),
325
+ value: z
326
+ .string()
327
+ .optional()
328
+ .describe("Text to type (`\n` in it presses Enter, `\t` presses Tab), key to press for `key` " +
329
+ "(e.g. `Enter`, `Escape`, `ArrowDown`, `Control+a`), option value to select, or URL to navigate to"),
238
330
  x: z.number().optional().describe("X coordinate for click/tap/swipe (used when no selector is given)"),
239
331
  y: z.number().optional().describe("Y coordinate for click/tap/swipe (used when no selector is given)"),
240
332
  delta_x: z.number().optional().describe("Horizontal distance for scroll/swipe"),
@@ -1 +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"]}
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\";\nimport { classifyNavigate } from \"../utils/vue-rules.js\";\nimport { detectVue, routerNavigate } from \"./vue.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 a replayable script (`framewatch_capture`, `framewatch_save_auth`) can perform. */\nexport const CAPTURE_ACTIONS = [\n \"click\",\n \"tap\",\n \"type\",\n \"key\",\n \"scroll\",\n \"swipe\",\n \"hover\",\n \"select\",\n \"wait\",\n \"navigate\",\n] as const;\n\n/** Actions `framewatch_interact` can perform as a one-off — everything but `wait`, which needs a script to sit in. */\nexport const INTERACT_ACTIONS = [\n \"click\",\n \"tap\",\n \"type\",\n \"key\",\n \"scroll\",\n \"swipe\",\n \"navigate\",\n \"select\",\n \"hover\",\n] 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, the field to focus for `key`, or the scroll container. */\n selector?: string;\n /** Text to type, key to press, 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/** What a step has to add to its own description once it has run. */\nexport interface InteractionOutcome {\n /** Appended to `describeInteraction(step)`, e.g. ` (vue-router)`. */\n note?: string;\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\"\n ? null\n : \"type needs a `value` (the text to type; `\\n` presses Enter and `\\t` presses Tab)\";\n\n case \"key\":\n return typeof value === \"string\" && value.length > 0\n ? null\n : \"key needs a `value` (the key to press, e.g. `Enter`, `Escape`, `ArrowDown` or `Control+a`)\";\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 \"key\":\n return `press \"${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(\n page: Page,\n interaction: Interaction,\n options: ExecuteOptions = {},\n): Promise<InteractionOutcome> {\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 return (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<InteractionOutcome | 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 await typeValue(page, selector, value!, timeout);\n return;\n\n case \"key\":\n // A key goes wherever focus is; `selector` is the way to say where that\n // should be without spending a separate click step on it.\n if (selector) await page.locator(selector).first().focus({ timeout });\n await page.keyboard.press(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 // A path on a page with vue-router goes through the router — same\n // document, state kept, no reload — which is what a person clicking a\n // link in the app gets. Anything else, or a path the router does not\n // know, is a full load. Playwright only resolves relative URLs against\n // a context `baseURL`; resolve against the current page instead, which\n // is what a script author means by \"navigate to /login\".\n const vue = await detectVue(page);\n const plan = classifyNavigate(value!, page.url(), vue?.router === true);\n if (plan.via === \"router\") {\n const outcome = await routerNavigate(page, plan.target);\n if (outcome.ok) return { note: \" (vue-router)\" };\n await page.goto(resolveUrl(value!, page.url()), { waitUntil: \"commit\", timeout: NAVIGATION_TIMEOUT_MS });\n return { note: ` (full load — vue-router: ${outcome.error})` };\n }\n await page.goto(plan.target, { waitUntil: \"commit\", timeout: NAVIGATION_TIMEOUT_MS });\n return;\n }\n\n case \"wait\":\n return;\n }\n}\n\n/** Characters a `type` value may carry inline to mean a key press. */\nconst TYPE_KEYS: Record<string, string> = { \"\\n\": \"Enter\", \"\\t\": \"Tab\" };\n\n/**\n * Type `value`, pressing Enter for each `\\n` in it and Tab for each `\\t`.\n *\n * Typing a newline is what ends most forms, and splitting \"duo\\n\" into a type\n * step and a key step is a step the caller should not have to think about.\n * The first text run still goes in with `fill` — it clears the field, so\n * replaying a script twice does not append to what is already there — and\n * everything after a key press is typed on top of wherever focus ended up,\n * which is what a person continuing to type would produce.\n */\nasync function typeValue(page: Page, selector: string | undefined, value: string, timeout: number): Promise<void> {\n // Keep the separators: split(/([\\n\\t])/) yields text, key, text, key, …\n const segments = value.split(/([\\n\\t])/).filter((segment) => segment !== \"\");\n // An empty value still means \"empty the field\", which is `fill(\"\")`.\n if (segments.length === 0) segments.push(\"\");\n\n let focused = false;\n for (const segment of segments) {\n const key = TYPE_KEYS[segment];\n if (key === undefined) {\n // The first run replaces the field's contents; later runs are typed at\n // the caret, since a key press may well have moved focus elsewhere.\n if (selector && !focused) await page.locator(selector).first().fill(segment, { timeout });\n else await page.keyboard.type(segment);\n focused = true;\n continue;\n }\n // A value that opens with a key (\"\\n\" alone, meaning \"press Enter in this\n // field\") still has to land on the element the caller named.\n if (selector && !focused) await page.locator(selector).first().focus({ timeout });\n focused = true;\n await page.keyboard.press(key);\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\n .string()\n .optional()\n .describe(\n \"CSS selector for click/tap/type/select/hover targets, the field to focus first for `key`, or the scroll container\",\n ),\n value: z\n .string()\n .optional()\n .describe(\n \"Text to type (`\\n` in it presses Enter, `\\t` presses Tab), key to press for `key` \" +\n \"(e.g. `Enter`, `Escape`, `ArrowDown`, `Control+a`), option value to select, or URL to navigate to\",\n ),\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,134 @@
1
+ import type { BrowserContext, Page } from "playwright";
2
+ /**
3
+ * Link engine.
4
+ *
5
+ * Three jobs, and nothing that judges: collect every address the rendered page
6
+ * refers to, remember what the browser itself made of the ones it loaded, and
7
+ * check the rest over HTTP. What any of that *means* is `utils/link-rules.ts`,
8
+ * which is pure and therefore testable without a socket.
9
+ *
10
+ * Two decisions shape everything here.
11
+ *
12
+ * The page is read after it has rendered, not from its HTML source, for the
13
+ * same reason the SEO engine does: a component framework ships an empty
14
+ * `<div id="app">` and writes every link into it afterwards, so reading the
15
+ * source would report a client-rendered site as having no links at all.
16
+ *
17
+ * And the checks go out through the browser context rather than through
18
+ * `fetch`, so they carry the same cookies, headers and proxy settings the page
19
+ * itself was loaded with. A link behind a login is then checked as the
20
+ * signed-in visitor sees it instead of coming back 401 for everyone.
21
+ */
22
+ /** A link a person clicks, or a resource the page pulls in on its own. */
23
+ export type LinkRole = "link" | "resource";
24
+ /** One address the rendered page refers to, exactly as it was written. */
25
+ export interface RawLink {
26
+ /** The href/src as authored — relative, empty, `javascript:` and all. */
27
+ href: string;
28
+ role: LinkRole;
29
+ tag: string;
30
+ /** Which attribute carried it, so a `srcset` candidate is not mistaken for a `src`. */
31
+ attribute: string;
32
+ /** How the element is named in the report — `a "Pricing"`, `img#hero`. */
33
+ description: string;
34
+ /** A CSS selector for the element, recomputed each visit rather than remembered. */
35
+ selector: string;
36
+ /** Which match of `selector` this is, for the common case where it is not unique. */
37
+ match_index: number;
38
+ rel?: string;
39
+ }
40
+ export interface CollectLinksOptions {
41
+ /** Only look inside this. Defaults to the whole document. */
42
+ selector?: string;
43
+ /** Also collect the things the page loads for itself — images, scripts, stylesheets. */
44
+ include_resources?: boolean;
45
+ }
46
+ /**
47
+ * Read every address out of the rendered document, in document order.
48
+ *
49
+ * Anchors come first because they are what the tool is about; a page's own
50
+ * resources are collected after them so that a `max_links` cap spends itself
51
+ * on links before it spends itself on sprite sheets.
52
+ */
53
+ export declare function collectLinks(page: Page, options?: CollectLinksOptions): Promise<RawLink[]>;
54
+ /**
55
+ * Which of these fragments actually point at something.
56
+ *
57
+ * A `#pricing` that matches no element is a broken link that no amount of HTTP
58
+ * checking will ever find — the request succeeds, the page loads, and the
59
+ * visitor simply does not go where the link said. It is only answerable with
60
+ * the document in hand, which is the one thing a link checker without a
61
+ * browser does not have.
62
+ *
63
+ * Both spellings count: an `id`, and the `<a name>` that predates it and still
64
+ * works in every browser.
65
+ */
66
+ export declare function findFragmentTargets(page: Page, fragments: readonly string[]): Promise<string[]>;
67
+ /** What Chromium made of one address it fetched for itself. */
68
+ export interface ObservedLoad {
69
+ status?: number;
70
+ /** Chromium's own error text, e.g. `net::ERR_NAME_NOT_RESOLVED`. */
71
+ failure?: string;
72
+ /** Every URL in the redirect chain, first to last. */
73
+ chain: string[];
74
+ }
75
+ export interface LoadWatch {
76
+ get(url: string): ObservedLoad | undefined;
77
+ detach(): void;
78
+ }
79
+ /**
80
+ * Record what the browser makes of every request it issues.
81
+ *
82
+ * The page has already fetched its own stylesheets, scripts and images by the
83
+ * time anything is checked, and Chromium's answer is better than a second
84
+ * request would be: it is what actually happened, in the right order, with the
85
+ * right headers, and it costs nothing. Re-requesting them all would double the
86
+ * load on the site under test and could easily disagree with the browser —
87
+ * which is the one verdict that matters.
88
+ *
89
+ * Intermediate redirects are skipped and recorded from the far end instead, so
90
+ * a resource that 301s to a working URL is not reported as a bare 301. An
91
+ * aborted request is recorded as nothing at all: `net::ERR_ABORTED` is what a
92
+ * cancelled preload and a navigation away both look like, and neither is a
93
+ * broken link.
94
+ */
95
+ export declare function watchLoads(page: Page): LoadWatch;
96
+ /** The outcome of one check, as the checker saw it — no judgement attached. */
97
+ export interface CheckOutcome {
98
+ /**
99
+ * Every URL requested, starting with the one that was asked for. One entry
100
+ * means no redirect was followed; a repeat means the chain came back round.
101
+ */
102
+ chain: string[];
103
+ /** The final status, when one arrived. */
104
+ status?: number;
105
+ /** The transport error, when the request never completed. */
106
+ error?: string;
107
+ /** Which verb produced the final answer. */
108
+ method: "HEAD" | "GET";
109
+ /** The chain was still going when `max_redirects` ran out. */
110
+ hops_exceeded?: boolean;
111
+ content_type?: string;
112
+ /** This verdict came from the browser's own load rather than from a fresh request. */
113
+ observed?: boolean;
114
+ }
115
+ export interface CheckOptions {
116
+ timeout_ms: number;
117
+ max_redirects?: number;
118
+ }
119
+ /**
120
+ * Ask a server about one URL, following its redirects by hand.
121
+ *
122
+ * Redirects are followed one hop at a time (`maxRedirects: 0`) rather than
123
+ * letting Playwright chase them, because the chain *is* part of the finding: a
124
+ * link that reaches its destination through four hops works and is still worth
125
+ * fixing, and a link that redirects into a 404 is a different bug from a plain
126
+ * 404.
127
+ *
128
+ * HEAD is an optimisation, never the test. A great many servers — CDNs, WAFs,
129
+ * a fair number of frameworks — answer 405 or 404 to a HEAD and serve the very
130
+ * same URL to a GET, so anything that comes back an error is asked for again
131
+ * properly before it is called broken. That costs a second request only on
132
+ * links that were about to be reported, which is the right place to spend it.
133
+ */
134
+ export declare function checkUrl(context: BrowserContext, url: string, options: CheckOptions): Promise<CheckOutcome>;