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,294 @@
|
|
|
1
|
+
import { MAX_FORM_FILL_LENGTH } from "../constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* Test data generators.
|
|
4
|
+
*
|
|
5
|
+
* One job: given a field and a strategy, say what should go in it. Nothing
|
|
6
|
+
* here touches a browser, so the interesting decisions — what "valid" means
|
|
7
|
+
* for a field called `user_email`, what happens when Arabic text meets
|
|
8
|
+
* `<input type="date">` — are testable on their own.
|
|
9
|
+
*
|
|
10
|
+
* Two rules run through all of it:
|
|
11
|
+
*
|
|
12
|
+
* - A value must be one the field can actually hold. Chromium refuses a
|
|
13
|
+
* malformed value outright for the constrained input types (number, date,
|
|
14
|
+
* color and friends), so a strategy that cannot be expressed in one of
|
|
15
|
+
* them falls back to a valid value rather than producing a fill error that
|
|
16
|
+
* says nothing about the app.
|
|
17
|
+
* - A value never exceeds the field's own `maxlength`. Otherwise every
|
|
18
|
+
* strategy would report the same truncation on the same short field, and
|
|
19
|
+
* the one report that matters — an app truncating input in JavaScript,
|
|
20
|
+
* with no maxlength anywhere — would be lost in it.
|
|
21
|
+
*/
|
|
22
|
+
export const FORM_STRATEGIES = [
|
|
23
|
+
"valid",
|
|
24
|
+
"empty",
|
|
25
|
+
"maxlength",
|
|
26
|
+
"special_chars",
|
|
27
|
+
"rtl_arabic",
|
|
28
|
+
"numbers_only",
|
|
29
|
+
"spaces_only",
|
|
30
|
+
"boundary",
|
|
31
|
+
"xss",
|
|
32
|
+
];
|
|
33
|
+
/**
|
|
34
|
+
* What `framewatch_form_test` runs when the caller names no strategies: the
|
|
35
|
+
* happy path, the empty submit, the characters that break naive escaping, and
|
|
36
|
+
* RTL text. Between them they catch most of what a form gets wrong.
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_FORM_STRATEGIES = ["valid", "empty", "special_chars", "rtl_arabic"];
|
|
39
|
+
/**
|
|
40
|
+
* The global an XSS payload sets when it executes.
|
|
41
|
+
*
|
|
42
|
+
* Payloads are written to assign this rather than to do anything: whether the
|
|
43
|
+
* marker is set after a submit is a yes/no answer to "did my input run as
|
|
44
|
+
* code", where looking for the payload in the DOM afterwards is guesswork —
|
|
45
|
+
* an input's own `value` serialises with its angle brackets intact and would
|
|
46
|
+
* read as an injection on a page that escaped everything correctly.
|
|
47
|
+
*/
|
|
48
|
+
export const XSS_MARKER = "__framewatch_xss";
|
|
49
|
+
/** Input types whose value Chromium validates syntactically, and will not hold arbitrary text. */
|
|
50
|
+
const CONSTRAINED = new Set(["number", "range", "date", "month", "week", "time", "datetime-local", "color"]);
|
|
51
|
+
/** A number field's value is a syntax, not a length — a thousand digits is not a longer number, it is Infinity. */
|
|
52
|
+
const MAX_NUMBER_DIGITS = 15;
|
|
53
|
+
export function describeStrategy(strategy) {
|
|
54
|
+
switch (strategy) {
|
|
55
|
+
case "valid":
|
|
56
|
+
return "realistic data of the right shape for each field — the happy path";
|
|
57
|
+
case "empty":
|
|
58
|
+
return "every field cleared, then submitted — does validation catch it";
|
|
59
|
+
case "maxlength":
|
|
60
|
+
return `every field filled to its maxlength, or ${MAX_FORM_FILL_LENGTH} characters when it has none`;
|
|
61
|
+
case "special_chars":
|
|
62
|
+
return "quotes, angle brackets, backslashes, an SQL fragment, CJK, Arabic and an emoji";
|
|
63
|
+
case "rtl_arabic":
|
|
64
|
+
return "Arabic text in every field — catches RTL layout and bidirectional text bugs";
|
|
65
|
+
case "numbers_only":
|
|
66
|
+
return "digits in every field, including the ones that expect words";
|
|
67
|
+
case "spaces_only":
|
|
68
|
+
return "whitespace only — input that looks filled in but is empty once trimmed";
|
|
69
|
+
case "boundary":
|
|
70
|
+
return "the edges: one character for text, min/max for numbers and dates";
|
|
71
|
+
case "xss":
|
|
72
|
+
return "harmless XSS payloads that set a marker instead of doing damage — reported if any of them runs";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The value to put in a text-like field.
|
|
77
|
+
*
|
|
78
|
+
* `variant` is the field's position in the form. It is used to walk through
|
|
79
|
+
* the XSS payload list, so a form with five fields tries five different
|
|
80
|
+
* injections in one pass instead of the same one five times.
|
|
81
|
+
*/
|
|
82
|
+
export function valueFor(field, strategy, variant = 0) {
|
|
83
|
+
if (strategy === "empty")
|
|
84
|
+
return "";
|
|
85
|
+
if (CONSTRAINED.has(field.type))
|
|
86
|
+
return clampToRange(field, constrainedValue(field, strategy));
|
|
87
|
+
return clampLength(field, textValue(field, strategy, variant));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Which option a `<select>` should end up on, or null to leave it alone.
|
|
91
|
+
*
|
|
92
|
+
* Disabled options are never chosen — picking one fails, and a form that
|
|
93
|
+
* offers nothing else has a bug worth reporting on its own.
|
|
94
|
+
*/
|
|
95
|
+
export function optionFor(field, strategy) {
|
|
96
|
+
const enabled = field.options.filter((option) => !option.disabled);
|
|
97
|
+
// The blank option is a placeholder ("Choose one…"), which is what "empty"
|
|
98
|
+
// means for a select — and the only way to clear one from the outside.
|
|
99
|
+
const placeholder = enabled.find((option) => option.value === "");
|
|
100
|
+
const real = enabled.filter((option) => option.value !== "");
|
|
101
|
+
if (strategy === "empty")
|
|
102
|
+
return placeholder ? placeholder.value : null;
|
|
103
|
+
if (strategy === "boundary")
|
|
104
|
+
return real.at(-1)?.value ?? placeholder?.value ?? null;
|
|
105
|
+
return real[0]?.value ?? placeholder?.value ?? null;
|
|
106
|
+
}
|
|
107
|
+
/** Whether a checkbox or radio should end up ticked. */
|
|
108
|
+
export function checkedFor(strategy) {
|
|
109
|
+
return strategy !== "empty";
|
|
110
|
+
}
|
|
111
|
+
/* ── Text fields ──────────────────────────────────────────────────────── */
|
|
112
|
+
const SPECIAL_CHARS = `O'Brien "quoted" <tag> & 100% \\ / ; -- ' OR '1'='1 — 你好 مرحبا ñ 🎉`;
|
|
113
|
+
const ARABIC = "مرحبا بالعالم — اختبار فريم ووتش";
|
|
114
|
+
const ARABIC_SHORT = "اختبار";
|
|
115
|
+
/** Payloads that announce themselves instead of doing damage. See XSS_MARKER. */
|
|
116
|
+
const XSS_PAYLOADS = [
|
|
117
|
+
`<img src=x onerror="window.${XSS_MARKER}=1">`,
|
|
118
|
+
`"><svg onload="window.${XSS_MARKER}=1">`,
|
|
119
|
+
`<script>window.${XSS_MARKER}=1</script>`,
|
|
120
|
+
`javascript:window.${XSS_MARKER}=1`,
|
|
121
|
+
`'><img src=x onerror=window.${XSS_MARKER}=1>`,
|
|
122
|
+
];
|
|
123
|
+
/** Repeated to fill a field to its limit. Not all one character, so a truncation point is visible in a screenshot. */
|
|
124
|
+
const FILLER = "FrameWatch0123456789 ";
|
|
125
|
+
function textValue(field, strategy, variant) {
|
|
126
|
+
switch (strategy) {
|
|
127
|
+
case "valid":
|
|
128
|
+
return validText(field);
|
|
129
|
+
case "maxlength":
|
|
130
|
+
return repeatTo(FILLER, field.maxlength ?? MAX_FORM_FILL_LENGTH);
|
|
131
|
+
case "special_chars":
|
|
132
|
+
return field.type === "email" ? `"${SPECIAL_CHARS}"@example.com` : SPECIAL_CHARS;
|
|
133
|
+
case "rtl_arabic":
|
|
134
|
+
return field.type === "email" ? `${ARABIC_SHORT}@example.com` : ARABIC;
|
|
135
|
+
case "numbers_only":
|
|
136
|
+
return "1234567890";
|
|
137
|
+
case "spaces_only":
|
|
138
|
+
return " ";
|
|
139
|
+
case "boundary":
|
|
140
|
+
// The shortest thing a field of this kind can hold: the classic
|
|
141
|
+
// off-by-one, and for the typed fields the shortest *valid* value.
|
|
142
|
+
return field.type === "email" ? "a@b.co" : field.type === "url" ? "http://a.co" : "a";
|
|
143
|
+
case "xss":
|
|
144
|
+
return XSS_PAYLOADS[variant % XSS_PAYLOADS.length];
|
|
145
|
+
case "empty":
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Realistic data for one field.
|
|
151
|
+
*
|
|
152
|
+
* The input type is the strongest signal and is read first. Beyond that a
|
|
153
|
+
* field is only ever `type="text"`, so the name, label and placeholder are
|
|
154
|
+
* matched for the handful of shapes that have a wrong answer — an address in
|
|
155
|
+
* a phone field tells you nothing about the app.
|
|
156
|
+
*/
|
|
157
|
+
function validText(field) {
|
|
158
|
+
switch (field.type) {
|
|
159
|
+
case "email":
|
|
160
|
+
return "framewatch@example.com";
|
|
161
|
+
case "tel":
|
|
162
|
+
return "+15550101234";
|
|
163
|
+
case "url":
|
|
164
|
+
return "https://example.com/framewatch";
|
|
165
|
+
case "password":
|
|
166
|
+
return "FrameWatch!2024";
|
|
167
|
+
case "search":
|
|
168
|
+
return "framewatch";
|
|
169
|
+
}
|
|
170
|
+
const hint = `${field.name ?? ""} ${field.label ?? ""} ${field.placeholder ?? ""}`.toLowerCase();
|
|
171
|
+
for (const [pattern, value] of HINTS) {
|
|
172
|
+
if (pattern.test(hint))
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
return field.type === "textarea" ? "FrameWatch test message. Nothing here is real data." : "FrameWatch test";
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Name/label patterns worth answering specifically, most specific first —
|
|
179
|
+
* "email address" must not be read as an address.
|
|
180
|
+
*/
|
|
181
|
+
const HINTS = [
|
|
182
|
+
[/e-?mail/, "framewatch@example.com"],
|
|
183
|
+
[/phone|mobile|\btel\b/, "+15550101234"],
|
|
184
|
+
[/first.?name|given.?name|forename/, "Ada"],
|
|
185
|
+
[/last.?name|surname|family.?name/, "Lovelace"],
|
|
186
|
+
[/full.?name|^\s*name|user.?name|\blogin\b|nickname/, "Ada Lovelace"],
|
|
187
|
+
[/company|organi[sz]ation|business/, "FrameWatch Ltd"],
|
|
188
|
+
[/street|address(?!.*e-?mail)/, "123 Test Street"],
|
|
189
|
+
[/\bcity\b|town/, "Springfield"],
|
|
190
|
+
[/\bstate\b|province|region/, "California"],
|
|
191
|
+
[/post.?code|\bzip\b|postal/, "94103"],
|
|
192
|
+
[/country/, "Testland"],
|
|
193
|
+
[/card.?number|\bcc.?num/, "4111111111111111"],
|
|
194
|
+
[/\bcvv\b|\bcvc\b|security.?code/, "123"],
|
|
195
|
+
[/\bage\b|quantity|\bqty\b|amount|\bcount\b/, "30"],
|
|
196
|
+
[/message|comment|feedback|notes?\b|bio\b|description|about/, "FrameWatch test message. Nothing here is real data."],
|
|
197
|
+
[/subject|title|headline/, "FrameWatch test"],
|
|
198
|
+
[/\burl\b|website|\blink\b/, "https://example.com/framewatch"],
|
|
199
|
+
[/password|passphrase|\bpin\b/, "FrameWatch!2024"],
|
|
200
|
+
[/coupon|promo|voucher|referral/, "TESTCODE"],
|
|
201
|
+
];
|
|
202
|
+
/** Cut a value to the field's own maxlength. See the second rule at the top of this file. */
|
|
203
|
+
function clampLength(field, value) {
|
|
204
|
+
const limit = field.maxlength;
|
|
205
|
+
if (limit === undefined || limit < 0 || value.length <= limit)
|
|
206
|
+
return value;
|
|
207
|
+
return value.slice(0, limit);
|
|
208
|
+
}
|
|
209
|
+
function repeatTo(pattern, length) {
|
|
210
|
+
if (length <= 0)
|
|
211
|
+
return "";
|
|
212
|
+
return pattern.repeat(Math.ceil(length / pattern.length)).slice(0, length);
|
|
213
|
+
}
|
|
214
|
+
/* ── Constrained fields ───────────────────────────────────────────────── */
|
|
215
|
+
/**
|
|
216
|
+
* A value a constrained input will accept, varied by strategy as far as the
|
|
217
|
+
* type allows. Arabic text cannot go in a date field; what a date field can
|
|
218
|
+
* still say is what happens at its declared minimum, so the strategies that
|
|
219
|
+
* have no expression here collapse onto the valid value.
|
|
220
|
+
*/
|
|
221
|
+
function constrainedValue(field, strategy) {
|
|
222
|
+
const extreme = strategy === "boundary" || strategy === "maxlength";
|
|
223
|
+
switch (field.type) {
|
|
224
|
+
case "number":
|
|
225
|
+
case "range":
|
|
226
|
+
return numberValue(field, strategy, extreme);
|
|
227
|
+
case "date":
|
|
228
|
+
return extreme ? (field.max ?? field.min ?? "9999-12-31") : "2024-06-15";
|
|
229
|
+
case "month":
|
|
230
|
+
return extreme ? (field.max ?? field.min ?? "9999-12") : "2024-06";
|
|
231
|
+
case "week":
|
|
232
|
+
return extreme ? (field.max ?? field.min ?? "9999-W52") : "2024-W24";
|
|
233
|
+
case "time":
|
|
234
|
+
return extreme ? (field.max ?? field.min ?? "23:59") : "14:30";
|
|
235
|
+
case "datetime-local":
|
|
236
|
+
return extreme ? (field.max ?? field.min ?? "9999-12-31T23:59") : "2024-06-15T14:30";
|
|
237
|
+
case "color":
|
|
238
|
+
return extreme ? "#000000" : strategy === "numbers_only" ? "#123456" : "#3366ff";
|
|
239
|
+
default:
|
|
240
|
+
return "";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function numberValue(field, strategy, extreme) {
|
|
244
|
+
const min = toNumber(field.min);
|
|
245
|
+
const max = toNumber(field.max);
|
|
246
|
+
if (extreme) {
|
|
247
|
+
if (max !== undefined)
|
|
248
|
+
return field.max;
|
|
249
|
+
if (min !== undefined)
|
|
250
|
+
return field.min;
|
|
251
|
+
return strategy === "maxlength" ? "9".repeat(MAX_NUMBER_DIGITS) : "0";
|
|
252
|
+
}
|
|
253
|
+
if (min !== undefined && max !== undefined)
|
|
254
|
+
return String(Math.round((min + max) / 2));
|
|
255
|
+
if (min !== undefined)
|
|
256
|
+
return field.min;
|
|
257
|
+
if (max !== undefined)
|
|
258
|
+
return field.max;
|
|
259
|
+
return strategy === "numbers_only" ? "1234567890" : "42";
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Keep a value inside the field's declared range. Every constrained type
|
|
263
|
+
* except colour orders lexicographically in its own syntax (ISO dates, times,
|
|
264
|
+
* weeks), so the same comparison works for all of them once numbers are
|
|
265
|
+
* handled numerically.
|
|
266
|
+
*/
|
|
267
|
+
function clampToRange(field, value) {
|
|
268
|
+
if (value === "" || field.type === "color")
|
|
269
|
+
return value;
|
|
270
|
+
if (field.type === "number" || field.type === "range") {
|
|
271
|
+
const parsed = Number(value);
|
|
272
|
+
if (!Number.isFinite(parsed))
|
|
273
|
+
return value;
|
|
274
|
+
const min = toNumber(field.min);
|
|
275
|
+
const max = toNumber(field.max);
|
|
276
|
+
if (min !== undefined && parsed < min)
|
|
277
|
+
return field.min;
|
|
278
|
+
if (max !== undefined && parsed > max)
|
|
279
|
+
return field.max;
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
if (field.min !== undefined && value < field.min)
|
|
283
|
+
return field.min;
|
|
284
|
+
if (field.max !== undefined && value > field.max)
|
|
285
|
+
return field.max;
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
function toNumber(value) {
|
|
289
|
+
if (value === undefined || value.trim() === "")
|
|
290
|
+
return undefined;
|
|
291
|
+
const parsed = Number(value);
|
|
292
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=test-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-data.js","sourceRoot":"","sources":["../../src/utils/test-data.ts"],"names":[],"mappings":"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","sourcesContent":["import { MAX_FORM_FILL_LENGTH } from \"../constants.js\";\n\n/**\n * Test data generators.\n *\n * One job: given a field and a strategy, say what should go in it. Nothing\n * here touches a browser, so the interesting decisions — what \"valid\" means\n * for a field called `user_email`, what happens when Arabic text meets\n * `<input type=\"date\">` — are testable on their own.\n *\n * Two rules run through all of it:\n *\n * - A value must be one the field can actually hold. Chromium refuses a\n * malformed value outright for the constrained input types (number, date,\n * color and friends), so a strategy that cannot be expressed in one of\n * them falls back to a valid value rather than producing a fill error that\n * says nothing about the app.\n * - A value never exceeds the field's own `maxlength`. Otherwise every\n * strategy would report the same truncation on the same short field, and\n * the one report that matters — an app truncating input in JavaScript,\n * with no maxlength anywhere — would be lost in it.\n */\n\nexport const FORM_STRATEGIES = [\n \"valid\",\n \"empty\",\n \"maxlength\",\n \"special_chars\",\n \"rtl_arabic\",\n \"numbers_only\",\n \"spaces_only\",\n \"boundary\",\n \"xss\",\n] as const;\n\nexport type FormStrategy = (typeof FORM_STRATEGIES)[number];\n\n/**\n * What `framewatch_form_test` runs when the caller names no strategies: the\n * happy path, the empty submit, the characters that break naive escaping, and\n * RTL text. Between them they catch most of what a form gets wrong.\n */\nexport const DEFAULT_FORM_STRATEGIES: FormStrategy[] = [\"valid\", \"empty\", \"special_chars\", \"rtl_arabic\"];\n\n/**\n * The global an XSS payload sets when it executes.\n *\n * Payloads are written to assign this rather than to do anything: whether the\n * marker is set after a submit is a yes/no answer to \"did my input run as\n * code\", where looking for the payload in the DOM afterwards is guesswork —\n * an input's own `value` serialises with its angle brackets intact and would\n * read as an injection on a page that escaped everything correctly.\n */\nexport const XSS_MARKER = \"__framewatch_xss\";\n\nexport interface OptionInfo {\n value: string;\n label: string;\n disabled: boolean;\n}\n\n/** What the generators need to know about a field. A subset of `FieldInfo` in engine/forms.ts. */\nexport interface FieldShape {\n /** Input type lowercased (\"text\", \"email\", …), \"textarea\", or \"select-one\" / \"select-multiple\". */\n type: string;\n name?: string;\n label?: string;\n placeholder?: string;\n maxlength?: number;\n min?: string;\n max?: string;\n step?: string;\n}\n\n/** Input types whose value Chromium validates syntactically, and will not hold arbitrary text. */\nconst CONSTRAINED = new Set([\"number\", \"range\", \"date\", \"month\", \"week\", \"time\", \"datetime-local\", \"color\"]);\n\n/** A number field's value is a syntax, not a length — a thousand digits is not a longer number, it is Infinity. */\nconst MAX_NUMBER_DIGITS = 15;\n\nexport function describeStrategy(strategy: FormStrategy): string {\n switch (strategy) {\n case \"valid\":\n return \"realistic data of the right shape for each field — the happy path\";\n case \"empty\":\n return \"every field cleared, then submitted — does validation catch it\";\n case \"maxlength\":\n return `every field filled to its maxlength, or ${MAX_FORM_FILL_LENGTH} characters when it has none`;\n case \"special_chars\":\n return \"quotes, angle brackets, backslashes, an SQL fragment, CJK, Arabic and an emoji\";\n case \"rtl_arabic\":\n return \"Arabic text in every field — catches RTL layout and bidirectional text bugs\";\n case \"numbers_only\":\n return \"digits in every field, including the ones that expect words\";\n case \"spaces_only\":\n return \"whitespace only — input that looks filled in but is empty once trimmed\";\n case \"boundary\":\n return \"the edges: one character for text, min/max for numbers and dates\";\n case \"xss\":\n return \"harmless XSS payloads that set a marker instead of doing damage — reported if any of them runs\";\n }\n}\n\n/**\n * The value to put in a text-like field.\n *\n * `variant` is the field's position in the form. It is used to walk through\n * the XSS payload list, so a form with five fields tries five different\n * injections in one pass instead of the same one five times.\n */\nexport function valueFor(field: FieldShape, strategy: FormStrategy, variant = 0): string {\n if (strategy === \"empty\") return \"\";\n if (CONSTRAINED.has(field.type)) return clampToRange(field, constrainedValue(field, strategy));\n return clampLength(field, textValue(field, strategy, variant));\n}\n\n/**\n * Which option a `<select>` should end up on, or null to leave it alone.\n *\n * Disabled options are never chosen — picking one fails, and a form that\n * offers nothing else has a bug worth reporting on its own.\n */\nexport function optionFor(field: FieldShape & { options: OptionInfo[] }, strategy: FormStrategy): string | null {\n const enabled = field.options.filter((option) => !option.disabled);\n // The blank option is a placeholder (\"Choose one…\"), which is what \"empty\"\n // means for a select — and the only way to clear one from the outside.\n const placeholder = enabled.find((option) => option.value === \"\");\n const real = enabled.filter((option) => option.value !== \"\");\n\n if (strategy === \"empty\") return placeholder ? placeholder.value : null;\n if (strategy === \"boundary\") return real.at(-1)?.value ?? placeholder?.value ?? null;\n return real[0]?.value ?? placeholder?.value ?? null;\n}\n\n/** Whether a checkbox or radio should end up ticked. */\nexport function checkedFor(strategy: FormStrategy): boolean {\n return strategy !== \"empty\";\n}\n\n/* ── Text fields ──────────────────────────────────────────────────────── */\n\nconst SPECIAL_CHARS = `O'Brien \"quoted\" <tag> & 100% \\\\ / ; -- ' OR '1'='1 — 你好 مرحبا ñ 🎉`;\nconst ARABIC = \"مرحبا بالعالم — اختبار فريم ووتش\";\nconst ARABIC_SHORT = \"اختبار\";\n\n/** Payloads that announce themselves instead of doing damage. See XSS_MARKER. */\nconst XSS_PAYLOADS = [\n `<img src=x onerror=\"window.${XSS_MARKER}=1\">`,\n `\"><svg onload=\"window.${XSS_MARKER}=1\">`,\n `<script>window.${XSS_MARKER}=1</script>`,\n `javascript:window.${XSS_MARKER}=1`,\n `'><img src=x onerror=window.${XSS_MARKER}=1>`,\n];\n\n/** Repeated to fill a field to its limit. Not all one character, so a truncation point is visible in a screenshot. */\nconst FILLER = \"FrameWatch0123456789 \";\n\nfunction textValue(field: FieldShape, strategy: FormStrategy, variant: number): string {\n switch (strategy) {\n case \"valid\":\n return validText(field);\n case \"maxlength\":\n return repeatTo(FILLER, field.maxlength ?? MAX_FORM_FILL_LENGTH);\n case \"special_chars\":\n return field.type === \"email\" ? `\"${SPECIAL_CHARS}\"@example.com` : SPECIAL_CHARS;\n case \"rtl_arabic\":\n return field.type === \"email\" ? `${ARABIC_SHORT}@example.com` : ARABIC;\n case \"numbers_only\":\n return \"1234567890\";\n case \"spaces_only\":\n return \" \";\n case \"boundary\":\n // The shortest thing a field of this kind can hold: the classic\n // off-by-one, and for the typed fields the shortest *valid* value.\n return field.type === \"email\" ? \"a@b.co\" : field.type === \"url\" ? \"http://a.co\" : \"a\";\n case \"xss\":\n return XSS_PAYLOADS[variant % XSS_PAYLOADS.length];\n case \"empty\":\n return \"\";\n }\n}\n\n/**\n * Realistic data for one field.\n *\n * The input type is the strongest signal and is read first. Beyond that a\n * field is only ever `type=\"text\"`, so the name, label and placeholder are\n * matched for the handful of shapes that have a wrong answer — an address in\n * a phone field tells you nothing about the app.\n */\nfunction validText(field: FieldShape): string {\n switch (field.type) {\n case \"email\":\n return \"framewatch@example.com\";\n case \"tel\":\n return \"+15550101234\";\n case \"url\":\n return \"https://example.com/framewatch\";\n case \"password\":\n return \"FrameWatch!2024\";\n case \"search\":\n return \"framewatch\";\n }\n\n const hint = `${field.name ?? \"\"} ${field.label ?? \"\"} ${field.placeholder ?? \"\"}`.toLowerCase();\n for (const [pattern, value] of HINTS) {\n if (pattern.test(hint)) return value;\n }\n return field.type === \"textarea\" ? \"FrameWatch test message. Nothing here is real data.\" : \"FrameWatch test\";\n}\n\n/**\n * Name/label patterns worth answering specifically, most specific first —\n * \"email address\" must not be read as an address.\n */\nconst HINTS: Array<[RegExp, string]> = [\n [/e-?mail/, \"framewatch@example.com\"],\n [/phone|mobile|\\btel\\b/, \"+15550101234\"],\n [/first.?name|given.?name|forename/, \"Ada\"],\n [/last.?name|surname|family.?name/, \"Lovelace\"],\n [/full.?name|^\\s*name|user.?name|\\blogin\\b|nickname/, \"Ada Lovelace\"],\n [/company|organi[sz]ation|business/, \"FrameWatch Ltd\"],\n [/street|address(?!.*e-?mail)/, \"123 Test Street\"],\n [/\\bcity\\b|town/, \"Springfield\"],\n [/\\bstate\\b|province|region/, \"California\"],\n [/post.?code|\\bzip\\b|postal/, \"94103\"],\n [/country/, \"Testland\"],\n [/card.?number|\\bcc.?num/, \"4111111111111111\"],\n [/\\bcvv\\b|\\bcvc\\b|security.?code/, \"123\"],\n [/\\bage\\b|quantity|\\bqty\\b|amount|\\bcount\\b/, \"30\"],\n [/message|comment|feedback|notes?\\b|bio\\b|description|about/, \"FrameWatch test message. Nothing here is real data.\"],\n [/subject|title|headline/, \"FrameWatch test\"],\n [/\\burl\\b|website|\\blink\\b/, \"https://example.com/framewatch\"],\n [/password|passphrase|\\bpin\\b/, \"FrameWatch!2024\"],\n [/coupon|promo|voucher|referral/, \"TESTCODE\"],\n];\n\n/** Cut a value to the field's own maxlength. See the second rule at the top of this file. */\nfunction clampLength(field: FieldShape, value: string): string {\n const limit = field.maxlength;\n if (limit === undefined || limit < 0 || value.length <= limit) return value;\n return value.slice(0, limit);\n}\n\nfunction repeatTo(pattern: string, length: number): string {\n if (length <= 0) return \"\";\n return pattern.repeat(Math.ceil(length / pattern.length)).slice(0, length);\n}\n\n/* ── Constrained fields ───────────────────────────────────────────────── */\n\n/**\n * A value a constrained input will accept, varied by strategy as far as the\n * type allows. Arabic text cannot go in a date field; what a date field can\n * still say is what happens at its declared minimum, so the strategies that\n * have no expression here collapse onto the valid value.\n */\nfunction constrainedValue(field: FieldShape, strategy: FormStrategy): string {\n const extreme = strategy === \"boundary\" || strategy === \"maxlength\";\n\n switch (field.type) {\n case \"number\":\n case \"range\":\n return numberValue(field, strategy, extreme);\n case \"date\":\n return extreme ? (field.max ?? field.min ?? \"9999-12-31\") : \"2024-06-15\";\n case \"month\":\n return extreme ? (field.max ?? field.min ?? \"9999-12\") : \"2024-06\";\n case \"week\":\n return extreme ? (field.max ?? field.min ?? \"9999-W52\") : \"2024-W24\";\n case \"time\":\n return extreme ? (field.max ?? field.min ?? \"23:59\") : \"14:30\";\n case \"datetime-local\":\n return extreme ? (field.max ?? field.min ?? \"9999-12-31T23:59\") : \"2024-06-15T14:30\";\n case \"color\":\n return extreme ? \"#000000\" : strategy === \"numbers_only\" ? \"#123456\" : \"#3366ff\";\n default:\n return \"\";\n }\n}\n\nfunction numberValue(field: FieldShape, strategy: FormStrategy, extreme: boolean): string {\n const min = toNumber(field.min);\n const max = toNumber(field.max);\n\n if (extreme) {\n if (max !== undefined) return field.max!;\n if (min !== undefined) return field.min!;\n return strategy === \"maxlength\" ? \"9\".repeat(MAX_NUMBER_DIGITS) : \"0\";\n }\n if (min !== undefined && max !== undefined) return String(Math.round((min + max) / 2));\n if (min !== undefined) return field.min!;\n if (max !== undefined) return field.max!;\n return strategy === \"numbers_only\" ? \"1234567890\" : \"42\";\n}\n\n/**\n * Keep a value inside the field's declared range. Every constrained type\n * except colour orders lexicographically in its own syntax (ISO dates, times,\n * weeks), so the same comparison works for all of them once numbers are\n * handled numerically.\n */\nfunction clampToRange(field: FieldShape, value: string): string {\n if (value === \"\" || field.type === \"color\") return value;\n\n if (field.type === \"number\" || field.type === \"range\") {\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) return value;\n const min = toNumber(field.min);\n const max = toNumber(field.max);\n if (min !== undefined && parsed < min) return field.min!;\n if (max !== undefined && parsed > max) return field.max!;\n return value;\n }\n\n if (field.min !== undefined && value < field.min) return field.min;\n if (field.max !== undefined && value > field.max) return field.max;\n return value;\n}\n\nfunction toNumber(value: string | undefined): number | undefined {\n if (value === undefined || value.trim() === \"\") return undefined;\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Vue support decides and says.
|
|
3
|
+
*
|
|
4
|
+
* Pure: what a Vite console line means, whether a `navigate` value goes
|
|
5
|
+
* through vue-router or the address bar, and the wording of every line about
|
|
6
|
+
* a Vue app or one of its components. The measurements come from
|
|
7
|
+
* `engine/vue.ts`; nothing here touches a browser.
|
|
8
|
+
*/
|
|
9
|
+
/** One line Vite's client wrote to the console, decoded. */
|
|
10
|
+
export type ViteEvent = {
|
|
11
|
+
kind: "update";
|
|
12
|
+
path: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "css";
|
|
15
|
+
path: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "reload";
|
|
18
|
+
path: string;
|
|
19
|
+
} | {
|
|
20
|
+
kind: "connected";
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Vite's client logs exactly these (unchanged since Vite 2):
|
|
24
|
+
* `[vite] hot updated: /src/App.vue`
|
|
25
|
+
* `[vite] css hot updated: /src/a.css?t=…`
|
|
26
|
+
* `[vite] page reload src/main.js`
|
|
27
|
+
* `[vite] connected.`
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseViteLine(text: string): ViteEvent | null;
|
|
30
|
+
export interface NavigatePlan {
|
|
31
|
+
/** `router` for vue-router's `push`; `load` for a full navigation. */
|
|
32
|
+
via: "router" | "load";
|
|
33
|
+
/** The router path (with query and hash) or the absolute URL to load. */
|
|
34
|
+
target: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A path, or a URL on the page's own origin, goes through the router when
|
|
38
|
+
* there is one — that keeps the app's state and skips the reload, which is
|
|
39
|
+
* what a person clicking a link in the app gets. Anything else is a full load.
|
|
40
|
+
*/
|
|
41
|
+
export declare function classifyNavigate(value: string, currentUrl: string, hasRouter: boolean): NavigatePlan;
|
|
42
|
+
/** The app on the page, as detected. */
|
|
43
|
+
export interface VueInfo {
|
|
44
|
+
version: string;
|
|
45
|
+
major: number;
|
|
46
|
+
router: boolean;
|
|
47
|
+
route?: {
|
|
48
|
+
path: string;
|
|
49
|
+
name?: string;
|
|
50
|
+
};
|
|
51
|
+
/** A production build: no component data on elements. */
|
|
52
|
+
production: boolean;
|
|
53
|
+
}
|
|
54
|
+
export declare function describeVue(info: VueInfo): string;
|
|
55
|
+
/** One component, as read off an element. Values are already serialised. */
|
|
56
|
+
export interface ComponentInfo {
|
|
57
|
+
name: string;
|
|
58
|
+
props: Record<string, string>;
|
|
59
|
+
state: Record<string, string>;
|
|
60
|
+
/** Ancestors, nearest first, up to the root. */
|
|
61
|
+
path: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface ComponentUnavailable {
|
|
64
|
+
unavailable: string;
|
|
65
|
+
}
|
|
66
|
+
export declare function formatComponentLine(info: ComponentInfo | ComponentUnavailable): string;
|
|
67
|
+
export interface ComponentNode {
|
|
68
|
+
name: string;
|
|
69
|
+
children: ComponentNode[];
|
|
70
|
+
}
|
|
71
|
+
/** The tree, indented by depth, identical leaf siblings collapsed with a count. */
|
|
72
|
+
export declare function formatComponentTree(root: ComponentNode, total: number): string[];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Vue support decides and says.
|
|
3
|
+
*
|
|
4
|
+
* Pure: what a Vite console line means, whether a `navigate` value goes
|
|
5
|
+
* through vue-router or the address bar, and the wording of every line about
|
|
6
|
+
* a Vue app or one of its components. The measurements come from
|
|
7
|
+
* `engine/vue.ts`; nothing here touches a browser.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Vite's client logs exactly these (unchanged since Vite 2):
|
|
11
|
+
* `[vite] hot updated: /src/App.vue`
|
|
12
|
+
* `[vite] css hot updated: /src/a.css?t=…`
|
|
13
|
+
* `[vite] page reload src/main.js`
|
|
14
|
+
* `[vite] connected.`
|
|
15
|
+
*/
|
|
16
|
+
export function parseViteLine(text) {
|
|
17
|
+
const line = text.trim();
|
|
18
|
+
let m = /^\[vite\] css hot updated: (.+)$/.exec(line);
|
|
19
|
+
if (m)
|
|
20
|
+
return { kind: "css", path: m[1].trim() };
|
|
21
|
+
m = /^\[vite\] hot updated: (.+)$/.exec(line);
|
|
22
|
+
if (m)
|
|
23
|
+
return { kind: "update", path: m[1].trim() };
|
|
24
|
+
m = /^\[vite\] page reload(?: (.+))?$/.exec(line);
|
|
25
|
+
if (m)
|
|
26
|
+
return { kind: "reload", path: (m[1] ?? "").trim() };
|
|
27
|
+
if (line === "[vite] connected.")
|
|
28
|
+
return { kind: "connected" };
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A path, or a URL on the page's own origin, goes through the router when
|
|
33
|
+
* there is one — that keeps the app's state and skips the reload, which is
|
|
34
|
+
* what a person clicking a link in the app gets. Anything else is a full load.
|
|
35
|
+
*/
|
|
36
|
+
export function classifyNavigate(value, currentUrl, hasRouter) {
|
|
37
|
+
let resolved;
|
|
38
|
+
try {
|
|
39
|
+
resolved = new URL(value, currentUrl);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { via: "load", target: value };
|
|
43
|
+
}
|
|
44
|
+
if (!hasRouter)
|
|
45
|
+
return { via: "load", target: resolved.href };
|
|
46
|
+
let origin;
|
|
47
|
+
try {
|
|
48
|
+
origin = new URL(currentUrl).origin;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { via: "load", target: resolved.href };
|
|
52
|
+
}
|
|
53
|
+
if (resolved.origin !== origin)
|
|
54
|
+
return { via: "load", target: resolved.href };
|
|
55
|
+
return { via: "router", target: `${resolved.pathname}${resolved.search}${resolved.hash}` };
|
|
56
|
+
}
|
|
57
|
+
export function describeVue(info) {
|
|
58
|
+
const head = `Vue ${info.version}${info.production ? " (production build)" : ""}`;
|
|
59
|
+
if (!info.router || !info.route)
|
|
60
|
+
return `${head} — no router`;
|
|
61
|
+
const name = info.route.name ? ` (${info.route.name})` : "";
|
|
62
|
+
return `${head} — route ${info.route.path}${name}`;
|
|
63
|
+
}
|
|
64
|
+
export function formatComponentLine(info) {
|
|
65
|
+
if ("unavailable" in info)
|
|
66
|
+
return `component: ${info.unavailable}`;
|
|
67
|
+
const sections = [];
|
|
68
|
+
const props = entries(info.props);
|
|
69
|
+
if (props)
|
|
70
|
+
sections.push(`props: ${props}`);
|
|
71
|
+
const state = entries(info.state);
|
|
72
|
+
if (state)
|
|
73
|
+
sections.push(`state: ${state}`);
|
|
74
|
+
let line = `component: ${info.name}`;
|
|
75
|
+
if (sections.length > 0)
|
|
76
|
+
line += ` (${sections.join("; ")})`;
|
|
77
|
+
if (info.path.length > 0)
|
|
78
|
+
line += ` in ${info.path.join(" > ")}`;
|
|
79
|
+
return line;
|
|
80
|
+
}
|
|
81
|
+
function entries(record) {
|
|
82
|
+
const parts = Object.entries(record).map(([key, value]) => `${key}=${value}`);
|
|
83
|
+
return parts.length > 0 ? parts.join(", ") : null;
|
|
84
|
+
}
|
|
85
|
+
/** The tree, indented by depth, identical leaf siblings collapsed with a count. */
|
|
86
|
+
export function formatComponentTree(root, total) {
|
|
87
|
+
const lines = [`Components (${total}):`];
|
|
88
|
+
const walk = (node, depth) => {
|
|
89
|
+
const indent = " ".repeat(depth + 1);
|
|
90
|
+
lines.push(`${indent}${node.name}`);
|
|
91
|
+
const children = node.children;
|
|
92
|
+
for (let i = 0; i < children.length; i++) {
|
|
93
|
+
const child = children[i];
|
|
94
|
+
if (child.children.length === 0) {
|
|
95
|
+
let run = 1;
|
|
96
|
+
while (i + run < children.length && children[i + run].name === child.name && children[i + run].children.length === 0)
|
|
97
|
+
run++;
|
|
98
|
+
lines.push(`${indent} ${child.name}${run > 1 ? ` ×${run}` : ""}`);
|
|
99
|
+
i += run - 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
walk(child, depth + 1);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
walk(root, 0);
|
|
106
|
+
return lines;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=vue-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vue-rules.js","sourceRoot":"","sources":["../../src/utils/vue-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the Vue support decides and says.\n *\n * Pure: what a Vite console line means, whether a `navigate` value goes\n * through vue-router or the address bar, and the wording of every line about\n * a Vue app or one of its components. The measurements come from\n * `engine/vue.ts`; nothing here touches a browser.\n */\n\n/** One line Vite's client wrote to the console, decoded. */\nexport type ViteEvent =\n | { kind: \"update\"; path: string }\n | { kind: \"css\"; path: string }\n | { kind: \"reload\"; path: string }\n | { kind: \"connected\" };\n\n/**\n * Vite's client logs exactly these (unchanged since Vite 2):\n * `[vite] hot updated: /src/App.vue`\n * `[vite] css hot updated: /src/a.css?t=…`\n * `[vite] page reload src/main.js`\n * `[vite] connected.`\n */\nexport function parseViteLine(text: string): ViteEvent | null {\n const line = text.trim();\n let m = /^\\[vite\\] css hot updated: (.+)$/.exec(line);\n if (m) return { kind: \"css\", path: m[1].trim() };\n m = /^\\[vite\\] hot updated: (.+)$/.exec(line);\n if (m) return { kind: \"update\", path: m[1].trim() };\n m = /^\\[vite\\] page reload(?: (.+))?$/.exec(line);\n if (m) return { kind: \"reload\", path: (m[1] ?? \"\").trim() };\n if (line === \"[vite] connected.\") return { kind: \"connected\" };\n return null;\n}\n\nexport interface NavigatePlan {\n /** `router` for vue-router's `push`; `load` for a full navigation. */\n via: \"router\" | \"load\";\n /** The router path (with query and hash) or the absolute URL to load. */\n target: string;\n}\n\n/**\n * A path, or a URL on the page's own origin, goes through the router when\n * there is one — that keeps the app's state and skips the reload, which is\n * what a person clicking a link in the app gets. Anything else is a full load.\n */\nexport function classifyNavigate(value: string, currentUrl: string, hasRouter: boolean): NavigatePlan {\n let resolved: URL;\n try {\n resolved = new URL(value, currentUrl);\n } catch {\n return { via: \"load\", target: value };\n }\n if (!hasRouter) return { via: \"load\", target: resolved.href };\n let origin: string;\n try {\n origin = new URL(currentUrl).origin;\n } catch {\n return { via: \"load\", target: resolved.href };\n }\n if (resolved.origin !== origin) return { via: \"load\", target: resolved.href };\n return { via: \"router\", target: `${resolved.pathname}${resolved.search}${resolved.hash}` };\n}\n\n/** The app on the page, as detected. */\nexport interface VueInfo {\n version: string;\n major: number;\n router: boolean;\n route?: { path: string; name?: string };\n /** A production build: no component data on elements. */\n production: boolean;\n}\n\nexport function describeVue(info: VueInfo): string {\n const head = `Vue ${info.version}${info.production ? \" (production build)\" : \"\"}`;\n if (!info.router || !info.route) return `${head} — no router`;\n const name = info.route.name ? ` (${info.route.name})` : \"\";\n return `${head} — route ${info.route.path}${name}`;\n}\n\n/** One component, as read off an element. Values are already serialised. */\nexport interface ComponentInfo {\n name: string;\n props: Record<string, string>;\n state: Record<string, string>;\n /** Ancestors, nearest first, up to the root. */\n path: string[];\n}\n\nexport interface ComponentUnavailable {\n unavailable: string;\n}\n\nexport function formatComponentLine(info: ComponentInfo | ComponentUnavailable): string {\n if (\"unavailable\" in info) return `component: ${info.unavailable}`;\n const sections: string[] = [];\n const props = entries(info.props);\n if (props) sections.push(`props: ${props}`);\n const state = entries(info.state);\n if (state) sections.push(`state: ${state}`);\n let line = `component: ${info.name}`;\n if (sections.length > 0) line += ` (${sections.join(\"; \")})`;\n if (info.path.length > 0) line += ` in ${info.path.join(\" > \")}`;\n return line;\n}\n\nfunction entries(record: Record<string, string>): string | null {\n const parts = Object.entries(record).map(([key, value]) => `${key}=${value}`);\n return parts.length > 0 ? parts.join(\", \") : null;\n}\n\nexport interface ComponentNode {\n name: string;\n children: ComponentNode[];\n}\n\n/** The tree, indented by depth, identical leaf siblings collapsed with a count. */\nexport function formatComponentTree(root: ComponentNode, total: number): string[] {\n const lines = [`Components (${total}):`];\n const walk = (node: ComponentNode, depth: number): void => {\n const indent = \" \".repeat(depth + 1);\n lines.push(`${indent}${node.name}`);\n const children = node.children;\n for (let i = 0; i < children.length; i++) {\n const child = children[i];\n if (child.children.length === 0) {\n let run = 1;\n while (i + run < children.length && children[i + run].name === child.name && children[i + run].children.length === 0) run++;\n lines.push(`${indent} ${child.name}${run > 1 ? ` ×${run}` : \"\"}`);\n i += run - 1;\n continue;\n }\n walk(child, depth + 1);\n }\n };\n walk(root, 0);\n return lines;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framewatch-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for smart visual testing of web apps — frame capture, diffing, and interaction replay",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "kekoDev (https://github.com/kekoDev)",
|
|
@@ -43,16 +43,18 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
46
|
+
"axe-core": "^4.10.0",
|
|
46
47
|
"playwright": "^1.62.0",
|
|
47
48
|
"sharp": "^0.35.0",
|
|
48
|
-
"zod": "^3.25.0"
|
|
49
|
-
"axe-core": "^4.10.0"
|
|
49
|
+
"zod": "^3.25.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^22.0.0",
|
|
53
53
|
"@vitest/coverage-v8": "^4.1.0",
|
|
54
54
|
"typescript": "^5.9.0",
|
|
55
|
-
"vitest": "^4.1.0"
|
|
55
|
+
"vitest": "^4.1.0",
|
|
56
|
+
"vue": "^3.5.42",
|
|
57
|
+
"vue-router": "^4.6.4"
|
|
56
58
|
},
|
|
57
59
|
"engines": {
|
|
58
60
|
"node": ">=20.9.0"
|