# Full Stack Screenshot Testing

> 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".

- Skill: `sunfmin/full-stack-screenshot-testing` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add sunfmin/full-stack-screenshot-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/sunfmin/full-stack-screenshot-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: sunfmin (https://skillmd.com/u/sunfmin)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/sunfmin/full-stack-screenshot-testing

---


# 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

```bash
# One-time: add rod to Go project
go get github.com/go-rod/rod

# Before each test run: build the frontend
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.

```go
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 {
            // SPA fallback
            http.ServeFile(w, r, filepath.Join(dir, "index.html"))
            return
        }
        fileServer.ServeHTTP(w, r)
    })
}
```

### Test Structure

```go
//go:build screenshot

func TestScreenshots(t *testing.T) {
    // 1. Real DB + business logic
    db := setupTestDB(t)
    engine := setupEngine(t, db)

    // 2. Create entities at EVERY lifecycle state
    happy := createOrder(t, db, engine, ...)    // completed flow
    mid := createOrder(t, db, engine, ...)      // in-progress
    empty := createOrder(t, db, engine, ...)    // just created
    failed := createOrder(t, db, engine, ...)   // error state
    // Advance each to its target state via real transitions

    // 3. Combined server: API + frontend
    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()

    // 4. Screenshot with Rod
    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, 1, false)
            page.MustNavigate(s.url)
            page.MustWaitStable()
            page.MustElement(s.wait)
            page.MustWaitStable()
            buf, _ := page.Screenshot(true, nil)
            os.WriteFile("screenshots/"+s.name+".png", buf, 0o644)
        })
    }
}
```

### Run Command

```bash
# With local Postgres
TEST_DATABASE_URL="postgres://user@localhost:5432/dbname?sslmode=disable" \
  go test -tags screenshot -run TestScreenshots ./tests/ -v

# With testcontainers
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.

