framewatch-mcp-server 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -7
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -1
- package/dist/engine/browser.d.ts +20 -4
- package/dist/engine/browser.js +22 -9
- package/dist/engine/browser.js.map +1 -1
- package/dist/engine/interaction.d.ts +6 -6
- package/dist/engine/interaction.js +89 -12
- package/dist/engine/interaction.js.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/accessibility.d.ts +4 -0
- package/dist/tools/accessibility.js +7 -1
- package/dist/tools/accessibility.js.map +1 -1
- package/dist/tools/capture.d.ts +21 -17
- package/dist/tools/capture.js +7 -1
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/compare.d.ts +4 -0
- package/dist/tools/compare.js +14 -4
- package/dist/tools/compare.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interact.d.ts +12 -6
- package/dist/tools/interact.js +21 -7
- package/dist/tools/interact.js.map +1 -1
- package/dist/tools/responsive.d.ts +4 -0
- package/dist/tools/responsive.js +20 -3
- package/dist/tools/responsive.js.map +1 -1
- package/dist/tools/save-auth.d.ts +263 -0
- package/dist/tools/save-auth.js +253 -0
- package/dist/tools/save-auth.js.map +1 -0
- package/dist/tools/screenshot.d.ts +4 -0
- package/dist/tools/screenshot.js +4 -1
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/utils/storage-state.d.ts +35 -0
- package/dist/utils/storage-state.js +84 -0
- package/dist/utils/storage-state.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ The problem with screenshots as a debugging tool is volume. A five-second record
|
|
|
14
14
|
| [`framewatch_responsive`](#framewatch_responsive) | Does it hold up at phone, tablet and desktop widths? |
|
|
15
15
|
| [`framewatch_accessibility`](#framewatch_accessibility) | What would an accessibility auditor flag? |
|
|
16
16
|
| [`framewatch_compare`](#framewatch_compare) | What changed between before and after? |
|
|
17
|
+
| [`framewatch_save_auth`](#framewatch_save_auth) | Sign in once, so every other tool starts past the login. |
|
|
17
18
|
| [`framewatch_start_server`](#framewatch_start_server--framewatch_stop_server) / [`framewatch_stop_server`](#framewatch_start_server--framewatch_stop_server) | Get the dev server up so there is something to point at. |
|
|
18
19
|
|
|
19
20
|
## Install
|
|
@@ -56,6 +57,7 @@ Take a single screenshot of a page (or of one element on it). Returns a PNG imag
|
|
|
56
57
|
| `selector` | string | — | CSS selector: screenshot only this element |
|
|
57
58
|
| `wait_for` | string | — | CSS selector to wait for (visible) before capturing |
|
|
58
59
|
| `wait_for_timeout_ms` | integer ≥ 1 | `10000` | Max wait for `wait_for` / `selector` |
|
|
60
|
+
| `storage_state` | string (path) | — | Auth state file from [`framewatch_save_auth`](#framewatch_save_auth) — open the page already signed in |
|
|
59
61
|
|
|
60
62
|
**Example call** — the whole page, then one element of it:
|
|
61
63
|
|
|
@@ -88,6 +90,7 @@ Record a page for a few seconds and return only the frames where something meani
|
|
|
88
90
|
| `include_network` | boolean | `false` | Attach network requests |
|
|
89
91
|
| `include_dom` | boolean | `false` | Attach a summary of the DOM mutations between frames |
|
|
90
92
|
| `include_performance` | boolean | `false` | Attach paint timing, LCP and layout shifts |
|
|
93
|
+
| `storage_state` | string (path) | — | Auth state file from [`framewatch_save_auth`](#framewatch_save_auth) — record the app, not its login screen |
|
|
91
94
|
|
|
92
95
|
**Example call** — record a splash screen, keeping more frames than usual because the interesting parts of an animation are small:
|
|
93
96
|
|
|
@@ -170,12 +173,24 @@ Pass `interactions` to drive the page while it records. Each step is `{ action,
|
|
|
170
173
|
}
|
|
171
174
|
```
|
|
172
175
|
|
|
173
|
-
`framewatch_capture` accepts `click`, `tap`, `type`, `scroll`, `swipe`, `wait` and `navigate
|
|
176
|
+
`framewatch_capture` accepts `click`, `tap`, `type`, `key`, `scroll`, `swipe`, `hover`, `select`, `wait` and `navigate` — the same set the executor supports, so nothing has to be worked around. A frame is forced right after every step, so the result of each action is always kept as an `[interaction]` card. The summary gains a line such as:
|
|
174
177
|
|
|
175
178
|
```
|
|
176
179
|
Interactions: 4/4 replayed — type "test@example.com" into "#email", type "password123" into "#password", click "button[type=submit]", wait 3000ms
|
|
177
180
|
```
|
|
178
181
|
|
|
182
|
+
**Pressing keys.** `type` only produces printable text, so a form that is submitted with the keyboard needs a key press. Either write it as its own step, or put it inline in the value — a `\n` in a typed value presses Enter and a `\t` presses Tab, at exactly that point in the text:
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{ "action": "type", "selector": "#search", "value": "framewatch" },
|
|
186
|
+
{ "action": "key", "value": "Enter" }
|
|
187
|
+
```
|
|
188
|
+
```json
|
|
189
|
+
{ "action": "type", "selector": "#search", "value": "framewatch\n" }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`key` takes any [Playwright key name](https://playwright.dev/docs/api/class-keyboard#keyboard-press) — `Enter`, `Escape`, `Tab`, `Backspace`, `ArrowDown`, `F5` — or a combo such as `Control+a` or `Shift+Tab`. It goes to whatever is focused; give it a `selector` to focus that element first. The first text run of a `type` still replaces the field's contents, so a script stays repeatable however many keys are in it.
|
|
193
|
+
|
|
179
194
|
Typed values are echoed in that line (elided only when long) and are visible in the frames themselves, so drive these flows with throwaway test credentials — a real password ends up in the tool output and therefore in the model's context.
|
|
180
195
|
|
|
181
196
|
A step that fails ends the script but **not** the capture — the frames recorded up to that point are the most useful thing FrameWatch can hand back, since they show the state the page was actually in. The failure is reported in the summary and the moment it happened is kept as an `[error]` card:
|
|
@@ -192,9 +207,9 @@ Perform **one** interaction and see what it did: before frame, after frame, and
|
|
|
192
207
|
|
|
193
208
|
| Param | Type | Default | Description |
|
|
194
209
|
| --- | --- | --- | --- |
|
|
195
|
-
| `action` | enum | — | `click`, `tap`, `type`, `scroll`, `swipe`, `navigate`, `select`, `hover` |
|
|
210
|
+
| `action` | enum | — | `click`, `tap`, `type`, `key`, `scroll`, `swipe`, `navigate`, `select`, `hover` |
|
|
196
211
|
| `selector` | string | — | CSS selector for the target |
|
|
197
|
-
| `value` | string | — | Text to type, option to select, or URL to navigate to |
|
|
212
|
+
| `value` | string | — | Text to type (`\n` presses Enter, `\t` Tab), key to press, option to select, or URL to navigate to |
|
|
198
213
|
| `x`, `y` | number | — | Coordinates for `click`/`tap`/`swipe` when no selector is given |
|
|
199
214
|
| `delta_x`, `delta_y` | number | — | Distance for `scroll` / `swipe` |
|
|
200
215
|
| `url` | string (URL) | — | Open this page first. Omit to act on the page left open by the previous call. |
|
|
@@ -205,6 +220,7 @@ Perform **one** interaction and see what it did: before frame, after frame, and
|
|
|
205
220
|
| `include_network` | boolean | `false` | Report network requests the action caused |
|
|
206
221
|
| `include_dom` | boolean | `false` | Report the DOM mutations the action caused |
|
|
207
222
|
| `include_performance` | boolean | `false` | Report paint timing and layout shifts around the action |
|
|
223
|
+
| `storage_state` | string (path) | — | Auth state file from [`framewatch_save_auth`](#framewatch_save_auth), applied when the session page is opened |
|
|
208
224
|
|
|
209
225
|
**Example call** — a session, one call at a time. Only the first needs a `url`:
|
|
210
226
|
|
|
@@ -235,7 +251,7 @@ Network:
|
|
|
235
251
|
|
|
236
252
|
Because the page is reused between calls, the layers are installed on it once and **emptied at the start of every call**, so each call reports only what it caused. A layer can be switched on mid-session — it is attached to the document that is already open, no reload needed — but one that was on for an earlier call keeps running silently, since a page cannot un-expose an injected observer.
|
|
237
253
|
|
|
238
|
-
Calls are serialised — they all drive the same page, so they queue rather than interleave. The first call needs a `url`; later calls can omit it. Cookies, storage, scroll position and in-page state all carry over.
|
|
254
|
+
Calls are serialised — they all drive the same page, so they queue rather than interleave. The first call needs a `url`; later calls can omit it. Cookies, storage, scroll position and in-page state all carry over. Two things cannot change in place, because both are fixed when the browser context is created: touch support, and the saved auth. So the first `tap` or `swipe` on a page opened without touch reopens the page, as does naming a `storage_state` the open session was not created with — the summary says which of the two it was, because either resets page state. Passing the *same* `storage_state` again, or omitting it, leaves the session exactly where it is. The session closes when the MCP server shuts down; `framewatch_capture` and `framewatch_screenshot` are unaffected by it, as they always use a fresh, isolated browser context.
|
|
239
255
|
|
|
240
256
|
### `framewatch_responsive`
|
|
241
257
|
|
|
@@ -248,6 +264,7 @@ Screenshot one page at several viewport sizes in a single call. Each size loads
|
|
|
248
264
|
| `wait_ms` | integer ≥ 0 | `2000` | Settle time after load, per viewport |
|
|
249
265
|
| `wait_for` | string | — | CSS selector to wait for (visible) at each viewport |
|
|
250
266
|
| `wait_for_timeout_ms` | integer ≥ 1 | `10000` | Max wait for `wait_for` |
|
|
267
|
+
| `storage_state` | string (path) | — | Auth state file from [`framewatch_save_auth`](#framewatch_save_auth), restored in every viewport's context |
|
|
251
268
|
|
|
252
269
|
**Example call** — the three defaults, or your own breakpoints:
|
|
253
270
|
|
|
@@ -292,6 +309,7 @@ Run an [axe-core](https://github.com/dequelabs/axe-core) audit and report the vi
|
|
|
292
309
|
| `max_violations` | integer 1–25 | `25` | Violation types to report (worst impact first) |
|
|
293
310
|
| `max_elements` | integer 1–20 | `3` | Offending elements listed under each violation |
|
|
294
311
|
| `viewport` | `{ width, height }` | `1280×720` | Some rules (reflow, target size) depend on it |
|
|
312
|
+
| `storage_state` | string (path) | — | Auth state file from [`framewatch_save_auth`](#framewatch_save_auth) — audit the page behind the login |
|
|
295
313
|
|
|
296
314
|
```
|
|
297
315
|
4 WCAG2AA (axe-core 4.13.0) violation types on http://localhost:3000/, affecting 7 elements — 3 critical, 1 serious.
|
|
@@ -327,6 +345,7 @@ Diff two pages — two URLs, or the same URL before and after a code change —
|
|
|
327
345
|
| `wait_for` | string | — | CSS selector to wait for (visible) on both sides |
|
|
328
346
|
| `wait_for_timeout_ms` | integer ≥ 1 | `10000` | Max wait for `wait_for` |
|
|
329
347
|
| `viewport` | `{ width, height }` | `1280×720` | Viewport for both sides |
|
|
348
|
+
| `storage_state` | string (path) | — | Auth state file from [`framewatch_save_auth`](#framewatch_save_auth), applied to both URL sides |
|
|
330
349
|
|
|
331
350
|
Returns the summary, both frames, and a **diff overlay**: side B with every differing pixel tinted, which is what turns "3.4% changed" into something you can act on.
|
|
332
351
|
|
|
@@ -350,6 +369,71 @@ The comparison is the same pixel comparison the capture engine uses between fram
|
|
|
350
369
|
|
|
351
370
|
Pass `"current"` as `url_a` to compare against the page `framewatch_interact` has open, in whatever state your interactions left it — three clicks into a flow, against a plain URL. That page is read, never touched: it is not reloaded, and an explicit `viewport` is ignored for it (with a note saying so), because resizing it would destroy the state being compared. Both sides are then captured at the open page's size, since frames of different sizes cannot be compared pixel for pixel.
|
|
352
371
|
|
|
372
|
+
### `framewatch_save_auth`
|
|
373
|
+
|
|
374
|
+
Run a login or gate flow **once** and save the browser state it produces. Every other tool takes that file as `storage_state` and opens the page already signed in, so an app behind a login stops costing seven interaction steps and ten seconds on every single call.
|
|
375
|
+
|
|
376
|
+
| Param | Type | Default | Description |
|
|
377
|
+
| --- | --- | --- | --- |
|
|
378
|
+
| `url` | string (URL) | — | Where the flow starts — the login page or the gate |
|
|
379
|
+
| `interactions` | array (≤ 50) | — | The steps to run, in order. Same shape and actions as a [capture script](#replaying-an-interaction-script), with `delay_ms` defaulting to `500` |
|
|
380
|
+
| `output_path` | string | `.framewatch/auth.json` | Where to write the state file |
|
|
381
|
+
| `wait_for` | string | — | CSS selector that only exists once signed in, e.g. `.feed`. **Strongly recommended** |
|
|
382
|
+
| `wait_for_timeout_ms` | integer ≥ 1 | `15000` | Max wait for `wait_for` after the last step |
|
|
383
|
+
| `timeout_ms` | integer ≥ 1 | `10000` | Max wait per step for its target element |
|
|
384
|
+
| `viewport` | `{ width, height, is_mobile, has_touch }` | `1280×720` | `is_mobile` emulates a phone; `has_touch` defaults to on when the flow taps or swipes, or when `is_mobile` is set |
|
|
385
|
+
|
|
386
|
+
**Example call** — a two-stage gate on a phone-shaped app, then everything after it:
|
|
387
|
+
|
|
388
|
+
```json
|
|
389
|
+
{
|
|
390
|
+
"url": "http://localhost:8080",
|
|
391
|
+
"interactions": [
|
|
392
|
+
{ "action": "type", "selector": "input", "value": "duo\n" },
|
|
393
|
+
{ "action": "wait", "delay_ms": 2000 },
|
|
394
|
+
{ "action": "type", "selector": "input", "value": "Keko" },
|
|
395
|
+
{ "action": "click", "selector": "button.go" },
|
|
396
|
+
{ "action": "click", "selector": "button:has-text('Open a room')", "delay_ms": 2000 }
|
|
397
|
+
],
|
|
398
|
+
"output_path": ".framewatch/duo-auth.json",
|
|
399
|
+
"wait_for": ".rail",
|
|
400
|
+
"viewport": { "width": 390, "height": 844, "is_mobile": true, "has_touch": true }
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
```json
|
|
404
|
+
{
|
|
405
|
+
"url": "http://localhost:8080",
|
|
406
|
+
"storage_state": ".framewatch/duo-auth.json",
|
|
407
|
+
"duration_ms": 5000,
|
|
408
|
+
"interactions": [{ "action": "tap", "x": 195, "y": 400, "delay_ms": 1000 }],
|
|
409
|
+
"viewport": { "width": 390, "height": 844 }
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Returns the final frame plus what was saved:
|
|
414
|
+
|
|
415
|
+
```
|
|
416
|
+
Saved auth state to .framewatch/duo-auth.json — 3 cookies, 1 origin with 2 stored keys.
|
|
417
|
+
Ran 5 steps: type "duo\n" into "input"; wait 2000ms; type "Keko" into "input"; click "button.go"; click "button:has-text('Open a room')"
|
|
418
|
+
Ended on http://localhost:8080/rooms
|
|
419
|
+
Pass storage_state: ".framewatch/duo-auth.json" to framewatch_screenshot, framewatch_capture, framewatch_interact,
|
|
420
|
+
framewatch_responsive, framewatch_accessibility or framewatch_compare to start past this flow. The file holds live
|
|
421
|
+
session credentials — keep it out of version control.
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Nothing is written unless the flow finished**, `wait_for` included. A state file that is not signed in is worse than no file at all: every later call would load it and quietly get the login screen back. When a step fails, or the success selector never appears, the result is an error carrying the frame the flow stopped on — which is the thing that explains why:
|
|
425
|
+
|
|
426
|
+
```
|
|
427
|
+
Saving auth state failed: the flow ran, but ".rail" never became visible within 15000ms, so it did not sign in
|
|
428
|
+
Completed before it stopped: type "duo" into "input"; click "button.go"
|
|
429
|
+
Page at that moment: http://localhost:8080/gate
|
|
430
|
+
Nothing was written — a state file that is not signed in would make every later call fail silently.
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
What is saved is Playwright's storage state: **cookies and localStorage**, per origin. A session kept only in `sessionStorage` or in a JavaScript variable cannot be saved this way — the tool says so when the flow stored nothing at all. The file is plain JSON containing live session credentials, so treat it as a secret: keep it out of version control (`.framewatch/` is a good thing to gitignore), and use throwaway test accounts, since anything typed also shows up in the frames.
|
|
434
|
+
|
|
435
|
+
There is **no auto-refresh**. When a saved session expires, the login screen simply appears in the next capture — which is the clearest possible signal to run this tool again. FrameWatch does not try to detect it, because a heuristic that guesses wrong is worse than a screenshot you can see.
|
|
436
|
+
|
|
353
437
|
### `framewatch_start_server` / `framewatch_stop_server`
|
|
354
438
|
|
|
355
439
|
Start the app's dev server so the rest of FrameWatch has something to point at, and stop it again.
|
|
@@ -460,11 +544,15 @@ Two things worth noticing. The agent did not get 66 screenshots and a guess: it
|
|
|
460
544
|
|
|
461
545
|
**An accessibility audit found nothing on a page you know is broken.** axe ran before the app rendered. Add `wait_for`, or raise `wait_ms` past the point where the content appears.
|
|
462
546
|
|
|
463
|
-
**`framewatch_interact` says the page was reopened.** Touch support
|
|
547
|
+
**`framewatch_interact` says the page was reopened.** Touch support and the saved auth are both fixed when the browser context is created, so the first `tap` or `swipe` on a page opened without touch — or a `storage_state` the open session was not created with — reopens the page and resets its state. Put the `tap` and the `storage_state` in the first call of the session.
|
|
464
548
|
|
|
465
549
|
**`framewatch_start_server` refuses with "port already in use".** Something is already listening — often a dev server from an earlier session. Stop it, or point FrameWatch at it directly and skip `framewatch_start_server` altogether.
|
|
466
550
|
|
|
467
|
-
**Everything times out on a page behind a login.**
|
|
551
|
+
**Everything times out on a page behind a login.** Sign in once with [`framewatch_save_auth`](#framewatch_save_auth) and pass the file it writes as `storage_state` to the other tools. For a one-off, `framewatch_interact` also works: the session page keeps cookies and storage between calls, and `framewatch_compare` can read it with `"url_a": "current"`.
|
|
552
|
+
|
|
553
|
+
**A tool that worked yesterday now shows the login screen.** The saved session expired. Re-run `framewatch_save_auth` — nothing refreshes it automatically, by design: the screenshot showing you a login form is a more reliable signal than any heuristic guess.
|
|
554
|
+
|
|
555
|
+
**`framewatch_save_auth` saved "no cookies and nothing in storage".** The flow did not actually sign in (add `wait_for` so a failure is reported instead of saved), or the app keeps its session in `sessionStorage` or in memory, neither of which can be saved. For those, drive the login with `framewatch_interact` and keep working in that session.
|
|
468
556
|
|
|
469
557
|
## Development
|
|
470
558
|
|
|
@@ -491,6 +579,7 @@ Tests launch real Chromium against local fixture pages in `test/fixtures/`, serv
|
|
|
491
579
|
| `responsive.html`, `responsive-late.html` | Breakpoints and overflow, at load and 400ms after it |
|
|
492
580
|
| `a11y-good.html`, `a11y-bad.html`, `a11y-frame.html`, `a11y-late.html`, `csp.html` | Clean, broken, broken-inside-an-iframe, broken-after-render, and behind a strict CSP |
|
|
493
581
|
| `compare-a.html` / `compare-b.html` | Two pages that differ in one region |
|
|
582
|
+
| `gate.html` | A gate that opens only for a cookie **and** a localStorage token — saving and restoring auth state |
|
|
494
583
|
|
|
495
584
|
The dev-server tests drive `test/helpers/fake-dev-server.mjs`, a stand-in that can be told to start slowly, never open its port, exit with an error, or ignore SIGTERM. `test/packaging.test.ts` packs the tarball, unpacks it and speaks MCP to the binary inside, which is the closest thing to testing `npx framewatch-mcp-server` without publishing. The stdio integration test screenshots `https://example.com`, so that one needs network access.
|
|
496
585
|
|
|
@@ -512,11 +601,12 @@ MCP client (Claude Code) ◄─stdio─► FrameWatch server ◄──► Playwr
|
|
|
512
601
|
- `src/engine/browser.ts` — one shared Chromium: a fresh context per tool call, plus the long-lived page `framewatch_interact` works on
|
|
513
602
|
- `src/engine/recorder.ts` — captures raw PNG frames at a fixed interval (plus forced frames on navigation)
|
|
514
603
|
- `src/engine/differ.ts` — grid-based frame selection, pixel-level change regions, crops
|
|
515
|
-
- `src/engine/interaction.ts` — validates and executes one interaction step (click, tap, type, scroll, swipe, hover, select, navigate)
|
|
604
|
+
- `src/engine/interaction.ts` — validates and executes one interaction step (click, tap, type, key, scroll, swipe, hover, select, navigate)
|
|
516
605
|
- `src/engine/layers/` — the context layers: `console.ts`, `network.ts` (Playwright events), `dom.ts`, `performance.ts` (injected observers), `probe.ts` (the injection plumbing they share), `session.ts` (layers for the long-lived interact page), and `index.ts` (attach, drain, split across cards)
|
|
517
606
|
- `src/utils/image.ts` — sharp wrappers, including the compare overlay
|
|
518
607
|
- `src/utils/bounded-log.ts` — capped log that evicts ordinary entries to keep errors
|
|
519
608
|
- `src/utils/server-process.ts` — dev server process manager (spawn, port readiness, process-group kill)
|
|
609
|
+
- `src/utils/storage-state.ts` — reads and writes the saved auth state file, and the `storage_state` input every page tool shares
|
|
520
610
|
- `src/utils/format.ts` — turns diff cards into MCP content blocks
|
|
521
611
|
- `src/tools/screenshot.ts` — the screenshot tool
|
|
522
612
|
- `src/tools/capture.ts` — the capture tool, including interaction replay
|
|
@@ -524,6 +614,7 @@ MCP client (Claude Code) ◄─stdio─► FrameWatch server ◄──► Playwr
|
|
|
524
614
|
- `src/tools/responsive.ts` — multi-viewport capture with the overflow check
|
|
525
615
|
- `src/tools/accessibility.ts` — the axe-core audit
|
|
526
616
|
- `src/tools/compare.ts` — before/after comparison and the diff overlay
|
|
617
|
+
- `src/tools/save-auth.ts` — runs a login flow once and saves the browser state it produced
|
|
527
618
|
- `src/tools/server.ts` — the two dev-server tools
|
|
528
619
|
|
|
529
620
|
All diagnostics go to stderr; stdout is reserved for the MCP protocol.
|
package/dist/constants.d.ts
CHANGED
|
@@ -153,6 +153,16 @@ export declare const MAX_A11Y_HTML_LENGTH = 160;
|
|
|
153
153
|
export declare const A11Y_RUN_TIMEOUT_MS = 60000;
|
|
154
154
|
/** How long axe waits for an iframe to answer before auditing without it. */
|
|
155
155
|
export declare const A11Y_FRAME_WAIT_MS = 5000;
|
|
156
|
+
/** Where `framewatch_save_auth` writes its state file when the caller names no path. */
|
|
157
|
+
export declare const DEFAULT_AUTH_STATE_PATH = ".framewatch/auth.json";
|
|
158
|
+
/**
|
|
159
|
+
* Settle time before each step of a saved login flow. Higher than a capture
|
|
160
|
+
* script's 0: this runs blind (nothing is watching the frames), so each step
|
|
161
|
+
* has to leave the next one something to act on.
|
|
162
|
+
*/
|
|
163
|
+
export declare const SAVE_AUTH_STEP_DELAY_MS = 500;
|
|
164
|
+
/** How long `wait_for` may take to prove the flow signed in. Logins wait on a network round trip. */
|
|
165
|
+
export declare const SAVE_AUTH_WAIT_FOR_TIMEOUT_MS = 15000;
|
|
156
166
|
/** Default regex matched against dev server output to spot its "ready" line. */
|
|
157
167
|
export declare const DEFAULT_READY_PATTERN = "ready|started|listening|Local:";
|
|
158
168
|
/** Bounds and default for how long `framewatch_start_server` waits for the port. */
|
package/dist/constants.js
CHANGED
|
@@ -147,6 +147,17 @@ export const MAX_A11Y_HTML_LENGTH = 160;
|
|
|
147
147
|
export const A11Y_RUN_TIMEOUT_MS = 60_000;
|
|
148
148
|
/** How long axe waits for an iframe to answer before auditing without it. */
|
|
149
149
|
export const A11Y_FRAME_WAIT_MS = 5000;
|
|
150
|
+
/* ── Auth state (v0.1.1) ──────────────────────────────────────────────── */
|
|
151
|
+
/** Where `framewatch_save_auth` writes its state file when the caller names no path. */
|
|
152
|
+
export const DEFAULT_AUTH_STATE_PATH = ".framewatch/auth.json";
|
|
153
|
+
/**
|
|
154
|
+
* Settle time before each step of a saved login flow. Higher than a capture
|
|
155
|
+
* script's 0: this runs blind (nothing is watching the frames), so each step
|
|
156
|
+
* has to leave the next one something to act on.
|
|
157
|
+
*/
|
|
158
|
+
export const SAVE_AUTH_STEP_DELAY_MS = 500;
|
|
159
|
+
/** How long `wait_for` may take to prove the flow signed in. Logins wait on a network round trip. */
|
|
160
|
+
export const SAVE_AUTH_WAIT_FOR_TIMEOUT_MS = 15_000;
|
|
150
161
|
/* ── Dev server (Phase 5) ─────────────────────────────────────────────── */
|
|
151
162
|
/** Default regex matched against dev server output to spot its "ready" line. */
|
|
152
163
|
export const DEFAULT_READY_PATTERN = "ready|started|listening|Local:";
|
package/dist/constants.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC,wEAAwE;AACxE,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC;AAE3B,sFAAsF;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,8GAA8G;AAC9G,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAElC,sEAAsE;AACtE,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAExC,gFAAgF;AAChF,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AAEnC,kEAAkE;AAClE,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAElC,mGAAmG;AACnG,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAEtC,mDAAmD;AACnD,MAAM,CAAC,MAAM,UAAU,GAAG,GAAG,CAAC;AAC9B,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAE/B,0DAA0D;AAC1D,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAEpC,2DAA2D;AAC3D,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,0DAA0D;AAC1D,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,mDAAmD;AACnD,MAAM,CAAC,MAAM,2BAA2B,GAAG,IAAI,CAAC;AAChD,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC;AACvC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAExC,iDAAiD;AACjD,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAW,CAAC;AAEtE,iEAAiE;AACjE,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAE/C,gDAAgD;AAChD,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAE1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAE/C;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAC3C,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAEzC,6DAA6D;AAC7D,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAE1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC9B,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAEtC;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAEnC,6EAA6E;AAC7E,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAE5C;;;;;GAKG;AAEH,iFAAiF;AACjF,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC,4FAA4F;AAC5F,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAE3C,qGAAqG;AACrG,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAEtC,sGAAsG;AACtG,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAE1C,8DAA8D;AAC9D,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AAEnC,mDAAmD;AACnD,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAEzC,uEAAuE;AACvE,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAEpC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,0HAA0H;AAC1H,MAAM,CAAC,MAAM,eAAe,GAAG,GAAG,CAAC;AAEnC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,8BAA8B,GAAG,IAAI,CAAC;AAEnD,6EAA6E;AAE7E,yEAAyE;AACzE,MAAM,CAAC,MAAM,4BAA4B,GAAG;IAC1C,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE;IAC3C,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAC5C,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;CACrC,CAAC;AAEX,qFAAqF;AACrF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC;AAE1C,wEAAwE;AACxE,MAAM,CAAC,MAAM,0BAA0B,GAAG,IAAI,CAAC;AAE/C;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,6EAA6E;AAE7E,sEAAsE;AACtE,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAE5C,0FAA0F;AAC1F,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAW,CAAC;AAChE,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC;AAEjC,6EAA6E;AAE7E,4DAA4D;AAC5D,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAEzC,wEAAwE;AACxE,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AACtC,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC;AAE9C,8EAA8E;AAC9E,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAExC,sEAAsE;AACtE,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAE1C,6EAA6E;AAC7E,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC;AAEvC,6EAA6E;AAE7E,wFAAwF;AACxF,MAAM,CAAC,MAAM,uBAAuB,GAAG,uBAAuB,CAAC;AAE/D;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAE3C,qGAAqG;AACrG,MAAM,CAAC,MAAM,6BAA6B,GAAG,MAAM,CAAC;AAEpD,6EAA6E;AAE7E,gFAAgF;AAChF,MAAM,CAAC,MAAM,qBAAqB,GAAG,gCAAgC,CAAC;AAEtE,oFAAoF;AACpF,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAChD,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AACzC,MAAM,CAAC,MAAM,qBAAqB,GAAG,OAAO,CAAC;AAE7C,wFAAwF;AACxF,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAExC,8DAA8D;AAC9D,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAE1C,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,6FAA6F;AAC7F,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AACvC,MAAM,CAAC,MAAM,4BAA4B,GAAG,IAAI,CAAC;AAEjD,kFAAkF;AAClF,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC","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/* ── Auth state (v0.1.1) ──────────────────────────────────────────────── */\n\n/** Where `framewatch_save_auth` writes its state file when the caller names no path. */\nexport const DEFAULT_AUTH_STATE_PATH = \".framewatch/auth.json\";\n\n/**\n * Settle time before each step of a saved login flow. Higher than a capture\n * script's 0: this runs blind (nothing is watching the frames), so each step\n * has to leave the next one something to act on.\n */\nexport const SAVE_AUTH_STEP_DELAY_MS = 500;\n\n/** How long `wait_for` may take to prove the flow signed in. Logins wait on a network round trip. */\nexport const SAVE_AUTH_WAIT_FOR_TIMEOUT_MS = 15_000;\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"]}
|
package/dist/engine/browser.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Browser, type BrowserContext, type BrowserContextOptions, type Page } from "playwright";
|
|
2
2
|
import type { Viewport } from "../types.js";
|
|
3
|
+
import type { StorageState } from "../utils/storage-state.js";
|
|
3
4
|
export declare function getBrowser(): Promise<Browser>;
|
|
4
5
|
export declare function closeBrowser(): Promise<void>;
|
|
5
6
|
export interface PageOptions {
|
|
@@ -12,11 +13,22 @@ export interface PageOptions {
|
|
|
12
13
|
* always closed afterwards, even if `fn` throws.
|
|
13
14
|
*/
|
|
14
15
|
export declare function withPage<T>(options: PageOptions, fn: (page: Page, context: BrowserContext) => Promise<T>): Promise<T>;
|
|
16
|
+
/** A loaded auth state and the path it was read from — the path is its identity. */
|
|
17
|
+
export interface SessionStorageState {
|
|
18
|
+
path: string;
|
|
19
|
+
state: StorageState;
|
|
20
|
+
}
|
|
15
21
|
export interface SessionOptions {
|
|
16
22
|
/** Resize the current page to this. Omit to leave the page exactly as it is. */
|
|
17
23
|
viewport?: Viewport;
|
|
18
24
|
/** Require a touch-capable page. A session without touch is reopened to get it. */
|
|
19
25
|
hasTouch?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Open the page with this saved auth. Cookies and storage are fixed when the
|
|
28
|
+
* context is created, so a session that was opened with different auth (or
|
|
29
|
+
* none) is reopened to take it. Omit to leave whatever the session has.
|
|
30
|
+
*/
|
|
31
|
+
storageState?: SessionStorageState;
|
|
20
32
|
}
|
|
21
33
|
export interface SessionPage {
|
|
22
34
|
page: Page;
|
|
@@ -26,16 +38,20 @@ export interface SessionPage {
|
|
|
26
38
|
* everything the old page held, which is worth telling the user about.
|
|
27
39
|
*/
|
|
28
40
|
previousUrl?: string;
|
|
41
|
+
/** Why it had to be discarded, so the caller can say so. */
|
|
42
|
+
reopenedFor?: "touch" | "auth";
|
|
29
43
|
}
|
|
30
44
|
/**
|
|
31
45
|
* The current page, opening one if there is none.
|
|
32
46
|
*
|
|
33
47
|
* An existing page is reused, resized only when `viewport` is given and
|
|
34
48
|
* differs — an omitted viewport means "leave the page alone", never "reset it
|
|
35
|
-
* to the default". The
|
|
36
|
-
* `hasTouch
|
|
37
|
-
*
|
|
38
|
-
*
|
|
49
|
+
* to the default". The two things that cannot be changed in place are
|
|
50
|
+
* `hasTouch` and the saved auth: both are fixed when the context is created,
|
|
51
|
+
* so a session that lacks touch, or that was opened with different auth than
|
|
52
|
+
* the caller now asks for, has to be reopened. That loses whatever the old
|
|
53
|
+
* page held, which is why the discarded URL and the reason come back to the
|
|
54
|
+
* caller.
|
|
39
55
|
*/
|
|
40
56
|
export declare function getSessionPage(options?: SessionOptions): Promise<SessionPage>;
|
|
41
57
|
/**
|
package/dist/engine/browser.js
CHANGED
|
@@ -67,15 +67,21 @@ let session = null;
|
|
|
67
67
|
*
|
|
68
68
|
* An existing page is reused, resized only when `viewport` is given and
|
|
69
69
|
* differs — an omitted viewport means "leave the page alone", never "reset it
|
|
70
|
-
* to the default". The
|
|
71
|
-
* `hasTouch
|
|
72
|
-
*
|
|
73
|
-
*
|
|
70
|
+
* to the default". The two things that cannot be changed in place are
|
|
71
|
+
* `hasTouch` and the saved auth: both are fixed when the context is created,
|
|
72
|
+
* so a session that lacks touch, or that was opened with different auth than
|
|
73
|
+
* the caller now asks for, has to be reopened. That loses whatever the old
|
|
74
|
+
* page held, which is why the discarded URL and the reason come back to the
|
|
75
|
+
* caller.
|
|
74
76
|
*/
|
|
75
77
|
export async function getSessionPage(options = {}) {
|
|
76
78
|
const wantsTouch = options.hasTouch === true;
|
|
77
79
|
const live = session !== null && !session.page.isClosed();
|
|
78
|
-
|
|
80
|
+
const hasTouch = live && session.hasTouch;
|
|
81
|
+
// An omitted `storageState` means "leave the session's auth alone"; a named
|
|
82
|
+
// one has to match the file the context was actually created from.
|
|
83
|
+
const hasAuth = !live || options.storageState === undefined || session.storageState?.path === options.storageState.path;
|
|
84
|
+
if (live && (hasTouch || !wantsTouch) && hasAuth) {
|
|
79
85
|
const page = session.page;
|
|
80
86
|
if (options.viewport && !sameSize(page.viewportSize(), options.viewport)) {
|
|
81
87
|
await page.setViewportSize(options.viewport);
|
|
@@ -83,20 +89,27 @@ export async function getSessionPage(options = {}) {
|
|
|
83
89
|
}
|
|
84
90
|
return { page };
|
|
85
91
|
}
|
|
86
|
-
// Either nothing is open, or what is open cannot do what was asked. Carry
|
|
87
|
-
//
|
|
92
|
+
// Either nothing is open, or what is open cannot do what was asked. Carry the
|
|
93
|
+
// old size and auth over so reopening reproduces the page as closely as it can.
|
|
88
94
|
const previousUrl = live ? safeUrl(session.page) : undefined;
|
|
95
|
+
const reopenedFor = live ? (wantsTouch && !hasTouch ? "touch" : "auth") : undefined;
|
|
89
96
|
const viewport = options.viewport ?? session?.viewport ?? { ...DEFAULT_VIEWPORT };
|
|
97
|
+
const storageState = options.storageState ?? session?.storageState;
|
|
90
98
|
await closeSession();
|
|
91
99
|
const browser = await getBrowser();
|
|
92
100
|
const context = await browser.newContext({
|
|
93
101
|
viewport,
|
|
94
102
|
deviceScaleFactor: 1,
|
|
95
103
|
...(wantsTouch ? { hasTouch: true } : {}),
|
|
104
|
+
...(storageState ? { storageState: storageState.state } : {}),
|
|
96
105
|
});
|
|
97
106
|
const page = await context.newPage();
|
|
98
|
-
session = { context, page, hasTouch: wantsTouch, viewport };
|
|
99
|
-
return {
|
|
107
|
+
session = { context, page, hasTouch: wantsTouch, viewport, ...(storageState ? { storageState } : {}) };
|
|
108
|
+
return {
|
|
109
|
+
page,
|
|
110
|
+
...(previousUrl !== undefined ? { previousUrl } : {}),
|
|
111
|
+
...(reopenedFor !== undefined ? { reopenedFor } : {}),
|
|
112
|
+
};
|
|
100
113
|
}
|
|
101
114
|
function sameSize(a, b) {
|
|
102
115
|
return a !== null && a.width === b.width && a.height === b.height;
|
|
@@ -1 +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"]}
|
|
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\";\nimport type { StorageState } from \"../utils/storage-state.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 /** The saved auth this context was created with, if any (see SessionOptions). */\n storageState?: SessionStorageState;\n}\n\n/** A loaded auth state and the path it was read from — the path is its identity. */\nexport interface SessionStorageState {\n path: string;\n state: StorageState;\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 * Open the page with this saved auth. Cookies and storage are fixed when the\n * context is created, so a session that was opened with different auth (or\n * none) is reopened to take it. Omit to leave whatever the session has.\n */\n storageState?: SessionStorageState;\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 /** Why it had to be discarded, so the caller can say so. */\n reopenedFor?: \"touch\" | \"auth\";\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 two things that cannot be changed in place are\n * `hasTouch` and the saved auth: both are fixed when the context is created,\n * so a session that lacks touch, or that was opened with different auth than\n * the caller now asks for, has to be reopened. That loses whatever the old\n * page held, which is why the discarded URL and the reason come back to the\n * 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 const hasTouch = live && session!.hasTouch;\n // An omitted `storageState` means \"leave the session's auth alone\"; a named\n // one has to match the file the context was actually created from.\n const hasAuth = !live || options.storageState === undefined || session!.storageState?.path === options.storageState.path;\n\n if (live && (hasTouch || !wantsTouch) && hasAuth) {\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 the\n // old size and auth over so reopening reproduces the page as closely as it can.\n const previousUrl = live ? safeUrl(session!.page) : undefined;\n const reopenedFor: \"touch\" | \"auth\" | undefined = live ? (wantsTouch && !hasTouch ? \"touch\" : \"auth\") : undefined;\n const viewport = options.viewport ?? session?.viewport ?? { ...DEFAULT_VIEWPORT };\n const storageState = options.storageState ?? session?.storageState;\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 ...(storageState ? { storageState: storageState.state } : {}),\n });\n const page = await context.newPage();\n session = { context, page, hasTouch: wantsTouch, viewport, ...(storageState ? { storageState } : {}) };\n return {\n page,\n ...(previousUrl !== undefined ? { previousUrl } : {}),\n ...(reopenedFor !== undefined ? { reopenedFor } : {}),\n };\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"]}
|
|
@@ -9,16 +9,16 @@ import type { Page } from "playwright";
|
|
|
9
9
|
* a single actionable line naming the step that failed — a recording is a bad
|
|
10
10
|
* place to surface a 30-line call log.
|
|
11
11
|
*/
|
|
12
|
-
/** Actions `framewatch_capture` can
|
|
13
|
-
export declare const CAPTURE_ACTIONS: readonly ["click", "tap", "type", "scroll", "swipe", "wait", "navigate"];
|
|
14
|
-
/** Actions `framewatch_interact` can perform as a one-off. */
|
|
15
|
-
export declare const INTERACT_ACTIONS: readonly ["click", "tap", "type", "scroll", "swipe", "navigate", "select", "hover"];
|
|
12
|
+
/** Actions a replayable script (`framewatch_capture`, `framewatch_save_auth`) can perform. */
|
|
13
|
+
export declare const CAPTURE_ACTIONS: readonly ["click", "tap", "type", "key", "scroll", "swipe", "hover", "select", "wait", "navigate"];
|
|
14
|
+
/** Actions `framewatch_interact` can perform as a one-off — everything but `wait`, which needs a script to sit in. */
|
|
15
|
+
export declare const INTERACT_ACTIONS: readonly ["click", "tap", "type", "key", "scroll", "swipe", "navigate", "select", "hover"];
|
|
16
16
|
export type InteractionAction = (typeof CAPTURE_ACTIONS)[number] | (typeof INTERACT_ACTIONS)[number];
|
|
17
17
|
export interface Interaction {
|
|
18
18
|
action: InteractionAction;
|
|
19
|
-
/** CSS selector for click/tap/type/select/hover, or the scroll container. */
|
|
19
|
+
/** CSS selector for click/tap/type/select/hover, the field to focus for `key`, or the scroll container. */
|
|
20
20
|
selector?: string;
|
|
21
|
-
/** Text to type, option value to select, or URL to navigate to. */
|
|
21
|
+
/** Text to type, key to press, option value to select, or URL to navigate to. */
|
|
22
22
|
value?: string;
|
|
23
23
|
x?: number;
|
|
24
24
|
y?: number;
|