@automatebrowser/mcp 0.3.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.
@@ -0,0 +1,486 @@
1
+ # Screenshots, logs and diagnostics
2
+
3
+ ## Contents
4
+
5
+ | Section | What it answers |
6
+ |---|---|
7
+ | [Screenshots](#screenshots) | Viewport, full page, one element, to a file |
8
+ | [A strip of stills](#a-strip-of-stills-when-one-picture-is-not-enough) | Several frames on a timer, and how fast they really are |
9
+ | [Background tabs and the banner](#screenshotting-a-tab-the-user-is-not-looking-at) | Why a banner sometimes appears |
10
+ | [Console logs](#console-logs) | What the page printed, including before you arrived |
11
+ | [The issues feed](#the-issues-feed-the-failures-with-no-console-error) | Failures that produce no console error |
12
+ | [Accessibility](#accessibility-a-floor-not-a-pass) | Auditing the page, and what an audit cannot tell you |
13
+ | [Network](#network) | The request list, and one request's body |
14
+ | [The footers](#the-footers-on-every-action) | The counts appended to each action |
15
+ | [Performance](#performance) | This machine, and what real visitors get |
16
+ | [Memory](#memory-is-this-page-leaking) | Watching the JS heap, and the snapshot DevTools opens |
17
+ | [Emulation](#emulation) | Pretending to be somewhere or something else |
18
+ | [Advanced mode](#advanced-mode-and-what-it-costs) | When the debugger is worth it |
19
+ | [Recipe: a slow page](#recipe-this-page-is-slow) | The order to diagnose it in |
20
+ | [Recipe: an accessibility audit](#recipe-auditing-a-page-for-accessibility) | Running one, and reporting it honestly |
21
+
22
+ ---
23
+
24
+ ## Screenshots
25
+
26
+ ```
27
+ browser_screenshot // the visible viewport
28
+ browser_screenshot { fullPage: true } // the whole scrollable page
29
+ browser_screenshot { ref: "e12" } // one element, cropped
30
+ browser_screenshot { filePath: "./shot.png" } // to disk, not into your context
31
+ ```
32
+
33
+ `format` takes `png` (default), `jpeg` or `webp`; `quality` applies to both lossy formats, not to png.
34
+ All three work with `fullPage` as well as the viewport.
35
+
36
+ **Photograph the element, not the page.** This is the single most expensive habit an agent has with
37
+ this server, and it is measurable: across 445 real captures, a viewport picture averaged **~1,531
38
+ tokens** and only **20** of those captures scoped themselves to one element. A picture does not leave
39
+ your context when you are finished with it — it is re-read on every turn for the rest of the session,
40
+ so ten page captures in a long session cost more than every other browser call put together.
41
+
42
+ So before capturing, ask what you are actually checking:
43
+
44
+ | What you want to know | Reach for |
45
+ |---|---|
46
+ | Did this one button / field / card render right? | `browser_screenshot { ref }` — a fraction of a page |
47
+ | Is this text on the page, is this value right? | `browser_eval` or `browser_find` — **no picture at all** |
48
+ | What is the overall layout doing? | a plain viewport capture, once |
49
+ | A record for a human to look at later | `filePath` — it never enters your context |
50
+
51
+ **Use `filePath` whenever you do not need to look at the image yourself.** An inlined screenshot is
52
+ one of the most expensive things you can put in a reply.
53
+
54
+ **An inline capture is held under 1536 x 4096 device pixels and may come back downscaled.** The
55
+ aspect ratio is kept, and the reply tells you the size before and after whenever it happened. **Do not
56
+ measure page coordinates off an image that says it was downscaled** — read them off the smaller image,
57
+ or take the capture again with `filePath`, which is never downscaled. The user's
58
+ `AUTOMATE_BROWSER_SCREENSHOT_MAX_WIDTH` / `..._MAX_HEIGHT` set the box, and `0` switches either half
59
+ off.
60
+
61
+ **For a smaller FILE, ask for `format: "webp"` with a `quality` — not a smaller size.** Downscaling a
62
+ screenshot and re-encoding it as PNG makes the file *bigger* (measured: a 332 KB 2K capture becomes
63
+ 529 KB at 1536 wide, against 74 KB as webp at quality 60), because a screenshot is flat colour and
64
+ sharp edges that PNG already compresses well.
65
+
66
+ **Three things to know about `ref` crops.** The first two were measured on a 1.5x display on
67
+ 2026-09-02:
68
+
69
+ - The crop is in **device** pixels, so on a scaled or Retina screen a 300x150 element comes back
70
+ 450x225. That is correct, not a bug — do not "correct" it back to CSS pixels.
71
+ - When the element's size lands on a **half** device pixel the crop rounds outward, so up to one
72
+ pixel of the surrounding page can show at the right and bottom edges. Rounding the other way would
73
+ shave the element instead.
74
+ - **A ref from a SAME-ORIGIN frame crops fine**, at any depth: the element's position is measured
75
+ inside its frame and translated up through each parent. A ref from a **cross-origin** frame is
76
+ **refused** — that chain cannot be walked across an origin boundary, the capture covers the whole
77
+ tab, and cropping anyway would hand you a confidently wrong region. Take the screenshot without
78
+ `ref` and find the frame's area in the full picture.
79
+
80
+ **Judging a crop by eye is unreliable on a busy page.** Overlapping text from behind the element will
81
+ appear inside the crop because it genuinely renders there, which looks exactly like a mis-aligned
82
+ crop. If you need to be sure, give the element a temporary border, or check against a background
83
+ colour that cannot occur inside it.
84
+
85
+ ## A strip of stills, when one picture is not enough
86
+
87
+ ```
88
+ browser_screenshot { frames: 10, intervalMs: 150, filePath: "./out/strip.png" }
89
+ // → out/strip-01.png … out/strip-10.png, plus one summary line. Never the images.
90
+ ```
91
+
92
+ For showing a **person** what happened — a transition, a flicker, a flow that scrolled past. Up to 30
93
+ frames, at least 100 ms apart. `filePath` is required: a strip is written to disk, never returned as
94
+ pictures, and asking for frames without one is refused.
95
+
96
+ **Check the achieved interval in the reply before you trust the strip.** It reports both what you
97
+ asked for and what it got, and they differ enormously depending on one thing:
98
+
99
+ | The tab is | You get | Good for |
100
+ |---|---|---|
101
+ | In the **foreground** | ~110 ms per frame | A fade, a transition, anything fast |
102
+ | In the **background** (the default) | **~3.9 seconds per frame** | Slow changes only |
103
+
104
+ Chrome does not draw a tab nobody is looking at, so each frame waits for one to be rendered. **Your
105
+ tab is a background tab unless you did something about it.** For a strip of something moving, call
106
+ `browser_switch_tab` first — it takes the user's focus, so ask, or accept the slow strip. The reply
107
+ tells you when this bit.
108
+
109
+ Two more things, neither of them optional to know:
110
+
111
+ - **It is stills, not video.** No audio, no file to play. Open them in order.
112
+ - **The debugger banner shows for the whole strip**, foreground or not, because the cheap capture path
113
+ is capped by Chrome at 2 frames per second. It attaches once and detaches at the end.
114
+
115
+ ## Screenshotting a tab the user is not looking at
116
+
117
+ This works, and it does **not** bring the tab forward. How it works is worth knowing because it has a
118
+ visible side effect.
119
+
120
+ The cheap capture the browser offers photographs *whatever is on screen* — the foreground tab of a
121
+ window. Aimed at a background tab it would return the wrong page; aimed at a window the operating
122
+ system is not drawing it returns a **stale or blank frame with no error at all**. So when your tab is
123
+ not the foreground tab of a drawn window, the server does not use that path. It renders your exact tab
124
+ through the debugger instead.
125
+
126
+ - **The cost:** the browser shows its "being debugged" banner for the duration, then detaches. This is
127
+ the same mechanism `fullPage: true` has always used.
128
+ - **The result says so** — it comes back flagged, with the reason. If a user asks why a banner
129
+ flashed up, that is why.
130
+ - **If the debugger cannot attach** — a restricted page, or policy forbids it — the call **fails with
131
+ that reason named**. It never quietly falls back to photographing whatever was on screen. A refusal
132
+ here is the tool protecting you from a wrong answer.
133
+ - **The user having DevTools open does NOT stop you.** Measured in Edge 152 on 2026-09-02: a
134
+ background tab with the DevTools panel open attached and captured correctly. Chromium allows
135
+ several debugger clients on one tab. This page said the opposite until that date, and the tool's
136
+ own error offered "close DevTools" as the first remedy — advice that could never have helped.
137
+ One caveat worth knowing: what you get back is **what that tab is actually rendering**, so if
138
+ their DevTools is in device-emulation mode you will receive the emulated phone-sized page, which is
139
+ correct but probably not what you expected.
140
+ - **It can stall, and stalling is not failing.** Chrome stops drawing a tab nobody is looking at, so
141
+ a capture sometimes waits on a frame that never comes. The tool nudges the page awake and tries
142
+ again on its own; if both attempts stall you get `CAPTURE_STALLED`, which is **marked retryable
143
+ because a screenshot changes nothing** — simply ask again. Measured before this handling existed:
144
+ 3 stalls in 8 captures of a background tab, every one of which succeeded on a retry. Re-measured
145
+ with it in place on 2026-09-02: **0 stalls in 11**, including two captures taken after the tab had
146
+ sat hidden and idle for 75 seconds. So treat `CAPTURE_STALLED` as rare rather than routine — if you
147
+ see it twice in a row on the same tab, that is worth reporting, not just retrying.
148
+
149
+ ## Console logs
150
+
151
+ `browser_get_console_logs` returns what the page printed, including uncaught errors with their stacks
152
+ and service-worker lifecycle events.
153
+
154
+ `includePreserved: true` also returns the **previous pages'** logs — the answer to "it errored, then
155
+ redirected, and now I cannot see it". Logs survive the browser shutting the extension down to save
156
+ memory.
157
+
158
+ You get the **50 newest entries**. If there are more, a footer names the page you are on, the total,
159
+ and the exact call for the next one — and `page: 2` goes *further back in time*, not forward. Stacks
160
+ are printed for the thrown errors **on the page you asked for**, so an error further back needs its
161
+ page fetched.
162
+
163
+ ## The issues feed: the failures with no console error
164
+
165
+ `browser_issues` is the only tool that sees problems the console never mentions:
166
+
167
+ - content blocked by a security policy
168
+ - a dropped third-party cookie
169
+ - mixed content
170
+ - a CORS refusal
171
+ - browser interventions
172
+ - failed and 4xx/5xx requests
173
+
174
+ **When something "works by hand but not here", this is usually why.** Reach for it before you start
175
+ theorising.
176
+
177
+ ## Accessibility: a floor, not a pass
178
+
179
+ ```
180
+ browser_snapshot // FIRST — this is what creates the refs
181
+ browser_issues { audit: "a11y" } // violations, worst first
182
+ browser_issues { audit: "a11y", page: 2 }
183
+ ```
184
+
185
+ Runs **axe-core** against the page you are on. Violations come back grouped by impact — **critical,
186
+ serious, moderate, minor** — with the number of elements each rule matched, up to **five examples**
187
+ each, and a link to that rule's fix guidance. **20 rules per page** by default (`limit` changes it),
188
+ worst first; the footer names the next call.
189
+
190
+ **Snapshot first.** Refs are written onto the page by `browser_snapshot`, so an audit run before any
191
+ snapshot reports CSS selectors and no refs. With a ref you can hand the failing element straight to
192
+ `browser_click`, `browser_get_html` or `browser_eval`; without one you are reading a selector. A
193
+ finding is never dropped for lacking a ref.
194
+
195
+ **Say the limit out loud when you report the result. Automated rules catch roughly a third of real
196
+ accessibility barriers.** They check that attributes exist, never that they are right — no rule here
197
+ can tell whether alt text describes its image, whether the focus order is sensible, or whether a
198
+ custom widget can actually be operated from the keyboard. **Never report "0 violations" as
199
+ "accessible."** Report it as what it is: the automatable third found nothing.
200
+
201
+ Rules axe could not decide alone are **counted at the end, not listed** — a contrast check over a
202
+ background image, say. If that count is high, the page needs a person, not another call.
203
+
204
+ No debugger and no banner, and it leaves nothing behind on the page.
205
+
206
+ ## Network
207
+
208
+ - **`browser_network_requests`** lists what the current page requested — method, URL, status, type.
209
+ `resourceTypes` filters; `includePreserved` reaches back through a redirect. You get the **50
210
+ newest** per call (`limit` sets the page size); `page: 2` is the 50 *before* those, and a footer
211
+ names the next call when more remain.
212
+ - **`browser_get_network_request { url | requestId }`** returns one request's **response body**, plus
213
+ its request and response headers. Needs advanced mode. When a URL substring matches several
214
+ requests, it returns the newest and lists the others so you can address the one you meant by
215
+ `requestId`. Header values that look like credentials come back `<redacted>`; that is the
216
+ [redaction rule](./sessions-and-state.md#cookies-and-storage), and `revealValues: true` opts out.
217
+
218
+ ## The footers on every action
219
+
220
+ Mutating actions come back with a short footer counting **new console errors** and **new browser
221
+ issues** caused by that action. It is the cheapest possible signal that a click that reported success
222
+ actually broke something.
223
+
224
+ It only counts what is *new* since your last look, so an error the page logged before you arrived is
225
+ never blamed on you. Set `AUTOMATE_BROWSER_DELTA_FOOTER=off` to silence it.
226
+
227
+ For detail in the same reply, ask for it:
228
+
229
+ ```
230
+ browser_click { ..., include: "console, network" }
231
+ ```
232
+
233
+ A section that cannot be fetched comes back labelled `(unavailable: ...)` and changes nothing else —
234
+ the action still succeeded, and any other section you asked for still arrives. A section NAME you get
235
+ wrong is the opposite case: it is refused **before** the action runs, so nothing happened and the
236
+ corrected call is safe to make.
237
+
238
+ ## Performance
239
+
240
+ - **`browser_perf_trace`** records a trace of this machine on this run and returns **Core Web Vitals**
241
+ — LCP, FCP, CLS, INP — rated against Google's thresholds, plus the long tasks that blocked the main
242
+ thread. Needs advanced mode. `action: "analyze"` re-reads a saved trace later with no browser at
243
+ all.
244
+ - **It also breaks the LCP down and names the cause.** Where the vitals tell you *that* a page was
245
+ slow, the breakdown tells you *which part* was: time to first byte, resource load delay, resource
246
+ load time and render delay, each with its share of the total, followed by one line naming the cause
247
+ and one naming the fix. Read the biggest span first — the advice for a slow server and the advice
248
+ for a late image have nothing in common, so acting on the total alone is guesswork.
249
+ - **Render-blocking resources are listed too** — the requests that finished before first paint and
250
+ held it up, slowest first. An async script is deliberately *not* listed: it is already doing the
251
+ right thing, and reporting it would be advice to break working code.
252
+ - **A span the trace cannot support is named, not omitted.** A text LCP has no resource to download,
253
+ so it correctly shows two spans rather than four zeroes; an image whose request cannot be matched
254
+ says so; and a trace with no document-request timing says that instead of reporting a breakdown it
255
+ cannot stand behind. **If you see one of those lines, the number you wanted was not measured** —
256
+ record the load itself with `{ action: "start", reload: true, autoStop: true }` rather than reading
257
+ a gap as good news.
258
+ - **`browser_perf_field_data { url }`** answers "how fast is this for *real* visitors", from Google's
259
+ public Chrome UX Report. It needs no browser — but it **sends the URL you ask about to a Google
260
+ API**, and needs `AUTOMATE_BROWSER_CRUX_KEY` set. It is the only outbound call this server makes.
261
+
262
+ Use them together: the trace tells you what this machine did, the field data tells you whether that
263
+ resembles reality.
264
+
265
+ ## Memory: is this page leaking?
266
+
267
+ Two actions on the same tool, split by how much each can honestly tell you.
268
+
269
+ ```
270
+ browser_perf_trace { action: "memory" } // watch the heap for 5s
271
+ browser_perf_trace { action: "memory", durationMs: 30000 } // watch it for 30s
272
+ ```
273
+
274
+ **`action: "memory"`** samples `performance.memory.usedJSHeapSize` every 500 ms across the window and
275
+ reports start, end, every reading, and a **least-squares trend in MB/s**. The trend is a fit, not
276
+ end-minus-start, because the heap saws: a collection mid-window drops it a long way, and a leaking
277
+ page can easily finish *lower* than it started.
278
+
279
+ - **It needs no advanced mode** — no debugger, no banner. This is the one performance action you can
280
+ run on a tab a person is looking at without changing anything for them.
281
+ - `durationMs` is **1000-30000**, default 5000, and out-of-range is **refused, not clamped**. For a
282
+ longer watch, call it again between your own interactions.
283
+ - The interval is fixed at 500 ms and is not an argument. Chrome quantises the reading into coarse
284
+ buckets, so sampling faster buys more points off the same staircase, not more resolution.
285
+ - **You will often get fewer samples than 500 ms implies, and that is fine.** Chrome throttles timers
286
+ in a tab it is not drawing to about once a second, and your tab is a background tab. The reply says
287
+ the cadence it **achieved**, and the trend is computed from the real elapsed time the page measured
288
+ — so the MB/s figure is right either way. `browser_switch_tab` is what changes it.
289
+ - **Chrome-family only.** `performance.memory` does not exist in Firefox or Safari; there the call
290
+ fails and says so. Verified working on **both Chrome and Edge** — the whole integration suite
291
+ passes 66/66 on each.
292
+
293
+ **The sentence that matters: a rising heap is not proof of a leak.** It may simply be memory a
294
+ collection has not reclaimed yet. The reply says so every time. To go from "rising" to "leaking",
295
+ take two snapshots.
296
+
297
+ **There is no heap snapshot here, and there cannot be.** It was built and Chrome refused it:
298
+ `chrome.debugger` exposes a fixed allow-list of DevTools Protocol domains and `HeapProfiler` is not on
299
+ it — the CPU `Profiler` is, the heap one is not. No extension can capture one. To find *what* grew,
300
+ a person opens **DevTools → Memory**, takes two snapshots and uses the comparison view. Ask for that
301
+ rather than looking for a tool.
302
+
303
+ ## Emulation
304
+
305
+ `browser_emulate` fakes a location, a user agent, extra headers, a colour scheme, a mobile viewport
306
+ with touch, a throttled network, or a slow CPU. `clear` takes them back off.
307
+
308
+ Everything here is **per tab** and evaporates with the tab — except `browser_proxy`, which changes the
309
+ browsing of the human sharing the browser. Treat that one as an interruption to a person, not a
310
+ setting.
311
+
312
+ ## Advanced mode, and what it costs
313
+
314
+ `browser_advanced_mode { enable: true }` attaches the Chrome debugger to your tab. While attached, the
315
+ browser shows a banner saying so.
316
+
317
+ **Required for:** `browser_upload_file`, `browser_get_network_request`, `browser_perf_trace` (all
318
+ actions except `memory`, which is debugger-free), some `browser_emulate` options, and
319
+ `browser_navigate`'s `initScript` / `handleBeforeUnload`.
320
+
321
+ **Attached for you automatically, briefly, by:** `fullPage: true` screenshots, and screenshots of a
322
+ background tab.
323
+
324
+ **While attached, a `browser_click` on a CROSS-origin frame's ref is refused** — the trusted click
325
+ path drives one debugger session and cannot reach another origin's frame. Turn advanced mode off and
326
+ the default click reaches it, because that one injects into the frame directly. A same-origin frame
327
+ works either way, with the frame's offset applied so the click lands on the element rather than on
328
+ whatever sits at that point in the top page.
329
+
330
+ **It does not change what your other calls return.** With it on, clicks and key presses are dispatched
331
+ as real OS-level input instead of synthetic events, but they still wait for the page to settle and
332
+ still report `navigated`, `elapsedMs` and — for a coordinate click — the `hit` naming what was under
333
+ the point. Until 2026-09-01 they did not, so turning this on for an unrelated reason quietly changed
334
+ both the shape and the timing of every click.
335
+
336
+ **But it makes clicking impossible on a background tab, which is where you normally work.** Chrome
337
+ **discards** real input aimed at a tab it is not drawing. So with advanced mode on, `browser_click`
338
+ and `browser_press_key` **refuse outright** on your background tab and tell you to bring it forward or
339
+ turn the mode off — they never report a success that did not happen. The practical rule: enable
340
+ advanced mode for what needs it (a response body, a trace, an upload), then **turn it off again before
341
+ you interact**. The default path works perfectly in a hidden tab; the trusted one cannot.
342
+
343
+ Everything else is debugger-free by design — that is the point of this server. Turn it off when done:
344
+ `browser_advanced_mode { enable: false }`. A tool that needs it and does not have it says so with
345
+ `ADVANCED_MODE_REQUIRED` rather than failing obscurely.
346
+
347
+ ### Reaching a staging site with a bad certificate
348
+
349
+ **This does not work, and you should not spend a call finding that out.** There is no argument for
350
+ it. `acceptInsecureCerts` existed until 2026-09-16 and never worked on any build: extensions get a
351
+ fixed list of debugger domains and the one it needed is not on it. It was deleted rather than kept
352
+ as a permanent error charged to every request you make.
353
+
354
+ **If you pass it anyway the call is refused by name**, not quietly ignored — so an older habit costs
355
+ you one error rather than a false belief that certificate checking is off.
356
+
357
+ **What to tell the user instead**, because this is theirs to fix and it takes them ten seconds:
358
+
359
+ - **Click through the warning page once, by hand.** Chrome remembers that host for the session, and
360
+ every AutomateBrowser tool then works against it normally. This is almost always the right answer.
361
+ - **Or start the browser with `--ignore-certificate-errors`** if it is going to keep happening.
362
+
363
+ **Do not retry, and do not look for another way round it** — there is not one from inside an extension.
364
+ Say plainly that the site's certificate is being rejected, name which of the two fixes you want, and
365
+ wait. A certificate warning is exactly the thing a person should look at once rather than have an agent
366
+ silently bypass.
367
+
368
+ One related fact is unchanged: a tab parked on the warning page refuses a debugger attachment at all.
369
+
370
+ ## Recipe: "this page is slow"
371
+
372
+ Work outwards from the cheapest evidence. Steps 1 and 2 need no debugger and no banner.
373
+
374
+ **1. Ask whether it is slow for anyone else.**
375
+
376
+ ```
377
+ browser_perf_field_data { url: "https://example.com/page" }
378
+ ```
379
+
380
+ Real visitors' vitals from Google's Chrome UX Report, with no browser involved. If the field data is
381
+ green and your run is not, you are measuring **this machine on this network**, not the site. Say that
382
+ before you go further. (Needs `AUTOMATE_BROWSER_CRUX_KEY`, and it sends the URL to a Google API.)
383
+
384
+ **2. Check for things failing silently.**
385
+
386
+ ```
387
+ browser_issues
388
+ ```
389
+
390
+ A blocked script, a CORS refusal or a 4xx on a render-blocking resource makes a page slow *and* prints
391
+ nothing in the console. Rule this out before you profile — it is the cheapest call here and it is
392
+ often the whole answer.
393
+
394
+ **3. Profile one full page load, in a single call.**
395
+
396
+ ```
397
+ browser_advanced_mode { enable: true }
398
+ browser_perf_trace { action: "start", reload: true, autoStop: true, filePath: "./trace.json" }
399
+ ```
400
+
401
+ `reload` + `autoStop` records from before navigation until loading finishes and returns LCP, FCP, CLS
402
+ and INP rated against Google's thresholds, plus the long tasks that blocked the main thread, **the LCP
403
+ breakdown, and the render-blocking resources**. **Pass `filePath`** — the path is checked before
404
+ recording starts, and it lets you re-read the trace later with `{ action: "analyze", filePath }`
405
+ instead of recording again.
406
+
407
+ **4. Read the LCP breakdown before you call anything else.**
408
+
409
+ It has already done the step this recipe used to send you off to do. Four spans, each with its share
410
+ of the total, and the cause and fix lines name the biggest one:
411
+
412
+ | Biggest span | What it means | What to do |
413
+ |---|---|---|
414
+ | **time to first byte** | The server was still thinking; nothing on the page could start | Nothing in the browser will fix this — it is the request handler or the cache |
415
+ | **resource load delay** | The image was discovered late, not downloaded slowly | Look at how it is referenced: a preload or a plain `<img>` in the initial HTML |
416
+ | **resource load time** | The image itself is too big for the connection | `browser_network_requests` for its size, then a smaller format |
417
+ | **render delay** | The bytes arrived and the page still could not paint | The render-blocking list right below it, and the long tasks above |
418
+
419
+ **5. Let any remaining failing vital pick the next call.**
420
+
421
+ | Vital | What it means | Next call |
422
+ |---|---|---|
423
+ | **CLS** high | The layout moved after paint | `browser_screenshot { frames: 8, intervalMs: 150, filePath: … }` — see the jump |
424
+ | **INP** slow, or long tasks | The main thread was blocked | The trace's long-task list already names the durations |
425
+
426
+ **6. If it is only slow after a while of use, it is a different question.** That is
427
+ `browser_perf_trace { action: "memory" }`, in the memory section above — and that one needs no
428
+ debugger at all.
429
+
430
+ **7. Turn advanced mode off before you interact again.**
431
+
432
+ ```
433
+ browser_advanced_mode { enable: false }
434
+ ```
435
+
436
+ With it on, `browser_click` and `browser_press_key` **refuse** on a background tab, which is where you
437
+ normally work. Profiling is the only thing you needed it for.
438
+
439
+ ## Recipe: auditing a page for accessibility
440
+
441
+ **1. Snapshot first — this is not optional.**
442
+
443
+ ```
444
+ browser_snapshot
445
+ browser_issues { audit: "a11y" }
446
+ ```
447
+
448
+ Refs are written onto the page by the snapshot. An audit run before any snapshot still reports every
449
+ finding, but names them with CSS selectors, so you cannot hand a failing element to another tool.
450
+
451
+ **2. Page through, worst first.** Twenty rules per page, most severe first, and the footer gives you
452
+ the exact next call. Stop when the impact drops below what you were asked to care about — you rarely
453
+ need the `minor` pages.
454
+
455
+ **3. Read the real markup before you propose a fix.**
456
+
457
+ ```
458
+ browser_get_html { ref: "e34" }
459
+ ```
460
+
461
+ The rule tells you what axe checked; the markup tells you why it failed. Guessing between those two is
462
+ how a fix gets proposed for something that is not actually broken.
463
+
464
+ **4. Audit dark mode too, if the site has one.** Contrast is the most common failure and it is
465
+ theme-specific, so a light-mode pass says nothing about the other theme:
466
+
467
+ ```
468
+ browser_advanced_mode { enable: true }
469
+ browser_emulate { colorScheme: "dark" }
470
+ browser_snapshot
471
+ browser_issues { audit: "a11y" }
472
+ browser_emulate { clear: ["colorScheme"] }
473
+ browser_advanced_mode { enable: false }
474
+ ```
475
+
476
+ `colorScheme` needs advanced mode — `prefers-color-scheme` cannot be overridden from page JavaScript
477
+ at all. Reading and auditing still work fine while it is attached; only clicking on a background tab
478
+ does not.
479
+
480
+ **5. Report the count of rules needing human review**, printed at the end of the audit. A high number
481
+ means the page needs a person, not another call.
482
+
483
+ **6. Never write "accessible".** Automated rules catch roughly a third of real barriers, and they
484
+ check that attributes exist rather than that they are right. The honest sentence is *"the automatable
485
+ third found nothing"* — followed by what a person still has to check: keyboard traversal, focus order,
486
+ whether the alt text actually describes the image.