| name | verify-browser |
| description | Verify Construct UI changes live in Chrome via the browser MCP tools — server setup, hidden-tab gotchas and workarounds, SSR assertions, theme/motion testing, dev-only surfaces. Use whenever confirming visual or interactive behavior in this repo (components, overlays, reveals, theming). |
Browser Verification for Construct
Hard-won playbook. The MCP browser tab is usually hidden/backgrounded, and hidden tabs lie to you — half of this file is about not being fooled.
Server setup
- Production build (what CI/users get):
npm run build, then node .output/server/index.mjs & (port 3000). Assert readiness with curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/.
- Dev server (needed for the
<Debug /> panel — it's import.meta.dev-gated — and for HMR while iterating): npm run dev in the background.
- Kill patterns that actually work:
pkill -f '.output/server/index.mjs' and pkill -f 'node_modules/.bin/nuxt'. (pkill -f 'nuxt dev' does NOT match the dev process and fails silently — both servers can then bind :3000 simultaneously and you won't know which one answered.)
- Debug panel visibility persists in localStorage:
localStorage.setItem('construct:debug-panel-visible', 'true') then reload. If the panel is mysteriously missing, check this flag before diagnosing.
Hidden-tab gotchas (and the workarounds)
A backgrounded MCP tab freezes or defers most of what UI code relies on:
| Frozen/deferred | Consequence | Workaround |
|---|
requestAnimationFrame | GSAP/p5/three/detect-scroll never tick | Set pinia.state.value.device.userMotionReduced = true before interacting — every house animation branches to synchronous gsap.set, so end-states are assertable immediately |
| IntersectionObserver | Reveals/observers never fire | Same PRM flag (CSS un-hides Reveal content), or foreground the window (below) |
| CSS transitions | Stuck at frame 0 | Assert the property values via getComputedStyle, not the visual motion |
Programmatic scrollTop | No scroll events dispatched | Dispatch synthetic events explicitly after setting scroll position |
focus() | activeElement moves but focus events defer until window focus | Don't test focus-event side effects hidden; assert document.activeElement only |
| Timers | Clamped (≥1s; up to 1/min under intensive throttling) | Keep setTimeout waits ≥ 300–500ms and don't chain many |
Foregrounding fix when real animation/observer behavior must run: AppleScript — iterate Chrome windows for the localhost tab, set active tab index, activate Chrome, then re-activate iTerm2 after. Only needed when the PRM-flag shortcut can't answer the question.
Interacting
tabs_context_mcp first, create a fresh tab (tabs_create_mcp), navigate. After a navigation error or user-closed tab, re-fetch context — stale tab ids throw.
- Reading
document.cookie in javascript_tool gets the result blocked by the extension — assert cookie effects via document.documentElement.dataset.* or SSR curl instead.
- Synthetic
change/input events work on selects/inputs (el.dispatchEvent(new Event('change', { bubbles: true }))) — but check the option's actual DOM value first (Tweakpane's select uses display labels as values).
- Console:
read_console_messages with a pattern — Vue warnings (readonly, Hydration) are the highest-signal filter. Console history persists across reloads; check timestamps before attributing a warning to your latest change, and clear: true after reading.
- p5 sketches need
PointerEvent, not MouseEvent (p5 v2).
SSR assertions (no browser needed — prefer these when they suffice)
curl -s -H 'Cookie: theme=light' http://localhost:3000/ | grep -o '<html[^>]*>'
curl -s http://localhost:3000/demo/utilities/theme | grep -c 'shiki-themes'
Anything claimed to be "SSR-rendered, no flash" must be proven with curl, not a screenshot.
Theme testing
- Full round-trip: drive the real UI (theme demo page buttons / Debug pane select) and assert
documentElement.dataset.theme plus a token via getComputedStyle(html).getPropertyValue('--background-primary').
- Quick CSS-only check: stamp
document.documentElement.dataset.theme = 'light' directly — valid for token/cascade assertions, but it bypasses the cookie/useHead path, so don't call the system verified from it alone.
- Check contrast-sensitive additions in both themes; a token that only exists at
:root renders wrong in light mode silently.
Cleanup
Kill every server you started (patterns above), note any localStorage flags you set, and never leave both dev and prod servers bound to :3000.