framewatch-mcp-server 0.1.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/LICENSE +21 -0
- package/README.md +537 -0
- package/dist/constants.d.ts +172 -0
- package/dist/constants.js +168 -0
- package/dist/constants.js.map +1 -0
- package/dist/engine/browser.d.ts +56 -0
- package/dist/engine/browser.js +142 -0
- package/dist/engine/browser.js.map +1 -0
- package/dist/engine/differ.d.ts +88 -0
- package/dist/engine/differ.js +373 -0
- package/dist/engine/differ.js.map +1 -0
- package/dist/engine/interaction.d.ts +76 -0
- package/dist/engine/interaction.js +254 -0
- package/dist/engine/interaction.js.map +1 -0
- package/dist/engine/layers/console.d.ts +63 -0
- package/dist/engine/layers/console.js +118 -0
- package/dist/engine/layers/console.js.map +1 -0
- package/dist/engine/layers/dom.d.ts +53 -0
- package/dist/engine/layers/dom.js +282 -0
- package/dist/engine/layers/dom.js.map +1 -0
- package/dist/engine/layers/index.d.ts +95 -0
- package/dist/engine/layers/index.js +184 -0
- package/dist/engine/layers/index.js.map +1 -0
- package/dist/engine/layers/network.d.ts +62 -0
- package/dist/engine/layers/network.js +169 -0
- package/dist/engine/layers/network.js.map +1 -0
- package/dist/engine/layers/performance.d.ts +55 -0
- package/dist/engine/layers/performance.js +215 -0
- package/dist/engine/layers/performance.js.map +1 -0
- package/dist/engine/layers/probe.d.ts +50 -0
- package/dist/engine/layers/probe.js +39 -0
- package/dist/engine/layers/probe.js.map +1 -0
- package/dist/engine/layers/session.d.ts +46 -0
- package/dist/engine/layers/session.js +131 -0
- package/dist/engine/layers/session.js.map +1 -0
- package/dist/engine/recorder.d.ts +61 -0
- package/dist/engine/recorder.js +256 -0
- package/dist/engine/recorder.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/accessibility.d.ts +140 -0
- package/dist/tools/accessibility.js +357 -0
- package/dist/tools/accessibility.js.map +1 -0
- package/dist/tools/capture.d.ts +279 -0
- package/dist/tools/capture.js +275 -0
- package/dist/tools/capture.js.map +1 -0
- package/dist/tools/compare.d.ts +86 -0
- package/dist/tools/compare.js +247 -0
- package/dist/tools/compare.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +25 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/interact.d.ts +160 -0
- package/dist/tools/interact.js +203 -0
- package/dist/tools/interact.js.map +1 -0
- package/dist/tools/responsive.d.ts +89 -0
- package/dist/tools/responsive.js +197 -0
- package/dist/tools/responsive.js.map +1 -0
- package/dist/tools/screenshot.d.ts +76 -0
- package/dist/tools/screenshot.js +117 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/server.d.ts +89 -0
- package/dist/tools/server.js +201 -0
- package/dist/tools/server.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/bounded-log.d.ts +41 -0
- package/dist/utils/bounded-log.js +78 -0
- package/dist/utils/bounded-log.js.map +1 -0
- package/dist/utils/format.d.ts +56 -0
- package/dist/utils/format.js +130 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/image.d.ts +44 -0
- package/dist/utils/image.js +81 -0
- package/dist/utils/image.js.map +1 -0
- package/dist/utils/server-process.d.ts +84 -0
- package/dist/utils/server-process.js +251 -0
- package/dist/utils/server-process.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** Default capture interval between raw frames (10 fps). */
|
|
2
|
+
export declare const CAPTURE_INTERVAL_MS = 100;
|
|
3
|
+
/** Grid used by the smart diff engine (GRID_SIZE x GRID_SIZE cells). */
|
|
4
|
+
export declare const GRID_SIZE = 8;
|
|
5
|
+
/** Mean absolute per-pixel difference (0–255) for a grid cell to count as changed. */
|
|
6
|
+
export declare const CELL_THRESHOLD = 15;
|
|
7
|
+
/** Absolute per-pixel grayscale difference (0–255) for a single pixel to count as changed (full-res bbox). */
|
|
8
|
+
export declare const PIXEL_THRESHOLD = 15;
|
|
9
|
+
/** Fraction of grid cells that must change for a frame to be kept. */
|
|
10
|
+
export declare const DEFAULT_SENSITIVITY = 0.06;
|
|
11
|
+
/** Kept frames closer than this are merged, keeping the later "settled" one. */
|
|
12
|
+
export declare const MERGE_WINDOW_MS = 200;
|
|
13
|
+
/** Padding added around the change bounding box when cropping. */
|
|
14
|
+
export declare const CROP_PADDING_PX = 20;
|
|
15
|
+
/** Skip the crop image when the padded bounding box covers at least this fraction of the frame. */
|
|
16
|
+
export declare const CROP_SKIP_COVERAGE = 0.9;
|
|
17
|
+
/** Low-res size used for fast frame comparison. */
|
|
18
|
+
export declare const DIFF_WIDTH = 320;
|
|
19
|
+
export declare const DIFF_HEIGHT = 240;
|
|
20
|
+
/** Maximum width of images returned to the MCP client. */
|
|
21
|
+
export declare const OUTPUT_MAX_WIDTH = 800;
|
|
22
|
+
/** Hard cap on diff cards returned by a single capture. */
|
|
23
|
+
export declare const MAX_FRAMES_CAP = 30;
|
|
24
|
+
/** Default number of diff cards returned by a capture. */
|
|
25
|
+
export declare const DEFAULT_MAX_FRAMES = 20;
|
|
26
|
+
/** Capture recording length bounds and default. */
|
|
27
|
+
export declare const DEFAULT_CAPTURE_DURATION_MS = 5000;
|
|
28
|
+
export declare const MIN_CAPTURE_DURATION_MS = 500;
|
|
29
|
+
export declare const MAX_CAPTURE_DURATION_MS = 30000;
|
|
30
|
+
/**
|
|
31
|
+
* Upper bound on a capture viewport. A recording holds every raw PNG in
|
|
32
|
+
* memory until the cards are built, so an unbounded viewport is an
|
|
33
|
+
* out-of-memory risk (a 30s 4K recording already holds hundreds of MB).
|
|
34
|
+
*/
|
|
35
|
+
export declare const MAX_VIEWPORT_WIDTH = 3840;
|
|
36
|
+
export declare const MAX_VIEWPORT_HEIGHT = 2160;
|
|
37
|
+
/** Default viewport for all page-based tools. */
|
|
38
|
+
export declare const DEFAULT_VIEWPORT: {
|
|
39
|
+
readonly width: 1280;
|
|
40
|
+
readonly height: 720;
|
|
41
|
+
};
|
|
42
|
+
/** Default wait after page load before a screenshot is taken. */
|
|
43
|
+
export declare const DEFAULT_SCREENSHOT_WAIT_MS = 1000;
|
|
44
|
+
/** Default navigation timeout for page.goto. */
|
|
45
|
+
export declare const NAVIGATION_TIMEOUT_MS = 30000;
|
|
46
|
+
/**
|
|
47
|
+
* Floor for the per-screenshot timeout. Chromium will not produce a screenshot
|
|
48
|
+
* while the main frame has a pending cross-document navigation or a blocked
|
|
49
|
+
* main thread, and Playwright's 30s default would stall the whole recording.
|
|
50
|
+
*/
|
|
51
|
+
export declare const SCREENSHOT_TIMEOUT_MS = 2000;
|
|
52
|
+
/**
|
|
53
|
+
* Bound on the final screenshot taken by stop() when the previous attempt
|
|
54
|
+
* timed out. Short, so an genuinely wedged page cannot stall shutdown, but
|
|
55
|
+
* still enough for a page that has since recovered (e.g. after a navigation).
|
|
56
|
+
*/
|
|
57
|
+
export declare const SCREENSHOT_FINAL_TIMEOUT_MS = 500;
|
|
58
|
+
/**
|
|
59
|
+
* Chromium refuses a screenshot until it has produced its first frame (right
|
|
60
|
+
* after a navigation commits). Retry that specific failure a few times.
|
|
61
|
+
*/
|
|
62
|
+
export declare const SCREENSHOT_RETRY_ATTEMPTS = 4;
|
|
63
|
+
export declare const SCREENSHOT_RETRY_DELAY_MS = 15;
|
|
64
|
+
/**
|
|
65
|
+
* Bound on the cosmetic page metadata read after a recording (the title).
|
|
66
|
+
* `page.title()` takes no timeout of its own and blocks for Playwright's full
|
|
67
|
+
* 30s default while the page's main thread is busy.
|
|
68
|
+
*/
|
|
69
|
+
export declare const PAGE_INFO_TIMEOUT_MS = 1000;
|
|
70
|
+
/** Default timeout when waiting for a selector to appear. */
|
|
71
|
+
export declare const SELECTOR_TIMEOUT_MS = 10000;
|
|
72
|
+
/**
|
|
73
|
+
* A swipe is dispatched as touchStart → SWIPE_STEPS touchMoves → touchEnd,
|
|
74
|
+
* with each move about a frame apart so the page's own velocity maths (a
|
|
75
|
+
* carousel, pull-to-refresh) sees a plausible gesture rather than a teleport.
|
|
76
|
+
*/
|
|
77
|
+
export declare const SWIPE_STEPS = 10;
|
|
78
|
+
export declare const SWIPE_STEP_DELAY_MS = 16;
|
|
79
|
+
/**
|
|
80
|
+
* Cap on the length of a replayed interaction script. Each step can carry its
|
|
81
|
+
* own delay, so an unbounded script would sidestep MAX_CAPTURE_DURATION_MS.
|
|
82
|
+
*/
|
|
83
|
+
export declare const MAX_INTERACTIONS = 50;
|
|
84
|
+
/** Default settle time between an interaction and its "after" screenshot. */
|
|
85
|
+
export declare const DEFAULT_INTERACT_WAIT_MS = 500;
|
|
86
|
+
/** Console entries kept per capture. Errors evict older non-errors once full. */
|
|
87
|
+
export declare const MAX_CONSOLE_ENTRIES = 100;
|
|
88
|
+
/** Console text longer than this is elided — one runaway log must not fill the response. */
|
|
89
|
+
export declare const MAX_CONSOLE_TEXT_LENGTH = 300;
|
|
90
|
+
/** Network events kept per capture. Failed/error responses evict older successful ones once full. */
|
|
91
|
+
export declare const MAX_NETWORK_EVENTS = 100;
|
|
92
|
+
/** URLs longer than this are shortened in the middle (query strings and data: URIs are unbounded). */
|
|
93
|
+
export declare const MAX_NETWORK_URL_LENGTH = 120;
|
|
94
|
+
/** DOM mutation records kept per capture, before grouping. */
|
|
95
|
+
export declare const MAX_DOM_RECORDS = 500;
|
|
96
|
+
/** Grouped DOM lines rendered on a single card. */
|
|
97
|
+
export declare const MAX_DOM_LINES_PER_CARD = 12;
|
|
98
|
+
/** Performance entries (paint, LCP, layout shift) kept per capture. */
|
|
99
|
+
export declare const MAX_PERF_SAMPLES = 500;
|
|
100
|
+
/**
|
|
101
|
+
* How long the in-page probes batch records before pushing them to Node.
|
|
102
|
+
* Roughly one animation frame: long enough to coalesce a burst of mutations
|
|
103
|
+
* into one binding call, short enough that little is lost if the document is
|
|
104
|
+
* replaced. Record timestamps are stamped when the record is made, not when
|
|
105
|
+
* the batch is flushed, so batching never affects which card a record lands on.
|
|
106
|
+
*/
|
|
107
|
+
export declare const LAYER_FLUSH_MS = 32;
|
|
108
|
+
/** Records one in-page batch may carry. A page that mutates more than this per flush is reporting a storm, not detail. */
|
|
109
|
+
export declare const MAX_LAYER_BATCH = 200;
|
|
110
|
+
/** Records an in-page probe may push over the lifetime of one document. */
|
|
111
|
+
export declare const MAX_LAYER_RECORDS_PER_DOCUMENT = 2000;
|
|
112
|
+
/** Viewports `framewatch_responsive` uses when the caller names none. */
|
|
113
|
+
export declare const DEFAULT_RESPONSIVE_VIEWPORTS: readonly [{
|
|
114
|
+
readonly name: "mobile";
|
|
115
|
+
readonly width: 375;
|
|
116
|
+
readonly height: 812;
|
|
117
|
+
}, {
|
|
118
|
+
readonly name: "tablet";
|
|
119
|
+
readonly width: 768;
|
|
120
|
+
readonly height: 1024;
|
|
121
|
+
}, {
|
|
122
|
+
readonly name: "desktop";
|
|
123
|
+
readonly width: 1440;
|
|
124
|
+
readonly height: 900;
|
|
125
|
+
}];
|
|
126
|
+
/** Viewports one responsive call may capture. Each one is a live browser context. */
|
|
127
|
+
export declare const MAX_RESPONSIVE_VIEWPORTS = 8;
|
|
128
|
+
/** Default settle time after load before each responsive screenshot. */
|
|
129
|
+
export declare const DEFAULT_RESPONSIVE_WAIT_MS = 2000;
|
|
130
|
+
/**
|
|
131
|
+
* Slack (px) allowed before content counts as overflowing its viewport.
|
|
132
|
+
* Sub-pixel layout rounding routinely puts scrollWidth one pixel over
|
|
133
|
+
* clientWidth on a page that is perfectly fine.
|
|
134
|
+
*/
|
|
135
|
+
export declare const OVERFLOW_TOLERANCE_PX = 1;
|
|
136
|
+
/** Default settle time after load before each compared screenshot. */
|
|
137
|
+
export declare const DEFAULT_COMPARE_WAIT_MS = 2000;
|
|
138
|
+
/** Colour painted over changed pixels in the compare overlay, and its opacity (0–255). */
|
|
139
|
+
export declare const OVERLAY_COLOUR: {
|
|
140
|
+
readonly r: 255;
|
|
141
|
+
readonly g: 0;
|
|
142
|
+
readonly b: 200;
|
|
143
|
+
};
|
|
144
|
+
export declare const OVERLAY_ALPHA = 190;
|
|
145
|
+
/** Default settle time after load before the audit runs. */
|
|
146
|
+
export declare const DEFAULT_A11Y_WAIT_MS = 1000;
|
|
147
|
+
/** Violations reported by one audit, and elements listed under each. */
|
|
148
|
+
export declare const MAX_A11Y_VIOLATIONS = 25;
|
|
149
|
+
export declare const MAX_A11Y_NODES_PER_VIOLATION = 3;
|
|
150
|
+
/** Length of one element's HTML in a violation report before it is elided. */
|
|
151
|
+
export declare const MAX_A11Y_HTML_LENGTH = 160;
|
|
152
|
+
/** Bound on the axe-core run itself (a huge DOM can take a while). */
|
|
153
|
+
export declare const A11Y_RUN_TIMEOUT_MS = 60000;
|
|
154
|
+
/** How long axe waits for an iframe to answer before auditing without it. */
|
|
155
|
+
export declare const A11Y_FRAME_WAIT_MS = 5000;
|
|
156
|
+
/** Default regex matched against dev server output to spot its "ready" line. */
|
|
157
|
+
export declare const DEFAULT_READY_PATTERN = "ready|started|listening|Local:";
|
|
158
|
+
/** Bounds and default for how long `framewatch_start_server` waits for the port. */
|
|
159
|
+
export declare const DEFAULT_SERVER_TIMEOUT_MS = 30000;
|
|
160
|
+
export declare const MIN_SERVER_TIMEOUT_MS = 100;
|
|
161
|
+
export declare const MAX_SERVER_TIMEOUT_MS = 300000;
|
|
162
|
+
/** Output lines kept from a dev server. Lines mentioning errors evict ordinary ones. */
|
|
163
|
+
export declare const MAX_SERVER_LOG_LINES = 200;
|
|
164
|
+
/** Length of one captured output line before it is elided. */
|
|
165
|
+
export declare const MAX_SERVER_LINE_LENGTH = 300;
|
|
166
|
+
/** Output lines quoted back when a server fails to start or is stopped. */
|
|
167
|
+
export declare const SERVER_OUTPUT_TAIL = 15;
|
|
168
|
+
/** How often the port is probed while waiting for the server, and the bound on one probe. */
|
|
169
|
+
export declare const SERVER_PORT_POLL_MS = 200;
|
|
170
|
+
export declare const SERVER_PORT_PROBE_TIMEOUT_MS = 1000;
|
|
171
|
+
/** Time a stopped server gets to exit on SIGTERM before it is killed outright. */
|
|
172
|
+
export declare const SERVER_STOP_GRACE_MS = 5000;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/** Default capture interval between raw frames (10 fps). */
|
|
2
|
+
export const CAPTURE_INTERVAL_MS = 100;
|
|
3
|
+
/** Grid used by the smart diff engine (GRID_SIZE x GRID_SIZE cells). */
|
|
4
|
+
export const GRID_SIZE = 8;
|
|
5
|
+
/** Mean absolute per-pixel difference (0–255) for a grid cell to count as changed. */
|
|
6
|
+
export const CELL_THRESHOLD = 15;
|
|
7
|
+
/** Absolute per-pixel grayscale difference (0–255) for a single pixel to count as changed (full-res bbox). */
|
|
8
|
+
export const PIXEL_THRESHOLD = 15;
|
|
9
|
+
/** Fraction of grid cells that must change for a frame to be kept. */
|
|
10
|
+
export const DEFAULT_SENSITIVITY = 0.06;
|
|
11
|
+
/** Kept frames closer than this are merged, keeping the later "settled" one. */
|
|
12
|
+
export const MERGE_WINDOW_MS = 200;
|
|
13
|
+
/** Padding added around the change bounding box when cropping. */
|
|
14
|
+
export const CROP_PADDING_PX = 20;
|
|
15
|
+
/** Skip the crop image when the padded bounding box covers at least this fraction of the frame. */
|
|
16
|
+
export const CROP_SKIP_COVERAGE = 0.9;
|
|
17
|
+
/** Low-res size used for fast frame comparison. */
|
|
18
|
+
export const DIFF_WIDTH = 320;
|
|
19
|
+
export const DIFF_HEIGHT = 240;
|
|
20
|
+
/** Maximum width of images returned to the MCP client. */
|
|
21
|
+
export const OUTPUT_MAX_WIDTH = 800;
|
|
22
|
+
/** Hard cap on diff cards returned by a single capture. */
|
|
23
|
+
export const MAX_FRAMES_CAP = 30;
|
|
24
|
+
/** Default number of diff cards returned by a capture. */
|
|
25
|
+
export const DEFAULT_MAX_FRAMES = 20;
|
|
26
|
+
/** Capture recording length bounds and default. */
|
|
27
|
+
export const DEFAULT_CAPTURE_DURATION_MS = 5000;
|
|
28
|
+
export const MIN_CAPTURE_DURATION_MS = 500;
|
|
29
|
+
export const MAX_CAPTURE_DURATION_MS = 30_000;
|
|
30
|
+
/**
|
|
31
|
+
* Upper bound on a capture viewport. A recording holds every raw PNG in
|
|
32
|
+
* memory until the cards are built, so an unbounded viewport is an
|
|
33
|
+
* out-of-memory risk (a 30s 4K recording already holds hundreds of MB).
|
|
34
|
+
*/
|
|
35
|
+
export const MAX_VIEWPORT_WIDTH = 3840;
|
|
36
|
+
export const MAX_VIEWPORT_HEIGHT = 2160;
|
|
37
|
+
/** Default viewport for all page-based tools. */
|
|
38
|
+
export const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
39
|
+
/** Default wait after page load before a screenshot is taken. */
|
|
40
|
+
export const DEFAULT_SCREENSHOT_WAIT_MS = 1000;
|
|
41
|
+
/** Default navigation timeout for page.goto. */
|
|
42
|
+
export const NAVIGATION_TIMEOUT_MS = 30_000;
|
|
43
|
+
/**
|
|
44
|
+
* Floor for the per-screenshot timeout. Chromium will not produce a screenshot
|
|
45
|
+
* while the main frame has a pending cross-document navigation or a blocked
|
|
46
|
+
* main thread, and Playwright's 30s default would stall the whole recording.
|
|
47
|
+
*/
|
|
48
|
+
export const SCREENSHOT_TIMEOUT_MS = 2000;
|
|
49
|
+
/**
|
|
50
|
+
* Bound on the final screenshot taken by stop() when the previous attempt
|
|
51
|
+
* timed out. Short, so an genuinely wedged page cannot stall shutdown, but
|
|
52
|
+
* still enough for a page that has since recovered (e.g. after a navigation).
|
|
53
|
+
*/
|
|
54
|
+
export const SCREENSHOT_FINAL_TIMEOUT_MS = 500;
|
|
55
|
+
/**
|
|
56
|
+
* Chromium refuses a screenshot until it has produced its first frame (right
|
|
57
|
+
* after a navigation commits). Retry that specific failure a few times.
|
|
58
|
+
*/
|
|
59
|
+
export const SCREENSHOT_RETRY_ATTEMPTS = 4;
|
|
60
|
+
export const SCREENSHOT_RETRY_DELAY_MS = 15;
|
|
61
|
+
/**
|
|
62
|
+
* Bound on the cosmetic page metadata read after a recording (the title).
|
|
63
|
+
* `page.title()` takes no timeout of its own and blocks for Playwright's full
|
|
64
|
+
* 30s default while the page's main thread is busy.
|
|
65
|
+
*/
|
|
66
|
+
export const PAGE_INFO_TIMEOUT_MS = 1000;
|
|
67
|
+
/** Default timeout when waiting for a selector to appear. */
|
|
68
|
+
export const SELECTOR_TIMEOUT_MS = 10_000;
|
|
69
|
+
/**
|
|
70
|
+
* A swipe is dispatched as touchStart → SWIPE_STEPS touchMoves → touchEnd,
|
|
71
|
+
* with each move about a frame apart so the page's own velocity maths (a
|
|
72
|
+
* carousel, pull-to-refresh) sees a plausible gesture rather than a teleport.
|
|
73
|
+
*/
|
|
74
|
+
export const SWIPE_STEPS = 10;
|
|
75
|
+
export const SWIPE_STEP_DELAY_MS = 16;
|
|
76
|
+
/**
|
|
77
|
+
* Cap on the length of a replayed interaction script. Each step can carry its
|
|
78
|
+
* own delay, so an unbounded script would sidestep MAX_CAPTURE_DURATION_MS.
|
|
79
|
+
*/
|
|
80
|
+
export const MAX_INTERACTIONS = 50;
|
|
81
|
+
/** Default settle time between an interaction and its "after" screenshot. */
|
|
82
|
+
export const DEFAULT_INTERACT_WAIT_MS = 500;
|
|
83
|
+
/* ── Context layers (Phase 4) ─────────────────────────────────────────────
|
|
84
|
+
* Every layer is bounded twice over: in the page (so a runaway app cannot
|
|
85
|
+
* grow the tab's memory) and in Node (so one capture cannot flood the MCP
|
|
86
|
+
* response). The Node-side caps are the ones a user notices, and each layer
|
|
87
|
+
* reports what it had to drop.
|
|
88
|
+
*/
|
|
89
|
+
/** Console entries kept per capture. Errors evict older non-errors once full. */
|
|
90
|
+
export const MAX_CONSOLE_ENTRIES = 100;
|
|
91
|
+
/** Console text longer than this is elided — one runaway log must not fill the response. */
|
|
92
|
+
export const MAX_CONSOLE_TEXT_LENGTH = 300;
|
|
93
|
+
/** Network events kept per capture. Failed/error responses evict older successful ones once full. */
|
|
94
|
+
export const MAX_NETWORK_EVENTS = 100;
|
|
95
|
+
/** URLs longer than this are shortened in the middle (query strings and data: URIs are unbounded). */
|
|
96
|
+
export const MAX_NETWORK_URL_LENGTH = 120;
|
|
97
|
+
/** DOM mutation records kept per capture, before grouping. */
|
|
98
|
+
export const MAX_DOM_RECORDS = 500;
|
|
99
|
+
/** Grouped DOM lines rendered on a single card. */
|
|
100
|
+
export const MAX_DOM_LINES_PER_CARD = 12;
|
|
101
|
+
/** Performance entries (paint, LCP, layout shift) kept per capture. */
|
|
102
|
+
export const MAX_PERF_SAMPLES = 500;
|
|
103
|
+
/**
|
|
104
|
+
* How long the in-page probes batch records before pushing them to Node.
|
|
105
|
+
* Roughly one animation frame: long enough to coalesce a burst of mutations
|
|
106
|
+
* into one binding call, short enough that little is lost if the document is
|
|
107
|
+
* replaced. Record timestamps are stamped when the record is made, not when
|
|
108
|
+
* the batch is flushed, so batching never affects which card a record lands on.
|
|
109
|
+
*/
|
|
110
|
+
export const LAYER_FLUSH_MS = 32;
|
|
111
|
+
/** Records one in-page batch may carry. A page that mutates more than this per flush is reporting a storm, not detail. */
|
|
112
|
+
export const MAX_LAYER_BATCH = 200;
|
|
113
|
+
/** Records an in-page probe may push over the lifetime of one document. */
|
|
114
|
+
export const MAX_LAYER_RECORDS_PER_DOCUMENT = 2000;
|
|
115
|
+
/* ── Responsive (Phase 5) ─────────────────────────────────────────────── */
|
|
116
|
+
/** Viewports `framewatch_responsive` uses when the caller names none. */
|
|
117
|
+
export const DEFAULT_RESPONSIVE_VIEWPORTS = [
|
|
118
|
+
{ name: "mobile", width: 375, height: 812 },
|
|
119
|
+
{ name: "tablet", width: 768, height: 1024 },
|
|
120
|
+
{ name: "desktop", width: 1440, height: 900 },
|
|
121
|
+
];
|
|
122
|
+
/** Viewports one responsive call may capture. Each one is a live browser context. */
|
|
123
|
+
export const MAX_RESPONSIVE_VIEWPORTS = 8;
|
|
124
|
+
/** Default settle time after load before each responsive screenshot. */
|
|
125
|
+
export const DEFAULT_RESPONSIVE_WAIT_MS = 2000;
|
|
126
|
+
/**
|
|
127
|
+
* Slack (px) allowed before content counts as overflowing its viewport.
|
|
128
|
+
* Sub-pixel layout rounding routinely puts scrollWidth one pixel over
|
|
129
|
+
* clientWidth on a page that is perfectly fine.
|
|
130
|
+
*/
|
|
131
|
+
export const OVERFLOW_TOLERANCE_PX = 1;
|
|
132
|
+
/* ── Compare (Phase 5) ────────────────────────────────────────────────── */
|
|
133
|
+
/** Default settle time after load before each compared screenshot. */
|
|
134
|
+
export const DEFAULT_COMPARE_WAIT_MS = 2000;
|
|
135
|
+
/** Colour painted over changed pixels in the compare overlay, and its opacity (0–255). */
|
|
136
|
+
export const OVERLAY_COLOUR = { r: 255, g: 0, b: 200 };
|
|
137
|
+
export const OVERLAY_ALPHA = 190;
|
|
138
|
+
/* ── Accessibility (Phase 5) ──────────────────────────────────────────── */
|
|
139
|
+
/** Default settle time after load before the audit runs. */
|
|
140
|
+
export const DEFAULT_A11Y_WAIT_MS = 1000;
|
|
141
|
+
/** Violations reported by one audit, and elements listed under each. */
|
|
142
|
+
export const MAX_A11Y_VIOLATIONS = 25;
|
|
143
|
+
export const MAX_A11Y_NODES_PER_VIOLATION = 3;
|
|
144
|
+
/** Length of one element's HTML in a violation report before it is elided. */
|
|
145
|
+
export const MAX_A11Y_HTML_LENGTH = 160;
|
|
146
|
+
/** Bound on the axe-core run itself (a huge DOM can take a while). */
|
|
147
|
+
export const A11Y_RUN_TIMEOUT_MS = 60_000;
|
|
148
|
+
/** How long axe waits for an iframe to answer before auditing without it. */
|
|
149
|
+
export const A11Y_FRAME_WAIT_MS = 5000;
|
|
150
|
+
/* ── Dev server (Phase 5) ─────────────────────────────────────────────── */
|
|
151
|
+
/** Default regex matched against dev server output to spot its "ready" line. */
|
|
152
|
+
export const DEFAULT_READY_PATTERN = "ready|started|listening|Local:";
|
|
153
|
+
/** Bounds and default for how long `framewatch_start_server` waits for the port. */
|
|
154
|
+
export const DEFAULT_SERVER_TIMEOUT_MS = 30_000;
|
|
155
|
+
export const MIN_SERVER_TIMEOUT_MS = 100;
|
|
156
|
+
export const MAX_SERVER_TIMEOUT_MS = 300_000;
|
|
157
|
+
/** Output lines kept from a dev server. Lines mentioning errors evict ordinary ones. */
|
|
158
|
+
export const MAX_SERVER_LOG_LINES = 200;
|
|
159
|
+
/** Length of one captured output line before it is elided. */
|
|
160
|
+
export const MAX_SERVER_LINE_LENGTH = 300;
|
|
161
|
+
/** Output lines quoted back when a server fails to start or is stopped. */
|
|
162
|
+
export const SERVER_OUTPUT_TAIL = 15;
|
|
163
|
+
/** How often the port is probed while waiting for the server, and the bound on one probe. */
|
|
164
|
+
export const SERVER_PORT_POLL_MS = 200;
|
|
165
|
+
export const SERVER_PORT_PROBE_TIMEOUT_MS = 1000;
|
|
166
|
+
/** Time a stopped server gets to exit on SIGTERM before it is killed outright. */
|
|
167
|
+
export const SERVER_STOP_GRACE_MS = 5000;
|
|
168
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"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","sourcesContent":["/** Default capture interval between raw frames (10 fps). */\nexport const CAPTURE_INTERVAL_MS = 100;\n\n/** Grid used by the smart diff engine (GRID_SIZE x GRID_SIZE cells). */\nexport const GRID_SIZE = 8;\n\n/** Mean absolute per-pixel difference (0–255) for a grid cell to count as changed. */\nexport const CELL_THRESHOLD = 15;\n\n/** Absolute per-pixel grayscale difference (0–255) for a single pixel to count as changed (full-res bbox). */\nexport const PIXEL_THRESHOLD = 15;\n\n/** Fraction of grid cells that must change for a frame to be kept. */\nexport const DEFAULT_SENSITIVITY = 0.06;\n\n/** Kept frames closer than this are merged, keeping the later \"settled\" one. */\nexport const MERGE_WINDOW_MS = 200;\n\n/** Padding added around the change bounding box when cropping. */\nexport const CROP_PADDING_PX = 20;\n\n/** Skip the crop image when the padded bounding box covers at least this fraction of the frame. */\nexport const CROP_SKIP_COVERAGE = 0.9;\n\n/** Low-res size used for fast frame comparison. */\nexport const DIFF_WIDTH = 320;\nexport const DIFF_HEIGHT = 240;\n\n/** Maximum width of images returned to the MCP client. */\nexport const OUTPUT_MAX_WIDTH = 800;\n\n/** Hard cap on diff cards returned by a single capture. */\nexport const MAX_FRAMES_CAP = 30;\n\n/** Default number of diff cards returned by a capture. */\nexport const DEFAULT_MAX_FRAMES = 20;\n\n/** Capture recording length bounds and default. */\nexport const DEFAULT_CAPTURE_DURATION_MS = 5000;\nexport const MIN_CAPTURE_DURATION_MS = 500;\nexport const MAX_CAPTURE_DURATION_MS = 30_000;\n\n/**\n * Upper bound on a capture viewport. A recording holds every raw PNG in\n * memory until the cards are built, so an unbounded viewport is an\n * out-of-memory risk (a 30s 4K recording already holds hundreds of MB).\n */\nexport const MAX_VIEWPORT_WIDTH = 3840;\nexport const MAX_VIEWPORT_HEIGHT = 2160;\n\n/** Default viewport for all page-based tools. */\nexport const DEFAULT_VIEWPORT = { width: 1280, height: 720 } as const;\n\n/** Default wait after page load before a screenshot is taken. */\nexport const DEFAULT_SCREENSHOT_WAIT_MS = 1000;\n\n/** Default navigation timeout for page.goto. */\nexport const NAVIGATION_TIMEOUT_MS = 30_000;\n\n/**\n * Floor for the per-screenshot timeout. Chromium will not produce a screenshot\n * while the main frame has a pending cross-document navigation or a blocked\n * main thread, and Playwright's 30s default would stall the whole recording.\n */\nexport const SCREENSHOT_TIMEOUT_MS = 2000;\n\n/**\n * Bound on the final screenshot taken by stop() when the previous attempt\n * timed out. Short, so an genuinely wedged page cannot stall shutdown, but\n * still enough for a page that has since recovered (e.g. after a navigation).\n */\nexport const SCREENSHOT_FINAL_TIMEOUT_MS = 500;\n\n/**\n * Chromium refuses a screenshot until it has produced its first frame (right\n * after a navigation commits). Retry that specific failure a few times.\n */\nexport const SCREENSHOT_RETRY_ATTEMPTS = 4;\nexport const SCREENSHOT_RETRY_DELAY_MS = 15;\n\n/**\n * Bound on the cosmetic page metadata read after a recording (the title).\n * `page.title()` takes no timeout of its own and blocks for Playwright's full\n * 30s default while the page's main thread is busy.\n */\nexport const PAGE_INFO_TIMEOUT_MS = 1000;\n\n/** Default timeout when waiting for a selector to appear. */\nexport const SELECTOR_TIMEOUT_MS = 10_000;\n\n/**\n * A swipe is dispatched as touchStart → SWIPE_STEPS touchMoves → touchEnd,\n * with each move about a frame apart so the page's own velocity maths (a\n * carousel, pull-to-refresh) sees a plausible gesture rather than a teleport.\n */\nexport const SWIPE_STEPS = 10;\nexport const SWIPE_STEP_DELAY_MS = 16;\n\n/**\n * Cap on the length of a replayed interaction script. Each step can carry its\n * own delay, so an unbounded script would sidestep MAX_CAPTURE_DURATION_MS.\n */\nexport const MAX_INTERACTIONS = 50;\n\n/** Default settle time between an interaction and its \"after\" screenshot. */\nexport const DEFAULT_INTERACT_WAIT_MS = 500;\n\n/* ── Context layers (Phase 4) ─────────────────────────────────────────────\n * Every layer is bounded twice over: in the page (so a runaway app cannot\n * grow the tab's memory) and in Node (so one capture cannot flood the MCP\n * response). The Node-side caps are the ones a user notices, and each layer\n * reports what it had to drop.\n */\n\n/** Console entries kept per capture. Errors evict older non-errors once full. */\nexport const MAX_CONSOLE_ENTRIES = 100;\n\n/** Console text longer than this is elided — one runaway log must not fill the response. */\nexport const MAX_CONSOLE_TEXT_LENGTH = 300;\n\n/** Network events kept per capture. Failed/error responses evict older successful ones once full. */\nexport const MAX_NETWORK_EVENTS = 100;\n\n/** URLs longer than this are shortened in the middle (query strings and data: URIs are unbounded). */\nexport const MAX_NETWORK_URL_LENGTH = 120;\n\n/** DOM mutation records kept per capture, before grouping. */\nexport const MAX_DOM_RECORDS = 500;\n\n/** Grouped DOM lines rendered on a single card. */\nexport const MAX_DOM_LINES_PER_CARD = 12;\n\n/** Performance entries (paint, LCP, layout shift) kept per capture. */\nexport const MAX_PERF_SAMPLES = 500;\n\n/**\n * How long the in-page probes batch records before pushing them to Node.\n * Roughly one animation frame: long enough to coalesce a burst of mutations\n * into one binding call, short enough that little is lost if the document is\n * replaced. Record timestamps are stamped when the record is made, not when\n * the batch is flushed, so batching never affects which card a record lands on.\n */\nexport const LAYER_FLUSH_MS = 32;\n\n/** Records one in-page batch may carry. A page that mutates more than this per flush is reporting a storm, not detail. */\nexport const MAX_LAYER_BATCH = 200;\n\n/** Records an in-page probe may push over the lifetime of one document. */\nexport const MAX_LAYER_RECORDS_PER_DOCUMENT = 2000;\n\n/* ── Responsive (Phase 5) ─────────────────────────────────────────────── */\n\n/** Viewports `framewatch_responsive` uses when the caller names none. */\nexport const DEFAULT_RESPONSIVE_VIEWPORTS = [\n { name: \"mobile\", width: 375, height: 812 },\n { name: \"tablet\", width: 768, height: 1024 },\n { name: \"desktop\", width: 1440, height: 900 },\n] as const;\n\n/** Viewports one responsive call may capture. Each one is a live browser context. */\nexport const MAX_RESPONSIVE_VIEWPORTS = 8;\n\n/** Default settle time after load before each responsive screenshot. */\nexport const DEFAULT_RESPONSIVE_WAIT_MS = 2000;\n\n/**\n * Slack (px) allowed before content counts as overflowing its viewport.\n * Sub-pixel layout rounding routinely puts scrollWidth one pixel over\n * clientWidth on a page that is perfectly fine.\n */\nexport const OVERFLOW_TOLERANCE_PX = 1;\n\n/* ── Compare (Phase 5) ────────────────────────────────────────────────── */\n\n/** Default settle time after load before each compared screenshot. */\nexport const DEFAULT_COMPARE_WAIT_MS = 2000;\n\n/** Colour painted over changed pixels in the compare overlay, and its opacity (0–255). */\nexport const OVERLAY_COLOUR = { r: 255, g: 0, b: 200 } as const;\nexport const OVERLAY_ALPHA = 190;\n\n/* ── Accessibility (Phase 5) ──────────────────────────────────────────── */\n\n/** Default settle time after load before the audit runs. */\nexport const DEFAULT_A11Y_WAIT_MS = 1000;\n\n/** Violations reported by one audit, and elements listed under each. */\nexport const MAX_A11Y_VIOLATIONS = 25;\nexport const MAX_A11Y_NODES_PER_VIOLATION = 3;\n\n/** Length of one element's HTML in a violation report before it is elided. */\nexport const MAX_A11Y_HTML_LENGTH = 160;\n\n/** Bound on the axe-core run itself (a huge DOM can take a while). */\nexport const A11Y_RUN_TIMEOUT_MS = 60_000;\n\n/** How long axe waits for an iframe to answer before auditing without it. */\nexport const A11Y_FRAME_WAIT_MS = 5000;\n\n/* ── Dev server (Phase 5) ─────────────────────────────────────────────── */\n\n/** Default regex matched against dev server output to spot its \"ready\" line. */\nexport const DEFAULT_READY_PATTERN = \"ready|started|listening|Local:\";\n\n/** Bounds and default for how long `framewatch_start_server` waits for the port. */\nexport const DEFAULT_SERVER_TIMEOUT_MS = 30_000;\nexport const MIN_SERVER_TIMEOUT_MS = 100;\nexport const MAX_SERVER_TIMEOUT_MS = 300_000;\n\n/** Output lines kept from a dev server. Lines mentioning errors evict ordinary ones. */\nexport const MAX_SERVER_LOG_LINES = 200;\n\n/** Length of one captured output line before it is elided. */\nexport const MAX_SERVER_LINE_LENGTH = 300;\n\n/** Output lines quoted back when a server fails to start or is stopped. */\nexport const SERVER_OUTPUT_TAIL = 15;\n\n/** How often the port is probed while waiting for the server, and the bound on one probe. */\nexport const SERVER_PORT_POLL_MS = 200;\nexport const SERVER_PORT_PROBE_TIMEOUT_MS = 1000;\n\n/** Time a stopped server gets to exit on SIGTERM before it is killed outright. */\nexport const SERVER_STOP_GRACE_MS = 5000;\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type Browser, type BrowserContext, type BrowserContextOptions, type Page } from "playwright";
|
|
2
|
+
import type { Viewport } from "../types.js";
|
|
3
|
+
export declare function getBrowser(): Promise<Browser>;
|
|
4
|
+
export declare function closeBrowser(): Promise<void>;
|
|
5
|
+
export interface PageOptions {
|
|
6
|
+
viewport?: Viewport;
|
|
7
|
+
/** Extra Playwright context options (user agent, locale, etc.). */
|
|
8
|
+
contextOptions?: Omit<BrowserContextOptions, "viewport">;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Run `fn` with a page inside a brand-new browser context. The context is
|
|
12
|
+
* always closed afterwards, even if `fn` throws.
|
|
13
|
+
*/
|
|
14
|
+
export declare function withPage<T>(options: PageOptions, fn: (page: Page, context: BrowserContext) => Promise<T>): Promise<T>;
|
|
15
|
+
export interface SessionOptions {
|
|
16
|
+
/** Resize the current page to this. Omit to leave the page exactly as it is. */
|
|
17
|
+
viewport?: Viewport;
|
|
18
|
+
/** Require a touch-capable page. A session without touch is reopened to get it. */
|
|
19
|
+
hasTouch?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface SessionPage {
|
|
22
|
+
page: Page;
|
|
23
|
+
/**
|
|
24
|
+
* URL of the page that had to be discarded to satisfy `options`, if any.
|
|
25
|
+
* The caller decides whether to navigate back to it — reopening resets
|
|
26
|
+
* everything the old page held, which is worth telling the user about.
|
|
27
|
+
*/
|
|
28
|
+
previousUrl?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The current page, opening one if there is none.
|
|
32
|
+
*
|
|
33
|
+
* An existing page is reused, resized only when `viewport` is given and
|
|
34
|
+
* differs — an omitted viewport means "leave the page alone", never "reset it
|
|
35
|
+
* to the default". The one thing that cannot be changed in place is
|
|
36
|
+
* `hasTouch`: it is fixed when the context is created, so a session that lacks
|
|
37
|
+
* touch has to be reopened to get it. That loses whatever the old page held,
|
|
38
|
+
* which is why the discarded URL comes back to the caller.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getSessionPage(options?: SessionOptions): Promise<SessionPage>;
|
|
41
|
+
/**
|
|
42
|
+
* Run `fn` with exclusive use of the session page.
|
|
43
|
+
*
|
|
44
|
+
* There is one session, one page and one hand: two callers at once would both
|
|
45
|
+
* find no session and open a context each (orphaning all but the last), and a
|
|
46
|
+
* step that needs touch could close the page another caller is half way
|
|
47
|
+
* through. An MCP client may well call tools in parallel, and
|
|
48
|
+
* `framewatch_compare` reads the same page `framewatch_interact` is driving,
|
|
49
|
+
* so the lock lives here with the session rather than inside either tool.
|
|
50
|
+
*
|
|
51
|
+
* `fn` is run whatever happened to the call before it, and the chain survives
|
|
52
|
+
* a rejection.
|
|
53
|
+
*/
|
|
54
|
+
export declare function withSessionLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
55
|
+
/** Close the current page, if any. The browser itself stays up. */
|
|
56
|
+
export declare function closeSession(): Promise<void>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { DEFAULT_VIEWPORT } from "../constants.js";
|
|
3
|
+
/**
|
|
4
|
+
* Playwright browser lifecycle.
|
|
5
|
+
*
|
|
6
|
+
* A single Chromium instance is shared across all tool calls: it is launched
|
|
7
|
+
* lazily on first use and kept alive until `closeBrowser()` (called on MCP
|
|
8
|
+
* server shutdown). Each tool call gets its own BrowserContext via `withPage`
|
|
9
|
+
* so state (cookies, storage, viewport) never leaks between calls.
|
|
10
|
+
*/
|
|
11
|
+
let browserPromise = null;
|
|
12
|
+
export async function getBrowser() {
|
|
13
|
+
if (!browserPromise) {
|
|
14
|
+
browserPromise = chromium.launch({ headless: true, handleSIGINT: false }).then((browser) => {
|
|
15
|
+
// If Chromium dies (crash, external kill), forget it so the next call relaunches.
|
|
16
|
+
browser.on("disconnected", () => {
|
|
17
|
+
if (browserPromise === thisLaunch)
|
|
18
|
+
browserPromise = null;
|
|
19
|
+
});
|
|
20
|
+
return browser;
|
|
21
|
+
});
|
|
22
|
+
const thisLaunch = browserPromise;
|
|
23
|
+
// If launch itself fails, clear the cached rejection so callers can retry.
|
|
24
|
+
browserPromise.catch(() => {
|
|
25
|
+
if (browserPromise === thisLaunch)
|
|
26
|
+
browserPromise = null;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return browserPromise;
|
|
30
|
+
}
|
|
31
|
+
export async function closeBrowser() {
|
|
32
|
+
const pending = browserPromise;
|
|
33
|
+
browserPromise = null;
|
|
34
|
+
session = null;
|
|
35
|
+
if (!pending)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
const browser = await pending;
|
|
39
|
+
await browser.close();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Already closed or never launched successfully — nothing to do.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Run `fn` with a page inside a brand-new browser context. The context is
|
|
47
|
+
* always closed afterwards, even if `fn` throws.
|
|
48
|
+
*/
|
|
49
|
+
export async function withPage(options, fn) {
|
|
50
|
+
const browser = await getBrowser();
|
|
51
|
+
const context = await browser.newContext({
|
|
52
|
+
viewport: options.viewport ?? { ...DEFAULT_VIEWPORT },
|
|
53
|
+
deviceScaleFactor: 1,
|
|
54
|
+
...options.contextOptions,
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
const page = await context.newPage();
|
|
58
|
+
return await fn(page, context);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await context.close().catch(() => { });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let session = null;
|
|
65
|
+
/**
|
|
66
|
+
* The current page, opening one if there is none.
|
|
67
|
+
*
|
|
68
|
+
* An existing page is reused, resized only when `viewport` is given and
|
|
69
|
+
* differs — an omitted viewport means "leave the page alone", never "reset it
|
|
70
|
+
* to the default". The one thing that cannot be changed in place is
|
|
71
|
+
* `hasTouch`: it is fixed when the context is created, so a session that lacks
|
|
72
|
+
* touch has to be reopened to get it. That loses whatever the old page held,
|
|
73
|
+
* which is why the discarded URL comes back to the caller.
|
|
74
|
+
*/
|
|
75
|
+
export async function getSessionPage(options = {}) {
|
|
76
|
+
const wantsTouch = options.hasTouch === true;
|
|
77
|
+
const live = session !== null && !session.page.isClosed();
|
|
78
|
+
if (live && (session.hasTouch || !wantsTouch)) {
|
|
79
|
+
const page = session.page;
|
|
80
|
+
if (options.viewport && !sameSize(page.viewportSize(), options.viewport)) {
|
|
81
|
+
await page.setViewportSize(options.viewport);
|
|
82
|
+
session.viewport = { ...options.viewport };
|
|
83
|
+
}
|
|
84
|
+
return { page };
|
|
85
|
+
}
|
|
86
|
+
// Either nothing is open, or what is open cannot do what was asked. Carry
|
|
87
|
+
// the old size over so reopening reproduces the page as closely as it can.
|
|
88
|
+
const previousUrl = live ? safeUrl(session.page) : undefined;
|
|
89
|
+
const viewport = options.viewport ?? session?.viewport ?? { ...DEFAULT_VIEWPORT };
|
|
90
|
+
await closeSession();
|
|
91
|
+
const browser = await getBrowser();
|
|
92
|
+
const context = await browser.newContext({
|
|
93
|
+
viewport,
|
|
94
|
+
deviceScaleFactor: 1,
|
|
95
|
+
...(wantsTouch ? { hasTouch: true } : {}),
|
|
96
|
+
});
|
|
97
|
+
const page = await context.newPage();
|
|
98
|
+
session = { context, page, hasTouch: wantsTouch, viewport };
|
|
99
|
+
return { page, ...(previousUrl !== undefined ? { previousUrl } : {}) };
|
|
100
|
+
}
|
|
101
|
+
function sameSize(a, b) {
|
|
102
|
+
return a !== null && a.width === b.width && a.height === b.height;
|
|
103
|
+
}
|
|
104
|
+
/** Serialises everything that touches the session page — see `withSessionLock`. */
|
|
105
|
+
let sessionQueue = Promise.resolve();
|
|
106
|
+
/**
|
|
107
|
+
* Run `fn` with exclusive use of the session page.
|
|
108
|
+
*
|
|
109
|
+
* There is one session, one page and one hand: two callers at once would both
|
|
110
|
+
* find no session and open a context each (orphaning all but the last), and a
|
|
111
|
+
* step that needs touch could close the page another caller is half way
|
|
112
|
+
* through. An MCP client may well call tools in parallel, and
|
|
113
|
+
* `framewatch_compare` reads the same page `framewatch_interact` is driving,
|
|
114
|
+
* so the lock lives here with the session rather than inside either tool.
|
|
115
|
+
*
|
|
116
|
+
* `fn` is run whatever happened to the call before it, and the chain survives
|
|
117
|
+
* a rejection.
|
|
118
|
+
*/
|
|
119
|
+
export function withSessionLock(fn) {
|
|
120
|
+
const result = sessionQueue.then(fn, fn);
|
|
121
|
+
sessionQueue = result.then(() => undefined, () => undefined);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
/** Close the current page, if any. The browser itself stays up. */
|
|
125
|
+
export async function closeSession() {
|
|
126
|
+
const current = session;
|
|
127
|
+
session = null;
|
|
128
|
+
if (!current)
|
|
129
|
+
return;
|
|
130
|
+
await current.context.close().catch(() => { });
|
|
131
|
+
}
|
|
132
|
+
/** `page.url()` throws once the page is gone; a dead page simply has no url. */
|
|
133
|
+
function safeUrl(page) {
|
|
134
|
+
try {
|
|
135
|
+
const url = page.url();
|
|
136
|
+
return url === "about:blank" ? undefined : url;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/engine/browser.ts"],"names":[],"mappings":"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","sourcesContent":["import { chromium, type Browser, type BrowserContext, type BrowserContextOptions, type Page } from \"playwright\";\nimport { DEFAULT_VIEWPORT } from \"../constants.js\";\nimport type { Viewport } from \"../types.js\";\n\n/**\n * Playwright browser lifecycle.\n *\n * A single Chromium instance is shared across all tool calls: it is launched\n * lazily on first use and kept alive until `closeBrowser()` (called on MCP\n * server shutdown). Each tool call gets its own BrowserContext via `withPage`\n * so state (cookies, storage, viewport) never leaks between calls.\n */\n\nlet browserPromise: Promise<Browser> | null = null;\n\nexport async function getBrowser(): Promise<Browser> {\n if (!browserPromise) {\n browserPromise = chromium.launch({ headless: true, handleSIGINT: false }).then((browser) => {\n // If Chromium dies (crash, external kill), forget it so the next call relaunches.\n browser.on(\"disconnected\", () => {\n if (browserPromise === thisLaunch) browserPromise = null;\n });\n return browser;\n });\n const thisLaunch = browserPromise;\n // If launch itself fails, clear the cached rejection so callers can retry.\n browserPromise.catch(() => {\n if (browserPromise === thisLaunch) browserPromise = null;\n });\n }\n return browserPromise;\n}\n\nexport async function closeBrowser(): Promise<void> {\n const pending = browserPromise;\n browserPromise = null;\n session = null;\n if (!pending) return;\n try {\n const browser = await pending;\n await browser.close();\n } catch {\n // Already closed or never launched successfully — nothing to do.\n }\n}\n\nexport interface PageOptions {\n viewport?: Viewport;\n /** Extra Playwright context options (user agent, locale, etc.). */\n contextOptions?: Omit<BrowserContextOptions, \"viewport\">;\n}\n\n/**\n * Run `fn` with a page inside a brand-new browser context. The context is\n * always closed afterwards, even if `fn` throws.\n */\nexport async function withPage<T>(options: PageOptions, fn: (page: Page, context: BrowserContext) => Promise<T>): Promise<T> {\n const browser = await getBrowser();\n const context = await browser.newContext({\n viewport: options.viewport ?? { ...DEFAULT_VIEWPORT },\n deviceScaleFactor: 1,\n ...options.contextOptions,\n });\n try {\n const page = await context.newPage();\n return await fn(page, context);\n } finally {\n await context.close().catch(() => {});\n }\n}\n\n/**\n * The interaction session — \"the current page\".\n *\n * `framewatch_interact` is for iterative testing: click, look, type, look\n * again. That only works if the page survives between tool calls, so unlike\n * `withPage` (a fresh context per call) the session keeps one context and one\n * page alive until the browser closes. Cookies, storage, scroll position and\n * anything the app has in memory carry over from call to call.\n */\ninterface Session {\n context: BrowserContext;\n page: Page;\n hasTouch: boolean;\n viewport: Viewport;\n}\n\nlet session: Session | null = null;\n\nexport interface SessionOptions {\n /** Resize the current page to this. Omit to leave the page exactly as it is. */\n viewport?: Viewport;\n /** Require a touch-capable page. A session without touch is reopened to get it. */\n hasTouch?: boolean;\n}\n\nexport interface SessionPage {\n page: Page;\n /**\n * URL of the page that had to be discarded to satisfy `options`, if any.\n * The caller decides whether to navigate back to it — reopening resets\n * everything the old page held, which is worth telling the user about.\n */\n previousUrl?: string;\n}\n\n/**\n * The current page, opening one if there is none.\n *\n * An existing page is reused, resized only when `viewport` is given and\n * differs — an omitted viewport means \"leave the page alone\", never \"reset it\n * to the default\". The one thing that cannot be changed in place is\n * `hasTouch`: it is fixed when the context is created, so a session that lacks\n * touch has to be reopened to get it. That loses whatever the old page held,\n * which is why the discarded URL comes back to the caller.\n */\nexport async function getSessionPage(options: SessionOptions = {}): Promise<SessionPage> {\n const wantsTouch = options.hasTouch === true;\n const live = session !== null && !session.page.isClosed();\n\n if (live && (session!.hasTouch || !wantsTouch)) {\n const page = session!.page;\n if (options.viewport && !sameSize(page.viewportSize(), options.viewport)) {\n await page.setViewportSize(options.viewport);\n session!.viewport = { ...options.viewport };\n }\n return { page };\n }\n\n // Either nothing is open, or what is open cannot do what was asked. Carry\n // the old size over so reopening reproduces the page as closely as it can.\n const previousUrl = live ? safeUrl(session!.page) : undefined;\n const viewport = options.viewport ?? session?.viewport ?? { ...DEFAULT_VIEWPORT };\n await closeSession();\n\n const browser = await getBrowser();\n const context = await browser.newContext({\n viewport,\n deviceScaleFactor: 1,\n ...(wantsTouch ? { hasTouch: true } : {}),\n });\n const page = await context.newPage();\n session = { context, page, hasTouch: wantsTouch, viewport };\n return { page, ...(previousUrl !== undefined ? { previousUrl } : {}) };\n}\n\nfunction sameSize(a: Viewport | null, b: Viewport): boolean {\n return a !== null && a.width === b.width && a.height === b.height;\n}\n\n/** Serialises everything that touches the session page — see `withSessionLock`. */\nlet sessionQueue: Promise<unknown> = Promise.resolve();\n\n/**\n * Run `fn` with exclusive use of the session page.\n *\n * There is one session, one page and one hand: two callers at once would both\n * find no session and open a context each (orphaning all but the last), and a\n * step that needs touch could close the page another caller is half way\n * through. An MCP client may well call tools in parallel, and\n * `framewatch_compare` reads the same page `framewatch_interact` is driving,\n * so the lock lives here with the session rather than inside either tool.\n *\n * `fn` is run whatever happened to the call before it, and the chain survives\n * a rejection.\n */\nexport function withSessionLock<T>(fn: () => Promise<T>): Promise<T> {\n const result = sessionQueue.then(fn, fn);\n sessionQueue = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n}\n\n/** Close the current page, if any. The browser itself stays up. */\nexport async function closeSession(): Promise<void> {\n const current = session;\n session = null;\n if (!current) return;\n await current.context.close().catch(() => {});\n}\n\n/** `page.url()` throws once the page is gone; a dead page simply has no url. */\nfunction safeUrl(page: Page): string | undefined {\n try {\n const url = page.url();\n return url === \"about:blank\" ? undefined : url;\n } catch {\n return undefined;\n }\n}\n"]}
|