# Sprint Retro — Harbormaster v13.0.0a1

**Theme:** Screenshot-diff harness. Pre-req that finally unblocks
the long-pending Tailwind v4 utility-class migration deferred at
v9.0.0a1. Closes a four-version-old infrastructure gap.

## What shipped

### `tests/ui/_screenshot_diff/` harness

A new opt-in pixel-diff suite living alongside the existing v3.0.0a10
browser smoke tests:

```
tests/ui/_screenshot_diff/
  __init__.py
  conftest.py            — surface/theme matrix + bootstrap-mode toggle
  helper.py              — assert_screenshot_matches() + viewport constants
  test_helper_unit.py    — 8 unit tests covering the diff math
  test_screenshots.py    — 10 parametrized browser tests (5 surfaces × 2 themes)
  baseline/              — committed PNG baselines (operator-blessed, not auto-generated)
```

### `assert_screenshot_matches(page, surface, theme)`

- Captures a Playwright PNG at a fixed `1280x720` viewport (matches
  the dashboard's design width and Playwright's default — removes one
  source of variance).
- Compares to `baseline/<surface>__<theme>.png` via
  `Pillow.ImageChops.difference`.
- Counts differing pixels via the diff image's L-channel histogram
  (`grey.histogram()[0] = identical pixels`). Avoids
  `Image.getdata()` which Pillow has scheduled for removal in 14.x.
- 0.5% tolerance — empirically two consecutive identical runs land
  around 0.05% diff (anti-aliasing noise on glyphs); 0.5% leaves a
  10x safety margin while still catching genuine palette / layout
  shifts (which produce multi-percent diffs).
- On failure, persists `<surface>__<theme>__actual.png` and `__diff.png`
  next to the baseline so the operator can eyeball what changed.

### Surface coverage (5 × 2 themes = 10 baselines once bootstrapped)

| Surface              | Route                          |
|----------------------|--------------------------------|
| `dashboard`          | `/`                            |
| `project_detail`     | `/projects/demo-browser`       |
| `fan_out`            | `/tools/fan-out`               |
| `network`            | `/network`                     |
| `dispatcher_trace`   | `/dispatcher/trace`            |

Theme is forced via the same `localStorage['hm-theme']` mechanism the
v12.0.0a7 toggle uses — no new code path under test.

### Bootstrap workflow

Baselines are NOT auto-generated by CI. A human reviews each one so
the suite catches accidental palette drift instead of normalizing it.

```
HM_SCREENSHOT_BOOTSTRAP=1 \
    uv run pytest tests/ui/_screenshot_diff/test_screenshots.py -m browser
```

writes `*__actual.png` + `pytest.skip()`s the assertion. Operator
visually reviews, then `cp` the actuals onto the baseline filenames
to bless. v13.0.0a2 (Tailwind utility migration) will run the
bootstrap once, freeze, then use the harness to verify zero visual
regression as utility classes are swept.

## Quality gates

```
mypy --strict src/harbormaster   →  Success: no issues found in 56 source files
ruff check src tests              →  All checks passed!
pytest -q                         →  1313 passed, 3 skipped in 39.5s
```

Test count delta: 1305 → 1313 (+8 unit tests for the diff math). The
10 browser screenshot tests don't add to the regular count — they
require `-m browser` + chromium + a blessed baseline.

## Dependencies

- `Pillow>=10.0` added to `[dev]` and `[ui-test]` extras. Used only
  by the screenshot-diff harness — the runtime ships unchanged.

## Patterns reused

- Same `pytest.importorskip` chain the v3.0.0a10 browser smoke suite
  uses (Playwright + pytest-playwright + Pillow).
- `pytestmark = pytest.mark.browser` so the heavy tests skip by
  default, like every other browser-driven test in this repo.
- `ui_url` session-scoped fixture from the parent `tests/ui/conftest.py`
  — same harbormaster-ui-on-loopback dance, no duplication.

## What's next (v13.0.0a2)

Tailwind v4 utility-class migration — the deferred work this harness
unblocks. Sweep all templates, replace raw color utilities
(`bg-cyan-700`, `text-gray-400`, etc.) with semantic tokens
(`bg-accent`, `text-muted`, etc.) defined in `tailwind.input.css`.
Authorize an a2/a2.5 split if scope explodes; the harness gives us
explicit go/no-go on whether each template's migration was visually
neutral.

## Quality of life

`tests/ui/_screenshot_diff/` is the cheapest place a future
template-touching PR can prove "no visual regression" without
human review. Extending it = add a row to `_SURFACES` + run
bootstrap once.
