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.
- package/README.md +895 -15
- package/dist/constants.d.ts +274 -0
- package/dist/constants.js +279 -0
- package/dist/constants.js.map +1 -1
- package/dist/engine/browser.d.ts +20 -4
- package/dist/engine/browser.js +26 -9
- package/dist/engine/browser.js.map +1 -1
- package/dist/engine/clicks.d.ts +221 -0
- package/dist/engine/clicks.js +801 -0
- package/dist/engine/clicks.js.map +1 -0
- package/dist/engine/forms.d.ts +137 -0
- package/dist/engine/forms.js +474 -0
- package/dist/engine/forms.js.map +1 -0
- package/dist/engine/hmr.d.ts +41 -0
- package/dist/engine/hmr.js +91 -0
- package/dist/engine/hmr.js.map +1 -0
- package/dist/engine/inspect.d.ts +31 -0
- package/dist/engine/inspect.js +383 -0
- package/dist/engine/inspect.js.map +1 -0
- package/dist/engine/interaction.d.ts +12 -7
- package/dist/engine/interaction.js +110 -18
- package/dist/engine/interaction.js.map +1 -1
- package/dist/engine/links.d.ts +134 -0
- package/dist/engine/links.js +384 -0
- package/dist/engine/links.js.map +1 -0
- package/dist/engine/mocks.d.ts +53 -0
- package/dist/engine/mocks.js +148 -0
- package/dist/engine/mocks.js.map +1 -0
- package/dist/engine/rtl.d.ts +129 -0
- package/dist/engine/rtl.js +540 -0
- package/dist/engine/rtl.js.map +1 -0
- package/dist/engine/seo.d.ts +189 -0
- package/dist/engine/seo.js +398 -0
- package/dist/engine/seo.js.map +1 -0
- package/dist/engine/snapshot.d.ts +29 -0
- package/dist/engine/snapshot.js +10 -0
- package/dist/engine/snapshot.js.map +1 -0
- package/dist/engine/vue.d.ts +54 -0
- package/dist/engine/vue.js +419 -0
- package/dist/engine/vue.js.map +1 -0
- package/dist/index.js +45 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/accessibility.d.ts +4 -0
- package/dist/tools/accessibility.js +9 -2
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/api-mock.d.ts +405 -0
- package/dist/tools/api-mock.js +186 -0
- package/dist/tools/api-mock.js.map +1 -0
- package/dist/tools/capture.d.ts +90 -26
- package/dist/tools/capture.js +109 -58
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/compare.d.ts +4 -0
- package/dist/tools/compare.js +16 -5
- package/dist/tools/compare.js.map +1 -1
- package/dist/tools/dead-clicks.d.ts +128 -0
- package/dist/tools/dead-clicks.js +570 -0
- package/dist/tools/dead-clicks.js.map +1 -0
- package/dist/tools/form-test.d.ts +112 -0
- package/dist/tools/form-test.js +477 -0
- package/dist/tools/form-test.js.map +1 -0
- package/dist/tools/index.d.ts +17 -1
- package/dist/tools/index.js +45 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inspect.d.ts +78 -0
- package/dist/tools/inspect.js +136 -0
- package/dist/tools/inspect.js.map +1 -0
- package/dist/tools/interact.d.ts +37 -18
- package/dist/tools/interact.js +113 -13
- package/dist/tools/interact.js.map +1 -1
- package/dist/tools/links.d.ts +129 -0
- package/dist/tools/links.js +640 -0
- package/dist/tools/links.js.map +1 -0
- package/dist/tools/responsive.d.ts +10 -6
- package/dist/tools/responsive.js +21 -4
- package/dist/tools/responsive.js.map +1 -1
- package/dist/tools/rtl.d.ts +241 -0
- package/dist/tools/rtl.js +410 -0
- package/dist/tools/rtl.js.map +1 -0
- package/dist/tools/save-auth.d.ts +263 -0
- package/dist/tools/save-auth.js +253 -0
- package/dist/tools/save-auth.js.map +1 -0
- package/dist/tools/screenshot.d.ts +4 -0
- package/dist/tools/screenshot.js +15 -4
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/seo.d.ts +113 -0
- package/dist/tools/seo.js +281 -0
- package/dist/tools/seo.js.map +1 -0
- package/dist/tools/snapshot.d.ts +122 -0
- package/dist/tools/snapshot.js +183 -0
- package/dist/tools/snapshot.js.map +1 -0
- package/dist/tools/wait-for.d.ts +107 -0
- package/dist/tools/wait-for.js +167 -0
- package/dist/tools/wait-for.js.map +1 -0
- package/dist/utils/arabic-text.d.ts +14 -0
- package/dist/utils/arabic-text.js +193 -0
- package/dist/utils/arabic-text.js.map +1 -0
- package/dist/utils/budget.d.ts +41 -0
- package/dist/utils/budget.js +182 -0
- package/dist/utils/budget.js.map +1 -0
- package/dist/utils/format.d.ts +11 -1
- package/dist/utils/format.js +27 -4
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/highlight.d.ts +69 -0
- package/dist/utils/highlight.js +181 -0
- package/dist/utils/highlight.js.map +1 -0
- package/dist/utils/link-rules.d.ts +100 -0
- package/dist/utils/link-rules.js +284 -0
- package/dist/utils/link-rules.js.map +1 -0
- package/dist/utils/mock-rules.d.ts +144 -0
- package/dist/utils/mock-rules.js +224 -0
- package/dist/utils/mock-rules.js.map +1 -0
- package/dist/utils/rtl-rules.d.ts +142 -0
- package/dist/utils/rtl-rules.js +296 -0
- package/dist/utils/rtl-rules.js.map +1 -0
- package/dist/utils/seo-rules.d.ts +129 -0
- package/dist/utils/seo-rules.js +726 -0
- package/dist/utils/seo-rules.js.map +1 -0
- package/dist/utils/snapshot-rules.d.ts +33 -0
- package/dist/utils/snapshot-rules.js +111 -0
- package/dist/utils/snapshot-rules.js.map +1 -0
- package/dist/utils/storage-state.d.ts +76 -0
- package/dist/utils/storage-state.js +195 -0
- package/dist/utils/storage-state.js.map +1 -0
- package/dist/utils/style-rules.d.ts +107 -0
- package/dist/utils/style-rules.js +223 -0
- package/dist/utils/style-rules.js.map +1 -0
- package/dist/utils/test-data.d.ts +75 -0
- package/dist/utils/test-data.js +294 -0
- package/dist/utils/test-data.js.map +1 -0
- package/dist/utils/vue-rules.d.ts +72 -0
- package/dist/utils/vue-rules.js +108 -0
- package/dist/utils/vue-rules.js.map +1 -0
- package/package.json +6 -4
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DEFAULT_FORM_WAIT_MS, DEFAULT_VIEWPORT, FORM_FILL_SETTLE_MS, FORM_SCREENSHOT_TIMEOUT_MS, MAX_FORM_CONSOLE_ENTRIES, MAX_FORM_ERRORS, MAX_FORM_FIELDS, MAX_FORM_FIELDS_LISTED, MAX_FORM_NETWORK_EVENTS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, NAVIGATION_TIMEOUT_MS, SELECTOR_TIMEOUT_MS, } from "../constants.js";
|
|
3
|
+
import { withPage } from "../engine/browser.js";
|
|
4
|
+
import { discoverForms, fillForm, newMessages, readValidation, readXssMarker, submitForm, } from "../engine/forms.js";
|
|
5
|
+
import { ConsoleCollector, NetworkCollector } from "../engine/layers/index.js";
|
|
6
|
+
import { resizeForOutput, toBase64 } from "../utils/image.js";
|
|
7
|
+
import { resolveStorageState, storageStateField, withAuthNote } from "../utils/storage-state.js";
|
|
8
|
+
import { DEFAULT_FORM_STRATEGIES, FORM_STRATEGIES, describeStrategy, } from "../utils/test-data.js";
|
|
9
|
+
export const FORM_TEST_TOOL_NAME = "framewatch_form_test";
|
|
10
|
+
export const formTestInputShape = {
|
|
11
|
+
url: z
|
|
12
|
+
.string()
|
|
13
|
+
.url()
|
|
14
|
+
.describe("URL of the page with the form, e.g. http://localhost:3000/signup"),
|
|
15
|
+
selector: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("CSS selector for one form, or for the container holding the fields. Omit to test every <form> on the " +
|
|
19
|
+
"page — or, on a page with no <form> element, every field on it."),
|
|
20
|
+
strategies: z
|
|
21
|
+
.array(z.enum(FORM_STRATEGIES))
|
|
22
|
+
.min(1)
|
|
23
|
+
.max(FORM_STRATEGIES.length)
|
|
24
|
+
.default([...DEFAULT_FORM_STRATEGIES])
|
|
25
|
+
.describe("Which kinds of data to try. Each one runs in its own fresh page: valid, empty, maxlength, " +
|
|
26
|
+
"special_chars, rtl_arabic, numbers_only, spaces_only, boundary, xss."),
|
|
27
|
+
submit: z
|
|
28
|
+
.boolean()
|
|
29
|
+
.default(false)
|
|
30
|
+
.describe("Submit the form after filling it. Off by default because a submit writes to the app under test — turn " +
|
|
31
|
+
"it on to find out what validation, requests and errors the submit produces."),
|
|
32
|
+
wait_ms: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.min(0)
|
|
36
|
+
.default(DEFAULT_FORM_WAIT_MS)
|
|
37
|
+
.describe("Wait time (ms) after the submit before the result is read and photographed"),
|
|
38
|
+
wait_for: z.string().optional().describe("CSS selector to wait for (visible) before the form is looked for"),
|
|
39
|
+
wait_for_timeout_ms: z
|
|
40
|
+
.number()
|
|
41
|
+
.int()
|
|
42
|
+
.min(1)
|
|
43
|
+
.default(SELECTOR_TIMEOUT_MS)
|
|
44
|
+
.describe("Max time (ms) to wait for `wait_for` to appear (must be > 0)"),
|
|
45
|
+
timeout_ms: z
|
|
46
|
+
.number()
|
|
47
|
+
.int()
|
|
48
|
+
.min(1)
|
|
49
|
+
.default(SELECTOR_TIMEOUT_MS)
|
|
50
|
+
.describe("Max time (ms) one field may take to accept its value, and the submit control to be clickable"),
|
|
51
|
+
max_fields: z
|
|
52
|
+
.number()
|
|
53
|
+
.int()
|
|
54
|
+
.min(1)
|
|
55
|
+
.max(MAX_FORM_FIELDS)
|
|
56
|
+
.default(MAX_FORM_FIELDS)
|
|
57
|
+
.describe("Maximum fields to fill per form"),
|
|
58
|
+
full_page: z
|
|
59
|
+
.boolean()
|
|
60
|
+
.default(false)
|
|
61
|
+
.describe("Photograph the whole document instead of the viewport — for a form taller than the screen"),
|
|
62
|
+
viewport: z
|
|
63
|
+
.object({
|
|
64
|
+
width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH).default(DEFAULT_VIEWPORT.width),
|
|
65
|
+
height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT).default(DEFAULT_VIEWPORT.height),
|
|
66
|
+
})
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Viewport size (defaults to 1280x720)"),
|
|
69
|
+
storage_state: storageStateField,
|
|
70
|
+
};
|
|
71
|
+
export const formTestInputSchema = z.object(formTestInputShape);
|
|
72
|
+
/**
|
|
73
|
+
* Fill a page's forms with deliberately awkward data and report what breaks.
|
|
74
|
+
*
|
|
75
|
+
* Every strategy gets its own fresh page. That costs a page load each, and it
|
|
76
|
+
* is the whole point: a form filled with valid data and then emptied is not
|
|
77
|
+
* the same test as a form that was empty from the start (the app has seen an
|
|
78
|
+
* input event on every field, live validation has run, and half the frameworks
|
|
79
|
+
* in use will have marked the form "dirty"), and an XSS payload that executed
|
|
80
|
+
* under one strategy would be reported under every later one if they shared a
|
|
81
|
+
* page.
|
|
82
|
+
*
|
|
83
|
+
* Strategies that only fill run concurrently, since filling a form changes
|
|
84
|
+
* nothing outside the browser. With `submit: true` they run one after another
|
|
85
|
+
* instead: submitting is a write to the app under test, and nine of them at
|
|
86
|
+
* once is not a test, it is a load spike.
|
|
87
|
+
*
|
|
88
|
+
* Console and network are cleared once the page has loaded, so everything
|
|
89
|
+
* reported here was caused by the form rather than by the page it lives on.
|
|
90
|
+
*/
|
|
91
|
+
export async function testForms(rawInput) {
|
|
92
|
+
const parsed = formTestInputSchema.safeParse(rawInput);
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; ");
|
|
95
|
+
return errorResult(`Form test failed: invalid input — ${issues}`);
|
|
96
|
+
}
|
|
97
|
+
const input = parsed.data;
|
|
98
|
+
const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };
|
|
99
|
+
// Read the auth state once, before any context exists: a missing or stale
|
|
100
|
+
// file is one problem with the call, not one problem per strategy.
|
|
101
|
+
let storageState;
|
|
102
|
+
let auth = null;
|
|
103
|
+
try {
|
|
104
|
+
auth = await resolveStorageState(input.storage_state);
|
|
105
|
+
storageState = auth?.state;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return errorResult(`Form test of ${input.url} failed: ${firstLine(error)}`);
|
|
109
|
+
}
|
|
110
|
+
const runs = input.submit
|
|
111
|
+
? await runInOrder(input, viewport, storageState)
|
|
112
|
+
: await Promise.all(input.strategies.map((strategy) => runStrategy(input, viewport, strategy, storageState)));
|
|
113
|
+
const done = runs.filter((run) => run.error === undefined);
|
|
114
|
+
if (done.length === 0) {
|
|
115
|
+
// Every strategy sees the same page, so they all fail the same way — the
|
|
116
|
+
// first reason is the reason.
|
|
117
|
+
return errorResult(`Form test of ${input.url} failed: ${runs[0]?.error ?? "nothing ran"}`);
|
|
118
|
+
}
|
|
119
|
+
return withAuthNote(await render(input, runs, done), auth);
|
|
120
|
+
}
|
|
121
|
+
async function runInOrder(input, viewport, storageState) {
|
|
122
|
+
const runs = [];
|
|
123
|
+
for (const strategy of input.strategies) {
|
|
124
|
+
runs.push(await runStrategy(input, viewport, strategy, storageState));
|
|
125
|
+
}
|
|
126
|
+
return runs;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* One strategy, start to finish, in a page of its own.
|
|
130
|
+
*
|
|
131
|
+
* Never throws: a strategy that cannot run carries its reason back so the
|
|
132
|
+
* others are still reported. Only a run where every strategy failed becomes an
|
|
133
|
+
* error result.
|
|
134
|
+
*/
|
|
135
|
+
async function runStrategy(input, viewport, strategy, storageState) {
|
|
136
|
+
const run = {
|
|
137
|
+
strategy,
|
|
138
|
+
fills: [],
|
|
139
|
+
appeared: [],
|
|
140
|
+
browser: [],
|
|
141
|
+
invalid: [],
|
|
142
|
+
console: [],
|
|
143
|
+
network: [],
|
|
144
|
+
dialogs: [],
|
|
145
|
+
xss_executed: false,
|
|
146
|
+
navigated: false,
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
await withPage({ viewport, contextOptions: storageState ? { storageState } : {} }, async (page) => {
|
|
150
|
+
const consoleLog = new ConsoleCollector(page).attach();
|
|
151
|
+
const network = new NetworkCollector(page).attach();
|
|
152
|
+
// A form that opens an alert would otherwise be dismissed silently by
|
|
153
|
+
// Playwright, taking the reason for a stalled submit with it.
|
|
154
|
+
page.on("dialog", (dialog) => {
|
|
155
|
+
run.dialogs.push(`${dialog.type()}: ${dialog.message()}`);
|
|
156
|
+
void dialog.dismiss().catch(() => { });
|
|
157
|
+
});
|
|
158
|
+
try {
|
|
159
|
+
await page.goto(input.url, { waitUntil: "load", timeout: NAVIGATION_TIMEOUT_MS });
|
|
160
|
+
if (input.wait_for) {
|
|
161
|
+
await page.waitForSelector(input.wait_for, { state: "visible", timeout: input.wait_for_timeout_ms });
|
|
162
|
+
}
|
|
163
|
+
const forms = (await discoverForms(page, { selector: input.selector, max_fields: input.max_fields })).filter((form) => form.fields.length > 0);
|
|
164
|
+
if (forms.length === 0) {
|
|
165
|
+
throw new Error(input.selector
|
|
166
|
+
? `\`${input.selector}\` matched something, but there are no fillable fields inside it`
|
|
167
|
+
: "the page has no fillable fields — no visible, enabled input, select or textarea");
|
|
168
|
+
}
|
|
169
|
+
run.forms = forms.map(summariseForm);
|
|
170
|
+
const openedAt = Date.now();
|
|
171
|
+
consoleLog.clear();
|
|
172
|
+
network.clear();
|
|
173
|
+
const before = await Promise.all(forms.map((form) => readValidation(page, form)));
|
|
174
|
+
for (const form of forms) {
|
|
175
|
+
run.fills.push(await fillForm(form, strategy, { timeout_ms: input.timeout_ms }));
|
|
176
|
+
}
|
|
177
|
+
run.forms = forms.map((form, index) => ({
|
|
178
|
+
...summariseForm(form),
|
|
179
|
+
filled_fields: run.fills[index].filled.length,
|
|
180
|
+
}));
|
|
181
|
+
await page.waitForTimeout(FORM_FILL_SETTLE_MS);
|
|
182
|
+
run.fill_png = await safeScreenshot(page, input.full_page);
|
|
183
|
+
if (input.submit) {
|
|
184
|
+
// The first form is the one that gets sent: a submit is a navigation
|
|
185
|
+
// or a state change, and the second form's submit would be measuring
|
|
186
|
+
// whatever the first one left behind.
|
|
187
|
+
run.submit = await submitForm(forms[0], { timeout_ms: input.timeout_ms });
|
|
188
|
+
if (input.wait_ms > 0)
|
|
189
|
+
await page.waitForTimeout(input.wait_ms);
|
|
190
|
+
run.submit_png = await safeScreenshot(page, input.full_page);
|
|
191
|
+
}
|
|
192
|
+
const after = await Promise.all(forms.map((form) => readValidation(page, form)));
|
|
193
|
+
run.appeared = dedupe(after.flatMap((snapshot, index) => newMessages(before[index], snapshot)));
|
|
194
|
+
run.browser = after.flatMap((snapshot) => snapshot.browser);
|
|
195
|
+
run.invalid = dedupe(after.flatMap((snapshot) => snapshot.invalid));
|
|
196
|
+
if (strategy === "xss")
|
|
197
|
+
run.xss_executed = await readXssMarker(page);
|
|
198
|
+
run.console = consoleLog.entries(openedAt);
|
|
199
|
+
run.network = network.events(openedAt);
|
|
200
|
+
run.final_url = safely(() => page.url());
|
|
201
|
+
run.navigated = run.final_url !== undefined && !sameUrl(run.final_url, input.url);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
consoleLog.detach();
|
|
205
|
+
network.detach();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
run.error = describeFormTestFailure(input, error);
|
|
211
|
+
}
|
|
212
|
+
return run;
|
|
213
|
+
}
|
|
214
|
+
function summariseForm(form) {
|
|
215
|
+
return {
|
|
216
|
+
description: form.description,
|
|
217
|
+
is_form: form.is_form,
|
|
218
|
+
total_fields: form.total_fields,
|
|
219
|
+
filled_fields: 0,
|
|
220
|
+
fields: form.fields.map((field) => describeField(field.info)),
|
|
221
|
+
skipped: form.skipped,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** `#email (email, required, max 20)` — what the field is, in the terms that decide what goes in it. */
|
|
225
|
+
function describeField(info) {
|
|
226
|
+
const parts = [info.type];
|
|
227
|
+
if (info.required)
|
|
228
|
+
parts.push("required");
|
|
229
|
+
if (info.maxlength !== undefined)
|
|
230
|
+
parts.push(`max ${info.maxlength}`);
|
|
231
|
+
return `${info.description} (${parts.join(", ")})`;
|
|
232
|
+
}
|
|
233
|
+
/* ── Rendering ────────────────────────────────────────────────────────── */
|
|
234
|
+
async function render(input, runs, done) {
|
|
235
|
+
const content = [{ type: "text", text: summarise(input, runs, done) }];
|
|
236
|
+
for (const run of runs) {
|
|
237
|
+
if (run.error !== undefined) {
|
|
238
|
+
content.push({ type: "text", text: `${run.strategy} — did not run: ${run.error}` });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (run.fill_png) {
|
|
242
|
+
content.push({ type: "image", data: toBase64(await resizeForOutput(run.fill_png)), mimeType: "image/png" });
|
|
243
|
+
}
|
|
244
|
+
content.push({ type: "text", text: describeFill(run) });
|
|
245
|
+
if (run.submit) {
|
|
246
|
+
if (run.submit_png) {
|
|
247
|
+
content.push({ type: "image", data: toBase64(await resizeForOutput(run.submit_png)), mimeType: "image/png" });
|
|
248
|
+
}
|
|
249
|
+
content.push({ type: "text", text: describeSubmit(input, run) });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { content };
|
|
253
|
+
}
|
|
254
|
+
/** The opening block: what was found, what was run, and anything alarming. */
|
|
255
|
+
function summarise(input, runs, done) {
|
|
256
|
+
const forms = done[0].forms ?? [];
|
|
257
|
+
const lines = [];
|
|
258
|
+
const shape = forms
|
|
259
|
+
.map((form) => `${form.description}: ${form.total_fields} fillable field${form.total_fields === 1 ? "" : "s"}` +
|
|
260
|
+
(form.skipped.length > 0 ? `, ${form.skipped.length} not fillable` : ""))
|
|
261
|
+
.join("; ");
|
|
262
|
+
lines.push(`Form test of ${input.url} — ${shape}. Ran ${count(done.length, "strategy", "strategies")}, ` +
|
|
263
|
+
`${input.submit ? "filling and submitting the form" : "filling the form only (`submit: false`)"}, ` +
|
|
264
|
+
"each in a page of its own.");
|
|
265
|
+
for (const form of forms) {
|
|
266
|
+
if (!form.is_form) {
|
|
267
|
+
lines.push(`${form.description} — there is no <form> element, so the fields were treated as one form.`);
|
|
268
|
+
}
|
|
269
|
+
lines.push(`Fields: ${list(form.fields, MAX_FORM_FIELDS_LISTED)}`);
|
|
270
|
+
if (form.skipped.length > 0) {
|
|
271
|
+
lines.push(`Not filled: ${list(form.skipped.map((skip) => `${skip.description} (${skip.reason})`), MAX_FORM_FIELDS_LISTED)}`);
|
|
272
|
+
}
|
|
273
|
+
if (form.total_fields > form.filled_fields && form.filled_fields > 0 && form.total_fields > input.max_fields) {
|
|
274
|
+
lines.push(`Only the first ${input.max_fields} fields of ${form.description} were filled (\`max_fields\`).`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (forms.length > 1 && input.submit) {
|
|
278
|
+
lines.push(`All ${forms.length} forms were filled, but only ${forms[0].description} was submitted — a submit changes ` +
|
|
279
|
+
"the page underneath the others. Pass `selector` to test one of the rest.");
|
|
280
|
+
}
|
|
281
|
+
for (const line of warnings(runs))
|
|
282
|
+
lines.push(line);
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* The findings that should not wait until the reader reaches the right
|
|
287
|
+
* strategy block. Deliberately few — a warning that fires on every form is
|
|
288
|
+
* one nobody reads.
|
|
289
|
+
*/
|
|
290
|
+
function warnings(runs) {
|
|
291
|
+
const lines = [];
|
|
292
|
+
if (runs.some((run) => run.xss_executed)) {
|
|
293
|
+
lines.push("Warning — an injected payload executed on this page: input is being written back into the document as " +
|
|
294
|
+
"markup. That is a reflected XSS. The payloads only set a marker; a real one would not be so polite.");
|
|
295
|
+
}
|
|
296
|
+
const failed = runs.flatMap((run) => run.fills.flatMap((fill) => fill.failed));
|
|
297
|
+
if (failed.length > 0) {
|
|
298
|
+
lines.push(`Warning — ${count(failed.length, "field", "fields")} would not take a value: ` +
|
|
299
|
+
`${list(dedupe(failed.map((problem) => `${problem.description} (${problem.reason})`)), 3)}`);
|
|
300
|
+
}
|
|
301
|
+
const dialogs = dedupe(runs.flatMap((run) => run.dialogs));
|
|
302
|
+
if (dialogs.length > 0) {
|
|
303
|
+
lines.push(`The page opened ${count(dialogs.length, "dialog", "dialogs")}, dismissed: ${list(dialogs, 3)}`);
|
|
304
|
+
}
|
|
305
|
+
return lines;
|
|
306
|
+
}
|
|
307
|
+
/** The block under the "after fill" frame. */
|
|
308
|
+
function describeFill(run) {
|
|
309
|
+
const filled = run.fills.flatMap((fill) => fill.filled);
|
|
310
|
+
const total = (run.forms ?? []).reduce((sum, form) => sum + form.total_fields, 0);
|
|
311
|
+
const lines = [`${run.strategy} — after fill: ${describeStrategy(run.strategy)}`];
|
|
312
|
+
lines.push(`${filled.length} of ${total} field${total === 1 ? "" : "s"} filled` +
|
|
313
|
+
(filled.length > 0
|
|
314
|
+
? `: ${list(filled.map((field) => `${field.description}=${quote(field)}`), MAX_FORM_FIELDS_LISTED)}`
|
|
315
|
+
: ""));
|
|
316
|
+
const truncated = filled.filter((field) => field.truncated);
|
|
317
|
+
if (truncated.length > 0) {
|
|
318
|
+
lines.push("Truncated by the page: " +
|
|
319
|
+
list(truncated.map((field) => `${field.description} kept ${field.landed_length} of ${field.requested_length} characters`), MAX_FORM_FIELDS_LISTED));
|
|
320
|
+
}
|
|
321
|
+
const skipped = run.fills.flatMap((fill) => fill.skipped);
|
|
322
|
+
if (skipped.length > 0) {
|
|
323
|
+
lines.push("Left alone: " +
|
|
324
|
+
list(skipped.map((problem) => `${problem.description} (${problem.reason})`), MAX_FORM_FIELDS_LISTED));
|
|
325
|
+
}
|
|
326
|
+
const failed = run.fills.flatMap((fill) => fill.failed);
|
|
327
|
+
if (failed.length > 0) {
|
|
328
|
+
lines.push("Would not take a value: " +
|
|
329
|
+
list(failed.map((problem) => `${problem.description} (${problem.reason})`), MAX_FORM_FIELDS_LISTED));
|
|
330
|
+
}
|
|
331
|
+
// Without a submit, whatever the page said it said while being typed into —
|
|
332
|
+
// live validation, and worth reporting on its own.
|
|
333
|
+
if (!run.submit)
|
|
334
|
+
appendReaction(lines, run, false);
|
|
335
|
+
return lines.join("\n");
|
|
336
|
+
}
|
|
337
|
+
/** The block under the "after submit" frame. */
|
|
338
|
+
function describeSubmit(input, run) {
|
|
339
|
+
const submitted = run.submit;
|
|
340
|
+
const lines = [
|
|
341
|
+
`${run.strategy} — after submit: ${submitted.ok ? submitted.how : `not submitted (${submitted.reason})`}` +
|
|
342
|
+
(submitted.ok && input.wait_ms > 0 ? `, waited ${input.wait_ms}ms` : ""),
|
|
343
|
+
];
|
|
344
|
+
if (run.navigated && run.final_url)
|
|
345
|
+
lines.push(`The page went to ${run.final_url}`);
|
|
346
|
+
appendReaction(lines, run, submitted.ok);
|
|
347
|
+
return lines.join("\n");
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* What the page did about it: its own messages, the browser's constraint
|
|
351
|
+
* validation, the requests, the console.
|
|
352
|
+
*
|
|
353
|
+
* After a submit the empty cases are printed too. "No request was made" and
|
|
354
|
+
* "nothing was shown" are the two findings this tool exists to produce — a
|
|
355
|
+
* form that silently does nothing looks exactly like a form that worked.
|
|
356
|
+
*/
|
|
357
|
+
function appendReaction(lines, run, submitted) {
|
|
358
|
+
if (run.appeared.length > 0) {
|
|
359
|
+
lines.push(`Page validation: ${list(run.appeared, MAX_FORM_ERRORS)}`);
|
|
360
|
+
}
|
|
361
|
+
else if (submitted) {
|
|
362
|
+
lines.push("Page validation: nothing new was shown");
|
|
363
|
+
}
|
|
364
|
+
if (run.invalid.length > 0) {
|
|
365
|
+
lines.push(`Fields the page marked invalid: ${list(run.invalid, MAX_FORM_FIELDS_LISTED)}`);
|
|
366
|
+
}
|
|
367
|
+
if (run.browser.length > 0) {
|
|
368
|
+
lines.push(`Browser validation: ${list(run.browser.map((entry) => `${entry.field} — ${entry.message}`), MAX_FORM_ERRORS)}`);
|
|
369
|
+
}
|
|
370
|
+
if (run.network.length > 0) {
|
|
371
|
+
lines.push("Network:");
|
|
372
|
+
for (const event of run.network.slice(0, MAX_FORM_NETWORK_EVENTS)) {
|
|
373
|
+
const outcome = event.status > 0 ? String(event.status) : (event.error ?? "no response");
|
|
374
|
+
lines.push(` ${event.method} ${event.url} → ${outcome} (${event.duration_ms}ms)`);
|
|
375
|
+
}
|
|
376
|
+
if (run.network.length > MAX_FORM_NETWORK_EVENTS) {
|
|
377
|
+
lines.push(` … and ${run.network.length - MAX_FORM_NETWORK_EVENTS} more requests`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else if (submitted) {
|
|
381
|
+
lines.push("Network: no request was made — the submit never left the page");
|
|
382
|
+
}
|
|
383
|
+
if (run.console.length > 0) {
|
|
384
|
+
lines.push("Console:");
|
|
385
|
+
for (const entry of run.console.slice(0, MAX_FORM_CONSOLE_ENTRIES)) {
|
|
386
|
+
lines.push(` [${entry.level}] ${entry.text}`);
|
|
387
|
+
}
|
|
388
|
+
if (run.console.length > MAX_FORM_CONSOLE_ENTRIES) {
|
|
389
|
+
lines.push(` … and ${run.console.length - MAX_FORM_CONSOLE_ENTRIES} more entries`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/* ── Helpers ──────────────────────────────────────────────────────────── */
|
|
394
|
+
/** A screenshot is always worth having and never worth failing the strategy over. */
|
|
395
|
+
async function safeScreenshot(page, fullPage) {
|
|
396
|
+
try {
|
|
397
|
+
if (page.isClosed())
|
|
398
|
+
return undefined;
|
|
399
|
+
return await page.screenshot({ type: "png", fullPage, timeout: FORM_SCREENSHOT_TIMEOUT_MS });
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function quote(field) {
|
|
406
|
+
return field.kind === "toggle" ? field.landed : `"${field.landed}"`;
|
|
407
|
+
}
|
|
408
|
+
function list(items, max) {
|
|
409
|
+
if (items.length <= max)
|
|
410
|
+
return items.join(", ");
|
|
411
|
+
return `${items.slice(0, max).join(", ")}, … and ${items.length - max} more`;
|
|
412
|
+
}
|
|
413
|
+
function dedupe(items) {
|
|
414
|
+
return [...new Set(items)];
|
|
415
|
+
}
|
|
416
|
+
function count(n, singular, plural) {
|
|
417
|
+
return `${n} ${n === 1 ? singular : plural}`;
|
|
418
|
+
}
|
|
419
|
+
function sameUrl(a, b) {
|
|
420
|
+
if (a === b)
|
|
421
|
+
return true;
|
|
422
|
+
try {
|
|
423
|
+
return new URL(a).href === new URL(b).href;
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function safely(read) {
|
|
430
|
+
try {
|
|
431
|
+
return read();
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function firstLine(error) {
|
|
438
|
+
return (error instanceof Error ? error.message : String(error)).split("\n")[0];
|
|
439
|
+
}
|
|
440
|
+
function errorResult(text) {
|
|
441
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* One actionable line for a strategy that could not run. Mirrors
|
|
445
|
+
* `describeFailure` in screenshot.ts: match on the failing Playwright call,
|
|
446
|
+
* never on substrings of a user-supplied selector.
|
|
447
|
+
*/
|
|
448
|
+
export function describeFormTestFailure(input, error) {
|
|
449
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
450
|
+
const line = firstLine(message);
|
|
451
|
+
if (/Executable doesn't exist|browserType\.launch/i.test(message)) {
|
|
452
|
+
return ("Playwright's Chromium browser is not installed. " +
|
|
453
|
+
`Run \`npx playwright install chromium\` and try again. (${line})`);
|
|
454
|
+
}
|
|
455
|
+
if (input.wait_for && /^page\.waitForSelector:/.test(message)) {
|
|
456
|
+
return `selector "${input.wait_for}" did not become visible within ${input.wait_for_timeout_ms}ms`;
|
|
457
|
+
}
|
|
458
|
+
if (/^page\.goto:/.test(message)) {
|
|
459
|
+
return `the page could not be opened — ${line}`;
|
|
460
|
+
}
|
|
461
|
+
return line;
|
|
462
|
+
}
|
|
463
|
+
export function registerFormTestTool(server) {
|
|
464
|
+
server.registerTool(FORM_TEST_TOOL_NAME, {
|
|
465
|
+
title: "Form test",
|
|
466
|
+
description: "Fill a page's forms with awkward data and report what breaks. Each strategy — valid data, everything " +
|
|
467
|
+
"empty, maxlength, special characters, Arabic (RTL), digits, whitespace, boundary values, and harmless " +
|
|
468
|
+
"XSS payloads — runs in its own fresh page and comes back as a screenshot plus what the page did: the " +
|
|
469
|
+
"validation messages it showed, the browser's own constraint validation, the requests it made, its " +
|
|
470
|
+
"console output, and any value it silently truncated. Set `submit: true` to send the form as well, " +
|
|
471
|
+
"which is what surfaces the two findings that matter most: a submit that shows no error and a submit " +
|
|
472
|
+
"that never makes a request. Works on a page with no <form> element too.",
|
|
473
|
+
inputSchema: formTestInputShape,
|
|
474
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
475
|
+
}, async (args) => testForms(args));
|
|
476
|
+
}
|
|
477
|
+
//# sourceMappingURL=form-test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-test.js","sourceRoot":"","sources":["../../src/tools/form-test.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\nimport type { Page } from \"playwright\";\nimport type { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport {\n DEFAULT_FORM_WAIT_MS,\n DEFAULT_VIEWPORT,\n FORM_FILL_SETTLE_MS,\n FORM_SCREENSHOT_TIMEOUT_MS,\n MAX_FORM_CONSOLE_ENTRIES,\n MAX_FORM_ERRORS,\n MAX_FORM_FIELDS,\n MAX_FORM_FIELDS_LISTED,\n MAX_FORM_NETWORK_EVENTS,\n MAX_VIEWPORT_HEIGHT,\n MAX_VIEWPORT_WIDTH,\n NAVIGATION_TIMEOUT_MS,\n SELECTOR_TIMEOUT_MS,\n} from \"../constants.js\";\nimport { withPage } from \"../engine/browser.js\";\nimport {\n discoverForms,\n fillForm,\n newMessages,\n readValidation,\n readXssMarker,\n submitForm,\n type DiscoveredForm,\n type FieldProblem,\n type FillResult,\n type SubmitResult,\n} from \"../engine/forms.js\";\nimport { ConsoleCollector, NetworkCollector } from \"../engine/layers/index.js\";\nimport type { ConsoleEntry, NetworkEvent, Viewport } from \"../types.js\";\nimport { resizeForOutput, toBase64 } from \"../utils/image.js\";\nimport { resolveStorageState, storageStateField, withAuthNote, type StorageState } from \"../utils/storage-state.js\";\nimport {\n DEFAULT_FORM_STRATEGIES,\n FORM_STRATEGIES,\n describeStrategy,\n type FormStrategy,\n} from \"../utils/test-data.js\";\n\nexport const FORM_TEST_TOOL_NAME = \"framewatch_form_test\";\n\nexport const formTestInputShape = {\n url: z\n .string()\n .url()\n .describe(\"URL of the page with the form, e.g. http://localhost:3000/signup\"),\n selector: z\n .string()\n .optional()\n .describe(\n \"CSS selector for one form, or for the container holding the fields. Omit to test every <form> on the \" +\n \"page — or, on a page with no <form> element, every field on it.\",\n ),\n strategies: z\n .array(z.enum(FORM_STRATEGIES))\n .min(1)\n .max(FORM_STRATEGIES.length)\n .default([...DEFAULT_FORM_STRATEGIES])\n .describe(\n \"Which kinds of data to try. Each one runs in its own fresh page: valid, empty, maxlength, \" +\n \"special_chars, rtl_arabic, numbers_only, spaces_only, boundary, xss.\",\n ),\n submit: z\n .boolean()\n .default(false)\n .describe(\n \"Submit the form after filling it. Off by default because a submit writes to the app under test — turn \" +\n \"it on to find out what validation, requests and errors the submit produces.\",\n ),\n wait_ms: z\n .number()\n .int()\n .min(0)\n .default(DEFAULT_FORM_WAIT_MS)\n .describe(\"Wait time (ms) after the submit before the result is read and photographed\"),\n wait_for: z.string().optional().describe(\"CSS selector to wait for (visible) before the form is looked for\"),\n wait_for_timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) to wait for `wait_for` to appear (must be > 0)\"),\n timeout_ms: z\n .number()\n .int()\n .min(1)\n .default(SELECTOR_TIMEOUT_MS)\n .describe(\"Max time (ms) one field may take to accept its value, and the submit control to be clickable\"),\n max_fields: z\n .number()\n .int()\n .min(1)\n .max(MAX_FORM_FIELDS)\n .default(MAX_FORM_FIELDS)\n .describe(\"Maximum fields to fill per form\"),\n full_page: z\n .boolean()\n .default(false)\n .describe(\"Photograph the whole document instead of the viewport — for a form taller than the screen\"),\n viewport: z\n .object({\n width: z.number().int().min(1).max(MAX_VIEWPORT_WIDTH).default(DEFAULT_VIEWPORT.width),\n height: z.number().int().min(1).max(MAX_VIEWPORT_HEIGHT).default(DEFAULT_VIEWPORT.height),\n })\n .optional()\n .describe(\"Viewport size (defaults to 1280x720)\"),\n storage_state: storageStateField,\n};\n\nexport const formTestInputSchema = z.object(formTestInputShape);\nexport type FormTestInput = z.input<typeof formTestInputSchema>;\ntype ParsedFormTestInput = z.output<typeof formTestInputSchema>;\n\n/** What one form looked like before anything was typed into it. */\ninterface FormSummary {\n description: string;\n is_form: boolean;\n total_fields: number;\n filled_fields: number;\n fields: string[];\n skipped: FieldProblem[];\n}\n\n/** Everything one strategy produced. */\ninterface StrategyRun {\n strategy: FormStrategy;\n /** Why this strategy produced nothing. Present instead of everything below. */\n error?: string;\n forms?: FormSummary[];\n fills: FillResult[];\n fill_png?: Buffer;\n submit?: SubmitResult;\n submit_png?: Buffer;\n /** Validation text that was not on the page before the form was touched. */\n appeared: string[];\n browser: Array<{ field: string; message: string }>;\n invalid: string[];\n console: ConsoleEntry[];\n network: NetworkEvent[];\n dialogs: string[];\n /** An XSS payload from this strategy ran as code (see engine/forms.ts). */\n xss_executed: boolean;\n final_url?: string;\n navigated: boolean;\n}\n\n/**\n * Fill a page's forms with deliberately awkward data and report what breaks.\n *\n * Every strategy gets its own fresh page. That costs a page load each, and it\n * is the whole point: a form filled with valid data and then emptied is not\n * the same test as a form that was empty from the start (the app has seen an\n * input event on every field, live validation has run, and half the frameworks\n * in use will have marked the form \"dirty\"), and an XSS payload that executed\n * under one strategy would be reported under every later one if they shared a\n * page.\n *\n * Strategies that only fill run concurrently, since filling a form changes\n * nothing outside the browser. With `submit: true` they run one after another\n * instead: submitting is a write to the app under test, and nine of them at\n * once is not a test, it is a load spike.\n *\n * Console and network are cleared once the page has loaded, so everything\n * reported here was caused by the form rather than by the page it lives on.\n */\nexport async function testForms(rawInput: FormTestInput): Promise<CallToolResult> {\n const parsed = formTestInputSchema.safeParse(rawInput);\n if (!parsed.success) {\n const issues = parsed.error.issues.map((i) => `${i.path.join(\".\") || \"input\"}: ${i.message}`).join(\"; \");\n return errorResult(`Form test failed: invalid input — ${issues}`);\n }\n const input = parsed.data;\n const viewport = input.viewport ?? { ...DEFAULT_VIEWPORT };\n\n // Read the auth state once, before any context exists: a missing or stale\n // file is one problem with the call, not one problem per strategy.\n let storageState: StorageState | undefined;\n let auth = null;\n try {\n auth = await resolveStorageState(input.storage_state);\n storageState = auth?.state;\n } catch (error) {\n return errorResult(`Form test of ${input.url} failed: ${firstLine(error)}`);\n }\n\n const runs: StrategyRun[] = input.submit\n ? await runInOrder(input, viewport, storageState)\n : await Promise.all(input.strategies.map((strategy) => runStrategy(input, viewport, strategy, storageState)));\n\n const done = runs.filter((run) => run.error === undefined);\n if (done.length === 0) {\n // Every strategy sees the same page, so they all fail the same way — the\n // first reason is the reason.\n return errorResult(`Form test of ${input.url} failed: ${runs[0]?.error ?? \"nothing ran\"}`);\n }\n\n return withAuthNote(await render(input, runs, done), auth);\n}\n\nasync function runInOrder(\n input: ParsedFormTestInput,\n viewport: Viewport,\n storageState?: StorageState,\n): Promise<StrategyRun[]> {\n const runs: StrategyRun[] = [];\n for (const strategy of input.strategies) {\n runs.push(await runStrategy(input, viewport, strategy, storageState));\n }\n return runs;\n}\n\n/**\n * One strategy, start to finish, in a page of its own.\n *\n * Never throws: a strategy that cannot run carries its reason back so the\n * others are still reported. Only a run where every strategy failed becomes an\n * error result.\n */\nasync function runStrategy(\n input: ParsedFormTestInput,\n viewport: Viewport,\n strategy: FormStrategy,\n storageState?: StorageState,\n): Promise<StrategyRun> {\n const run: StrategyRun = {\n strategy,\n fills: [],\n appeared: [],\n browser: [],\n invalid: [],\n console: [],\n network: [],\n dialogs: [],\n xss_executed: false,\n navigated: false,\n };\n\n try {\n await withPage({ viewport, contextOptions: storageState ? { storageState } : {} }, async (page) => {\n const consoleLog = new ConsoleCollector(page).attach();\n const network = new NetworkCollector(page).attach();\n // A form that opens an alert would otherwise be dismissed silently by\n // Playwright, taking the reason for a stalled submit with it.\n page.on(\"dialog\", (dialog) => {\n run.dialogs.push(`${dialog.type()}: ${dialog.message()}`);\n void dialog.dismiss().catch(() => {});\n });\n\n try {\n await page.goto(input.url, { waitUntil: \"load\", timeout: NAVIGATION_TIMEOUT_MS });\n if (input.wait_for) {\n await page.waitForSelector(input.wait_for, { state: \"visible\", timeout: input.wait_for_timeout_ms });\n }\n\n const forms = (await discoverForms(page, { selector: input.selector, max_fields: input.max_fields })).filter(\n (form) => form.fields.length > 0,\n );\n if (forms.length === 0) {\n throw new Error(\n input.selector\n ? `\\`${input.selector}\\` matched something, but there are no fillable fields inside it`\n : \"the page has no fillable fields — no visible, enabled input, select or textarea\",\n );\n }\n run.forms = forms.map(summariseForm);\n\n const openedAt = Date.now();\n consoleLog.clear();\n network.clear();\n\n const before = await Promise.all(forms.map((form) => readValidation(page, form)));\n for (const form of forms) {\n run.fills.push(await fillForm(form, strategy, { timeout_ms: input.timeout_ms }));\n }\n run.forms = forms.map((form, index) => ({\n ...summariseForm(form),\n filled_fields: run.fills[index].filled.length,\n }));\n\n await page.waitForTimeout(FORM_FILL_SETTLE_MS);\n run.fill_png = await safeScreenshot(page, input.full_page);\n\n if (input.submit) {\n // The first form is the one that gets sent: a submit is a navigation\n // or a state change, and the second form's submit would be measuring\n // whatever the first one left behind.\n run.submit = await submitForm(forms[0], { timeout_ms: input.timeout_ms });\n if (input.wait_ms > 0) await page.waitForTimeout(input.wait_ms);\n run.submit_png = await safeScreenshot(page, input.full_page);\n }\n\n const after = await Promise.all(forms.map((form) => readValidation(page, form)));\n run.appeared = dedupe(after.flatMap((snapshot, index) => newMessages(before[index], snapshot)));\n run.browser = after.flatMap((snapshot) => snapshot.browser);\n run.invalid = dedupe(after.flatMap((snapshot) => snapshot.invalid));\n if (strategy === \"xss\") run.xss_executed = await readXssMarker(page);\n\n run.console = consoleLog.entries(openedAt);\n run.network = network.events(openedAt);\n run.final_url = safely(() => page.url());\n run.navigated = run.final_url !== undefined && !sameUrl(run.final_url, input.url);\n } finally {\n consoleLog.detach();\n network.detach();\n }\n });\n } catch (error) {\n run.error = describeFormTestFailure(input, error);\n }\n\n return run;\n}\n\nfunction summariseForm(form: DiscoveredForm): FormSummary {\n return {\n description: form.description,\n is_form: form.is_form,\n total_fields: form.total_fields,\n filled_fields: 0,\n fields: form.fields.map((field) => describeField(field.info)),\n skipped: form.skipped,\n };\n}\n\n/** `#email (email, required, max 20)` — what the field is, in the terms that decide what goes in it. */\nfunction describeField(info: { description: string; type: string; required: boolean; maxlength?: number }): string {\n const parts = [info.type];\n if (info.required) parts.push(\"required\");\n if (info.maxlength !== undefined) parts.push(`max ${info.maxlength}`);\n return `${info.description} (${parts.join(\", \")})`;\n}\n\n/* ── Rendering ────────────────────────────────────────────────────────── */\n\nasync function render(input: ParsedFormTestInput, runs: StrategyRun[], done: StrategyRun[]): Promise<CallToolResult> {\n const content: CallToolResult[\"content\"] = [{ type: \"text\", text: summarise(input, runs, done) }];\n\n for (const run of runs) {\n if (run.error !== undefined) {\n content.push({ type: \"text\", text: `${run.strategy} — did not run: ${run.error}` });\n continue;\n }\n if (run.fill_png) {\n content.push({ type: \"image\", data: toBase64(await resizeForOutput(run.fill_png)), mimeType: \"image/png\" });\n }\n content.push({ type: \"text\", text: describeFill(run) });\n\n if (run.submit) {\n if (run.submit_png) {\n content.push({ type: \"image\", data: toBase64(await resizeForOutput(run.submit_png)), mimeType: \"image/png\" });\n }\n content.push({ type: \"text\", text: describeSubmit(input, run) });\n }\n }\n\n return { content };\n}\n\n/** The opening block: what was found, what was run, and anything alarming. */\nfunction summarise(input: ParsedFormTestInput, runs: StrategyRun[], done: StrategyRun[]): string {\n const forms = done[0].forms ?? [];\n const lines: string[] = [];\n\n const shape = forms\n .map(\n (form) =>\n `${form.description}: ${form.total_fields} fillable field${form.total_fields === 1 ? \"\" : \"s\"}` +\n (form.skipped.length > 0 ? `, ${form.skipped.length} not fillable` : \"\"),\n )\n .join(\"; \");\n lines.push(\n `Form test of ${input.url} — ${shape}. Ran ${count(done.length, \"strategy\", \"strategies\")}, ` +\n `${input.submit ? \"filling and submitting the form\" : \"filling the form only (`submit: false`)\"}, ` +\n \"each in a page of its own.\",\n );\n\n for (const form of forms) {\n if (!form.is_form) {\n lines.push(`${form.description} — there is no <form> element, so the fields were treated as one form.`);\n }\n lines.push(`Fields: ${list(form.fields, MAX_FORM_FIELDS_LISTED)}`);\n if (form.skipped.length > 0) {\n lines.push(\n `Not filled: ${list(\n form.skipped.map((skip) => `${skip.description} (${skip.reason})`),\n MAX_FORM_FIELDS_LISTED,\n )}`,\n );\n }\n if (form.total_fields > form.filled_fields && form.filled_fields > 0 && form.total_fields > input.max_fields) {\n lines.push(`Only the first ${input.max_fields} fields of ${form.description} were filled (\\`max_fields\\`).`);\n }\n }\n\n if (forms.length > 1 && input.submit) {\n lines.push(\n `All ${forms.length} forms were filled, but only ${forms[0].description} was submitted — a submit changes ` +\n \"the page underneath the others. Pass `selector` to test one of the rest.\",\n );\n }\n\n for (const line of warnings(runs)) lines.push(line);\n\n return lines.join(\"\\n\");\n}\n\n/**\n * The findings that should not wait until the reader reaches the right\n * strategy block. Deliberately few — a warning that fires on every form is\n * one nobody reads.\n */\nfunction warnings(runs: StrategyRun[]): string[] {\n const lines: string[] = [];\n\n if (runs.some((run) => run.xss_executed)) {\n lines.push(\n \"Warning — an injected payload executed on this page: input is being written back into the document as \" +\n \"markup. That is a reflected XSS. The payloads only set a marker; a real one would not be so polite.\",\n );\n }\n\n const failed = runs.flatMap((run) => run.fills.flatMap((fill) => fill.failed));\n if (failed.length > 0) {\n lines.push(\n `Warning — ${count(failed.length, \"field\", \"fields\")} would not take a value: ` +\n `${list(dedupe(failed.map((problem) => `${problem.description} (${problem.reason})`)), 3)}`,\n );\n }\n\n const dialogs = dedupe(runs.flatMap((run) => run.dialogs));\n if (dialogs.length > 0) {\n lines.push(`The page opened ${count(dialogs.length, \"dialog\", \"dialogs\")}, dismissed: ${list(dialogs, 3)}`);\n }\n\n return lines;\n}\n\n/** The block under the \"after fill\" frame. */\nfunction describeFill(run: StrategyRun): string {\n const filled = run.fills.flatMap((fill) => fill.filled);\n const total = (run.forms ?? []).reduce((sum, form) => sum + form.total_fields, 0);\n const lines = [`${run.strategy} — after fill: ${describeStrategy(run.strategy)}`];\n\n lines.push(\n `${filled.length} of ${total} field${total === 1 ? \"\" : \"s\"} filled` +\n (filled.length > 0\n ? `: ${list(\n filled.map((field) => `${field.description}=${quote(field)}`),\n MAX_FORM_FIELDS_LISTED,\n )}`\n : \"\"),\n );\n\n const truncated = filled.filter((field) => field.truncated);\n if (truncated.length > 0) {\n lines.push(\n \"Truncated by the page: \" +\n list(\n truncated.map((field) => `${field.description} kept ${field.landed_length} of ${field.requested_length} characters`),\n MAX_FORM_FIELDS_LISTED,\n ),\n );\n }\n\n const skipped = run.fills.flatMap((fill) => fill.skipped);\n if (skipped.length > 0) {\n lines.push(\n \"Left alone: \" +\n list(\n skipped.map((problem) => `${problem.description} (${problem.reason})`),\n MAX_FORM_FIELDS_LISTED,\n ),\n );\n }\n\n const failed = run.fills.flatMap((fill) => fill.failed);\n if (failed.length > 0) {\n lines.push(\n \"Would not take a value: \" +\n list(\n failed.map((problem) => `${problem.description} (${problem.reason})`),\n MAX_FORM_FIELDS_LISTED,\n ),\n );\n }\n\n // Without a submit, whatever the page said it said while being typed into —\n // live validation, and worth reporting on its own.\n if (!run.submit) appendReaction(lines, run, false);\n\n return lines.join(\"\\n\");\n}\n\n/** The block under the \"after submit\" frame. */\nfunction describeSubmit(input: ParsedFormTestInput, run: StrategyRun): string {\n const submitted = run.submit!;\n const lines = [\n `${run.strategy} — after submit: ${submitted.ok ? submitted.how : `not submitted (${submitted.reason})`}` +\n (submitted.ok && input.wait_ms > 0 ? `, waited ${input.wait_ms}ms` : \"\"),\n ];\n\n if (run.navigated && run.final_url) lines.push(`The page went to ${run.final_url}`);\n appendReaction(lines, run, submitted.ok);\n\n return lines.join(\"\\n\");\n}\n\n/**\n * What the page did about it: its own messages, the browser's constraint\n * validation, the requests, the console.\n *\n * After a submit the empty cases are printed too. \"No request was made\" and\n * \"nothing was shown\" are the two findings this tool exists to produce — a\n * form that silently does nothing looks exactly like a form that worked.\n */\nfunction appendReaction(lines: string[], run: StrategyRun, submitted: boolean): void {\n if (run.appeared.length > 0) {\n lines.push(`Page validation: ${list(run.appeared, MAX_FORM_ERRORS)}`);\n } else if (submitted) {\n lines.push(\"Page validation: nothing new was shown\");\n }\n\n if (run.invalid.length > 0) {\n lines.push(`Fields the page marked invalid: ${list(run.invalid, MAX_FORM_FIELDS_LISTED)}`);\n }\n\n if (run.browser.length > 0) {\n lines.push(\n `Browser validation: ${list(\n run.browser.map((entry) => `${entry.field} — ${entry.message}`),\n MAX_FORM_ERRORS,\n )}`,\n );\n }\n\n if (run.network.length > 0) {\n lines.push(\"Network:\");\n for (const event of run.network.slice(0, MAX_FORM_NETWORK_EVENTS)) {\n const outcome = event.status > 0 ? String(event.status) : (event.error ?? \"no response\");\n lines.push(` ${event.method} ${event.url} → ${outcome} (${event.duration_ms}ms)`);\n }\n if (run.network.length > MAX_FORM_NETWORK_EVENTS) {\n lines.push(` … and ${run.network.length - MAX_FORM_NETWORK_EVENTS} more requests`);\n }\n } else if (submitted) {\n lines.push(\"Network: no request was made — the submit never left the page\");\n }\n\n if (run.console.length > 0) {\n lines.push(\"Console:\");\n for (const entry of run.console.slice(0, MAX_FORM_CONSOLE_ENTRIES)) {\n lines.push(` [${entry.level}] ${entry.text}`);\n }\n if (run.console.length > MAX_FORM_CONSOLE_ENTRIES) {\n lines.push(` … and ${run.console.length - MAX_FORM_CONSOLE_ENTRIES} more entries`);\n }\n }\n}\n\n/* ── Helpers ──────────────────────────────────────────────────────────── */\n\n/** A screenshot is always worth having and never worth failing the strategy over. */\nasync function safeScreenshot(page: Page, fullPage: boolean): Promise<Buffer | undefined> {\n try {\n if (page.isClosed()) return undefined;\n return await page.screenshot({ type: \"png\", fullPage, timeout: FORM_SCREENSHOT_TIMEOUT_MS });\n } catch {\n return undefined;\n }\n}\n\nfunction quote(field: { landed: string; kind: string }): string {\n return field.kind === \"toggle\" ? field.landed : `\"${field.landed}\"`;\n}\n\nfunction list(items: string[], max: number): string {\n if (items.length <= max) return items.join(\", \");\n return `${items.slice(0, max).join(\", \")}, … and ${items.length - max} more`;\n}\n\nfunction dedupe(items: string[]): string[] {\n return [...new Set(items)];\n}\n\nfunction count(n: number, singular: string, plural: string): string {\n return `${n} ${n === 1 ? singular : plural}`;\n}\n\nfunction sameUrl(a: string, b: string): boolean {\n if (a === b) return true;\n try {\n return new URL(a).href === new URL(b).href;\n } catch {\n return false;\n }\n}\n\nfunction safely<T>(read: () => T): T | undefined {\n try {\n return read();\n } catch {\n return undefined;\n }\n}\n\nfunction firstLine(error: unknown): string {\n return (error instanceof Error ? error.message : String(error)).split(\"\\n\")[0];\n}\n\nfunction errorResult(text: string): CallToolResult {\n return { isError: true, content: [{ type: \"text\", text }] };\n}\n\n/**\n * One actionable line for a strategy that could not run. Mirrors\n * `describeFailure` in screenshot.ts: match on the failing Playwright call,\n * never on substrings of a user-supplied selector.\n */\nexport function describeFormTestFailure(\n input: Pick<ParsedFormTestInput, \"url\" | \"wait_for\" | \"wait_for_timeout_ms\">,\n error: unknown,\n): string {\n const message = error instanceof Error ? error.message : String(error);\n const line = firstLine(message);\n\n if (/Executable doesn't exist|browserType\\.launch/i.test(message)) {\n return (\n \"Playwright's Chromium browser is not installed. \" +\n `Run \\`npx playwright install chromium\\` and try again. (${line})`\n );\n }\n if (input.wait_for && /^page\\.waitForSelector:/.test(message)) {\n return `selector \"${input.wait_for}\" did not become visible within ${input.wait_for_timeout_ms}ms`;\n }\n if (/^page\\.goto:/.test(message)) {\n return `the page could not be opened — ${line}`;\n }\n return line;\n}\n\nexport function registerFormTestTool(server: McpServer): void {\n server.registerTool(\n FORM_TEST_TOOL_NAME,\n {\n title: \"Form test\",\n description:\n \"Fill a page's forms with awkward data and report what breaks. Each strategy — valid data, everything \" +\n \"empty, maxlength, special characters, Arabic (RTL), digits, whitespace, boundary values, and harmless \" +\n \"XSS payloads — runs in its own fresh page and comes back as a screenshot plus what the page did: the \" +\n \"validation messages it showed, the browser's own constraint validation, the requests it made, its \" +\n \"console output, and any value it silently truncated. Set `submit: true` to send the form as well, \" +\n \"which is what surfaces the two findings that matter most: a submit that shows no error and a submit \" +\n \"that never makes a request. Works on a page with no <form> element too.\",\n inputSchema: formTestInputShape,\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },\n },\n async (args) => testForms(args),\n );\n}\n"]}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
export { registerScreenshotTool, takeScreenshot, SCREENSHOT_TOOL_NAME } from "./screenshot.js";
|
|
3
3
|
export { registerCaptureTool, capturePage, CAPTURE_TOOL_NAME } from "./capture.js";
|
|
4
|
+
export { registerApiMockTool, mockApi, API_MOCK_TOOL_NAME } from "./api-mock.js";
|
|
4
5
|
export { registerInteractTool, performInteraction, INTERACT_TOOL_NAME } from "./interact.js";
|
|
6
|
+
export { registerSnapshotTool, snapshotPage, SNAPSHOT_TOOL_NAME } from "./snapshot.js";
|
|
7
|
+
export { registerInspectTool, inspectElements, INSPECT_TOOL_NAME } from "./inspect.js";
|
|
8
|
+
export { registerWaitForTool, waitFor, WAIT_FOR_TOOL_NAME } from "./wait-for.js";
|
|
5
9
|
export { registerResponsiveTool, captureResponsive, RESPONSIVE_TOOL_NAME } from "./responsive.js";
|
|
6
10
|
export { registerAccessibilityTool, auditAccessibility, ACCESSIBILITY_TOOL_NAME } from "./accessibility.js";
|
|
7
11
|
export { registerCompareTool, comparePages, COMPARE_TOOL_NAME, CURRENT_PAGE } from "./compare.js";
|
|
12
|
+
export { registerSaveAuthTool, saveAuth, SAVE_AUTH_TOOL_NAME } from "./save-auth.js";
|
|
13
|
+
export { registerFormTestTool, testForms, FORM_TEST_TOOL_NAME } from "./form-test.js";
|
|
14
|
+
export { registerSeoTool, auditSeo, SEO_TOOL_NAME } from "./seo.js";
|
|
15
|
+
export { registerDeadClicksTool, findDeadClicks, DEAD_CLICKS_TOOL_NAME } from "./dead-clicks.js";
|
|
16
|
+
export { registerLinksTool, checkLinks, LINKS_TOOL_NAME } from "./links.js";
|
|
17
|
+
export { registerRtlTool, testRtl, RTL_TOOL_NAME } from "./rtl.js";
|
|
8
18
|
export { registerServerTools, startServer, stopServer, START_SERVER_TOOL_NAME, STOP_SERVER_TOOL_NAME, } from "./server.js";
|
|
9
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Register every FrameWatch tool on the given server.
|
|
21
|
+
*
|
|
22
|
+
* Every handler is wrapped so its result passes through the image budget on
|
|
23
|
+
* the way out (see `utils/budget.ts`): a result over the client's cap loses
|
|
24
|
+
* all its images, so this is the one place that guarantees none ever is.
|
|
25
|
+
*/
|
|
10
26
|
export declare function registerAllTools(server: McpServer): void;
|