framewatch-mcp-server 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +797 -8
  2. package/dist/constants.d.ts +264 -0
  3. package/dist/constants.js +268 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/engine/browser.js +4 -0
  6. package/dist/engine/browser.js.map +1 -1
  7. package/dist/engine/clicks.d.ts +221 -0
  8. package/dist/engine/clicks.js +801 -0
  9. package/dist/engine/clicks.js.map +1 -0
  10. package/dist/engine/forms.d.ts +137 -0
  11. package/dist/engine/forms.js +474 -0
  12. package/dist/engine/forms.js.map +1 -0
  13. package/dist/engine/hmr.d.ts +41 -0
  14. package/dist/engine/hmr.js +91 -0
  15. package/dist/engine/hmr.js.map +1 -0
  16. package/dist/engine/inspect.d.ts +31 -0
  17. package/dist/engine/inspect.js +383 -0
  18. package/dist/engine/inspect.js.map +1 -0
  19. package/dist/engine/interaction.d.ts +6 -1
  20. package/dist/engine/interaction.js +21 -6
  21. package/dist/engine/interaction.js.map +1 -1
  22. package/dist/engine/links.d.ts +134 -0
  23. package/dist/engine/links.js +384 -0
  24. package/dist/engine/links.js.map +1 -0
  25. package/dist/engine/mocks.d.ts +53 -0
  26. package/dist/engine/mocks.js +148 -0
  27. package/dist/engine/mocks.js.map +1 -0
  28. package/dist/engine/rtl.d.ts +129 -0
  29. package/dist/engine/rtl.js +540 -0
  30. package/dist/engine/rtl.js.map +1 -0
  31. package/dist/engine/seo.d.ts +189 -0
  32. package/dist/engine/seo.js +398 -0
  33. package/dist/engine/seo.js.map +1 -0
  34. package/dist/engine/snapshot.d.ts +29 -0
  35. package/dist/engine/snapshot.js +10 -0
  36. package/dist/engine/snapshot.js.map +1 -0
  37. package/dist/engine/vue.d.ts +54 -0
  38. package/dist/engine/vue.js +419 -0
  39. package/dist/engine/vue.js.map +1 -0
  40. package/dist/index.js +42 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/tools/accessibility.js +4 -3
  43. package/dist/tools/accessibility.js.map +1 -1
  44. package/dist/tools/api-mock.d.ts +405 -0
  45. package/dist/tools/api-mock.js +186 -0
  46. package/dist/tools/api-mock.js.map +1 -0
  47. package/dist/tools/capture.d.ts +83 -23
  48. package/dist/tools/capture.js +105 -60
  49. package/dist/tools/capture.js.map +1 -1
  50. package/dist/tools/compare.js +4 -3
  51. package/dist/tools/compare.js.map +1 -1
  52. package/dist/tools/dead-clicks.d.ts +128 -0
  53. package/dist/tools/dead-clicks.js +570 -0
  54. package/dist/tools/dead-clicks.js.map +1 -0
  55. package/dist/tools/form-test.d.ts +112 -0
  56. package/dist/tools/form-test.js +477 -0
  57. package/dist/tools/form-test.js.map +1 -0
  58. package/dist/tools/index.d.ts +16 -1
  59. package/dist/tools/index.js +42 -1
  60. package/dist/tools/index.js.map +1 -1
  61. package/dist/tools/inspect.d.ts +78 -0
  62. package/dist/tools/inspect.js +136 -0
  63. package/dist/tools/inspect.js.map +1 -0
  64. package/dist/tools/interact.d.ts +29 -16
  65. package/dist/tools/interact.js +99 -13
  66. package/dist/tools/interact.js.map +1 -1
  67. package/dist/tools/links.d.ts +129 -0
  68. package/dist/tools/links.js +640 -0
  69. package/dist/tools/links.js.map +1 -0
  70. package/dist/tools/responsive.d.ts +6 -6
  71. package/dist/tools/responsive.js +9 -9
  72. package/dist/tools/responsive.js.map +1 -1
  73. package/dist/tools/rtl.d.ts +241 -0
  74. package/dist/tools/rtl.js +410 -0
  75. package/dist/tools/rtl.js.map +1 -0
  76. package/dist/tools/save-auth.d.ts +22 -22
  77. package/dist/tools/screenshot.js +13 -5
  78. package/dist/tools/screenshot.js.map +1 -1
  79. package/dist/tools/seo.d.ts +113 -0
  80. package/dist/tools/seo.js +281 -0
  81. package/dist/tools/seo.js.map +1 -0
  82. package/dist/tools/snapshot.d.ts +122 -0
  83. package/dist/tools/snapshot.js +183 -0
  84. package/dist/tools/snapshot.js.map +1 -0
  85. package/dist/tools/wait-for.d.ts +107 -0
  86. package/dist/tools/wait-for.js +167 -0
  87. package/dist/tools/wait-for.js.map +1 -0
  88. package/dist/utils/arabic-text.d.ts +14 -0
  89. package/dist/utils/arabic-text.js +193 -0
  90. package/dist/utils/arabic-text.js.map +1 -0
  91. package/dist/utils/budget.d.ts +41 -0
  92. package/dist/utils/budget.js +182 -0
  93. package/dist/utils/budget.js.map +1 -0
  94. package/dist/utils/format.d.ts +11 -1
  95. package/dist/utils/format.js +27 -4
  96. package/dist/utils/format.js.map +1 -1
  97. package/dist/utils/highlight.d.ts +69 -0
  98. package/dist/utils/highlight.js +181 -0
  99. package/dist/utils/highlight.js.map +1 -0
  100. package/dist/utils/link-rules.d.ts +100 -0
  101. package/dist/utils/link-rules.js +284 -0
  102. package/dist/utils/link-rules.js.map +1 -0
  103. package/dist/utils/mock-rules.d.ts +144 -0
  104. package/dist/utils/mock-rules.js +224 -0
  105. package/dist/utils/mock-rules.js.map +1 -0
  106. package/dist/utils/rtl-rules.d.ts +142 -0
  107. package/dist/utils/rtl-rules.js +296 -0
  108. package/dist/utils/rtl-rules.js.map +1 -0
  109. package/dist/utils/seo-rules.d.ts +129 -0
  110. package/dist/utils/seo-rules.js +726 -0
  111. package/dist/utils/seo-rules.js.map +1 -0
  112. package/dist/utils/snapshot-rules.d.ts +33 -0
  113. package/dist/utils/snapshot-rules.js +111 -0
  114. package/dist/utils/snapshot-rules.js.map +1 -0
  115. package/dist/utils/storage-state.d.ts +42 -1
  116. package/dist/utils/storage-state.js +115 -4
  117. package/dist/utils/storage-state.js.map +1 -1
  118. package/dist/utils/style-rules.d.ts +107 -0
  119. package/dist/utils/style-rules.js +223 -0
  120. package/dist/utils/style-rules.js.map +1 -0
  121. package/dist/utils/test-data.d.ts +75 -0
  122. package/dist/utils/test-data.js +294 -0
  123. package/dist/utils/test-data.js.map +1 -0
  124. package/dist/utils/vue-rules.d.ts +72 -0
  125. package/dist/utils/vue-rules.js +108 -0
  126. package/dist/utils/vue-rules.js.map +1 -0
  127. package/package.json +6 -4
@@ -0,0 +1,224 @@
1
+ import { MAX_MOCK_URLS_LISTED, MAX_UNMATCHED_LISTED, MOCK_SLOW_DELAY_MS, } from "../constants.js";
2
+ /**
3
+ * What a mock means, and what one run of them is worth reporting.
4
+ *
5
+ * Everything here is pure: no Playwright, no page, no socket. The engine turns
6
+ * a `ResolvedMock` into a real route and counts what happened; this file
7
+ * decides what a scenario expands to, how a body is encoded, which mock wins
8
+ * when two patterns match, and how the result reads.
9
+ */
10
+ /* ── Scenarios ────────────────────────────────────────────────────────── */
11
+ /**
12
+ * The shorthands. Each expands to a response the caller can then override
13
+ * field by field, which is what makes them worth having: `slow` with your own
14
+ * body is one word plus a body, not a hand-written response.
15
+ */
16
+ export const MOCK_SCENARIOS = [
17
+ "empty",
18
+ "error",
19
+ "unauthorized",
20
+ "not_found",
21
+ "slow",
22
+ "malformed",
23
+ "offline",
24
+ ];
25
+ /**
26
+ * Failures the browser can be told to report. These four are the ones a
27
+ * developer actually means: the request failed, it hung, nothing was
28
+ * listening, or there is no network at all.
29
+ */
30
+ export const ABORT_REASONS = ["failed", "timedout", "connectionrefused", "internetdisconnected"];
31
+ /** What each scenario means, before the caller's own `response` is laid over it. */
32
+ const SCENARIOS = {
33
+ // An empty array, not `{ items: [] }`: no shape is right for every API, so
34
+ // the shorthand covers the common one and the report prints what it served,
35
+ // which is how a mismatch becomes visible instead of mysterious.
36
+ empty: { status: 200, body: [] },
37
+ error: { status: 500, body: { error: "Internal server error" } },
38
+ unauthorized: { status: 401, body: { error: "Unauthorized" } },
39
+ not_found: { status: 404, body: { error: "Not found" } },
40
+ slow: { status: 200, delay_ms: MOCK_SLOW_DELAY_MS },
41
+ // Deliberately not JSON, deliberately served as JSON. See `encodeBody`.
42
+ malformed: { status: 200, body: "not json" },
43
+ offline: { abort: "failed" },
44
+ };
45
+ /**
46
+ * Turn one written mock into one the engine can install.
47
+ *
48
+ * The scenario supplies defaults and the caller's own fields win one at a
49
+ * time, so `{ scenario: "slow", response: { body } }` keeps the delay and takes
50
+ * the body. An explicit `abort` beats a scenario that would have answered: a
51
+ * caller who asked for a dead request means it.
52
+ */
53
+ export function resolveMock(spec) {
54
+ const scenario = spec.scenario;
55
+ const preset = scenario ? SCENARIOS[scenario] : {};
56
+ const response = spec.response ?? {};
57
+ const reason = spec.abort ?? preset.abort;
58
+ if (reason !== undefined) {
59
+ return {
60
+ kind: "abort",
61
+ url_pattern: spec.url_pattern ?? "",
62
+ reason,
63
+ ...(spec.times !== undefined ? { times: spec.times } : {}),
64
+ ...(scenario !== undefined ? { scenario } : {}),
65
+ };
66
+ }
67
+ const body = response.body !== undefined ? response.body : preset.body;
68
+ const encoded = encodeBody(body);
69
+ const headers = {};
70
+ if (encoded.content_type !== undefined)
71
+ headers["content-type"] = encoded.content_type;
72
+ // The caller's headers land last and are lower-cased on the way in, so a
73
+ // `Content-Type` of their own replaces the one the body implied rather than
74
+ // sitting next to it as a second header.
75
+ for (const [name, value] of Object.entries(response.headers ?? {})) {
76
+ headers[name.toLowerCase()] = value;
77
+ }
78
+ return {
79
+ kind: "fulfill",
80
+ url_pattern: spec.url_pattern ?? "",
81
+ status: response.status ?? preset.status ?? 200,
82
+ body: encoded.body,
83
+ headers,
84
+ delay_ms: response.delay_ms ?? preset.delay_ms ?? 0,
85
+ ...(spec.times !== undefined ? { times: spec.times } : {}),
86
+ ...(scenario !== undefined ? { scenario } : {}),
87
+ };
88
+ }
89
+ /**
90
+ * Encode a body into the bytes the page will actually receive.
91
+ *
92
+ * A string is sent verbatim. This is the whole reason the malformed scenario
93
+ * works: `JSON.stringify("not json")` is `"not json"`, which parses perfectly,
94
+ * so encoding a string would make a broken API impossible to simulate. It goes
95
+ * out as `application/json` because these stand in for API responses — anyone
96
+ * mocking a page or a script says so in `headers`.
97
+ *
98
+ * Anything else is JSON, including `null`. Only an absent body means "no body",
99
+ * and it carries no content type at all.
100
+ */
101
+ export function encodeBody(body) {
102
+ if (body === undefined)
103
+ return { body: "" };
104
+ if (typeof body === "string")
105
+ return { body, content_type: "application/json" };
106
+ return { body: JSON.stringify(body), content_type: "application/json" };
107
+ }
108
+ /**
109
+ * The order to hand mocks to Playwright.
110
+ *
111
+ * Playwright checks routes last-registered-first; a caller reads their list
112
+ * top-down and expects the first pattern that matches to win. Reversing on the
113
+ * way in is what reconciles the two, and is the only reason this is not just a
114
+ * loop in the engine.
115
+ */
116
+ export function installOrder(mocks) {
117
+ return [...mocks].reverse();
118
+ }
119
+ /**
120
+ * The advice for a pattern that matched nothing, when the pattern itself looks
121
+ * like the reason.
122
+ *
123
+ * Patterns are matched against the whole URL, so `/api/orders` matches nothing
124
+ * on a page served from `http://localhost:3000` — by far the most common way
125
+ * to write a mock that silently does nothing. A pattern that already starts
126
+ * with a wildcard, or that is a full URL, is left alone: a wrong guess here is
127
+ * worse than no guess.
128
+ */
129
+ export function patternHint(pattern) {
130
+ if (pattern.startsWith("*") || pattern.includes("://"))
131
+ return undefined;
132
+ const path = pattern.startsWith("/") ? pattern : `/${pattern}`;
133
+ const suggestion = `**${path}${path.endsWith("*") ? "" : "*"}`;
134
+ return `patterns are matched against the whole URL, so a path needs a leading \`**\` — try \`${suggestion}\``;
135
+ }
136
+ /**
137
+ * The mock report, as lines for the capture summary.
138
+ *
139
+ * A mock that never matched leads, because it is the finding this tool exists
140
+ * to surface: mock the products endpoint, watch the app call `/api/product-list`,
141
+ * see a page that looks perfectly fine, and learn nothing at all. The same
142
+ * reasoning puts the unmatched requests in — a run that quietly used the real
143
+ * backend for half its data is not the test anybody thought they ran.
144
+ */
145
+ export function formatMockReport(input) {
146
+ const { activity, unmatched, block_unmatched, duration_ms } = input;
147
+ const served = activity.filter((entry) => entry.hits > 0);
148
+ const missed = activity.filter((entry) => entry.hits === 0);
149
+ const requests = served.reduce((total, entry) => total + entry.hits, 0);
150
+ const headline = `API mocks — ${activity.length} declared, ` +
151
+ `${served.length} served ${count(requests, "request")}` +
152
+ (missed.length > 0 ? `, ${missed.length} never matched.` : ".");
153
+ const lines = [headline];
154
+ for (const entry of activity) {
155
+ lines.push(` ${formatActivity(entry, duration_ms)}`);
156
+ }
157
+ if (unmatched.length > 0) {
158
+ lines.push(block_unmatched
159
+ ? " Unmatched, blocked before they left the browser:"
160
+ : " Unmatched, answered by the real server:");
161
+ for (const request of unmatched.slice(0, MAX_UNMATCHED_LISTED)) {
162
+ const outcome = request.blocked ? "" : request.status !== undefined ? ` → ${request.status}` : " → no response";
163
+ lines.push(` ${request.method} ${request.url}${outcome}`);
164
+ }
165
+ if (unmatched.length > MAX_UNMATCHED_LISTED) {
166
+ lines.push(` … and ${unmatched.length - MAX_UNMATCHED_LISTED} more`);
167
+ }
168
+ }
169
+ else {
170
+ lines.push(" Every request the page made was matched by a mock.");
171
+ }
172
+ return lines;
173
+ }
174
+ /** One mock's line in the report: the mark, the pattern, what it serves, and what it did. */
175
+ function formatActivity(entry, duration_ms) {
176
+ const { mock, hits, answered, urls } = entry;
177
+ if (hits === 0) {
178
+ const hint = patternHint(mock.url_pattern);
179
+ return `✗ ${mock.url_pattern} — no request matched it${hint ? ` (${hint})` : ""}.`;
180
+ }
181
+ const late = mock.kind === "fulfill" && answered < hits && mock.delay_ms >= duration_ms;
182
+ const mark = late ? "!" : "✓";
183
+ let line = `${mark} ${mock.url_pattern} → ${describeMock(mock)} ×${hits}`;
184
+ if (late && mock.kind === "fulfill") {
185
+ line +=
186
+ ` — delayed ${mock.delay_ms}ms, longer than the ${duration_ms}ms recording, ` +
187
+ `so ${hits === 1 ? "the request" : `${hits - answered} of them`} never got an answer.`;
188
+ }
189
+ else if (urls.length > 0) {
190
+ // Only a handful of URLs are ever kept, so the remainder is counted off the
191
+ // hits — a mock that answered fifty polls must not report "and 1 more"
192
+ // simply because three is all the sample it has.
193
+ const listed = urls.slice(0, MAX_MOCK_URLS_LISTED);
194
+ const more = hits > listed.length ? `, … and ${hits - listed.length} more` : "";
195
+ line += ` (${listed.join(", ")}${more})`;
196
+ }
197
+ return line;
198
+ }
199
+ /**
200
+ * What a mock serves, in a few words. Names the scenario when there was one —
201
+ * "200 empty" says more than "200" and is what the caller wrote.
202
+ */
203
+ export function describeMock(mock) {
204
+ const parts = [];
205
+ if (mock.kind === "abort") {
206
+ parts.push(`abort ${mock.reason}`);
207
+ }
208
+ else {
209
+ parts.push(String(mock.status));
210
+ if (mock.scenario !== undefined && mock.scenario !== "slow")
211
+ parts.push(mock.scenario);
212
+ if (mock.delay_ms > 0)
213
+ parts.push(`after ${mock.delay_ms}ms`);
214
+ }
215
+ let text = parts.join(" ");
216
+ if (mock.times !== undefined) {
217
+ text += mock.times === 1 ? ", first request only" : `, first ${mock.times} requests only`;
218
+ }
219
+ return text;
220
+ }
221
+ function count(n, noun) {
222
+ return `${n} ${noun}${n === 1 ? "" : "s"}`;
223
+ }
224
+ //# sourceMappingURL=mock-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-rules.js","sourceRoot":"","sources":["../../src/utils/mock-rules.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n MAX_MOCK_URLS_LISTED,\n MAX_UNMATCHED_LISTED,\n MOCK_SLOW_DELAY_MS,\n} from \"../constants.js\";\n\n/**\n * What a mock means, and what one run of them is worth reporting.\n *\n * Everything here is pure: no Playwright, no page, no socket. The engine turns\n * a `ResolvedMock` into a real route and counts what happened; this file\n * decides what a scenario expands to, how a body is encoded, which mock wins\n * when two patterns match, and how the result reads.\n */\n\n/* ── Scenarios ────────────────────────────────────────────────────────── */\n\n/**\n * The shorthands. Each expands to a response the caller can then override\n * field by field, which is what makes them worth having: `slow` with your own\n * body is one word plus a body, not a hand-written response.\n */\nexport const MOCK_SCENARIOS = [\n \"empty\",\n \"error\",\n \"unauthorized\",\n \"not_found\",\n \"slow\",\n \"malformed\",\n \"offline\",\n] as const;\n\nexport type MockScenario = (typeof MOCK_SCENARIOS)[number];\n\n/**\n * Failures the browser can be told to report. These four are the ones a\n * developer actually means: the request failed, it hung, nothing was\n * listening, or there is no network at all.\n */\nexport const ABORT_REASONS = [\"failed\", \"timedout\", \"connectionrefused\", \"internetdisconnected\"] as const;\n\nexport type AbortReason = (typeof ABORT_REASONS)[number];\n\n/** One mock exactly as the caller wrote it. */\nexport interface MockSpec {\n url_pattern?: string;\n scenario?: MockScenario;\n response?: {\n status?: number;\n body?: unknown;\n delay_ms?: number;\n headers?: Record<string, string>;\n };\n abort?: AbortReason;\n times?: number;\n}\n\n/** A mock with every question answered, ready for the engine to install. */\nexport type ResolvedMock =\n | {\n kind: \"abort\";\n url_pattern: string;\n reason: AbortReason;\n times?: number;\n scenario?: MockScenario;\n }\n | {\n kind: \"fulfill\";\n url_pattern: string;\n status: number;\n /** Already encoded — the exact bytes the page will receive. */\n body: string;\n /** Header names lower-cased, so `content-type` can only be set once. */\n headers: Record<string, string>;\n delay_ms: number;\n times?: number;\n scenario?: MockScenario;\n };\n\n/** What each scenario means, before the caller's own `response` is laid over it. */\nconst SCENARIOS: Readonly<Record<MockScenario, { status?: number; body?: unknown; delay_ms?: number; abort?: AbortReason }>> =\n {\n // An empty array, not `{ items: [] }`: no shape is right for every API, so\n // the shorthand covers the common one and the report prints what it served,\n // which is how a mismatch becomes visible instead of mysterious.\n empty: { status: 200, body: [] },\n error: { status: 500, body: { error: \"Internal server error\" } },\n unauthorized: { status: 401, body: { error: \"Unauthorized\" } },\n not_found: { status: 404, body: { error: \"Not found\" } },\n slow: { status: 200, delay_ms: MOCK_SLOW_DELAY_MS },\n // Deliberately not JSON, deliberately served as JSON. See `encodeBody`.\n malformed: { status: 200, body: \"not json\" },\n offline: { abort: \"failed\" },\n };\n\n/**\n * Turn one written mock into one the engine can install.\n *\n * The scenario supplies defaults and the caller's own fields win one at a\n * time, so `{ scenario: \"slow\", response: { body } }` keeps the delay and takes\n * the body. An explicit `abort` beats a scenario that would have answered: a\n * caller who asked for a dead request means it.\n */\nexport function resolveMock(spec: MockSpec): ResolvedMock {\n const scenario = spec.scenario;\n const preset = scenario ? SCENARIOS[scenario] : {};\n const response = spec.response ?? {};\n\n const reason = spec.abort ?? preset.abort;\n if (reason !== undefined) {\n return {\n kind: \"abort\",\n url_pattern: spec.url_pattern ?? \"\",\n reason,\n ...(spec.times !== undefined ? { times: spec.times } : {}),\n ...(scenario !== undefined ? { scenario } : {}),\n };\n }\n\n const body = response.body !== undefined ? response.body : preset.body;\n const encoded = encodeBody(body);\n const headers: Record<string, string> = {};\n if (encoded.content_type !== undefined) headers[\"content-type\"] = encoded.content_type;\n // The caller's headers land last and are lower-cased on the way in, so a\n // `Content-Type` of their own replaces the one the body implied rather than\n // sitting next to it as a second header.\n for (const [name, value] of Object.entries(response.headers ?? {})) {\n headers[name.toLowerCase()] = value;\n }\n\n return {\n kind: \"fulfill\",\n url_pattern: spec.url_pattern ?? \"\",\n status: response.status ?? preset.status ?? 200,\n body: encoded.body,\n headers,\n delay_ms: response.delay_ms ?? preset.delay_ms ?? 0,\n ...(spec.times !== undefined ? { times: spec.times } : {}),\n ...(scenario !== undefined ? { scenario } : {}),\n };\n}\n\n/**\n * Encode a body into the bytes the page will actually receive.\n *\n * A string is sent verbatim. This is the whole reason the malformed scenario\n * works: `JSON.stringify(\"not json\")` is `\"not json\"`, which parses perfectly,\n * so encoding a string would make a broken API impossible to simulate. It goes\n * out as `application/json` because these stand in for API responses — anyone\n * mocking a page or a script says so in `headers`.\n *\n * Anything else is JSON, including `null`. Only an absent body means \"no body\",\n * and it carries no content type at all.\n */\nexport function encodeBody(body: unknown): { body: string; content_type?: string } {\n if (body === undefined) return { body: \"\" };\n if (typeof body === \"string\") return { body, content_type: \"application/json\" };\n return { body: JSON.stringify(body), content_type: \"application/json\" };\n}\n\n/**\n * The order to hand mocks to Playwright.\n *\n * Playwright checks routes last-registered-first; a caller reads their list\n * top-down and expects the first pattern that matches to win. Reversing on the\n * way in is what reconciles the two, and is the only reason this is not just a\n * loop in the engine.\n */\nexport function installOrder<T>(mocks: readonly T[]): T[] {\n return [...mocks].reverse();\n}\n\n/**\n * The advice for a pattern that matched nothing, when the pattern itself looks\n * like the reason.\n *\n * Patterns are matched against the whole URL, so `/api/orders` matches nothing\n * on a page served from `http://localhost:3000` — by far the most common way\n * to write a mock that silently does nothing. A pattern that already starts\n * with a wildcard, or that is a full URL, is left alone: a wrong guess here is\n * worse than no guess.\n */\nexport function patternHint(pattern: string): string | undefined {\n if (pattern.startsWith(\"*\") || pattern.includes(\"://\")) return undefined;\n const path = pattern.startsWith(\"/\") ? pattern : `/${pattern}`;\n const suggestion = `**${path}${path.endsWith(\"*\") ? \"\" : \"*\"}`;\n return `patterns are matched against the whole URL, so a path needs a leading \\`**\\` — try \\`${suggestion}\\``;\n}\n\n/* ── The report ───────────────────────────────────────────────────────── */\n\n/** What one mock did during a run. */\nexport interface MockActivity {\n mock: ResolvedMock;\n /** Requests this mock intercepted. */\n hits: number;\n /**\n * Requests it actually answered. Lower than `hits` when a delayed response\n * outlived the recording — the request was caught but the page never got a\n * reply, which is a finding rather than a bug in the count.\n */\n answered: number;\n /** The first few URLs it intercepted, for the report. */\n urls: string[];\n}\n\n/** A request no mock matched. */\nexport interface UnmatchedRequest {\n method: string;\n url: string;\n /** What the real server answered, when it was allowed to. */\n status?: number;\n /** Set when `block_unmatched` killed it before it left the browser. */\n blocked?: boolean;\n}\n\nexport interface MockReportInput {\n activity: MockActivity[];\n unmatched: UnmatchedRequest[];\n block_unmatched: boolean;\n /** The recording length, so a delay that outlives it can be named. */\n duration_ms: number;\n}\n\n/**\n * The mock report, as lines for the capture summary.\n *\n * A mock that never matched leads, because it is the finding this tool exists\n * to surface: mock the products endpoint, watch the app call `/api/product-list`,\n * see a page that looks perfectly fine, and learn nothing at all. The same\n * reasoning puts the unmatched requests in — a run that quietly used the real\n * backend for half its data is not the test anybody thought they ran.\n */\nexport function formatMockReport(input: MockReportInput): string[] {\n const { activity, unmatched, block_unmatched, duration_ms } = input;\n const served = activity.filter((entry) => entry.hits > 0);\n const missed = activity.filter((entry) => entry.hits === 0);\n const requests = served.reduce((total, entry) => total + entry.hits, 0);\n\n const headline =\n `API mocks — ${activity.length} declared, ` +\n `${served.length} served ${count(requests, \"request\")}` +\n (missed.length > 0 ? `, ${missed.length} never matched.` : \".\");\n const lines = [headline];\n\n for (const entry of activity) {\n lines.push(` ${formatActivity(entry, duration_ms)}`);\n }\n\n if (unmatched.length > 0) {\n lines.push(\n block_unmatched\n ? \" Unmatched, blocked before they left the browser:\"\n : \" Unmatched, answered by the real server:\",\n );\n for (const request of unmatched.slice(0, MAX_UNMATCHED_LISTED)) {\n const outcome = request.blocked ? \"\" : request.status !== undefined ? ` → ${request.status}` : \" → no response\";\n lines.push(` ${request.method} ${request.url}${outcome}`);\n }\n if (unmatched.length > MAX_UNMATCHED_LISTED) {\n lines.push(` … and ${unmatched.length - MAX_UNMATCHED_LISTED} more`);\n }\n } else {\n lines.push(\" Every request the page made was matched by a mock.\");\n }\n\n return lines;\n}\n\n/** One mock's line in the report: the mark, the pattern, what it serves, and what it did. */\nfunction formatActivity(entry: MockActivity, duration_ms: number): string {\n const { mock, hits, answered, urls } = entry;\n\n if (hits === 0) {\n const hint = patternHint(mock.url_pattern);\n return `✗ ${mock.url_pattern} — no request matched it${hint ? ` (${hint})` : \"\"}.`;\n }\n\n const late = mock.kind === \"fulfill\" && answered < hits && mock.delay_ms >= duration_ms;\n const mark = late ? \"!\" : \"✓\";\n let line = `${mark} ${mock.url_pattern} → ${describeMock(mock)} ×${hits}`;\n\n if (late && mock.kind === \"fulfill\") {\n line +=\n ` — delayed ${mock.delay_ms}ms, longer than the ${duration_ms}ms recording, ` +\n `so ${hits === 1 ? \"the request\" : `${hits - answered} of them`} never got an answer.`;\n } else if (urls.length > 0) {\n // Only a handful of URLs are ever kept, so the remainder is counted off the\n // hits — a mock that answered fifty polls must not report \"and 1 more\"\n // simply because three is all the sample it has.\n const listed = urls.slice(0, MAX_MOCK_URLS_LISTED);\n const more = hits > listed.length ? `, … and ${hits - listed.length} more` : \"\";\n line += ` (${listed.join(\", \")}${more})`;\n }\n\n return line;\n}\n\n/**\n * What a mock serves, in a few words. Names the scenario when there was one —\n * \"200 empty\" says more than \"200\" and is what the caller wrote.\n */\nexport function describeMock(mock: ResolvedMock): string {\n const parts: string[] = [];\n\n if (mock.kind === \"abort\") {\n parts.push(`abort ${mock.reason}`);\n } else {\n parts.push(String(mock.status));\n if (mock.scenario !== undefined && mock.scenario !== \"slow\") parts.push(mock.scenario);\n if (mock.delay_ms > 0) parts.push(`after ${mock.delay_ms}ms`);\n }\n\n let text = parts.join(\" \");\n if (mock.times !== undefined) {\n text += mock.times === 1 ? \", first request only\" : `, first ${mock.times} requests only`;\n }\n return text;\n}\n\nfunction count(n: number, noun: string): string {\n return `${n} ${noun}${n === 1 ? \"\" : \"s\"}`;\n}\n"]}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * What counts as an RTL bug.
3
+ *
4
+ * Everything here is pure: it takes one element measured in both directions
5
+ * and returns a verdict. Nothing in this file opens a browser or touches a
6
+ * page, which is the point — "did this box mirror?", "is this alignment
7
+ * deliberate or forgotten?" and "is that overflow new?" are all judgements
8
+ * with edge cases, and judgements need unit tests that run in milliseconds.
9
+ *
10
+ * The bias throughout is against false alarms, and it is stronger here than
11
+ * anywhere else in this codebase, because static RTL analysis is notorious for
12
+ * crying wolf. `text-align: left` is *correct* on a code block, a number
13
+ * column and a Latin brand name; `padding-left` is correct on anything that
14
+ * should not mirror; `flex-direction: row` is correct wherever the order is
15
+ * physical rather than semantic. A report that flags all of those is a report
16
+ * whose real findings are never read.
17
+ *
18
+ * So nothing is judged from the RTL rendering alone. Every verdict is a
19
+ * *comparison*: the element is measured in LTR and again in RTL, and the only
20
+ * findings are the things that failed to change when the LTR measurement
21
+ * proves they should have. An element that is left-aligned in both directions
22
+ * has forgotten to mirror; one that is left in LTR and right in RTL is
23
+ * working exactly as intended, and this file says nothing about it.
24
+ */
25
+ /**
26
+ * One element as measured in one direction.
27
+ *
28
+ * Deliberately flat and JSON-ish: this is what crosses the boundary out of
29
+ * `page.evaluate`, so it can hold nothing but structured-cloneable values.
30
+ */
31
+ export interface ElementMeasurement {
32
+ /** Stable identity across the two renders — see `keyFor` in engine/rtl.ts. */
33
+ key: string;
34
+ /** A CSS selector for the element, for the report and the highlight overlay. */
35
+ selector: string;
36
+ /** Which match of `selector` this is, when the selector is not unique. */
37
+ match_index: number;
38
+ tag: string;
39
+ /** Visible text, elided. Named in the report so a finding is recognisable. */
40
+ text: string;
41
+ /** Border-box in document coordinates. */
42
+ x: number;
43
+ y: number;
44
+ width: number;
45
+ height: number;
46
+ /** The viewport this was measured in, so mirroring can be computed. */
47
+ viewport_width: number;
48
+ /** Computed `text-align`, already resolved from `start`/`end` by the browser. */
49
+ text_align: string;
50
+ /** Computed `direction` on the element itself. */
51
+ direction: string;
52
+ /** Computed `flex-direction`, or "" when the element is not a flex container. */
53
+ flex_direction: string;
54
+ /** Physical padding and margin, to spot asymmetry that never mirrored. */
55
+ padding_left: number;
56
+ padding_right: number;
57
+ margin_left: number;
58
+ margin_right: number;
59
+ /** Overflow of this element past its own content box, and past the viewport. */
60
+ scroll_width: number;
61
+ client_width: number;
62
+ /** How far the box sticks out past the right/left edge of the viewport, 0 when it fits. */
63
+ overflow_right: number;
64
+ overflow_left: number;
65
+ /** A transform that already mirrors the element — `scaleX(-1)` on an icon. */
66
+ transform: string;
67
+ /** Set when this element is one an author would expect to mirror: an arrow, a chevron. */
68
+ mirrorable?: boolean;
69
+ }
70
+ /** One element measured in both directions. */
71
+ export interface ElementPair {
72
+ ltr: ElementMeasurement;
73
+ rtl: ElementMeasurement;
74
+ }
75
+ /**
76
+ * What can be wrong with an element in RTL.
77
+ *
78
+ * Each of these is a *failure to change*, except `overflow`, which is a change
79
+ * that should not have happened. That asymmetry is the whole design: RTL bugs
80
+ * are things that stayed put.
81
+ */
82
+ export type RtlIssueKind = "not_mirrored" | "alignment" | "flex_not_reversed" | "padding_not_mirrored" | "overflow" | "icon_not_mirrored" | "direction_not_applied";
83
+ export interface RtlIssue {
84
+ kind: RtlIssueKind;
85
+ /** How bad it is. `problem` leads the report; `warning` follows it. */
86
+ severity: "problem" | "warning";
87
+ /** One line naming what is wrong, in the report's voice. */
88
+ message: string;
89
+ /** The measured evidence, quoted so the reader can check the verdict. */
90
+ evidence: string;
91
+ }
92
+ /** Every issue found on one element, with the element itself. */
93
+ export interface ElementFinding {
94
+ /** 1-based, and the number the highlight overlay is labelled with. */
95
+ index: number;
96
+ selector: string;
97
+ match_index: number;
98
+ /** `button "Add to cart"` — how the element is named in the report. */
99
+ description: string;
100
+ issues: RtlIssue[];
101
+ /** The worst severity among `issues`, so the report can sort and colour. */
102
+ severity: "problem" | "warning";
103
+ }
104
+ /**
105
+ * Judge one element, having seen it in both directions.
106
+ *
107
+ * Returns every issue it has; an element with none is not a finding and is
108
+ * never mentioned. The checks are deliberately independent — an element can
109
+ * both fail to mirror and newly overflow, and those are two different bugs
110
+ * with two different fixes.
111
+ */
112
+ export declare function judgeElement(pair: ElementPair): RtlIssue[];
113
+ /** Where a box's mirror image starts, in the same coordinate space. */
114
+ export declare function mirroredX(m: ElementMeasurement): number;
115
+ /**
116
+ * Does this transform flip the element horizontally?
117
+ *
118
+ * A computed transform is always a matrix, so the sign of `a` (the horizontal
119
+ * scale) is the answer. `scaleX(-1)` computes to `matrix(-1, 0, 0, 1, 0, 0)`.
120
+ */
121
+ export declare function isMirroredTransform(transform: string): boolean;
122
+ /**
123
+ * How an element is named in the report: `button "Add to cart"`, `div .card`.
124
+ *
125
+ * An icon-only control has no text at all, and a bare selector is the least
126
+ * recognisable thing to call it by — so text wins when there is any, and the
127
+ * selector is the fallback.
128
+ */
129
+ export declare function describeElement(m: Pick<ElementMeasurement, "tag" | "text" | "selector">): string;
130
+ /**
131
+ * Turn judged elements into the findings the report prints, numbered and
132
+ * ordered worst-first.
133
+ *
134
+ * Order is by severity, then by how many things are wrong with the element,
135
+ * then down the page. The numbering happens *after* that sort, so the numbers
136
+ * in the report count from the top of the list and match the labels drawn on
137
+ * the screenshot.
138
+ */
139
+ export declare function buildFindings(pairs: readonly ElementPair[]): ElementFinding[];
140
+ /** What each kind of issue is called as a section heading, and how it is explained. */
141
+ export declare function describeIssueKind(kind: RtlIssueKind): string;
142
+ export declare function elide(value: string, max: number): string;