# Browser Microscope

> 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.

- Skill: `orakitine/browser-microscope` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add orakitine/browser-microscope`
- Raw SKILL.md: https://api.skillmd.com/api/skills/orakitine/browser-microscope/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: orakitine (https://skillmd.com/u/orakitine)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/orakitine/browser-microscope

---


# 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

1. **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"`

2. **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"]'`

3. **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'`

4. **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>`

5. **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'`

6. **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';})()"`

7. **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.

