| name | full-stack-screenshot-testing |
| description | Use when reviewing or iterating on frontend UI with real backend data. Use when needing visual screenshots of admin pages, dashboards, or detail views backed by a real database and API — zero mocks. Use when the UI needs design review with realistic data across multiple states (happy path, error, empty, edge cases). Triggers on "screenshot the page", "render with real data", "visual review", "design check with real backend". |
Full-Stack Screenshot Testing
Overview
Render frontend pages with a real backend (real DB, real business logic, zero mocks) and capture screenshots from a single Go test. The Go test is the sole orchestrator — it creates data, starts the API, serves the pre-built frontend, and drives a headless browser.
Core insight: A Go httptest.Server can serve both /api/v1/* (real handlers) and /* (pre-built SPA static files) on the same port. No process coordination, no dev server startup, no MSW.
When to Use
- Reviewing UI design with realistic data at every lifecycle state
- Iterating on frontend changes with fast feedback (build → screenshot → review → fix)
- Verifying that admin pages render correctly after backend/model changes
- Catching visual regressions across order states, error conditions, empty states
When NOT to use:
- Unit testing component logic (use Vitest)
- Testing user interactions/flows (use Playwright E2E)
- API contract testing (use Go integration tests)
Architecture
Go Test (single binary, self-contained)
├── Real DB ← testcontainers or local Postgres
├── Real Engine ← business logic, state machines, etc.
├── Real API Server ← httptest.Server with actual handlers
├── SPA File Server ← serves console/dist/ with index.html fallback
└── Rod Browser ← headless Chromium, screenshots to disk
Prerequisites
go get github.com/go-rod/rod
cd console && pnpm build
Quick Reference
| Step | What | How |
|---|
| 1. Isolate | Build tag | //go:build screenshot |
| 2. Data | Create via real service layer | createFixtureOrder(), advanceOrder() |
| 3. Server | Combined API + SPA | httptest.NewServer(mux) |
| 4. SPA fallback | Serve index.html for unknown paths | spaFileServer("../console/dist") |
| 5. Browser | Rod headless | rod.New().ControlURL(launcher.New().Headless(true).Launch()) |
| 6. Screenshot | Navigate + wait + capture | page.MustNavigate(url).MustWaitStable() |
| 7. Review | Read PNG in Claude | Read tool on screenshots/*.png |
Implementation
SPA File Server (reusable)
The frontend uses client-side routing — /orders/abc must serve index.html, not 404.
func spaFileServer(dir string) http.Handler {
fsys := os.DirFS(dir)
fileServer := http.FileServerFS(fsys)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
if path == "" {
path = "."
}
if _, err := fs.Stat(fsys, path); err != nil {
http.ServeFile(w, r, filepath.Join(dir, "index.html"))
return
}
fileServer.ServeHTTP(w, r)
})
}
Test Structure
func TestScreenshots(t *testing.T) {
db := setupTestDB(t)
engine := setupEngine(t, db)
happy := createOrder(t, db, engine, ...)
mid := createOrder(t, db, engine, ...)
empty := createOrder(t, db, engine, ...)
failed := createOrder(t, db, engine, ...)
apiHandler := buildAPIHandler(db, engine)
distDir, _ := filepath.Abs("../console/dist")
mux := http.NewServeMux()
mux.Handle("/api/v1/", apiHandler)
mux.Handle("/", spaFileServer(distDir))
server := httptest.NewServer(mux)
defer server.Close()
browser := launchBrowser(t)
defer browser.Close()
for _, s := range []struct{ name, url, wait string }{
{"list", server.URL + "/orders", "table tbody tr"},
{"detail-happy", server.URL + "/orders/" + happy.ID, "h2"},
{"detail-failed", server.URL + "/orders/" + failed.ID, "h2"},
} {
t.Run(s.name, func(t *testing.T) {
page := browser.MustPage("")
defer page.Close()
page.MustSetViewport(1440, 900, , )
page.MustNavigate(s.url)
page.MustWaitStable()
page.MustElement(s.wait)
page.MustWaitStable()
buf, _ := page.Screenshot(, )
os.WriteFile(+s.name+, buf, )
})
}
}
Run Command
TEST_DATABASE_URL="postgres://user@localhost:5432/dbname?sslmode=disable" \
go test -tags screenshot -run TestScreenshots ./tests/ -v
USE_TESTCONTAINERS=1 \
go test -tags screenshot -run TestScreenshots ./tests/ -v
Iteration Loop
The power of this setup is the fast iteration cycle:
1. Edit frontend code
2. pnpm build (~1.5s)
3. go test -tags screenshot (~27s, includes 6 screenshots)
4. Review screenshots (Read tool on PNGs)
5. Identify issues → goto 1
Steps 2-4 can be run as a single command. The Go test creates fresh data each run — no stale state.
Common Mistakes
| Mistake | Fix |
|---|
Forgetting pnpm build after frontend changes | Screenshots show stale UI. Always rebuild before testing. |
| Missing SPA fallback | Direct navigation to /orders/abc returns 404. Use spaFileServer. |
| Not creating all lifecycle states | Missing edge cases (empty, error, cancelled). Create one entity per state. |
MustWaitStable() too early | Call AFTER MustElement(selector) to ensure content rendered, not just page loaded. |
| Rod downloads Chromium on first run | First run ~40s (download). Subsequent runs ~27s. Cache at ~/.cache/rod/. |
| Not using build tag | //go:build screenshot prevents slow browser tests from running in normal go test. |
Coverage Strategy
Create one test entity per visual state the UI can show:
Happy path complete → all pipeline stages green
Mid-progress → partial pipeline, action buttons visible
Just created → empty state, minimal data
Error/failed → error banner, retry options
Cancelled → cancellation banner, no pipeline
Multiple items → table with 3+ rows
Single item → table with 1 row
This ensures screenshots cover every visual branch of the UI.