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
@@ -0,0 +1,474 @@
1
+ import { MAX_FORM_FIELDS, MAX_FORM_MESSAGE_LENGTH, MAX_FORM_MESSAGES, MAX_FORM_TEXT_LENGTH, SELECTOR_TIMEOUT_MS } from "../constants.js";
2
+ import { XSS_MARKER, checkedFor, optionFor, valueFor, } from "../utils/test-data.js";
3
+ /**
4
+ * Form engine.
5
+ *
6
+ * Finds the forms on a page, fills them according to a strategy, submits them,
7
+ * and reads back what the page said about it. Everything here works on real
8
+ * input — Playwright's `fill`, `check` and `selectOption` dispatch the same
9
+ * events a person's typing does — because the bugs this is looking for live in
10
+ * the handlers those events run. Setting `.value` from inside the page would
11
+ * skip every one of them, and a React-controlled input would not even keep the
12
+ * value.
13
+ *
14
+ * Nothing here decides *what* to type; that is `utils/test-data.ts`. This
15
+ * module is about the page: which fields exist, which of them can be filled,
16
+ * what landed in them, and what came back.
17
+ */
18
+ /** Elements that can hold a value. */
19
+ const FIELD_SELECTOR = "input, select, textarea";
20
+ /** Elements that might submit a form. `[role=button]` catches the div-with-a-handler school of markup. */
21
+ const SUBMIT_SELECTOR = 'button, input[type="submit"], input[type="image"], input[type="button"], [role="button"]';
22
+ /** Words on a button that mean "this one sends the form", when nothing declares it. */
23
+ const SUBMIT_WORDS = /\b(submit|send|save|sign\s?in|sign\s?up|log\s?in|register|create|continue|next|confirm|apply|search|subscribe|order|pay|checkout|update|post|add)\b/i;
24
+ /**
25
+ * Elements that carry a validation message. Class-name matching is deliberately
26
+ * broad — every framework spells it differently — and the noise it lets in is
27
+ * dealt with afterwards: only visible elements with short text count, only the
28
+ * innermost of a nest, and only messages that were not already on the page
29
+ * before the form was touched.
30
+ */
31
+ const MESSAGE_SELECTOR = [
32
+ '[role="alert"]',
33
+ '[role="status"]',
34
+ '[aria-live="polite"]',
35
+ '[aria-live="assertive"]',
36
+ '[class*="error" i]',
37
+ '[class*="invalid" i]',
38
+ '[class*="warning" i]',
39
+ '[class*="danger" i]',
40
+ '[class*="toast" i]',
41
+ '[class*="notice" i]',
42
+ '[id*="error" i]',
43
+ ].join(", ");
44
+ /**
45
+ * Find the forms on a page.
46
+ *
47
+ * With a `selector`, whatever it matches is the form — a `<form>` element or
48
+ * any container, since the fields are looked for underneath it either way.
49
+ * Without one, every `<form>` on the page is returned in document order.
50
+ *
51
+ * A page with no `<form>` element at all is not a page without a form: React,
52
+ * Vue and Svelte apps routinely put inputs and a button in a `<div>` and wire
53
+ * the submit up in JavaScript. So when there is no form, the page itself is
54
+ * treated as one (`is_form: false`), and everything works as it does for a
55
+ * real form except that Enter cannot be used to submit it.
56
+ */
57
+ export async function discoverForms(page, options = {}) {
58
+ const maxFields = options.max_fields ?? MAX_FORM_FIELDS;
59
+ const roots = await findRoots(page, options.selector);
60
+ const forms = [];
61
+ for (const root of roots) {
62
+ const handles = await root.handle.$$(FIELD_SELECTOR);
63
+ const described = await Promise.all(handles.map((handle) => handle.evaluate(describeField)));
64
+ const fields = [];
65
+ const skipped = [];
66
+ let fillable = 0;
67
+ for (let i = 0; i < handles.length; i++) {
68
+ const raw = described[i];
69
+ if (raw.skip !== undefined) {
70
+ skipped.push({ description: raw.description, reason: raw.skip });
71
+ continue;
72
+ }
73
+ fillable++;
74
+ if (fields.length >= maxFields)
75
+ continue;
76
+ fields.push({ handle: handles[i], info: toFieldInfo(raw) });
77
+ }
78
+ forms.push({
79
+ index: forms.length + 1,
80
+ description: root.description,
81
+ is_form: root.is_form,
82
+ total_fields: fillable,
83
+ fields,
84
+ skipped,
85
+ root: root.handle,
86
+ page,
87
+ });
88
+ }
89
+ return forms;
90
+ }
91
+ async function findRoots(page, selector) {
92
+ if (selector !== undefined) {
93
+ const handles = await page.$$(selector);
94
+ if (handles.length === 0) {
95
+ throw new Error(`no element on the page matches \`${selector}\``);
96
+ }
97
+ return Promise.all(handles.map(async (handle) => ({ handle, ...(await handle.evaluate(describeRoot)) })));
98
+ }
99
+ const forms = await page.$$("form");
100
+ if (forms.length > 0) {
101
+ return Promise.all(forms.map(async (handle) => ({ handle, ...(await handle.evaluate(describeRoot)) })));
102
+ }
103
+ const body = await page.$("body");
104
+ if (!body)
105
+ return [];
106
+ return [{ handle: body, description: "the page (it has no <form> element)", is_form: false }];
107
+ }
108
+ /**
109
+ * Fill every field of `form` according to `strategy`.
110
+ *
111
+ * A field that will not take its value is recorded and the rest are still
112
+ * filled: a form where one field is locked is a finding about that field, and
113
+ * abandoning the other twenty would throw away the run that found it.
114
+ */
115
+ export async function fillForm(form, strategy, options = {}) {
116
+ const timeout = options.timeout_ms ?? SELECTOR_TIMEOUT_MS;
117
+ const result = { filled: [], failed: [], skipped: [] };
118
+ /** Radio groups already answered — one choice per group, not one per button. */
119
+ const groups = new Set();
120
+ for (let i = 0; i < form.fields.length; i++) {
121
+ const { info, handle } = form.fields[i];
122
+ try {
123
+ const outcome = await fillField(handle, info, strategy, i, timeout, groups);
124
+ if ("reason" in outcome)
125
+ result.skipped.push({ description: info.description, reason: outcome.reason });
126
+ else
127
+ result.filled.push(outcome);
128
+ }
129
+ catch (error) {
130
+ result.failed.push({ description: info.description, reason: firstLine(error) });
131
+ }
132
+ }
133
+ return result;
134
+ }
135
+ async function fillField(handle, info, strategy, variant, timeout, groups) {
136
+ if (info.kind === "toggle") {
137
+ const want = checkedFor(strategy);
138
+ if (info.type === "radio") {
139
+ // Clicking a radio cannot clear it, only move the choice elsewhere.
140
+ if (!want)
141
+ return { reason: "a radio button cannot be cleared, only replaced" };
142
+ const group = info.name ?? info.description;
143
+ if (groups.has(group))
144
+ return { reason: `another option in the "${group}" group was chosen` };
145
+ groups.add(group);
146
+ await handle.check({ timeout });
147
+ }
148
+ else {
149
+ await handle.setChecked(want, { timeout });
150
+ }
151
+ const state = await readBack(handle);
152
+ return {
153
+ description: info.description,
154
+ kind: info.kind,
155
+ requested: want ? "checked" : "unchecked",
156
+ requested_length: 0,
157
+ landed: state.checked ? "checked" : "unchecked",
158
+ landed_length: 0,
159
+ truncated: false,
160
+ checked: state.checked,
161
+ };
162
+ }
163
+ if (info.kind === "choice") {
164
+ const option = optionFor({ ...info, options: info.options ?? [] }, strategy);
165
+ if (option === null)
166
+ return { reason: "no option to choose — every option is disabled" };
167
+ await handle.selectOption(option, { timeout });
168
+ const state = await readBack(handle);
169
+ return {
170
+ description: info.description,
171
+ kind: info.kind,
172
+ requested: elide(option),
173
+ requested_length: option.length,
174
+ landed: elide(state.value),
175
+ landed_length: state.value.length,
176
+ truncated: false,
177
+ };
178
+ }
179
+ const requested = valueFor(info, strategy, variant);
180
+ await handle.fill(requested, { timeout });
181
+ const state = await readBack(handle);
182
+ return {
183
+ description: info.description,
184
+ kind: info.kind,
185
+ requested: elide(requested),
186
+ requested_length: requested.length,
187
+ landed: elide(state.value),
188
+ landed_length: state.value.length,
189
+ // Only ever shorter: a page that appends to what was typed (a formatter
190
+ // adding separators) is not truncating it.
191
+ truncated: state.value.length < requested.length,
192
+ };
193
+ }
194
+ function readBack(handle) {
195
+ return handle.evaluate((node) => ({
196
+ value: typeof node.value === "string" ? node.value : "",
197
+ checked: node.checked === true,
198
+ }));
199
+ }
200
+ /**
201
+ * Send the form the way a person would.
202
+ *
203
+ * A real submit control comes first, then a button whose text says what it
204
+ * does, then Enter in a text field — which is how a search box with no button
205
+ * is submitted, and the only route left when the markup names nothing. A form
206
+ * that offers none of those is reported rather than forced: calling
207
+ * `form.submit()` from script would skip the page's own submit handler and
208
+ * validation, which is the thing under test.
209
+ */
210
+ export async function submitForm(form, options = {}) {
211
+ const timeout = options.timeout_ms ?? SELECTOR_TIMEOUT_MS;
212
+ const control = await findSubmitControl(form);
213
+ if (control) {
214
+ try {
215
+ await control.handle.click({ timeout });
216
+ return { ok: true, how: `clicked "${control.description}"` };
217
+ }
218
+ catch (error) {
219
+ // A click that navigates can detach the button underneath Playwright;
220
+ // the submit happened either way, which the URL proves.
221
+ if (form.page.url() !== control.url_before) {
222
+ return { ok: true, how: `clicked "${control.description}"` };
223
+ }
224
+ return { ok: false, how: `tried to click "${control.description}"`, reason: firstLine(error) };
225
+ }
226
+ }
227
+ const typed = form.fields.find((field) => field.info.kind === "text");
228
+ if (form.is_form && typed) {
229
+ try {
230
+ await typed.handle.press("Enter", { timeout });
231
+ return { ok: true, how: `pressed Enter in "${typed.info.description}"` };
232
+ }
233
+ catch (error) {
234
+ return { ok: false, how: `tried to press Enter in "${typed.info.description}"`, reason: firstLine(error) };
235
+ }
236
+ }
237
+ return {
238
+ ok: false,
239
+ how: "nothing",
240
+ reason: form.is_form
241
+ ? "no submit control found — no button, and no text field to press Enter in"
242
+ : "no submit control found — no button, and this is not a <form>, so Enter has nothing to submit",
243
+ };
244
+ }
245
+ async function findSubmitControl(form) {
246
+ const handles = await form.root.$$(SUBMIT_SELECTOR);
247
+ if (handles.length === 0)
248
+ return null;
249
+ const described = await Promise.all(handles.map((handle) => handle.evaluate(describeButton)));
250
+ const usable = described
251
+ .map((button, index) => ({ button, index }))
252
+ .filter(({ button }) => button.usable);
253
+ if (usable.length === 0)
254
+ return null;
255
+ const declared = usable.find(({ button }) => button.type === "submit" || button.type === "image");
256
+ const worded = usable.find(({ button }) => SUBMIT_WORDS.test(button.text));
257
+ const chosen = declared ?? worded ?? usable[0];
258
+ return {
259
+ handle: handles[chosen.index],
260
+ description: chosen.button.description,
261
+ url_before: form.page.url(),
262
+ };
263
+ }
264
+ /**
265
+ * What the page says about the state it is in.
266
+ *
267
+ * Two independent sources, because apps use one, the other or both: the
268
+ * browser's constraint validation (`required`, `type=email`, `min`), which is
269
+ * true whether or not the app ever shows it, and the text actually visible on
270
+ * screen, which is what a user would see. Neither is meaningful alone —
271
+ * constraint validation on a form with `novalidate` never reaches the user,
272
+ * and visible text tells you nothing about which field it belongs to.
273
+ */
274
+ export async function readValidation(page, form) {
275
+ const states = await Promise.all(form.fields.map(async (field) => ({
276
+ field: field.info.description,
277
+ ...(await field.handle.evaluate(readFieldValidity).catch(() => ({ valid: true, message: "", invalid: false }))),
278
+ })));
279
+ const messages = await page
280
+ .evaluate(scanMessages, { selector: MESSAGE_SELECTOR, max: MAX_FORM_MESSAGES, max_length: MAX_FORM_MESSAGE_LENGTH })
281
+ .catch(() => []);
282
+ return {
283
+ browser: states
284
+ .filter((state) => !state.valid && state.message !== "")
285
+ .map((state) => ({ field: state.field, message: state.message })),
286
+ messages,
287
+ invalid: states.filter((state) => state.invalid).map((state) => state.field),
288
+ };
289
+ }
290
+ /** Messages that were not on the page before — the ones this strategy caused. */
291
+ export function newMessages(before, after) {
292
+ return after.messages.filter((message) => !before.messages.includes(message));
293
+ }
294
+ /**
295
+ * Whether an XSS payload has executed on this page (see `XSS_MARKER`).
296
+ *
297
+ * Reading a marker the payload itself sets is the only honest answer to "did
298
+ * my input run as code". Looking for the payload in the DOM afterwards is not:
299
+ * an input's `value` serialises with its angle brackets intact, so a page that
300
+ * escaped everything perfectly would still look like a hit.
301
+ */
302
+ export async function readXssMarker(page) {
303
+ try {
304
+ return await page.evaluate((name) => Boolean(globalThis[name]), XSS_MARKER);
305
+ }
306
+ catch {
307
+ return false;
308
+ }
309
+ }
310
+ function describeField(node) {
311
+ const tag = String(node.tagName ?? "").toLowerCase();
312
+ const type = tag === "textarea" ? "textarea" : String(node.type ?? "text").toLowerCase();
313
+ const attr = (name) => {
314
+ const value = node.getAttribute ? node.getAttribute(name) : null;
315
+ return value === null || value === undefined ? undefined : String(value);
316
+ };
317
+ const flatten = (text) => String(text ?? "").replace(/\s+/g, " ").trim();
318
+ const id = node.id ? String(node.id) : "";
319
+ const name = node.name ? String(node.name) : "";
320
+ const description = id ? `#${id}` : name ? `[name="${name}"]` : tag === "input" ? `input[type=${type}]` : tag;
321
+ let label = "";
322
+ try {
323
+ const labels = node.labels;
324
+ if (labels && labels.length > 0)
325
+ label = flatten(labels[0].textContent);
326
+ if (!label)
327
+ label = flatten(attr("aria-label"));
328
+ if (!label) {
329
+ const by = attr("aria-labelledby");
330
+ const target = by ? globalThis.document?.getElementById(by.split(/\s+/)[0]) : null;
331
+ if (target)
332
+ label = flatten(target.textContent);
333
+ }
334
+ if (!label && node.closest)
335
+ label = flatten(node.closest("label")?.textContent);
336
+ }
337
+ catch {
338
+ label = "";
339
+ }
340
+ const visible = (() => {
341
+ try {
342
+ return node.getClientRects ? node.getClientRects().length > 0 : true;
343
+ }
344
+ catch {
345
+ return true;
346
+ }
347
+ })();
348
+ const skip = (() => {
349
+ if (type === "hidden")
350
+ return "hidden input";
351
+ if (node.disabled === true)
352
+ return "disabled";
353
+ if (node.readOnly === true)
354
+ return "read-only";
355
+ if (type === "file")
356
+ return "file input — a file cannot be chosen from a script";
357
+ if (type === "submit" || type === "button" || type === "reset" || type === "image")
358
+ return "not an input field";
359
+ if (!visible)
360
+ return "not visible";
361
+ return undefined;
362
+ })();
363
+ const maxLength = typeof node.maxLength === "number" && node.maxLength > 0 ? node.maxLength : undefined;
364
+ const options = tag === "select" && node.options
365
+ ? Array.from(node.options).map((option) => ({
366
+ value: String(option.value ?? ""),
367
+ label: flatten(option.textContent),
368
+ disabled: option.disabled === true,
369
+ }))
370
+ : undefined;
371
+ return {
372
+ tag,
373
+ type,
374
+ description,
375
+ ...(name ? { name } : {}),
376
+ ...(label ? { label: label.slice(0, 80) } : {}),
377
+ ...(attr("placeholder") ? { placeholder: attr("placeholder") } : {}),
378
+ ...(maxLength !== undefined ? { maxlength: maxLength } : {}),
379
+ ...(attr("min") !== undefined ? { min: attr("min") } : {}),
380
+ ...(attr("max") !== undefined ? { max: attr("max") } : {}),
381
+ ...(attr("step") !== undefined ? { step: attr("step") } : {}),
382
+ required: node.required === true,
383
+ ...(options ? { options } : {}),
384
+ ...(skip !== undefined ? { skip } : {}),
385
+ };
386
+ }
387
+ function describeRoot(node) {
388
+ const tag = String(node.tagName ?? "").toLowerCase();
389
+ const id = node.id ? `#${String(node.id)}` : "";
390
+ const name = node.name ? `[name="${String(node.name)}"]` : "";
391
+ const action = node.getAttribute && node.getAttribute("action") ? `[action="${node.getAttribute("action")}"]` : "";
392
+ return { description: id || name || `${tag}${action}`, is_form: tag === "form" };
393
+ }
394
+ function describeButton(node) {
395
+ const tag = String(node.tagName ?? "").toLowerCase();
396
+ const type = String(node.type ?? "").toLowerCase();
397
+ const flatten = (text) => String(text ?? "").replace(/\s+/g, " ").trim();
398
+ const id = node.id ? String(node.id) : "";
399
+ const name = node.name ? String(node.name) : "";
400
+ const text = flatten(node.textContent) || flatten(node.value) || flatten(node.getAttribute?.("aria-label"));
401
+ let visible = true;
402
+ try {
403
+ visible = node.getClientRects ? node.getClientRects().length > 0 : true;
404
+ }
405
+ catch {
406
+ visible = true;
407
+ }
408
+ return {
409
+ usable: visible && node.disabled !== true,
410
+ type,
411
+ text,
412
+ description: id ? `#${id}` : name ? `[name="${name}"]` : text ? `${tag} "${text.slice(0, 40)}"` : tag,
413
+ };
414
+ }
415
+ function readFieldValidity(node) {
416
+ // `validity`, never `checkValidity()`: the call fires an `invalid` event the
417
+ // page can see and act on, and reading the state must not change it.
418
+ const validity = node.validity;
419
+ return {
420
+ valid: validity ? validity.valid === true : true,
421
+ message: String(node.validationMessage ?? ""),
422
+ invalid: node.getAttribute ? node.getAttribute("aria-invalid") === "true" : false,
423
+ };
424
+ }
425
+ function scanMessages(options) {
426
+ const doc = globalThis.document;
427
+ if (!doc)
428
+ return [];
429
+ let nodes = [];
430
+ try {
431
+ nodes = Array.from(doc.querySelectorAll(options.selector));
432
+ }
433
+ catch {
434
+ return [];
435
+ }
436
+ const visible = nodes.filter((node) => {
437
+ try {
438
+ return node.getClientRects && node.getClientRects().length > 0;
439
+ }
440
+ catch {
441
+ return false;
442
+ }
443
+ });
444
+ // Keep the innermost of a nest: a wrapper element matching on its class name
445
+ // would otherwise contribute the whole panel's text as one "message".
446
+ const innermost = visible.filter((node) => !visible.some((other) => other !== node && node.contains(other)));
447
+ const messages = [];
448
+ for (const node of innermost) {
449
+ const text = String(node.textContent ?? "")
450
+ .replace(/\s+/g, " ")
451
+ .trim();
452
+ if (text === "" || text.length > options.max_length)
453
+ continue;
454
+ if (!messages.includes(text))
455
+ messages.push(text);
456
+ if (messages.length >= options.max)
457
+ break;
458
+ }
459
+ return messages;
460
+ }
461
+ /* ── Helpers ──────────────────────────────────────────────────────────── */
462
+ function toFieldInfo(raw) {
463
+ const kind = raw.tag === "select" ? "choice" : raw.type === "checkbox" || raw.type === "radio" ? "toggle" : "text";
464
+ const { skip: _skip, ...rest } = raw;
465
+ return { ...rest, kind };
466
+ }
467
+ function elide(value, max = MAX_FORM_TEXT_LENGTH) {
468
+ const flat = value.replace(/\s+/g, " ");
469
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
470
+ }
471
+ function firstLine(error) {
472
+ return (error instanceof Error ? error.message : String(error)).split("\n")[0];
473
+ }
474
+ //# sourceMappingURL=forms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms.js","sourceRoot":"","sources":["../../src/engine/forms.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ElementHandle, Page } from \"playwright\";\nimport { MAX_FORM_FIELDS, MAX_FORM_MESSAGE_LENGTH, MAX_FORM_MESSAGES, MAX_FORM_TEXT_LENGTH, SELECTOR_TIMEOUT_MS } from \"../constants.js\";\nimport {\n XSS_MARKER,\n checkedFor,\n optionFor,\n valueFor,\n type FieldShape,\n type FormStrategy,\n type OptionInfo,\n} from \"../utils/test-data.js\";\n\n/**\n * Form engine.\n *\n * Finds the forms on a page, fills them according to a strategy, submits them,\n * and reads back what the page said about it. Everything here works on real\n * input — Playwright's `fill`, `check` and `selectOption` dispatch the same\n * events a person's typing does — because the bugs this is looking for live in\n * the handlers those events run. Setting `.value` from inside the page would\n * skip every one of them, and a React-controlled input would not even keep the\n * value.\n *\n * Nothing here decides *what* to type; that is `utils/test-data.ts`. This\n * module is about the page: which fields exist, which of them can be filled,\n * what landed in them, and what came back.\n */\n\n/** Elements that can hold a value. */\nconst FIELD_SELECTOR = \"input, select, textarea\";\n\n/** Elements that might submit a form. `[role=button]` catches the div-with-a-handler school of markup. */\nconst SUBMIT_SELECTOR = 'button, input[type=\"submit\"], input[type=\"image\"], input[type=\"button\"], [role=\"button\"]';\n\n/** Words on a button that mean \"this one sends the form\", when nothing declares it. */\nconst SUBMIT_WORDS =\n /\\b(submit|send|save|sign\\s?in|sign\\s?up|log\\s?in|register|create|continue|next|confirm|apply|search|subscribe|order|pay|checkout|update|post|add)\\b/i;\n\n/**\n * Elements that carry a validation message. Class-name matching is deliberately\n * broad — every framework spells it differently — and the noise it lets in is\n * dealt with afterwards: only visible elements with short text count, only the\n * innermost of a nest, and only messages that were not already on the page\n * before the form was touched.\n */\nconst MESSAGE_SELECTOR = [\n '[role=\"alert\"]',\n '[role=\"status\"]',\n '[aria-live=\"polite\"]',\n '[aria-live=\"assertive\"]',\n '[class*=\"error\" i]',\n '[class*=\"invalid\" i]',\n '[class*=\"warning\" i]',\n '[class*=\"danger\" i]',\n '[class*=\"toast\" i]',\n '[class*=\"notice\" i]',\n '[id*=\"error\" i]',\n].join(\", \");\n\n/** How a field is filled. */\nexport type FieldKind = \"text\" | \"choice\" | \"toggle\";\n\nexport interface FieldInfo extends FieldShape {\n tag: string;\n kind: FieldKind;\n required: boolean;\n options?: OptionInfo[];\n /** How the field is named in the report — `#email`, `[name=\"plan\"]`, `input[type=text]`. */\n description: string;\n}\n\nexport interface DiscoveredField {\n info: FieldInfo;\n handle: ElementHandle;\n}\n\n/** A field that was not filled, and why. */\nexport interface FieldProblem {\n description: string;\n reason: string;\n}\n\nexport interface DiscoveredForm {\n /** 1-based position among the forms found on the page. */\n index: number;\n description: string;\n /** False when this is not a real `<form>` — see `discoverForms`. */\n is_form: boolean;\n /** Fillable fields found, before `max_fields` trimmed the list. */\n total_fields: number;\n fields: DiscoveredField[];\n /** Fields that exist but cannot be filled (hidden, disabled, read-only, file). */\n skipped: FieldProblem[];\n root: ElementHandle;\n page: Page;\n}\n\nexport interface DiscoverOptions {\n /** CSS selector for one form, or for a container to treat as one. Defaults to every `<form>` on the page. */\n selector?: string;\n max_fields?: number;\n}\n\n/**\n * Find the forms on a page.\n *\n * With a `selector`, whatever it matches is the form — a `<form>` element or\n * any container, since the fields are looked for underneath it either way.\n * Without one, every `<form>` on the page is returned in document order.\n *\n * A page with no `<form>` element at all is not a page without a form: React,\n * Vue and Svelte apps routinely put inputs and a button in a `<div>` and wire\n * the submit up in JavaScript. So when there is no form, the page itself is\n * treated as one (`is_form: false`), and everything works as it does for a\n * real form except that Enter cannot be used to submit it.\n */\nexport async function discoverForms(page: Page, options: DiscoverOptions = {}): Promise<DiscoveredForm[]> {\n const maxFields = options.max_fields ?? MAX_FORM_FIELDS;\n const roots = await findRoots(page, options.selector);\n\n const forms: DiscoveredForm[] = [];\n for (const root of roots) {\n const handles = await root.handle.$$(FIELD_SELECTOR);\n const described = await Promise.all(handles.map((handle) => handle.evaluate(describeField)));\n\n const fields: DiscoveredField[] = [];\n const skipped: FieldProblem[] = [];\n let fillable = 0;\n\n for (let i = 0; i < handles.length; i++) {\n const raw = described[i];\n if (raw.skip !== undefined) {\n skipped.push({ description: raw.description, reason: raw.skip });\n continue;\n }\n fillable++;\n if (fields.length >= maxFields) continue;\n fields.push({ handle: handles[i], info: toFieldInfo(raw) });\n }\n\n forms.push({\n index: forms.length + 1,\n description: root.description,\n is_form: root.is_form,\n total_fields: fillable,\n fields,\n skipped,\n root: root.handle,\n page,\n });\n }\n\n return forms;\n}\n\ninterface Root {\n handle: ElementHandle;\n description: string;\n is_form: boolean;\n}\n\nasync function findRoots(page: Page, selector?: string): Promise<Root[]> {\n if (selector !== undefined) {\n const handles = await page.$$(selector);\n if (handles.length === 0) {\n throw new Error(`no element on the page matches \\`${selector}\\``);\n }\n return Promise.all(handles.map(async (handle) => ({ handle, ...(await handle.evaluate(describeRoot)) })));\n }\n\n const forms = await page.$$(\"form\");\n if (forms.length > 0) {\n return Promise.all(forms.map(async (handle) => ({ handle, ...(await handle.evaluate(describeRoot)) })));\n }\n\n const body = await page.$(\"body\");\n if (!body) return [];\n return [{ handle: body, description: \"the page (it has no <form> element)\", is_form: false }];\n}\n\n/**\n * Fill every field of `form` according to `strategy`.\n *\n * A field that will not take its value is recorded and the rest are still\n * filled: a form where one field is locked is a finding about that field, and\n * abandoning the other twenty would throw away the run that found it.\n */\nexport async function fillForm(\n form: DiscoveredForm,\n strategy: FormStrategy,\n options: { timeout_ms?: number } = {},\n): Promise<FillResult> {\n const timeout = options.timeout_ms ?? SELECTOR_TIMEOUT_MS;\n const result: FillResult = { filled: [], failed: [], skipped: [] };\n /** Radio groups already answered — one choice per group, not one per button. */\n const groups = new Set<string>();\n\n for (let i = 0; i < form.fields.length; i++) {\n const { info, handle } = form.fields[i];\n try {\n const outcome = await fillField(handle, info, strategy, i, timeout, groups);\n if (\"reason\" in outcome) result.skipped.push({ description: info.description, reason: outcome.reason });\n else result.filled.push(outcome);\n } catch (error) {\n result.failed.push({ description: info.description, reason: firstLine(error) });\n }\n }\n\n return result;\n}\n\nexport interface FilledField {\n description: string;\n kind: FieldKind;\n /** What the strategy asked for, elided for the report. */\n requested: string;\n requested_length: number;\n /** What the field actually holds now, elided for the report. */\n landed: string;\n landed_length: number;\n /** True when the page kept less than it was given — a limit nothing declared. */\n truncated: boolean;\n /** For checkboxes and radios. */\n checked?: boolean;\n}\n\nexport interface FillResult {\n filled: FilledField[];\n failed: FieldProblem[];\n /** Fields this strategy deliberately left alone, with the reason. */\n skipped: FieldProblem[];\n}\n\nasync function fillField(\n handle: ElementHandle,\n info: FieldInfo,\n strategy: FormStrategy,\n variant: number,\n timeout: number,\n groups: Set<string>,\n): Promise<FilledField | { reason: string }> {\n if (info.kind === \"toggle\") {\n const want = checkedFor(strategy);\n if (info.type === \"radio\") {\n // Clicking a radio cannot clear it, only move the choice elsewhere.\n if (!want) return { reason: \"a radio button cannot be cleared, only replaced\" };\n const group = info.name ?? info.description;\n if (groups.has(group)) return { reason: `another option in the \"${group}\" group was chosen` };\n groups.add(group);\n await handle.check({ timeout });\n } else {\n await handle.setChecked(want, { timeout });\n }\n const state = await readBack(handle);\n return {\n description: info.description,\n kind: info.kind,\n requested: want ? \"checked\" : \"unchecked\",\n requested_length: 0,\n landed: state.checked ? \"checked\" : \"unchecked\",\n landed_length: 0,\n truncated: false,\n checked: state.checked,\n };\n }\n\n if (info.kind === \"choice\") {\n const option = optionFor({ ...info, options: info.options ?? [] }, strategy);\n if (option === null) return { reason: \"no option to choose — every option is disabled\" };\n await handle.selectOption(option, { timeout });\n const state = await readBack(handle);\n return {\n description: info.description,\n kind: info.kind,\n requested: elide(option),\n requested_length: option.length,\n landed: elide(state.value),\n landed_length: state.value.length,\n truncated: false,\n };\n }\n\n const requested = valueFor(info, strategy, variant);\n await handle.fill(requested, { timeout });\n const state = await readBack(handle);\n return {\n description: info.description,\n kind: info.kind,\n requested: elide(requested),\n requested_length: requested.length,\n landed: elide(state.value),\n landed_length: state.value.length,\n // Only ever shorter: a page that appends to what was typed (a formatter\n // adding separators) is not truncating it.\n truncated: state.value.length < requested.length,\n };\n}\n\nfunction readBack(handle: ElementHandle): Promise<{ value: string; checked: boolean }> {\n return handle.evaluate((node: any) => ({\n value: typeof node.value === \"string\" ? node.value : \"\",\n checked: node.checked === true,\n }));\n}\n\nexport interface SubmitResult {\n ok: boolean;\n /** What was done — `clicked \"#submit\"`, `pressed Enter in \"#q\"`. */\n how: string;\n /** Why nothing was done, when `ok` is false. */\n reason?: string;\n}\n\n/**\n * Send the form the way a person would.\n *\n * A real submit control comes first, then a button whose text says what it\n * does, then Enter in a text field — which is how a search box with no button\n * is submitted, and the only route left when the markup names nothing. A form\n * that offers none of those is reported rather than forced: calling\n * `form.submit()` from script would skip the page's own submit handler and\n * validation, which is the thing under test.\n */\nexport async function submitForm(form: DiscoveredForm, options: { timeout_ms?: number } = {}): Promise<SubmitResult> {\n const timeout = options.timeout_ms ?? SELECTOR_TIMEOUT_MS;\n const control = await findSubmitControl(form);\n\n if (control) {\n try {\n await control.handle.click({ timeout });\n return { ok: true, how: `clicked \"${control.description}\"` };\n } catch (error) {\n // A click that navigates can detach the button underneath Playwright;\n // the submit happened either way, which the URL proves.\n if (form.page.url() !== control.url_before) {\n return { ok: true, how: `clicked \"${control.description}\"` };\n }\n return { ok: false, how: `tried to click \"${control.description}\"`, reason: firstLine(error) };\n }\n }\n\n const typed = form.fields.find((field) => field.info.kind === \"text\");\n if (form.is_form && typed) {\n try {\n await typed.handle.press(\"Enter\", { timeout });\n return { ok: true, how: `pressed Enter in \"${typed.info.description}\"` };\n } catch (error) {\n return { ok: false, how: `tried to press Enter in \"${typed.info.description}\"`, reason: firstLine(error) };\n }\n }\n\n return {\n ok: false,\n how: \"nothing\",\n reason: form.is_form\n ? \"no submit control found — no button, and no text field to press Enter in\"\n : \"no submit control found — no button, and this is not a <form>, so Enter has nothing to submit\",\n };\n}\n\ninterface SubmitControl {\n handle: ElementHandle;\n description: string;\n url_before: string;\n}\n\nasync function findSubmitControl(form: DiscoveredForm): Promise<SubmitControl | null> {\n const handles = await form.root.$$(SUBMIT_SELECTOR);\n if (handles.length === 0) return null;\n\n const described = await Promise.all(handles.map((handle) => handle.evaluate(describeButton)));\n const usable = described\n .map((button, index) => ({ button, index }))\n .filter(({ button }) => button.usable);\n if (usable.length === 0) return null;\n\n const declared = usable.find(({ button }) => button.type === \"submit\" || button.type === \"image\");\n const worded = usable.find(({ button }) => SUBMIT_WORDS.test(button.text));\n const chosen = declared ?? worded ?? usable[0];\n\n return {\n handle: handles[chosen.index],\n description: chosen.button.description,\n url_before: form.page.url(),\n };\n}\n\nexport interface ValidationSnapshot {\n /** The browser's own constraint validation, per field that fails it. */\n browser: Array<{ field: string; message: string }>;\n /** Messages visible on the page — the app's own validation, plus any alert or toast. */\n messages: string[];\n /** Fields the page marked `aria-invalid`. */\n invalid: string[];\n}\n\n/**\n * What the page says about the state it is in.\n *\n * Two independent sources, because apps use one, the other or both: the\n * browser's constraint validation (`required`, `type=email`, `min`), which is\n * true whether or not the app ever shows it, and the text actually visible on\n * screen, which is what a user would see. Neither is meaningful alone —\n * constraint validation on a form with `novalidate` never reaches the user,\n * and visible text tells you nothing about which field it belongs to.\n */\nexport async function readValidation(page: Page, form: DiscoveredForm): Promise<ValidationSnapshot> {\n const states = await Promise.all(\n form.fields.map(async (field) => ({\n field: field.info.description,\n ...(await field.handle.evaluate(readFieldValidity).catch(() => ({ valid: true, message: \"\", invalid: false }))),\n })),\n );\n\n const messages = await page\n .evaluate(scanMessages, { selector: MESSAGE_SELECTOR, max: MAX_FORM_MESSAGES, max_length: MAX_FORM_MESSAGE_LENGTH })\n .catch(() => [] as string[]);\n\n return {\n browser: states\n .filter((state) => !state.valid && state.message !== \"\")\n .map((state) => ({ field: state.field, message: state.message })),\n messages,\n invalid: states.filter((state) => state.invalid).map((state) => state.field),\n };\n}\n\n/** Messages that were not on the page before — the ones this strategy caused. */\nexport function newMessages(before: ValidationSnapshot, after: ValidationSnapshot): string[] {\n return after.messages.filter((message) => !before.messages.includes(message));\n}\n\n/**\n * Whether an XSS payload has executed on this page (see `XSS_MARKER`).\n *\n * Reading a marker the payload itself sets is the only honest answer to \"did\n * my input run as code\". Looking for the payload in the DOM afterwards is not:\n * an input's `value` serialises with its angle brackets intact, so a page that\n * escaped everything perfectly would still look like a hit.\n */\nexport async function readXssMarker(page: Page): Promise<boolean> {\n try {\n return await page.evaluate((name: string) => Boolean((globalThis as any)[name]), XSS_MARKER);\n } catch {\n return false;\n }\n}\n\n/* ── In-page readers ──────────────────────────────────────────────────────\n * Everything below runs inside Chromium, so it is written against\n * `globalThis` and untyped nodes: this package is compiled with the Node lib\n * only, and the page it lands in may be mid-teardown, using a framework that\n * has patched half of these properties, or both.\n */\n\ninterface RawField {\n tag: string;\n type: string;\n description: string;\n name?: string;\n label?: string;\n placeholder?: string;\n maxlength?: number;\n min?: string;\n max?: string;\n step?: string;\n required: boolean;\n options?: OptionInfo[];\n /** Why this field cannot be filled. Absent when it can. */\n skip?: string;\n}\n\nfunction describeField(node: any): RawField {\n const tag = String(node.tagName ?? \"\").toLowerCase();\n const type = tag === \"textarea\" ? \"textarea\" : String(node.type ?? \"text\").toLowerCase();\n const attr = (name: string): string | undefined => {\n const value = node.getAttribute ? node.getAttribute(name) : null;\n return value === null || value === undefined ? undefined : String(value);\n };\n\n const flatten = (text: unknown): string => String(text ?? \"\").replace(/\\s+/g, \" \").trim();\n const id = node.id ? String(node.id) : \"\";\n const name = node.name ? String(node.name) : \"\";\n const description = id ? `#${id}` : name ? `[name=\"${name}\"]` : tag === \"input\" ? `input[type=${type}]` : tag;\n\n let label = \"\";\n try {\n const labels = node.labels;\n if (labels && labels.length > 0) label = flatten(labels[0].textContent);\n if (!label) label = flatten(attr(\"aria-label\"));\n if (!label) {\n const by = attr(\"aria-labelledby\");\n const target = by ? (globalThis as any).document?.getElementById(by.split(/\\s+/)[0]) : null;\n if (target) label = flatten(target.textContent);\n }\n if (!label && node.closest) label = flatten(node.closest(\"label\")?.textContent);\n } catch {\n label = \"\";\n }\n\n const visible = (() => {\n try {\n return node.getClientRects ? node.getClientRects().length > 0 : true;\n } catch {\n return true;\n }\n })();\n\n const skip = ((): string | undefined => {\n if (type === \"hidden\") return \"hidden input\";\n if (node.disabled === true) return \"disabled\";\n if (node.readOnly === true) return \"read-only\";\n if (type === \"file\") return \"file input — a file cannot be chosen from a script\";\n if (type === \"submit\" || type === \"button\" || type === \"reset\" || type === \"image\") return \"not an input field\";\n if (!visible) return \"not visible\";\n return undefined;\n })();\n\n const maxLength = typeof node.maxLength === \"number\" && node.maxLength > 0 ? node.maxLength : undefined;\n const options =\n tag === \"select\" && node.options\n ? Array.from(node.options as any[]).map((option: any) => ({\n value: String(option.value ?? \"\"),\n label: flatten(option.textContent),\n disabled: option.disabled === true,\n }))\n : undefined;\n\n return {\n tag,\n type,\n description,\n ...(name ? { name } : {}),\n ...(label ? { label: label.slice(0, 80) } : {}),\n ...(attr(\"placeholder\") ? { placeholder: attr(\"placeholder\") } : {}),\n ...(maxLength !== undefined ? { maxlength: maxLength } : {}),\n ...(attr(\"min\") !== undefined ? { min: attr(\"min\") } : {}),\n ...(attr(\"max\") !== undefined ? { max: attr(\"max\") } : {}),\n ...(attr(\"step\") !== undefined ? { step: attr(\"step\") } : {}),\n required: node.required === true,\n ...(options ? { options } : {}),\n ...(skip !== undefined ? { skip } : {}),\n };\n}\n\nfunction describeRoot(node: any): { description: string; is_form: boolean } {\n const tag = String(node.tagName ?? \"\").toLowerCase();\n const id = node.id ? `#${String(node.id)}` : \"\";\n const name = node.name ? `[name=\"${String(node.name)}\"]` : \"\";\n const action = node.getAttribute && node.getAttribute(\"action\") ? `[action=\"${node.getAttribute(\"action\")}\"]` : \"\";\n return { description: id || name || `${tag}${action}`, is_form: tag === \"form\" };\n}\n\nfunction describeButton(node: any): { usable: boolean; type: string; text: string; description: string } {\n const tag = String(node.tagName ?? \"\").toLowerCase();\n const type = String(node.type ?? \"\").toLowerCase();\n const flatten = (text: unknown): string => String(text ?? \"\").replace(/\\s+/g, \" \").trim();\n const id = node.id ? String(node.id) : \"\";\n const name = node.name ? String(node.name) : \"\";\n const text = flatten(node.textContent) || flatten(node.value) || flatten(node.getAttribute?.(\"aria-label\"));\n\n let visible = true;\n try {\n visible = node.getClientRects ? node.getClientRects().length > 0 : true;\n } catch {\n visible = true;\n }\n\n return {\n usable: visible && node.disabled !== true,\n type,\n text,\n description: id ? `#${id}` : name ? `[name=\"${name}\"]` : text ? `${tag} \"${text.slice(0, 40)}\"` : tag,\n };\n}\n\nfunction readFieldValidity(node: any): { valid: boolean; message: string; invalid: boolean } {\n // `validity`, never `checkValidity()`: the call fires an `invalid` event the\n // page can see and act on, and reading the state must not change it.\n const validity = node.validity;\n return {\n valid: validity ? validity.valid === true : true,\n message: String(node.validationMessage ?? \"\"),\n invalid: node.getAttribute ? node.getAttribute(\"aria-invalid\") === \"true\" : false,\n };\n}\n\nfunction scanMessages(options: { selector: string; max: number; max_length: number }): string[] {\n const doc = (globalThis as any).document;\n if (!doc) return [];\n\n let nodes: any[] = [];\n try {\n nodes = Array.from(doc.querySelectorAll(options.selector) as any[]);\n } catch {\n return [];\n }\n\n const visible = nodes.filter((node) => {\n try {\n return node.getClientRects && node.getClientRects().length > 0;\n } catch {\n return false;\n }\n });\n // Keep the innermost of a nest: a wrapper element matching on its class name\n // would otherwise contribute the whole panel's text as one \"message\".\n const innermost = visible.filter((node) => !visible.some((other) => other !== node && node.contains(other)));\n\n const messages: string[] = [];\n for (const node of innermost) {\n const text = String(node.textContent ?? \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n if (text === \"\" || text.length > options.max_length) continue;\n if (!messages.includes(text)) messages.push(text);\n if (messages.length >= options.max) break;\n }\n return messages;\n}\n\n/* ── Helpers ──────────────────────────────────────────────────────────── */\n\nfunction toFieldInfo(raw: RawField): FieldInfo {\n const kind: FieldKind =\n raw.tag === \"select\" ? \"choice\" : raw.type === \"checkbox\" || raw.type === \"radio\" ? \"toggle\" : \"text\";\n const { skip: _skip, ...rest } = raw;\n return { ...rest, kind };\n}\n\nfunction elide(value: string, max: number = MAX_FORM_TEXT_LENGTH): string {\n const flat = value.replace(/\\s+/g, \" \");\n return flat.length > max ? `${flat.slice(0, max)}…` : flat;\n}\n\nfunction firstLine(error: unknown): string {\n return (error instanceof Error ? error.message : String(error)).split(\"\\n\")[0];\n}\n"]}
@@ -0,0 +1,41 @@
1
+ import type { Page } from "playwright";
2
+ /**
3
+ * What Vite did to the session page while nobody was looking.
4
+ *
5
+ * Vite's client announces every hot update in the console, so a watcher on
6
+ * the page can tell the difference between "the page has not changed since
7
+ * you saved" and "it changed 300ms ago" without reloading anything. The
8
+ * watcher belongs to the session page and lives as long as it does: it is
9
+ * attached when the page is created (see `getSessionPage`), and every tool
10
+ * that touches the page stamps `markSeen` on its way out. "Since the last
11
+ * look" is then exactly the window `framewatch_wait_for` cares about — an
12
+ * update that landed between the agent's save and its next call still
13
+ * counts, and one from an earlier edit does not.
14
+ */
15
+ export interface HmrEvent {
16
+ /** Epoch ms. */
17
+ t: number;
18
+ kind: "update" | "css" | "reload";
19
+ path: string;
20
+ }
21
+ export declare class HmrWatcher {
22
+ #private;
23
+ constructor(page: Page);
24
+ /** Vite's client said hello, or has sent an update: this is a dev-server page. */
25
+ get connected(): boolean;
26
+ /** When a tool last looked at this page. */
27
+ get lastSeen(): number;
28
+ markSeen(): void;
29
+ /** The most recent event, whenever it happened. */
30
+ get latest(): HmrEvent | undefined;
31
+ /**
32
+ * The first event newer than `since`, waiting up to `timeoutMs` for one to
33
+ * arrive. Resolves immediately when one already has.
34
+ */
35
+ waitForEvent(since: number, timeoutMs: number): Promise<HmrEvent | null>;
36
+ detach(): void;
37
+ }
38
+ /** The watcher for `page`, created (and attached) on first use. */
39
+ export declare function hmrFor(page: Page): HmrWatcher;
40
+ /** Whether the page loaded Vite's client script — a dev-server page even before it logged anything. */
41
+ export declare function hasViteClient(page: Page): Promise<boolean>;