full-stack-screenshot-testing

Installation
SKILL.md

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

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

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: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

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

Installs
1
First Seen
Apr 14, 2026