framewatch-mcp-server 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +895 -15
  2. package/dist/constants.d.ts +274 -0
  3. package/dist/constants.js +279 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/engine/browser.d.ts +20 -4
  6. package/dist/engine/browser.js +26 -9
  7. package/dist/engine/browser.js.map +1 -1
  8. package/dist/engine/clicks.d.ts +221 -0
  9. package/dist/engine/clicks.js +801 -0
  10. package/dist/engine/clicks.js.map +1 -0
  11. package/dist/engine/forms.d.ts +137 -0
  12. package/dist/engine/forms.js +474 -0
  13. package/dist/engine/forms.js.map +1 -0
  14. package/dist/engine/hmr.d.ts +41 -0
  15. package/dist/engine/hmr.js +91 -0
  16. package/dist/engine/hmr.js.map +1 -0
  17. package/dist/engine/inspect.d.ts +31 -0
  18. package/dist/engine/inspect.js +383 -0
  19. package/dist/engine/inspect.js.map +1 -0
  20. package/dist/engine/interaction.d.ts +12 -7
  21. package/dist/engine/interaction.js +110 -18
  22. package/dist/engine/interaction.js.map +1 -1
  23. package/dist/engine/links.d.ts +134 -0
  24. package/dist/engine/links.js +384 -0
  25. package/dist/engine/links.js.map +1 -0
  26. package/dist/engine/mocks.d.ts +53 -0
  27. package/dist/engine/mocks.js +148 -0
  28. package/dist/engine/mocks.js.map +1 -0
  29. package/dist/engine/rtl.d.ts +129 -0
  30. package/dist/engine/rtl.js +540 -0
  31. package/dist/engine/rtl.js.map +1 -0
  32. package/dist/engine/seo.d.ts +189 -0
  33. package/dist/engine/seo.js +398 -0
  34. package/dist/engine/seo.js.map +1 -0
  35. package/dist/engine/snapshot.d.ts +29 -0
  36. package/dist/engine/snapshot.js +10 -0
  37. package/dist/engine/snapshot.js.map +1 -0
  38. package/dist/engine/vue.d.ts +54 -0
  39. package/dist/engine/vue.js +419 -0
  40. package/dist/engine/vue.js.map +1 -0
  41. package/dist/index.js +45 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/tools/accessibility.d.ts +4 -0
  44. package/dist/tools/accessibility.js +9 -2
  45. package/dist/tools/accessibility.js.map +1 -1
  46. package/dist/tools/api-mock.d.ts +405 -0
  47. package/dist/tools/api-mock.js +186 -0
  48. package/dist/tools/api-mock.js.map +1 -0
  49. package/dist/tools/capture.d.ts +90 -26
  50. package/dist/tools/capture.js +109 -58
  51. package/dist/tools/capture.js.map +1 -1
  52. package/dist/tools/compare.d.ts +4 -0
  53. package/dist/tools/compare.js +16 -5
  54. package/dist/tools/compare.js.map +1 -1
  55. package/dist/tools/dead-clicks.d.ts +128 -0
  56. package/dist/tools/dead-clicks.js +570 -0
  57. package/dist/tools/dead-clicks.js.map +1 -0
  58. package/dist/tools/form-test.d.ts +112 -0
  59. package/dist/tools/form-test.js +477 -0
  60. package/dist/tools/form-test.js.map +1 -0
  61. package/dist/tools/index.d.ts +17 -1
  62. package/dist/tools/index.js +45 -1
  63. package/dist/tools/index.js.map +1 -1
  64. package/dist/tools/inspect.d.ts +78 -0
  65. package/dist/tools/inspect.js +136 -0
  66. package/dist/tools/inspect.js.map +1 -0
  67. package/dist/tools/interact.d.ts +37 -18
  68. package/dist/tools/interact.js +113 -13
  69. package/dist/tools/interact.js.map +1 -1
  70. package/dist/tools/links.d.ts +129 -0
  71. package/dist/tools/links.js +640 -0
  72. package/dist/tools/links.js.map +1 -0
  73. package/dist/tools/responsive.d.ts +10 -6
  74. package/dist/tools/responsive.js +21 -4
  75. package/dist/tools/responsive.js.map +1 -1
  76. package/dist/tools/rtl.d.ts +241 -0
  77. package/dist/tools/rtl.js +410 -0
  78. package/dist/tools/rtl.js.map +1 -0
  79. package/dist/tools/save-auth.d.ts +263 -0
  80. package/dist/tools/save-auth.js +253 -0
  81. package/dist/tools/save-auth.js.map +1 -0
  82. package/dist/tools/screenshot.d.ts +4 -0
  83. package/dist/tools/screenshot.js +15 -4
  84. package/dist/tools/screenshot.js.map +1 -1
  85. package/dist/tools/seo.d.ts +113 -0
  86. package/dist/tools/seo.js +281 -0
  87. package/dist/tools/seo.js.map +1 -0
  88. package/dist/tools/snapshot.d.ts +122 -0
  89. package/dist/tools/snapshot.js +183 -0
  90. package/dist/tools/snapshot.js.map +1 -0
  91. package/dist/tools/wait-for.d.ts +107 -0
  92. package/dist/tools/wait-for.js +167 -0
  93. package/dist/tools/wait-for.js.map +1 -0
  94. package/dist/utils/arabic-text.d.ts +14 -0
  95. package/dist/utils/arabic-text.js +193 -0
  96. package/dist/utils/arabic-text.js.map +1 -0
  97. package/dist/utils/budget.d.ts +41 -0
  98. package/dist/utils/budget.js +182 -0
  99. package/dist/utils/budget.js.map +1 -0
  100. package/dist/utils/format.d.ts +11 -1
  101. package/dist/utils/format.js +27 -4
  102. package/dist/utils/format.js.map +1 -1
  103. package/dist/utils/highlight.d.ts +69 -0
  104. package/dist/utils/highlight.js +181 -0
  105. package/dist/utils/highlight.js.map +1 -0
  106. package/dist/utils/link-rules.d.ts +100 -0
  107. package/dist/utils/link-rules.js +284 -0
  108. package/dist/utils/link-rules.js.map +1 -0
  109. package/dist/utils/mock-rules.d.ts +144 -0
  110. package/dist/utils/mock-rules.js +224 -0
  111. package/dist/utils/mock-rules.js.map +1 -0
  112. package/dist/utils/rtl-rules.d.ts +142 -0
  113. package/dist/utils/rtl-rules.js +296 -0
  114. package/dist/utils/rtl-rules.js.map +1 -0
  115. package/dist/utils/seo-rules.d.ts +129 -0
  116. package/dist/utils/seo-rules.js +726 -0
  117. package/dist/utils/seo-rules.js.map +1 -0
  118. package/dist/utils/snapshot-rules.d.ts +33 -0
  119. package/dist/utils/snapshot-rules.js +111 -0
  120. package/dist/utils/snapshot-rules.js.map +1 -0
  121. package/dist/utils/storage-state.d.ts +76 -0
  122. package/dist/utils/storage-state.js +195 -0
  123. package/dist/utils/storage-state.js.map +1 -0
  124. package/dist/utils/style-rules.d.ts +107 -0
  125. package/dist/utils/style-rules.js +223 -0
  126. package/dist/utils/style-rules.js.map +1 -0
  127. package/dist/utils/test-data.d.ts +75 -0
  128. package/dist/utils/test-data.js +294 -0
  129. package/dist/utils/test-data.js.map +1 -0
  130. package/dist/utils/vue-rules.d.ts +72 -0
  131. package/dist/utils/vue-rules.js +108 -0
  132. package/dist/utils/vue-rules.js.map +1 -0
  133. package/package.json +6 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mocks.js","sourceRoot":"","sources":["../../src/engine/mocks.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Page, Request, Route } from \"playwright\";\nimport { MAX_MOCK_URLS_LISTED, MAX_UNMATCHED_TRACKED } from \"../constants.js\";\nimport { shortenUrl } from \"./layers/network.js\";\nimport {\n installOrder,\n type MockActivity,\n type ResolvedMock,\n type UnmatchedRequest,\n} from \"../utils/mock-rules.js\";\n\n/**\n * Request interception for `framewatch_api_mock`.\n *\n * Installs one Playwright route per mock, answers the requests they match,\n * and keeps the tally the report is built from: what each mock served, what it\n * never served, and what the page asked for that no mock had an opinion about.\n *\n * Two rules do most of the work here:\n *\n * The page's own navigation is never mocked. A catch-all pattern is a\n * perfectly reasonable thing to write, and if the main document went through\n * the mock the page under test would be replaced by a JSON body — there would\n * be nothing left to photograph. The document is how the page got here, not\n * something the page asked for, so it is passed through untouched and left out\n * of the unmatched tally as well. Sub-resources and everything an iframe or the\n * app itself fetches are fair game.\n *\n * `times` is counted here rather than handed to Playwright. Playwright spends a\n * route's remaining uses whenever the pattern matches, which would let the\n * document request consume a `times: 1` mock before the app ever fetched\n * anything.\n */\n\nexport interface MockRouterOptions {\n /** The mocks in the order the caller wrote them; the first match wins. */\n mocks: ResolvedMock[];\n /** Abort anything no mock matched instead of letting it reach the network. */\n block_unmatched: boolean;\n}\n\nexport class MockRouter {\n readonly #blockUnmatched: boolean;\n readonly #activity: MockActivity[];\n /** Unmatched requests keyed by identity, so a response can fill in its status later. */\n readonly #unmatched = new Map<Request, UnmatchedRequest>();\n #page: Page | null = null;\n /** Sleeping delays, so a recording that ends first does not leave timers behind. */\n readonly #waits = new Set<{ timer: NodeJS.Timeout; resolve: () => void }>();\n #disposed = false;\n\n readonly #onResponse = (response: { request(): Request; status(): number }): void => {\n const record = this.#unmatched.get(response.request());\n if (record && record.blocked !== true) record.status = response.status();\n };\n\n constructor(options: MockRouterOptions) {\n this.#blockUnmatched = options.block_unmatched;\n this.#activity = options.mocks.map((mock) => ({ mock, hits: 0, answered: 0, urls: [] }));\n }\n\n /**\n * Install the routes. Call before the page navigates.\n *\n * The catch-all goes on first *because* Playwright checks routes\n * last-registered-first: registering it before the mocks is what puts it\n * behind them. The mocks themselves go on in reverse (see `installOrder`),\n * which leaves the first one the caller wrote at the front of the queue.\n */\n async install(page: Page): Promise<void> {\n this.#page = page;\n page.on(\"response\", this.#onResponse);\n\n await page.route(\"**/*\", (route, request) => this.#handleUnmatched(route, request));\n\n for (const entry of installOrder(this.#activity)) {\n await page.route(entry.mock.url_pattern, (route, request) => this.#handleMock(entry, route, request));\n }\n }\n\n /** What each mock did, in the order the caller wrote them. */\n get activity(): MockActivity[] {\n return this.#activity;\n }\n\n /** Requests no mock matched, in the order the page made them. */\n get unmatched(): UnmatchedRequest[] {\n return [...this.#unmatched.values()];\n }\n\n /**\n * Stop answering. Called once the recording is over and before the page is\n * closed: a mock still sleeping out a long delay has nothing left to answer,\n * and its timer would otherwise outlive the tool call.\n */\n dispose(): void {\n if (this.#disposed) return;\n this.#disposed = true;\n this.#page?.off(\"response\", this.#onResponse);\n for (const wait of this.#waits) {\n clearTimeout(wait.timer);\n wait.resolve();\n }\n this.#waits.clear();\n }\n\n /** One mock's turn at a request. */\n async #handleMock(entry: MockActivity, route: Route, request: Request): Promise<void> {\n const { mock } = entry;\n\n // Not ours: the page's own document, or a mock that has already been spent.\n if (this.#isMainDocument(request) || (mock.times !== undefined && entry.hits >= mock.times)) {\n await this.#safely(() => route.fallback());\n return;\n }\n\n entry.hits++;\n if (entry.urls.length < MAX_MOCK_URLS_LISTED) entry.urls.push(shortenUrl(request.url()));\n\n if (mock.kind === \"abort\") {\n if (await this.#safely(() => route.abort(mock.reason))) entry.answered++;\n return;\n }\n\n if (mock.delay_ms > 0) await this.#wait(mock.delay_ms);\n // The recording ended while this was sleeping; there is nobody left to answer.\n if (this.#disposed) return;\n\n const answered = await this.#safely(() =>\n route.fulfill({ status: mock.status, headers: mock.headers, body: mock.body }),\n );\n if (answered) entry.answered++;\n }\n\n /** Everything no mock wanted. */\n async #handleUnmatched(route: Route, request: Request): Promise<void> {\n if (this.#isMainDocument(request)) {\n await this.#safely(() => route.continue());\n return;\n }\n\n if (this.#unmatched.size < MAX_UNMATCHED_TRACKED) {\n this.#unmatched.set(request, {\n method: request.method(),\n url: shortenUrl(request.url()),\n ...(this.#blockUnmatched ? { blocked: true } : {}),\n });\n }\n\n await this.#safely(() => (this.#blockUnmatched ? route.abort(\"failed\") : route.continue()));\n }\n\n /**\n * The page's own navigation, which is never mocked and never counted. A\n * request whose frame has already gone cannot be the main document any more,\n * so a throw here means \"no\".\n */\n #isMainDocument(request: Request): boolean {\n try {\n return request.resourceType() === \"document\" && request.frame() === this.#page?.mainFrame();\n } catch {\n return false;\n }\n }\n\n /** A delay that a `dispose()` can cut short. */\n #wait(ms: number): Promise<void> {\n if (this.#disposed) return Promise.resolve();\n return new Promise<void>((resolve) => {\n const wait = {\n timer: setTimeout(() => {\n this.#waits.delete(wait);\n resolve();\n }, ms),\n resolve,\n };\n this.#waits.add(wait);\n });\n }\n\n /**\n * Run one route call, swallowing the failure.\n *\n * A route whose page has navigated away, closed or crashed rejects, and none\n * of that is worth failing a recording over — the frames are the point. It\n * also has to be caught rather than left floating: an unhandled rejection\n * from a route handler would surface as a crash somewhere unrelated.\n */\n async #safely(run: () => Promise<void>): Promise<boolean> {\n try {\n await run();\n return true;\n } catch {\n return false;\n }\n }\n}\n"]}
@@ -0,0 +1,129 @@
1
+ import type { Page } from "playwright";
2
+ import type { ElementMeasurement, ElementPair } from "../utils/rtl-rules.js";
3
+ /**
4
+ * RTL engine.
5
+ *
6
+ * Puts a page into RTL, optionally replaces its text with Arabic, and measures
7
+ * every laid-out element. Nothing here decides what is *wrong*; that is
8
+ * `utils/rtl-rules.ts`, which is pure and unit-tested without a browser.
9
+ *
10
+ * The whole design rests on one idea: an RTL bug is something that failed to
11
+ * change. So the same page is measured twice — once as it ships, once flipped
12
+ * — and the two measurements are paired element by element. That pairing is
13
+ * the hard part, because the two renders are two separate page loads: element
14
+ * identity has to survive a reload, and it cannot be an ElementHandle, an
15
+ * index, or a position (position is precisely the thing under test).
16
+ *
17
+ * `keyFor` solves that with a structural key — tag, id, class, and the
18
+ * element's path among its siblings — computed identically in both passes.
19
+ * Anything that does not appear in both is simply dropped: an element that
20
+ * exists in only one direction is a page that renders differently, which is
21
+ * interesting but is not a mirroring bug, and guessing at a pairing would
22
+ * invent findings.
23
+ */
24
+ /**
25
+ * How to make the page render right-to-left.
26
+ *
27
+ * Four triggers, because apps do this four different ways and picking the
28
+ * wrong one silently measures the page twice in LTR — which reports no
29
+ * findings at all and looks exactly like a page with no RTL bugs. That failure
30
+ * is why `verifyDirection` exists.
31
+ */
32
+ export type RtlTrigger = {
33
+ type: "attribute";
34
+ attr: string;
35
+ value: string;
36
+ target: string;
37
+ } | {
38
+ type: "class";
39
+ class: string;
40
+ target: string;
41
+ } | {
42
+ type: "locale";
43
+ locale: string;
44
+ } | {
45
+ type: "url";
46
+ rtl_url: string;
47
+ };
48
+ /** The trigger used when the caller names none: the way the platform itself defines RTL. */
49
+ export declare const DEFAULT_RTL_TRIGGER: RtlTrigger;
50
+ /**
51
+ * Apply the trigger to an already-loaded page.
52
+ *
53
+ * `url` is not applied here — it is a different page and is navigated to by
54
+ * the caller before this is reached. `locale` likewise is a context option,
55
+ * fixed when the browser context is created; what this does for it is set
56
+ * `dir` as well, because a locale alone changes number and date formatting but
57
+ * does not flip the layout: `Accept-Language: ar` does not put a page in RTL,
58
+ * and a tool that assumed it did would report a clean bill of health on a
59
+ * completely broken page.
60
+ */
61
+ export declare function applyRtlTrigger(page: Page, trigger: RtlTrigger): Promise<void>;
62
+ /**
63
+ * Check that the page really is rendering right-to-left.
64
+ *
65
+ * The single most damaging way this tool can fail is quietly: a trigger that
66
+ * does not match the app under test measures LTR twice, finds every element
67
+ * identical, and reports that the page has no RTL problems. That is worse than
68
+ * an error, because it is a confident wrong answer. So the direction that
69
+ * actually took effect is read back off the document and the body, and the
70
+ * caller refuses to report anything if it is still `ltr`.
71
+ */
72
+ export declare function readDirection(page: Page): Promise<{
73
+ html: string;
74
+ body: string;
75
+ }>;
76
+ /**
77
+ * Replace every visible text node with Arabic of about the same length.
78
+ *
79
+ * Run in *both* passes, not just the RTL one. That is not obvious and it is
80
+ * essential: the two passes are compared to each other, so they have to differ
81
+ * in exactly one variable. If only the RTL pass got Arabic, every box on the
82
+ * page would change width for reasons of text metrics rather than direction,
83
+ * and the mirroring comparison — which asks whether a box moved — would be
84
+ * measuring the font, not the layout.
85
+ *
86
+ * Text inside `<script>`, `<style>` and friends is left alone, as is anything
87
+ * inside an element the caller excluded. Input `value`s and `placeholder`s are
88
+ * replaced too, since a form is where RTL text is most likely to be mishandled.
89
+ */
90
+ export declare function injectArabic(page: Page, exclude?: string): Promise<InjectionResult>;
91
+ /** What an injection did, or why it did nothing. */
92
+ export interface InjectionResult {
93
+ replaced: number;
94
+ /** Set when the injection could not run at all — never confused with "no text to replace". */
95
+ error?: string;
96
+ }
97
+ export interface MeasureOptions {
98
+ /** Only measure inside this. Defaults to the whole document. */
99
+ selector?: string;
100
+ /** Never measure this, or anything inside it. */
101
+ exclude?: string;
102
+ /** Stop after this many elements. */
103
+ max_elements?: number;
104
+ }
105
+ /**
106
+ * Measure every laid-out element on the page.
107
+ *
108
+ * "Laid out" is doing a lot of work: elements with no box, no size, or no
109
+ * visibility are skipped, because an element that is not rendered cannot have
110
+ * mirrored and reporting it would be noise. So are the structural wrappers
111
+ * that carry no text and exactly one child — a `<div>` whose only job is to
112
+ * hold another `<div>` has no independent layout to get wrong, and including
113
+ * them would multiply every real finding by the depth of the tree it sits in.
114
+ */
115
+ export declare function measureElements(page: Page, options?: MeasureOptions): Promise<ElementMeasurement[]>;
116
+ /**
117
+ * Pair up two measurements of the same page.
118
+ *
119
+ * An element is only paired when its structural key appears exactly once in
120
+ * each pass. A key that appears twice in either pass is ambiguous — pairing it
121
+ * by order would be a guess, and a wrong guess invents a mirroring bug out of
122
+ * two different elements — so it is dropped along with anything that appears
123
+ * in only one direction. `unpaired` counts what was dropped so the report can
124
+ * say so rather than silently measuring less than it claims.
125
+ */
126
+ export declare function pairMeasurements(ltr: readonly ElementMeasurement[], rtl: readonly ElementMeasurement[]): {
127
+ pairs: ElementPair[];
128
+ unpaired: number;
129
+ };