Paste the Broken UI Into Claude Code, Then Make Playwright Prove the Fix
Book: Ship It — the pragmatic startup tech stack Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools Me: xgabriel.com | GitHub "The Ap

Posted on Nov 3 • Edited on Nov 3 • AI-assisted
"The Apply button looks off next to the promo code field." You type that, the agent changes a margin, and the button is still off, now in a slightly different way. The sentence was true, but the agent can't tell which edge you meant, or whether "off" means the height or the position.
Claude Code accepts images. You can drag one into the window, paste it with Ctrl+V ( Alt+V on Windows and WSL), or give it a file path. A screenshot carries all of that information at once. An agent can also report "fixed" before anything has been checked, so this tutorial adds a second half: Playwright takes the screenshot after the fix, and a test decides whether the fix holds.
A promo code row: one input and one button in a flex container. Copy these two files into an empty folder.
<!-- index.html --> <!doctype html> <link rel= "stylesheet" href= "styles.css" > <form class= "promo" > <input id= "code" placeholder= "Promo code" > <button type= "submit" > Apply </button> </form> Enter fullscreen mode Exit fullscreen mode /* styles.css */.promo { display: flex; align-items: baseline; gap: 8px; }.promo input { font-size: 16px; padding: 10px 12px; }.promo button { font-size: 14px; padding: 6px 16px; } Enter fullscreen mode Exit fullscreen mode align-items: baseline lines up the text baselines of the two items ( MDN ). The input has a larger font and more padding, so the button ends up as a shorter box. Its text sits on the input's text line, and its top and bottom edges match nothing.
It looks like the button is floating. You can see that at a glance, and it's awkward to put into words, which is the case where an image beats a description.
Serve the folder with Python's built-in http.server:
python3 -m http.server 3000 Enter fullscreen mode Exit fullscreen mode Step 1: capture the broken state with Playwright You could take the screenshot with your OS tool, and that works. Taking it with Playwright is better, because you'll take the "after" shot the same way: same browser, same viewport, same wait. Then the two images differ only where the CSS changed.
Set up Playwright once with the official installer, which also offers to install the browsers:
npm init playwright@latest Enter fullscreen mode Exit fullscreen mode The installer also writes a sample tests/example.spec.ts that visits playwright.dev. Delete it; you only need your own spec. The generated playwright.config.ts gets replaced by the one in step 2.
Then capture the page. screenshot is a Playwright CLI command that takes a URL and an output file. It accepts --viewport-size, --wait-for-selector and --full-page, and it uses Chromium by default ( CLI source ):
npx playwright screenshot \ --viewport-size "800, 400" \ --wait-for-selector ".promo button" \ http://localhost:3000 before.png Enter fullscreen mode Exit fullscreen mode --wait-for-selector holds the capture until the button exists. Without it, a slow page gives you a blank image of a page that hasn't loaded yet.
Some bugs take one glance to see and a paragraph to describe.
Write the tests before the agent touches anything, so the agent has something to run and you know the tests fail for the right reason. locator.boundingBox() returns x, y, width and height in pixels, or null when the element isn't visible.
// playwright.config.ts import { defineConfig } from ' @playwright/test '; export default defineConfig ({ testDir: './tests ', use: { baseURL: ' http://localhost:3000 ' }, webServer: { command: ' python3 -m http.server 3000 ', url: ' http://localhost:3000 ', reuseExistingServer:! process. env. CI, }, }); Enter fullscreen mode Exit fullscreen mode The webServer block starts the server before the tests and reuses one that's already running when you're not in CI ( web server docs ).
// tests/promo.spec.ts import { test, expect } from ' @playwright/test '; test ( ' Apply lines up with the input ', async ({ page }) => { await page. goto ( ' / ' ); const input = await page. locator ( ' #code ' ). boundingBox (); const btn = await page. locator ( '.promo button ' ). boundingBox (); expect ( input ). not. toBeNull (); expect ( btn ). not. toBeNull (); expect ( btn!. y ). toBeCloseTo ( input!. y, 0 ); expect ( btn!. height ). toBeCloseTo ( input!. height, 0 ); }); test ( ' promo row looks right ', async ({ page }) => { await page. goto ( ' / ' ); await expect ( page. locator ( '.promo ' )). toHaveScreenshot ( ' promo-row.png ' ); }); Enter fullscreen mode Exit fullscreen mode The first test is the red-to-green proof. With baseline, the button's top and height differ from the input's and the test fails. After the fix they match to within half a pixel ( toBeCloseTo with 0 digits), and it passes.
It compares the two boxes to each other, not to a stored image, so it means the same thing on every machine.
The second test is the visual comparison. On its first run there's no baseline yet, so the test fails, reports the missing snapshot and saves the current capture as the baseline. After that, any run that differs fails. Pixels may differ by a color threshold that defaults to 0.2, and maxDiffPixels is unset unless you configure it.
Run the suite once now, against the broken CSS. To make the baseline step explicit, use:
npx playwright test --update-snapshots Enter fullscreen mode Exit fullscreen mode The geometry test fails, which is what you want. The stored baseline is now the broken row, and that is deliberate: when the agent changes the CSS, the screenshot test will show you exactly which pixels moved.
The image shows what looks wrong. It doesn't say what "right" is, how to reproduce the bug, or how the agent should prove the fix. Put those in the prompt with the image. In Claude Code, paste before.png with Ctrl+V or drag it in, and Claude Code labels it [Image #1]. You can also name the file and Claude will read it from disk:
[Image #1] is before.png: the promo row at http://localhost:3000, 800x400, Chromium. Bug: the Apply button is shorter than the input and sits off its edges. Expected: button and input share the same top edge and the same height. Constraints: change styles.css only. Keep the 8px gap. When done: 1. Re-run the same npx playwright screenshot command to after.png and look at it next to before.png. 2. Run npx playwright test and show me the output. Enter fullscreen mode Exit fullscreen mode The last two lines matter most. They change "I fixed it" from a claim into a check the agent has to run. The image path works on the way back too: the agent can open after.png itself and compare it with what you pasted.
The fix you're hoping for is one line. With align-items: stretch (or by deleting the line, since normal behaves as stretch for flex items), an item whose height is auto stretches to fill the row ( MDN ). The button grows to the input's height:
.promo { display: flex; align-items: stretch; gap: 8px; } Enter fullscreen mode Exit fullscreen mode If the agent comes back with a margin-top: 3px on the button instead, that's a fix tuned to one font at one size. The constraints line and the geometry test are how you catch it.
A good fix leaves a diff that covers the button and nothing else.
After the agent's change, the geometry test should pass and the visual test should fail, because the row changed. Open the report:
npx playwright show-report Enter fullscreen mode Exit fullscreen mode Or run npx playwright test --ui. In UI mode you can compare the expected image, the actual image and the diff, and drag a slider across the two. Read the diff like a code review. If the changed pixels are only the button growing to the input's height, the fix did one thing. If the input moved or the gap changed, the agent touched more than you asked, and you send it back with that diff.
Once the diff looks right and the geometry test passes, accept the new state as the baseline and commit the image:
npx playwright test --update-snapshots Enter fullscreen mode Exit fullscreen mode From now on the test guards the fix. The next time an agent, or a person, touches styles.css, the row is checked pixel by pixel.
One caveat to plan for before this runs in CI: rendering varies with the OS, browser version, settings and hardware, so baseline names carry the platform (and the project name, if you define projects), like promo-row-darwin.png. A baseline made on your Mac will fail on a Linux runner. Generate baselines in the same environment CI uses. The geometry test doesn't have this problem, which is one more reason to keep both.
If you run Claude Code inside Hermes IDE, there's a second way to do the paste. I build Hermes, so weigh this with that in mind. By default every agent in Hermes runs in its own real terminal with its own interface, so everything above works unchanged there.
Per session, you can also tick Agent view for Claude. Hermes then renders the conversation itself, with thinking blocks, tool-call cards and diff previews, instead of Claude's terminal interface. You can paste images straight into its composer, and Claude sees the actual pixels ( README ). The terminal stays the default. Agent view runs on the Claude Agent SDK in a small per-session bridge, and it only exists for Claude. Codex and the other agents keep their own terminal interfaces.
For this tutorial, that means step 3 is: screenshot to clipboard, paste into the composer, type the expectations and the two verification lines, send. When the agent edits styles.css, the diff preview shows the CSS change inline, and you can check it against the pixel diff from step 4. Since 2.2.9, Hermes also deletes pasted screenshots older than a week ( release notes ), so captures of a staging page don't pile up on your disk.
Hermes is source-available (BSL 1.1) and runs on macOS, Windows and Linux. Download it or read the source on GitHub.
A visual test that only passes on your laptop isn't done. It's done when CI runs it on every pull request, on the same OS the baselines were made on. Ship It covers that stretch for a small team: hosting, deploys, CI/CD, auth, payments and monitoring, everything between localhost and production. It's the book I'd give someone whose test suite is green locally and nowhere else.