Purpose
Dissect a single frozen frame of a live web page to explain layout and
interaction bugs that jsdom and screenshots cannot — a click that does not
register, an element covered by an invisible overlay, wrong scroll/snap math,
a token that resolves to the wrong value, or a defect that only appears at
certain viewport widths. It drives the locally-installed Playwright package
through one bundled script and returns JSON, so findings are exact numbers, not
guesses. This is the introspection primitive; browser / browser-qa walk user
journeys, this one inspects the DOM truth at a moment in time.
Prerequisites
- The
playwright (or playwright-core) npm package must be resolvable from the
target project — usually a devDep, so run commands from the project directory.
Pass --playwright <path> to point elsewhere. The script exits with code 3 and
a remediation hint if it cannot find it — it never hard-stops the way a global
playwright-cli dependency would.
Variables
PROBE: ./scripts/probe.mjs # Bundled introspection engine (relative to skill root)
VIEWPORT: 1200x900 # Default viewport WxH; cross a breakpoint by changing width
WAIT_MS: 1000 # Settle delay after load before probing
Workflow
Run from the target project
cd into the repo so the script resolves its playwright devDep, then call
the bundled engine. Read node <PROBE> --help once if you need the full flag list.
- IF: the page is behind staging basic-auth → add
--http-auth user:pass
(username may be blank: --http-auth ":pass") and --ignore-https-errors.
- Example:
cd ~/Documents/app && node <PROBE> eval --url https://stg.example.com --http-auth ":s3cret" --ignore-https-errors --js "document.title"
When a click does not register → hit-test
- The most common "button is broken" cause: another element covers it.
hit-test does a real elementFromPoint at the element's center and reports
whether the click lands on the target or what is on top.
- IF:
lands: false → the covering element (tag/class/outerHTML) is eating the
click — inspect its z-index, position, or whether it is a stray overlay.
- IF:
visible: false (zero-size) → the control is display:none / collapsed,
not covered — check container queries and parent layout instead.
- Example:
node <PROBE> hit-test --url <url> --selector 'button[aria-label="Previous"]'
When geometry looks wrong → box
- Dumps
getBoundingClientRect + scrollLeft/scrollWidth/clientWidth/maxScrollLeft
- computed margins/padding/overflow/scroll-padding/transform for one element.
- IF:
scroll.overflowsX is true but maxScrollLeft is tiny → the rail barely
overflows; index-based "next/prev" math can clamp short of a slide (a real
class of carousel bug).
- Example:
node <PROBE> box --url <url> --selector '#rail .viewport'
Find the REAL scroll container → find-scroller
- A
querySelector('[class*=viewport]') can match a non-scrolling wrapper.
find-scroller returns elements whose COMPUTED overflowX is scroll/auto,
with their scroll metrics — use this to grab the actual scroller, then box it.
- Example:
node <PROBE> find-scroller --url <url>
Read design tokens live → tokens
- Resolves CSS custom properties through
getComputedStyle. Confirms a token
(e.g. --bleed-left, --theme-bg-default) actually computes to what you expect.
- IF: a specific token must be read → pass
--names '--a,--b' (enumeration is
best-effort; named reads are guaranteed). Default selector is :root.
- Example:
node <PROBE> tokens --url <url> --selector '#card' --names '--bleed-left,--bleed-right'
When a bug only appears at some widths → sweep
- Loads at each
--widths value and runs a per-width probe (--selector hit-test,
or a custom --js), so you can see exactly what flips across a container-query /
breakpoint threshold.
- Example:
node <PROBE> sweep --url <url> --widths 600,768,1200 --js "(()=>{const vp=[...document.querySelectorAll('[class*=viewport]')].find(e=>getComputedStyle(e).overflowX==='scroll'); return vp?{max:vp.scrollWidth-vp.clientWidth}:'none';})()"
Run anything else → eval
--js '<expression>' runs arbitrary JS in the page and returns its JSON result
(multi-statement: wrap as an IIFE "(()=>{...})()"). This is the escape hatch
for focus order, stacking context, computed values, ARIA state — anything.
- Add
--console to any command to capture page console errors alongside the result.
- Example:
node <PROBE> eval --url <url> --js "getComputedStyle(document.activeElement).outlineWidth"
Gotchas
- Programmatic
.click() lies. element.click() and Playwright's locator.click()
on a forced node bypass hit-testing, so a covered button can look like it "works"
in script while a real user's pointer is blocked. Trust hit-test
(elementFromPoint), not a synthetic click, to decide whether a click truly lands.
- The local Playwright package is CommonJS.
import { chromium } fails under ESM;
the bundled script already handles this (default import then destructure) — keep that
pattern if you hand-roll a one-off.
- Class-substring selectors are treacherous with hashed CS-module names
(
_viewport_ab12 vs _viewportWrapper_cd34 both match [class*=viewport], and
querySelector returns the first in document order — often the wrong one). Prefer
find-scroller to grab the element by COMPUTED overflowX.
References
Worked diagnoses and advanced recipes
- IF: you want a full worked example (the Carousel "left button dead ≥692px" case),
more
eval snippets (focus order, stacking, container-query state), or screenshot
capture for visual diffing
- THEN: Read
./reference/recipes.md
- EXAMPLES:
- "show me how this found the carousel bug"
- "how do I check focus order / z-index stacking with this"
- "capture a screenshot at a specific width"
Works well with
Optional collaborators — browser-microscope runs standalone and these degrade gracefully if absent.
browser — the journey-walking complement; use browser to navigate, browser-microscope to dissect why a click won't land or a layout breaks.
browser-review / browser-qa — reach for the microscope to diagnose the layout failures these surface.
ios-simulator-microscope — the real-device counterpart; escalate there when a bug only reproduces in real iOS Safari (toolbar collapse, dynamic-viewport units, touch dynamics) that desktop Playwright can't model.
1---2name: browser-microscope3description: Real-browser DOM and layout microscope. Runs arbitrary JS in a live page, hit-tests why a click won't land (elementFromPoint — what element is covering it), dumps scroll and box geometry (scrollWidth, offsetLeft, overflow, scroll-padding), reads CSS custom-property design tokens via getComputedStyle, and sweeps viewport widths to find container-query / breakpoint thresholds. Use when a button does not respond to clicks, an element is mysteriously overlapped, covered, clipped or off-screen, scroll / snap geometry looks wrong, computed styles or tokens need reading live, z-index / stacking is suspect, or a layout bug only appears at certain widths. Drives the local Playwright npm package (no global CLI required) and supports HTTP basic-auth and self-signed staging certs. The layout-forensics complement to the journey-walking browser and browser-qa skills.4---56# Purpose78Dissect a single frozen frame of a live web page to explain layout and9interaction bugs that jsdom and screenshots cannot — a click that does not10register, an element covered by an invisible overlay, wrong scroll/snap math,11a token that resolves to the wrong value, or a defect that only appears at12certain viewport widths. It drives the locally-installed Playwright package13through one bundled script and returns JSON, so findings are exact numbers, not14guesses. This is the introspection primitive; `browser` / `browser-qa` walk user15journeys, this one inspects the DOM truth at a moment in time.1617## Prerequisites1819- The `playwright` (or `playwright-core`) npm package must be resolvable from the20 target project — usually a devDep, so run commands from the project directory.21 Pass `--playwright <path>` to point elsewhere. The script exits with code 3 and22 a remediation hint if it cannot find it — it never hard-stops the way a global23 `playwright-cli` dependency would.2425## Variables2627PROBE: ./scripts/probe.mjs # Bundled introspection engine (relative to skill root)28VIEWPORT: 1200x900 # Default viewport WxH; cross a breakpoint by changing width29WAIT_MS: 1000 # Settle delay after load before probing3031## Workflow32331. **Run from the target project**34 - `cd` into the repo so the script resolves its `playwright` devDep, then call35 the bundled engine. Read `node <PROBE> --help` once if you need the full flag list.36 - IF: the page is behind staging basic-auth → add `--http-auth user:pass`37 (username may be blank: `--http-auth ":pass"`) and `--ignore-https-errors`.38 - Example: `cd ~/Documents/app && node <PROBE> eval --url https://stg.example.com --http-auth ":s3cret" --ignore-https-errors --js "document.title"`39402. **When a click does not register → `hit-test`**41 - The most common "button is broken" cause: another element covers it.42 `hit-test` does a real `elementFromPoint` at the element's center and reports43 whether the click lands on the target or what is on top.44 - IF: `lands: false` → the `covering` element (tag/class/outerHTML) is eating the45 click — inspect its `z-index`, `position`, or whether it is a stray overlay.46 - IF: `visible: false` (zero-size) → the control is `display:none` / collapsed,47 not covered — check container queries and parent layout instead.48 - Example: `node <PROBE> hit-test --url <url> --selector 'button[aria-label="Previous"]'`49503. **When geometry looks wrong → `box`**51 - Dumps `getBoundingClientRect` + `scrollLeft/scrollWidth/clientWidth/maxScrollLeft`52 + computed margins/padding/overflow/scroll-padding/transform for one element.53 - IF: `scroll.overflowsX` is true but `maxScrollLeft` is tiny → the rail barely54 overflows; index-based "next/prev" math can clamp short of a slide (a real55 class of carousel bug).56 - Example: `node <PROBE> box --url <url> --selector '#rail .viewport'`57584. **Find the REAL scroll container → `find-scroller`**59 - A `querySelector('[class*=viewport]')` can match a non-scrolling wrapper.60 `find-scroller` returns elements whose COMPUTED `overflowX` is `scroll`/`auto`,61 with their scroll metrics — use this to grab the actual scroller, then `box` it.62 - Example: `node <PROBE> find-scroller --url <url>`63645. **Read design tokens live → `tokens`**65 - Resolves CSS custom properties through `getComputedStyle`. Confirms a token66 (e.g. `--bleed-left`, `--theme-bg-default`) actually computes to what you expect.67 - IF: a specific token must be read → pass `--names '--a,--b'` (enumeration is68 best-effort; named reads are guaranteed). Default selector is `:root`.69 - Example: `node <PROBE> tokens --url <url> --selector '#card' --names '--bleed-left,--bleed-right'`70716. **When a bug only appears at some widths → `sweep`**72 - Loads at each `--widths` value and runs a per-width probe (`--selector` hit-test,73 or a custom `--js`), so you can see exactly what flips across a container-query /74 breakpoint threshold.75 - Example: `node <PROBE> sweep --url <url> --widths 600,768,1200 --js "(()=>{const vp=[...document.querySelectorAll('[class*=viewport]')].find(e=>getComputedStyle(e).overflowX==='scroll'); return vp?{max:vp.scrollWidth-vp.clientWidth}:'none';})()"`76777. **Run anything else → `eval`**78 - `--js '<expression>'` runs arbitrary JS in the page and returns its JSON result79 (multi-statement: wrap as an IIFE `"(()=>{...})()"`). This is the escape hatch80 for focus order, stacking context, computed values, ARIA state — anything.81 - Add `--console` to any command to capture page console errors alongside the result.82 - Example: `node <PROBE> eval --url <url> --js "getComputedStyle(document.activeElement).outlineWidth"`8384## Gotchas8586- **Programmatic `.click()` lies.** `element.click()` and Playwright's `locator.click()`87 on a forced node bypass hit-testing, so a covered button can look like it "works"88 in script while a real user's pointer is blocked. Trust `hit-test`89 (`elementFromPoint`), not a synthetic click, to decide whether a click truly lands.90- **The local Playwright package is CommonJS.** `import { chromium }` fails under ESM;91 the bundled script already handles this (default import then destructure) — keep that92 pattern if you hand-roll a one-off.93- **Class-substring selectors are treacherous** with hashed CS-module names94 (`_viewport_ab12` vs `_viewportWrapper_cd34` both match `[class*=viewport]`, and95 `querySelector` returns the first in document order — often the wrong one). Prefer96 `find-scroller` to grab the element by COMPUTED `overflowX`.9798## References99100### Worked diagnoses and advanced recipes101102- IF: you want a full worked example (the Carousel "left button dead ≥692px" case),103 more `eval` snippets (focus order, stacking, container-query state), or screenshot104 capture for visual diffing105- THEN: Read `./reference/recipes.md`106- EXAMPLES:107 - "show me how this found the carousel bug"108 - "how do I check focus order / z-index stacking with this"109 - "capture a screenshot at a specific width"110111## Works well with112113Optional collaborators — `browser-microscope` runs standalone and these degrade gracefully if absent.114115- **`browser`** — the journey-walking complement; use `browser` to navigate, `browser-microscope` to dissect why a click won't land or a layout breaks.116- **`browser-review` / `browser-qa`** — reach for the microscope to diagnose the layout failures these surface.117- **`ios-simulator-microscope`** — the real-device counterpart; escalate there when a bug only reproduces in real iOS Safari (toolbar collapse, dynamic-viewport units, touch dynamics) that desktop Playwright can't model.