| name | web-testing |
| description | Mandatory visual testing protocol for all web projects using playwright-cli. Enforces pre-flight site health check and post-build visual verification. Use when building any web UI (Next.js, React, Vue, Angular, HTML/CSS).
|
| user-invocable | false |
| metadata | {"category":"skill"} |
Visual Testing Protocol for Web Projects
PRE-FLIGHT CHECK: Verify the website loads BEFORE you start coding
Before writing ANY new code, verify the existing site works:
cd {{PROJECT_PATH}} && npm run dev &
sleep 3
PORT=$(lsof -nP -iTCP -sTCP:LISTEN | awk '/node/ && /127.0.0.1|localhost/ {print $9}' | sed -E 's/.*:([0-9]+)->?/\1/' | head -1)
[ -z "$PORT" ] && PORT=$(grep -oE 'localhost:[0-9]+' .next/dev/logs/* 2>/dev/null | head -1 | cut -d: -f2)
[ -z "$PORT" ] && PORT=3000
playwright-cli open "http://localhost:$PORT"
playwright-cli snapshot
playwright-cli close
kill %1 2>/dev/null || true
If the page does NOT load or shows errors, FIX THAT FIRST before doing your task. Do not build on a broken foundation.
MANDATORY: You MUST Use playwright-cli To Verify Your Work AFTER Building
THIS IS A HARD REQUIREMENT, NOT A SUGGESTION. Your task is NOT complete until you have run playwright-cli to visually verify your UI renders correctly. Do NOT skip this. Do NOT substitute with curl or npm run build alone.
You MUST execute these exact shell commands after you finish building:
cd {{PROJECT_PATH}} && npm run dev &
sleep 3
PORT=$(lsof -nP -iTCP -sTCP:LISTEN | awk '/node/ && /127.0.0.1|localhost/ {print $9}' | sed -E 's/.*:([0-9]+)->?/\1/' | head -1)
[ -z "$PORT" ] && PORT=$(grep -oE 'localhost:[0-9]+' .next/dev/logs/* 2>/dev/null | head -1 | cut -d: -f2)
[ -z "$PORT" ] && PORT=3000
playwright-cli open "http://localhost:$PORT"
playwright-cli snapshot
playwright-cli screenshot
playwright-cli click <ref-from-snapshot>
playwright-cli close
kill %1 2>/dev/null || true
playwright-cli is installed at /Users/jackjin/.nvm/versions/node/v20.19.5/bin/playwright-cli. It is a real tool. Run it via your Shell tool. If you do not run playwright-cli, your work will be rejected by the verifier.
What to Verify
- Page renders without blank screens or errors
- Components are visible and properly styled
- Forms accept input and show validation errors
- Navigation between pages/steps works
- No console errors (check browser dev tools)
JOURNEY VERIFICATION (the part the postal-checkout run skipped)
After your component renders correctly, walk the user journey from its natural start all the way to your component's output. Not just "my page loads" — "I got here from Step 1 and I can get to Step N."
Snapshots of individual pages are not enough. A flow is only real when a user can traverse it.
Extend tests/e2e/journey.spec.ts every step
This is an append-only spec that grows as the app grows. Each step adds a block for its segment. If the file doesn't exist, create it. Never rewrite prior blocks — extend.
import { test, expect, Page } from '@playwright/test'
export async function completePriorSteps(page: Page, opts: { through: number }) {
if (opts.through >= 1) {
await page.goto('/')
await page.getByRole('button', { name: /start/i }).click()
await page.fill('[name="email"]', 'journey@test.local')
await page.click('button[type="submit"]')
await expect(page).toHaveURL(/\/step-2/)
}
if (opts.through >= 2) {
}
}
test('step 14: pickup slot selection persists to confirmation', async ({ page }) => {
await completePriorSteps(page, { through: 13 })
await page.click('[data-slot="tuesday-10am"]')
await page.click('button:has-text("Continue")')
await expect(page).toHaveURL(/\/review/)
await expect(page.getByText(/tuesday.*10:00/i)).toBeVisible()
})
Run the WHOLE journey.spec.ts before declaring your step done
Not just your new block:
cd {{PROJECT_PATH}} && npm run dev &
sleep 3
npx playwright test tests/e2e/journey.spec.ts
kill %1 2>/dev/null || true
If an earlier block now fails, you broke it. Fix it before moving on. Do not open a new step until journey.spec.ts is green.
What counts as a journey-verifying step
Extend journey.spec.ts whenever your step:
- Adds a new page/route a user can reach
- Adds a form submission that should persist
- Adds navigation between steps of a flow
- Adds a data-dependent render (showing persisted state from a prior step)
You can skip extending it for: pure refactors, style-only changes, non-user-facing config.
Data dependency — do not hardcode your way out of integration
If the journey requires data that should come from an API/DB and the API/DB isn't ready, do not mock it with hardcoded component defaults and call the step done. Either:
- Seed the DB (preferred — this is the real fix), or
- Use
page.route() to intercept the API call in the test and honestly label it as a mock in known_gaps in your handoff
Silently hardcoding data into components is the failure mode that shipped the undemoable postal-checkout app.
Fallback (if playwright-cli is unavailable)
If playwright-cli is not available, fall back to:
npm run build — verifies compilation
curl "http://localhost:${PORT:-3000}" — verifies server responds
- Check for runtime errors in server output
Do NOT skip visual testing. A component that compiles but renders a blank page is a failure.
UI Libraries (MANDATORY)
For any UI work on a web goal, use an existing component library for inputs that carry internal state. Do NOT hand-roll Select, Combobox, DatePicker, Autocomplete, Menu, or any compound input.
Required, in priority order:
shadcn/ui — first choice. Install components via npx shadcn@latest add <component>. Uses Radix primitives under the hood.
@radix-ui/react-* — if shadcn isn't installed and cannot be added (template restriction), use Radix primitives directly and style them.
@headlessui/react — acceptable fallback for Tailwind projects.
Forbidden unless the task explicitly asks for a custom implementation:
- Custom
Select / Combobox / DatePicker / Menu built with raw useState and divs. The v2.1.6 postal-checkout run shipped a 326-line custom Select whose SelectValue subcomponent never showed the selected country because useState was local to the parent but SelectValue read from its own never-updated state. This is the #1 integration bug source and is invisible to unit tests.
- Controlled inputs that
useState their own value while claiming to be "connected to react-hook-form". Pick one: either the parent form owns the value via useController, or the component owns it via useState. Do not do both.
How to decide between shadcn and Radix raw:
- If
components.json exists in the project, shadcn is already installed — use npx shadcn@latest add to pull the component you need.
- If no
components.json, prefer initializing shadcn (npx shadcn@latest init) over hand-rolling — it is 5 minutes and saves hours of Select bugs.
- Exception: if the PROJECT is a design system / UI component library itself, then hand-rolled components are the point of the task and this rule does not apply.
Document your choice in the structured handoff under what_i_built, e.g. what_i_built: "shadcn/ui Select + Combobox for country/state; form state owned by react-hook-form".