| name | cdp-render-verification |
| description | Prove a web change actually rendered correctly by driving a local headless Chrome over the DevTools Protocol (CDP) — using only Node's built-in WebSocket and fetch, no Puppeteer or Playwright install. Use before declaring ANY visual or interactive web change done: to screenshot desktop + real mobile emulation, read computed styles (the reliable way to check colors/fonts/spacing), detect layout overflow, catch raw code that leaked as visible text, verify a video is actually playing, and drive multi-step flows (click through a form/booking/checkout and read the resulting state). Use when the user says "verify it looks right", "check it on mobile", "did that change actually take", "make sure nothing is broken", or after any CSS/template/JS edit you are about to ship. |
CDP Render Verification
Render-verify web changes in a real browser before calling them done. Drives a
local headless Chrome over the DevTools Protocol with zero dependencies —
Node's global WebSocket/fetch and child_process. Screenshots are for a human
glance; the computed-style and flow probes are the actual proof.
Why this exists: shipping a visual change you only think is right is how you
put broken pixels or raw code in front of a user. A 40-line CDP harness lets you
assert the exact outcome: page returns 200, the intended style changed, nothing
leaked, mobile does not overflow, the flow reaches the right end state.
When to use
- Before shipping any CSS / template / JS / content change to a live page.
- To check a specific property changed (e.g., an accent color is no longer
green) — computed styles are far more reliable than eyeballing a screenshot.
- To catch raw code leaking as text (a
<style>/<script> printed on the
page — see [[everjust-website-customization]] Markup trap).
- To confirm mobile behavior (real device-metrics emulation, overflow).
- To drive a multi-step flow (pick date → time → fill form → review) and read
the composed result — integration bugs a static screenshot cannot show.
- To verify a video autoplays/loops.
When NOT to use
- Logic that a unit/integration test runner covers better.
- When a dedicated preview/browser MCP is connected and sufficient — though CDP
still wins for computed-style assertions and scripted flow-driving.
The harness (copy, adapt)
import { spawn } from 'node:child_process';
import fs from 'node:fs';
const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const port = 9333;
spawn(CHROME, ["--headless=new", `--remote-debugging-port=${port}`, "--disable-gpu",
"--hide-scrollbars", "--no-first-run", "--autoplay-policy=no-user-gesture-required",
"--user-data-dir=/tmp/cdp-"+port], { stdio: "ignore" });
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function ws_url(){ for(let i=0;i<40;i++){ try{ return (await (await fetch(`http://127.0.0.1:${port}/json/version`)).json()).webSocketDebuggerUrl; }catch{ await ();} } }
ws = ( ()); id=; pend= ();
ws.(, { m=.(e.); (m.&&pend.(m.)){ pend.(m.)(m); pend.(m.);} });
(ws.(, r));
=()=> ({ i=++id; pend.(i,res); ws.(.({:i,method,params,...(s?{:s}:{})})); });
{ :{ targetId } } = (, { : });
{ :{ :sess } } = (, { targetId, : });
(, {}, sess);
= x => ( (, { :x, :, : }, sess)).?.?.;
= name => { r= (,{:,:},sess); fs.(name, .(r..,)); };
(, { :, :, :, : }, sess);
(, { : }, sess); ();
.(.( ()));
();
(, { :, :, :, : }, sess);
(, { : }, sess); ();
.(, ());
();
process.();
Run with node harness.mjs (Node 18+; the WebSocket global is built in on 20+/22).
Probes that matter
- Computed style —
getComputedStyle(el).color / backgroundColor. Assert the
value, e.g. !/166a0b|22,\s*106,\s*11/i.test(color) to prove a color is gone.
- Overflow —
documentElement.scrollWidth vs window.innerWidth; unequal on
mobile = a horizontal-scroll bug.
- Code leak —
/<style|<script|</.test(body.innerText.slice(0,4000)).
- Video playing — read
.readyState (4 = enough data), .currentTime (>0 after
a wait = advancing), .paused (false). Needs --autoplay-policy=no-user-gesture-required
and a muted/playsinline video.
- Console errors — subscribe to
Runtime.exceptionThrown messages.
Driving a multi-step flow
Assert the end state, not just that a button exists:
await ev(`document.querySelector('.calendar-day.available').click()`); await sleep(2500);
await ev(`document.querySelector('.slot').click()`); await sleep(800);
await ev(`(function(){var e=document.getElementById('email');e.value='a@b.com';e.dispatchEvent(new Event('input',{bubbles:true}));})()`);
await ev(`document.querySelector('.next').click()`); await sleep(500);
console.log("composed summary:", await ev(`document.getElementById('review').textContent`));
This catches integration bugs (JS not wired, wrong data composed, a step not
advancing) that screenshots never reveal.
Gotchas
- Wait after
Page.navigate (~4s) — assets, fonts, and deferred module JS
settle after DOMContentLoaded.
- Fresh
--user-data-dir per run avoids stale state; unique port per parallel run.
- Reading an image screenshot verifies layout/vibe; for colors/fonts/spacing
trust
getComputedStyle, not the screenshot.
- To force a fresh asset after a server change, navigate again; bumping the URL or
cache-busting the asset avoids a stale bundle.
Related
- [[everjust-website-customization]] — the changes this verifies (and its Markup trap).
- [[web-embed-video-optimization]] — pairs with the video-playback probe.
- [[ui-ux-audit]] / [[admin-dashboard-verification]] — broader UX/feature review.