| 1 | --- |
| 2 | name: "design-visual-verification" |
| 3 | description: "Visually verify that what Amy ships matches what Calculon specified — screenshot diffs at viewport × theme × page matrix using Playwright." |
| 4 | domain: "design, frontend, testing" |
| 5 | confidence: "low" |
| 6 | source: "manual" |
| 7 | tools: [] |
| 8 | --- |
| 9 | |
| 10 | ## Context |
| 11 | |
| 12 | Before approving any frontend PR that changes layouts, typography, components, or theme tokens, Calculon runs a Playwright screenshot pass against the live Hugo preview of the PR branch. Screenshots are diffed against a baseline saved on `main`. Mismatches are compared against the acceptance criteria in `docs/processed/redesign-proposal-2026-05.md`. |
| 13 | |
| 14 | This skill was established 2026-05-25. Bump confidence to **medium** after it has been used successfully on 2+ PRs. |
| 15 | |
| 16 | --- |
| 17 | |
| 18 | ## When to Use |
| 19 | |
| 20 | - Any frontend PR touching: CSS/design tokens, Hugo templates, layout, typography, spacing, color, dark-mode overrides, or the cost-dashboard shortcode. |
| 21 | - After each redesign phase (#170–#175) lands — update baseline immediately so the next phase starts clean. |
| 22 | - **Do not skip** even for "small" changes — a one-line token change can affect every heading on every page. |
| 23 | |
| 24 | --- |
| 25 | |
| 26 | ## Workflow |
| 27 | |
| 28 | ### 1. Checkout the PR branch and start Hugo |
| 29 | |
| 30 | ```bash |
| 31 | git checkout <pr-branch> |
| 32 | hugo server -D --bind 0.0.0.0 |
| 33 | # Hugo serves at http://localhost:1313/SquadScope/ by default |
| 34 | ``` |
| 35 | |
| 36 | Wait for Hugo to print `Web Server is available at …` before running the script. |
| 37 | |
| 38 | ### 2. Run the verification script |
| 39 | |
| 40 | ```bash |
| 41 | # Run with default base URL (http://localhost:1313/SquadScope/) |
| 42 | node scripts/design/verify-visual.mjs |
| 43 | |
| 44 | # Or specify a different base URL |
| 45 | node scripts/design/verify-visual.mjs --base http://localhost:1313/SquadScope/ |
| 46 | ``` |
| 47 | |
| 48 | Screenshots land in `screenshots/design-verification/YYYY-MM-DD/`. |
| 49 | |
| 50 | ### 3. Diff against main baseline |
| 51 | |
| 52 | ```bash |
| 53 | # Checkout main baseline first time (or use git stash approach) |
| 54 | git stash |
| 55 | hugo server -D --bind 0.0.0.0 & |
| 56 | node scripts/design/verify-visual.mjs --out screenshots/baseline/ |
| 57 | git stash pop |
| 58 | ``` |
| 59 | |
| 60 | Compare the two `screenshots/` folders visually (Preview / Finder diff, or use `pixelmatch` CLI). |
| 61 | |
| 62 | For structured regression, run via Playwright snapshot mode: |
| 63 | |
| 64 | ```bash |
| 65 | # Generate/update baseline on main |
| 66 | npx playwright test tests/visual/ --update-snapshots |
| 67 | |
| 68 | # On PR branch — test against baseline |
| 69 | npx playwright test tests/visual/ |
| 70 | ``` |
| 71 | |
| 72 | ### 4. Compare against acceptance criteria |
| 73 | |
| 74 | Open `docs/processed/redesign-proposal-2026-05.md`. Each phase has an **Acceptance Criteria** section. For each viewport × theme screenshot, check: |
| 75 | |
| 76 | - Heading typescale matches spec (`--heading-xl`, `--heading-lg`, etc.) |
| 77 | - Accent color, bg, and text color match palette tokens |
| 78 | - No unexpected overflow or scroll bars at mobile (375px) |
| 79 | - Dark mode: no near-black-on-black contrast failures |
| 80 | - Cost dashboard date element is hidden in screenshots (see Pitfalls) |
| 81 | |
| 82 | ### 5. Comment on the PR |
| 83 | |
| 84 | Post a PR comment containing: |
| 85 | |
| 86 | ```markdown |
| 87 | ## 🎨 Visual Verification — Calculon Review |
| 88 | |
| 89 | | Page | Viewport | Light | Dark | Notes | |
| 90 | |------|----------|-------|------|-------| |
| 91 | | `/` | 375 | ✅ / ⚠️ | ✅ / ⚠️ | … | |
| 92 | | `/` | 768 | | | | |
| 93 | | `/` | 1280 | | | | |
| 94 | | `/` | 1920 | | | | |
| 95 | | `/weekly/2026/w22/` | … | | | | |
| 96 | | `/monthly/2026/05/` | … | | | | |
| 97 | | `/yearly/2026/` | … | | | | |
| 98 | |
| 99 | ### Mismatches against spec |
| 100 | - [ ] List any deviations from `docs/processed/redesign-proposal-2026-05.md` acceptance criteria |
| 101 | |
| 102 | **Decision:** ✅ Approved / 🔄 Changes requested |
| 103 | ``` |
| 104 | |
| 105 | --- |
| 106 | |
| 107 | ## Viewport Matrix |
| 108 | |
| 109 | | Name | Width | Height | |
| 110 | |------|-------|--------| |
| 111 | | mobile | 375 | 667 | |
| 112 | | tablet | 768 | 1024 | |
| 113 | | desktop | 1280 | 800 | |
| 114 | | wide | 1920 | 1080 | |
| 115 | |
| 116 | --- |
| 117 | |
| 118 | ## Theme Matrix |
| 119 | |
| 120 | | Theme | Playwright setting | |
| 121 | |-------|--------------------| |
| 122 | | light | `page.emulateMedia({ colorScheme: 'light' })` | |
| 123 | | dark | `page.emulateMedia({ colorScheme: 'dark' })` | |
| 124 | |
| 125 | --- |
| 126 | |
| 127 | ## Pages Matrix |
| 128 | |
| 129 | | Key | Path | |
| 130 | |-----|------| |
| 131 | | home | `/` | |
| 132 | | latest-weekly | `/weekly/2026/w22/` | |
| 133 | | monthly-rollup | `/monthly/2026/05/` | |
| 134 | | yearly-rollup | `/yearly/2026/` | |
| 135 | |
| 136 | --- |
| 137 | |
| 138 | ## Patterns |
| 139 | |
| 140 | ### Navigate, emulate, screenshot |
| 141 | |
| 142 | ```js |
| 143 | import { chromium } from 'playwright'; |
| 144 | |
| 145 | const browser = await chromium.launch(); |
| 146 | const context = await browser.newContext({ |
| 147 | viewport: { width: 1280, height: 800 }, |
| 148 | colorScheme: 'dark', // or 'light' |
| 149 | }); |
| 150 | const page = await context.newPage(); |
| 151 | |
| 152 | // Wait for fonts to load before screenshotting |
| 153 | await page.goto('http://localhost:1313/SquadScope/'); |
| 154 | await page.waitForLoadState('networkidle'); |
| 155 | |
| 156 | // Hide dynamic elements that create screenshot noise |
| 157 | await page.addStyleTag({ content: '.cost-dashboard__date { visibility: hidden !important; }' }); |
| 158 | |
| 159 | await page.screenshot({ path: 'home-1280-dark.png', fullPage: true }); |
| 160 | await browser.close(); |
| 161 | ``` |
| 162 | |
| 163 | ### Playwright Test snapshot assertion |
| 164 | |
| 165 | ```ts |
| 166 | // tests/visual/home.spec.ts |
| 167 | import { test, expect } from '@playwright/test'; |
| 168 | |
| 169 | test('home — desktop — dark', async ({ page }) => { |
| 170 | await page.emulateMedia({ colorScheme: 'dark' }); |
| 171 | await page.goto('/'); |
| 172 | await page.waitForLoadState('networkidle'); |
| 173 | await page.addStyleTag({ content: '.cost-dashboard__date { visibility: hidden !important; }' }); |
| 174 | await expect(page).toHaveScreenshot('home-desktop-dark.png', { maxDiffPixels: 150 }); |
| 175 | }); |
| 176 | ``` |
| 177 | |
| 178 | ### Update snapshots after intentional design change |
| 179 | |
| 180 | ```bash |
| 181 | npx playwright test tests/visual/ --update-snapshots |
| 182 | git add tests/visual/*.spec.ts-snapshots/ |
| 183 | git commit -m "chore: update visual baselines for phase N" |
| 184 | ``` |
| 185 | |
| 186 | --- |
| 187 | |
| 188 | ## Anti-Patterns |
| 189 | |
| 190 | - **Don't screenshot before `networkidle`** — fonts and theme CSS may not have applied yet, producing false diffs. |
| 191 | - **Don't leave the cost-dashboard date visible** — it changes every run and will always fail snapshot comparison. Hide it with `page.addStyleTag`. |
| 192 | - **Don't set `maxDiffPixels: 0`** — sub-pixel anti-aliasing differences on text will produce constant noise. Start at 150 and tighten per page. |
| 193 | - **Don't compare across OS/CI and local** — browser rendering differs. Always run baseline and comparison in the same environment. |
| 194 | - **Don't approve a PR without running dark mode** — dark-mode bugs are invisible in light screenshots. |
| 195 | |
| 196 | --- |
| 197 | |
| 198 | ## Pitfalls |
| 199 | |
| 200 | 1. **Font rendering noise** — Use `page.waitForLoadState('networkidle')` and consider adding a small `page.waitForTimeout(500)` after load to ensure web fonts have rendered before capturing. |
| 201 | 2. **Dynamic date in cost dashboard** — The `{{< cost-dashboard >}}` shortcode displays a run date. Hide or mock this element via `page.addStyleTag` before screenshotting. |
| 202 | 3. **Run counters / live data** — Any element showing "last crawl N repos" is dynamic. Add `.dynamic-counter { visibility: hidden !important; }` to the style injection. |
| 203 | 4. **Hugo draft pages** — Run `hugo server -D` (the `-D` flag) to include draft content matching the pages matrix. |
| 204 | 5. **Pixel diff thresholds** — Set `maxDiffPixels: 150` as default; tighten to 50 once stable; loosen temporarily for pages with many text nodes if needed. |
| 205 | |
| 206 | --- |
| 207 | |
| 208 | ## Confidence Bump Criteria |
| 209 | |
| 210 | - **low → medium:** Used successfully on 2+ merged frontend PRs. At least one mismatch caught and fixed by the process. |
| 211 | - **medium → high:** Baseline management is automated in CI (not just local); thresholds are stable; no false positives in 5+ consecutive PRs. |