validate-ui
Automates UI smoke-testing for Color Lab to prove functionality through browser actions.
Install
mkdir -p .claude/skills/validate-ui && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/19011" && unzip -o skill.zip -d .claude/skills/validate-ui && rm skill.zipInstalls to .claude/skills/validate-ui
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Drive Color Lab's real UI in a browser and verify a feature area actually works — routes, controls, selectors, and the exact observable that proves each step. Use this whenever the user asks to validate, check, verify, exercise, smoke-test, or "click through" any part of the app, and whenever they name a feature area to validate ("validate scale", "validate groups", "check the preview still works", "does export still work?", "make sure saving a palette works"). Also use before shipping a UI change, when reproducing a reported UI bug, or any time you are about to explore the app in a browser to find out how it works — that exploration is what this skill exists to replace. Do NOT use for `pnpm validate` (typecheck + lint + tests) — that is a different thing entirely.Key capabilities
- →Drive Color Lab UI in a browser
- →Verify feature areas like routes and controls
- →Perform assertions after each action
- →Reproduce UI bugs
- →Validate UI changes before shipping
How it works
The skill drives the Color Lab UI in a browser, executing actions, observing changes, and asserting expected outcomes for specific feature areas.
Inputs & outputs
When to use validate-ui
- →Smoke-test UI changes
- →Verify feature functionality
- →Reproduce UI bugs
About this skill
Validate the Color Lab UI
Drive the real app as a user and prove a feature works. Assert between actions — never chain two on an assumption; the loop below is why.
Read this file, then read exactly one references/<area>.md for the area you're validating. Don't load them all.
The loop
1. act — one command
2. observe — snapshot (or get the specific observable)
3. assert — the thing you expected to change actually changed
4. repeat
You cannot see the page. You are working from a mental model, and the model is wrong the moment you act. So you re-read after every action — not when something looks broken, because it will not look broken.
✓ Done is not a result
agent-browser prints ✓ Done when the command ran. It says nothing about whether it did anything. Every trap in this file is an instance of that:
| Command | Prints | Actually |
|---|---|---|
fill on a slider | ✓ Done | value jumped to the range's midpoint |
type / keyboard type / select on a slider | ✓ Done | nothing at all |
focus on a control in a collapsed panel | ✓ Done | focus went nowhere (inert) — every later press is swallowed |
clicking a [disabled] button | ✓ Done | nothing at all — and the unchanged state reads as a real result |
find role button click --name "Save" | ✓ Done | hit Save palette, not the modal's Save |
| clicking a toggle whose state was persisted open | ✓ Done | you just closed it |
None of these error. All of them produce a green, confident, wrong result. The only defence is to look.
So: never chain two actions on an assumption. After a click that opens something, assert it opened. After a click that toggles something, assert which way it went. Re-snapshot before reusing any @ref — refs die on every re-render.
A covered control is not one of these traps: any click on one errors and names the covering element (✗ covered by <header.fixed.top-0>). That's a signal, not a failure — read it and clear the coverer.
Prefer click @ref over find role … click anyway: find matches by substring, so a name that looks unique often isn't — --name "Save" hits Save palette, and a bare Select matches Select <Name>, Select variant and Select lock at once. A @ref names exactly one.
What the app is
An OKLCH color-scale generator. You pick base colors; it generates a tonal scale (3–20 steps) for each. Everything else — curves, groups, presets, export, contrast tools — feeds or renders that.
The palette IS the URL. Every structural edit rewrites the address bar; back/forward is undo/redo. This is why most assertions are URL assertions: if the URL didn't change, the edit didn't happen.
Routes: / redirects to /p. /p/<slug> is the entire generator. /palettes is the saved list (auth-gated). /about, /privacy, /terms, /oklch-vs-hsl, /custom-color-scales are static.
Boot
The dev server is https://color-lab.localhost (portless proxy) — not localhost:3000.
pnpm dev # if it isn't already up
agent-browser open https://color-lab.localhost/p/Primary-73.0_0.23001_321
agent-browser set viewport 1440 900
agent-browser wait --load networkidle
Two seed palettes, from e2e/__setup__/constants.ts:
- single —
/p/Primary-73.0_0.23001_321 - palette —
/p/Primary-60_0.21_150/Secondary-60_0.139_227/Tertiary-60_0.266_304
Each area file names the seed it wants.
The single seed rewrites itself on load. That is expected. Plain precision rounding, in P3 and sRGB alike — formatOklchUrl (src/utils/color.ts) rounds L to 2 decimals and C to 3:
/p/Primary-73.0_0.23001_321 → /p/Primary-73_0.23_321
^^^^ trailing zero ^^^^^^^ 0.23001 → 0.23
Assert against Primary-73_0.23_321. The palette seed is canonical and stays put. Gamut has nothing to do with it — e2e/basic.spec.ts:56 asserts the rewritten form while forcing P3.
Step zero: read the opening state, don't assume it
appStore persists to localStorage (key color-lab). A pre-paint script in app/layout.tsx stamps some of it onto <html> before React mounts. The starting state is restorable, not fixed.
Persisted: gamut, showSidebar, showPreview, showColorOptionsPanel, showPaletteOptionsPanel, view, colorSpacing, exportFormatType, exportColorFormat.
view, colorSpacing and the two export settings are persisted but are not on <html>. A reload restores whatever you last left — not List view, not Wide spacing, not the default export tabs.
A toggle you click may already be on, so your click turns it off. Read the state first; if it isn't what the area file expects, set it explicitly or clear storage.
The three observables
Don't eyeball screenshots. Screenshots are for showing the human.
1. The URL — the palette state. Colors are path segments (/p/Name-L_C_H); options are short query keys. Per-color overrides ride on the segment after a -, comma-separated (Primary-60_0.21_150-f:1.2,k:400).
| Key | Means | Key | Means | |
|---|---|---|---|---|
i | steps (3–20) | s / o | saturation / apply-to-all | |
m | mode l/d/r | k | locked step | |
v | variant | f | lightness curve | |
c | chroma curve | h | hue shift | |
n / x | min / max lightness | g | group: b/n/s/d |
Defaults are dropped from the URL entirely — so "reset" is asserted as the param disappearing, not as it returning to a value.
2. <html> — the app's chrome state. Global layout flags with no accessible surface, so there is nothing in the tree to read instead:
agent-browser eval "JSON.stringify(document.documentElement.dataset)"
# {"gamut":"srgb","p3Supported":"false","sidebar":"open","preview":"open",
# "colorOptions":"closed","paletteOptions":"closed"}
data-gamut (p3|srgb) · data-sidebar · data-preview · data-color-options · data-palette-options (each open|closed) · data-p3-supported. dataset camelCases them — dataset.colorOptions, not dataset['data-color-options']. Dark mode is class="dark", not a data attribute.
Three different surfaces say "color options" — keep them apart. data-color-options tracks only the global Advanced Options panel (showColorOptionsPanel). The per-color Change color options Collapse has no chrome flag — it's proven open by its inputs being mounted. Steps/variant/lock live under data-palette-options.
Headless Chrome reports p3Supported: "false", so the app opens in gamut: srgb — the environment, not a bug. It does mean you validate the sRGB path by default, which the e2e suite never exercises (it forces P3 on).
3. ARIA — element state. aria-current on the active ColorItem · aria-pressed on group chips · aria-selected on tabs · aria-checked on radios · aria-expanded on Toggle Bottom Bar.
agent-browser snapshot -i # your default read
agent-browser get count '[data-testid=Scale]'
agent-browser get attr @e5 aria-pressed
agent-browser get url
The eval rule
Read state through the accessibility tree, not the DOM. eval has exactly three legitimate uses:
- Reading
<html>'sdata-*chrome state (observable 2 — it has no ARIA surface by design). - Patching
navigator.clipboard(clipboard reads are blocked). setRange— setting a channel slider's value, because agent-browser genuinely cannot (see Sliders).
Everything else — element counts, aria-*, text, tab selection, panel contents — comes from snapshot / get.
Reaching for
evalto prove a control's state —className,getComputedStyle,getBoundingClientRect, a baredata-*— means stop. That control has no accessible state, and that is an accessibility bug. Report it. Class-sniffing hides the defect and trains the next agent to hide it too.
The distinction that matters: eval is banned as a substitute for looking. It is correct as an implementation of an action agent-browser can't perform. setRange is the second; it is not a licence for the first.
eval reuses one JS context, so a bare const x = … throws Identifier 'x' has already been declared on the second call. Wrap every body in an IIFE.
Sliders
Every slider is an <input type="range">, but they come from two libraries that commit on different events. That, and only that, is why they differ.
| Slider | Where | Commits on | Drive with |
|---|---|---|---|
Channel — Lightness, Chroma, Hue | sidebar ColorItem | input | setRange |
| Option — steps, saturation, every curve | Palette Options, Advanced Options, per-color overrides | onChangeEnd | focus + press |
Both land exactly, in one step. Never fill either.
Option sliders → focus + press
HeroUI/react-aria. They commit on onChangeEnd, which only real input fires — a synthetic value change does nothing, so setRange will not move the URL here. e2e drives them this way and explains why at e2e/scale.spec.ts:44-48.
agent-browser focus 'input[name="lightnessCurve"]'
agent-browser press PageDown # 1.3 → 1.2, URL gains ?f=1.2
press takes a key, not a selector — it acts on whatever is focused, so every keyboard step is two commands. Focus persists across presses. One press = one tick: PageUp/PageDown = 0.1 on the curve sliders, 10 on hueShift; arrows = 1 on steps; Home/End = min/max.
Assert the panel is open before you focus. A collapsed panel's content is inert, and an inert input cannot take focus — focus still prints ✓ Done, and every press is silently swallowed. How you assert open depends on the panel: Advanced Options → data-color-options (it flips in lockstep with inert); Palette Options → data-palette-options; a per-color panel → its inputs being present (it unmounts them when closed — see
Content truncated.
When not to use it
- →When performing `pnpm validate` for typecheck, lint, and tests
- →When the user is not asking to validate, check, verify, exercise, smoke-test, or click through the app
- →When the user is not naming a specific feature area to validate
Limitations
- →Cannot see the page, relies on a mental model.
- →Assertions are required after every action to confirm changes.
- →Some UI actions may report success without actual effect if not observed.
How it compares
This skill provides a structured, observable method for UI validation, preventing assumptions and ensuring actual changes are verified, unlike manual clicking without explicit assertions.
Compared to similar skills
validate-ui side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| validate-ui (this skill) | 0 | 12d | Review | Intermediate |
| webapp-testing | 353 | 3mo | Review | Intermediate |
| ui-ux-expert-skill | 91 | 9mo | Review | Advanced |
| accessibility | 42 | 5mo | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
You might also like
webapp-testing
anthropics
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
ui-ux-expert-skill
fercracix33
Technical workflow for implementing accessible React user interfaces with shadcn/ui, Tailwind CSS, and TanStack Query. Includes 6-phase process with mandatory Style Guide compliance, Context7 best practices consultation, Chrome DevTools validation, and WCAG 2.1 AA accessibility standards. Use after Test Agent, Implementer, and Supabase agents complete their work.
accessibility
tech-leads-club
Audit and improve web accessibility following WCAG 2.1 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible".
playwright-browser-automation
lackeyjb
Complete browser automation with Playwright. Auto-detects dev servers, writes clean test scripts to /tmp. Test pages, fill forms, take screenshots, check responsive design, validate UX, test login flows, check links, automate any browser task. Use when user wants to test websites, automate browser interactions, validate web functionality, or perform any browser-based testing.
svelte-expert
Raudbjorn
Expert Svelte/SvelteKit development assistant for building components, utilities, and applications. Use when creating Svelte components, SvelteKit applications, implementing reactive patterns, handling state management, working with stores, transitions, animations, or any Svelte/SvelteKit development task. Includes comprehensive documentation access, code validation with svelte-autofixer, and playground link generation.
browser-daemon
noiv
Persistent browser automation via Playwright daemon. Keep a browser window open and send it commands (navigate, execute JS, inspect console). Perfect for interactive debugging, development, and testing web applications. Use when you need to interact with a browser repeatedly without opening/closing it.