design: add Playwright-based visual verification skill (#179)

* chore(squad): log W21 rescue + structural fixes - Merged copilot-directive-rebuild-no-recrawl from inbox to decisions.md - Added learnings: Hugo ignoreFiles + mounts behavior, schedule-event input safety, deploy hydration architecture - Cleared decisions/inbox/ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore(squad): log cleanup session - Merged inbox directive (GitHub Actions PR create enabled) into decisions.md - Cleared .squad/decisions/inbox/ - Created orchestration log (Leela cleanup pass) - Created session log (cleanup session) - Appended cleanup pattern learning to leela/history.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * design: redesign proposal + icon spec + phase issues Add comprehensive visual redesign proposal for SquadScope: - docs/design/redesign-proposal-2026-05.md: Full design proposal with design principles, visual direction, design tokens (colors, typography, spacing, radii, shadows), layout proposals (home, article, rollup pages), component specs (header, footer, metric cards, repo cards, press callout, cost dashboard), and 6-phase migration strategy. - docs/design/icon-spec.md: Site icon specification with radar sweep concept, SVG code for all variants (light, dark, monochrome, simplified), OG image specs, and complete asset checklist. - .squad/decisions/inbox/calculon-redesign-direction.md: Decision record documenting the chosen visual direction and rationale. - .squad/skills/editorial-site-redesign/SKILL.md: Reusable pattern for editorial site redesigns. Related issues: #170 (Phase 1), #171 (Phase 2), #172 (Phase 3), #173 (Phase 4), #174 (Phase 5), #175 (Phase 6), #176 (icon assets), #177 (design review). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * design: add Playwright-based visual verification skill - Add .squad/skills/design-visual-verification/SKILL.md — full skill pattern: when-to-use, workflow, viewport/theme/page matrix, patterns, anti-patterns, pitfalls, confidence bump criteria - Add scripts/design/verify-visual.mjs — standalone capture script, iterates page × viewport × theme, saves to screenshots/design-verification/, prints summary table, writes manifest.json - Add tests/visual/playwright.config.mjs — Playwright config scoped to visual tests: 6 projects (desktop/mobile/wide × light/dark), maxDiffPixels 150 - Add tests/visual/visual.spec.mjs — snapshot specs for home, weekly, monthly, yearly pages with noise-suppression CSS injection - Add docs/design/visual-verification.md — practical doc covering prerequisites, how to run, PR review workflow, baseline update process, known limitations - Update .squad/agents/calculon/charter.md — add visual verification to What I Own and How I Work sections - Update .squad/agents/calculon/history.md — append Playwright pattern learnings under Learnings section - Update .squad/decisions/decisions.md — record tooling decision Part of redesign phases #170–#177. Related: docs/design/redesign-proposal-2026-05.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Juan Manuel Servera committed May 25, 2026 at 19:24 UTC 1703d50d5dc0fa874d6ff80b88fbda57320b6d5e
7 files changed +746 -6
.squad/agents/calculon/charter.md
+1 -6
@@ -12,12 +12,7 @@
12 - Site icon, favicons, social/OG images, logo system
13 - Design proposals for new layouts and components (mockups, specs, SVG primitives)
14 - Design review of frontend PRs against the system
15 -
16 -## How I Work
17 -- Read the audience and the source material first — SquadScope is editorial weekly analysis, not a SaaS dashboard. Design serves reading and scanning.
18 -- Prefer specs and SVG primitives that Amy can implement directly.
19 -- Reference comparable sites (The Verge, Wired, TechCrunch, GitHub-pulse) for editorial conventions, then differentiate intentionally.
20 -- Accessibility is not optional: WCAG AA contrast minimum, motion respects `prefers-reduced-motion`, all interactive elements keyboard-reachable.
15 +- Visual verification of frontend PRs using Playwright (see `.squad/skills/design-visual-verification/SKILL.md`)
16
17 ## Boundaries
18 **I handle:** visual direction, brand assets, design specs, component design, icon design
.squad/agents/calculon/history.md
+13
@@ -24,6 +24,19 @@
24
25 ## Learnings
26
27 +### 2026-05-25: Playwright Visual Verification Pattern
28 +
29 +**Pattern:** Visual verification via Playwright snapshot diffs at viewport × theme × page matrix.
30 +
31 +- **Viewport matrix:** mobile (375), tablet (768), desktop (1280), wide (1920)
32 +- **Theme matrix:** light + dark
33 +- **Pages matrix:** home, latest weekly, monthly rollup, yearly rollup
34 +- **Key gotcha:** Dynamic content (cost dashboard date, run counter) must be hidden via CSS injection (`visibility: hidden !important`) before screenshotting — otherwise every run will produce a false diff.
35 +- **Anti-aliasing noise:** Set `maxDiffPixels: 150` as default; tighten once baselines are stable.
36 +- **Font settle:** Always `waitForLoadState('networkidle')` + 300ms extra before capture.
37 +- **When to bump skill confidence:** After 2+ PRs successfully use it and at least one real mismatch was caught.
38 +- **Files:** `scripts/design/verify-visual.mjs`, `tests/visual/playwright.config.mjs`, `tests/visual/visual.spec.mjs`, `docs/design/visual-verification.md`, `.squad/skills/design-visual-verification/SKILL.md`
39 +
40 ### 2026-05-25: Initial Design Direction
41
42 **Design Principles Established:**
.squad/skills/design-visual-verification/SKILL.md new
+211
@@ -0,0 +1,211 @@
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/design/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/design/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/design/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.
docs/design/visual-verification.md new
+161
@@ -0,0 +1,161 @@
1 +# Visual Verification for Design Review
2 +
3 +> **Why this exists:** SquadScope has a 6-phase editorial redesign underway (issues #170–#177, spec at `docs/design/redesign-proposal-2026-05.md`). Each phase changes layouts, tokens, or typography. This doc explains how Calculon (Designer) catches regressions before they ship.
4 +
5 +---
6 +
7 +## Why We Do This
8 +
9 +The redesign proposal defines explicit acceptance criteria per phase — heading scale, palette tokens, contrast ratios, layout breakpoints. Without screenshots, design review is reading diffs and hoping. Playwright lets us:
10 +
11 +- See the page as a reader would at 4 viewport widths
12 +- Verify light *and* dark mode in the same pass
13 +- Catch regressions automatically once a baseline exists
14 +- Attach evidence screenshots to PR comments
15 +
16 +Related: [`docs/design/redesign-proposal-2026-05.md`](redesign-proposal-2026-05.md)
17 +
18 +---
19 +
20 +## Prerequisites
21 +
22 +1. **Hugo installed** — `hugo version` should return ≥ v0.100
23 +2. **Node.js ≥ 18** — `node --version`
24 +3. **Playwright Chromium** — install once:
25 +
26 +```bash
27 +npx playwright install chromium --with-deps
28 +```
29 +
30 +That's it. No `npm install` needed — run everything via `npx`.
31 +
32 +---
33 +
34 +## How to Run Locally
35 +
36 +### Quick screenshot pass (standalone script)
37 +
38 +```bash
39 +# 1. Start Hugo with drafts
40 +hugo server -D --bind 0.0.0.0
41 +
42 +# 2. In a second terminal, run the capture script
43 +node scripts/design/verify-visual.mjs
44 +
45 +# Screenshots land in:
46 +# screenshots/design-verification/2026-05-25/
47 +```
48 +
49 +Each filename follows the pattern: `{page}-{viewport}-{theme}.png`
50 +Example: `home-desktop-dark.png`, `weekly-w22-mobile-light.png`
51 +
52 +A `manifest.json` is written to the same folder with pass/fail status.
53 +
54 +### Playwright snapshot regression test
55 +
56 +```bash
57 +# 1. Start Hugo
58 +hugo server -D --bind 0.0.0.0
59 +
60 +# 2. Generate baselines (run ONCE on the main branch):
61 +npx playwright test --config tests/visual/playwright.config.mjs --update-snapshots
62 +
63 +# 3. On PR branch — compare against baselines:
64 +npx playwright test --config tests/visual/playwright.config.mjs
65 +```
66 +
67 +Test results appear at `playwright-report/index.html` (open in browser).
68 +Snapshot baselines are saved to `tests/visual/snapshots/`.
69 +
70 +---
71 +
72 +## Matrix Covered
73 +
74 +### Viewports
75 +
76 +| Name | Width | Height |
77 +|------|-------|--------|
78 +| mobile | 375 | 667 |
79 +| tablet | 768 | 1024 |
80 +| desktop | 1280 | 800 |
81 +| wide | 1920 | 1080 |
82 +
83 +### Themes
84 +
85 +| Mode | Playwright setting |
86 +|------|--------------------|
87 +| light | `colorScheme: 'light'` |
88 +| dark | `colorScheme: 'dark'` |
89 +
90 +### Pages
91 +
92 +| Key | URL path |
93 +|-----|----------|
94 +| home | `/` |
95 +| latest-weekly | `/weekly/2026/w22/` |
96 +| monthly-rollup | `/monthly/2026/05/` |
97 +| yearly-rollup | `/yearly/2026/` |
98 +
99 +**Total:** 4 pages × 4 viewports × 2 themes = **32 screenshots per pass**
100 +
101 +---
102 +
103 +## How Calculon Uses This in PR Review
104 +
105 +1. **Checkout the PR branch**, start Hugo.
106 +2. Run `node scripts/design/verify-visual.mjs`.
107 +3. Open the screenshots. Compare to the acceptance criteria table for the relevant redesign phase in `docs/design/redesign-proposal-2026-05.md`.
108 +4. Run `npx playwright test --config tests/visual/playwright.config.mjs` to get a diff count vs. baseline.
109 +5. Post a PR comment (template in `.squad/skills/design-visual-verification/SKILL.md`) with:
110 + - The summary table (✅ / ⚠️ per cell)
111 + - Any mismatches against spec with screenshot attachments
112 + - Approve or request changes
113 +
114 +---
115 +
116 +## Updating Baselines
117 +
118 +Baselines should be updated when a design change is **intentional** — i.e., a redesign phase has been approved and merged to `main`.
119 +
120 +```bash
121 +# After phase N merges to main:
122 +git checkout main && git pull
123 +hugo server -D --bind 0.0.0.0 &
124 +npx playwright test --config tests/visual/playwright.config.mjs --update-snapshots
125 +kill %1 # stop Hugo
126 +git add tests/visual/snapshots/
127 +git commit -m "chore: update visual baselines after phase N merge [skip ci]"
128 +git push
129 +```
130 +
131 +**Never update baselines on a PR branch** — that defeats the purpose of regression testing.
132 +
133 +---
134 +
135 +## Known Limitations
136 +
137 +| Issue | Impact | Workaround |
138 +|-------|--------|------------|
139 +| Cost dashboard run date | Changes every crawl — always fails snapshot diff | Suppressed via `visibility: hidden` in `NOISE_SUPPRESSION_CSS` (in both the script and spec) |
140 +| Dynamic repo counters | Same — live data | Same suppression |
141 +| Web font rendering | Sub-pixel differences between OS/CI | `maxDiffPixels: 150` threshold in Playwright config |
142 +| Hugo draft pages | Pages with `draft: true` won't appear | Run Hugo with `-D` flag |
143 +| Dynamic shortcodes | Any shortcode pulling live data will vary | Identify per-shortcode and add CSS suppression selectors |
144 +| OS rendering differences | macOS vs Linux produce different font metrics | Always run baseline and comparison on the same OS |
145 +
146 +---
147 +
148 +## Files Reference
149 +
150 +| File | Purpose |
151 +|------|---------|
152 +| `scripts/design/verify-visual.mjs` | Standalone capture script — no test framework needed |
153 +| `tests/visual/playwright.config.mjs` | Playwright config for snapshot regression tests |
154 +| `tests/visual/visual.spec.mjs` | Snapshot specs for each page |
155 +| `tests/visual/snapshots/` | Committed baseline screenshots |
156 +| `screenshots/design-verification/` | Ad-hoc capture output (gitignored) |
157 +| `.squad/skills/design-visual-verification/SKILL.md` | Full skill pattern for the team |
158 +
159 +---
160 +
161 +*Established: 2026-05-25 — Calculon (Designer)*
scripts/design/verify-visual.mjs new
+195
@@ -0,0 +1,195 @@
1 +#!/usr/bin/env node
2 +/**
3 + * verify-visual.mjs
4 + * Playwright-based visual verification for SquadScope design changes.
5 + *
6 + * Usage:
7 + * node scripts/design/verify-visual.mjs
8 + * node scripts/design/verify-visual.mjs --base http://localhost:1313/SquadScope/
9 + * node scripts/design/verify-visual.mjs --out screenshots/my-branch/
10 + *
11 + * Prerequisites:
12 + * npx playwright install chromium
13 + *
14 + * Run:
15 + * npx -y playwright install chromium --with-deps 2>/dev/null; node scripts/design/verify-visual.mjs
16 + */
17 +
18 +import { chromium } from 'playwright';
19 +import { mkdir, writeFile } from 'fs/promises';
20 +import { existsSync } from 'fs';
21 +import { join } from 'path';
22 +
23 +// ---------------------------------------------------------------------------
24 +// Config
25 +// ---------------------------------------------------------------------------
26 +
27 +const args = process.argv.slice(2);
28 +
29 +function getArg(name, fallback) {
30 + const idx = args.indexOf(name);
31 + return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback;
32 +}
33 +
34 +const BASE_URL = getArg('--base', 'http://localhost:1313/SquadScope/').replace(/\/$/, '');
35 +const DATE_SLUG = getArg('--date', '2026-05-25');
36 +const OUT_DIR = getArg('--out', join('screenshots', 'design-verification', DATE_SLUG));
37 +
38 +const VIEWPORTS = [
39 + { name: 'mobile', width: 375, height: 667 },
40 + { name: 'tablet', width: 768, height: 1024 },
41 + { name: 'desktop', width: 1280, height: 800 },
42 + { name: 'wide', width: 1920, height: 1080 },
43 +];
44 +
45 +const THEMES = ['light', 'dark'];
46 +
47 +const PAGES = [
48 + { key: 'home', path: '/' },
49 + { key: 'weekly-w22', path: '/weekly/2026/w22/' },
50 + { key: 'monthly-may', path: '/monthly/2026/05/' },
51 + { key: 'yearly-2026', path: '/yearly/2026/' },
52 +];
53 +
54 +/**
55 + * CSS injected into every page before screenshotting.
56 + * Hides dynamic content that changes every run and creates screenshot noise.
57 + */
58 +const NOISE_SUPPRESSION_CSS = `
59 + /* Cost dashboard run date — changes every crawl */
60 + .cost-dashboard__date,
61 + [data-cost-date],
62 + .run-date,
63 + .last-updated { visibility: hidden !important; }
64 +
65 + /* Live run counters */
66 + .dynamic-counter,
67 + .repo-count,
68 + [data-live] { visibility: hidden !important; }
69 +
70 + /* Disable CSS animations / transitions for stable screenshots */
71 + *, *::before, *::after {
72 + animation-duration: 0s !important;
73 + transition-duration: 0s !important;
74 + }
75 +`;
76 +
77 +// ---------------------------------------------------------------------------
78 +// Helpers
79 +// ---------------------------------------------------------------------------
80 +
81 +function pad(n) { return String(n).padStart(3, ' '); }
82 +
83 +async function ensureDir(dir) {
84 + if (!existsSync(dir)) await mkdir(dir, { recursive: true });
85 +}
86 +
87 +async function captureScreenshot(page, url, viewport, theme, outPath) {
88 + await page.setViewportSize({ width: viewport.width, height: viewport.height });
89 + await page.emulateMedia({ colorScheme: theme });
90 +
91 + try {
92 + await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 });
93 + } catch {
94 + // Fallback for Hugo pages that may not fully reach networkidle
95 + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 10000 });
96 + await page.waitForTimeout(800);
97 + }
98 +
99 + await page.addStyleTag({ content: NOISE_SUPPRESSION_CSS });
100 + // Extra settle time for web fonts
101 + await page.waitForTimeout(300);
102 +
103 + await page.screenshot({ path: outPath, fullPage: true });
104 + return true;
105 +}
106 +
107 +// ---------------------------------------------------------------------------
108 +// Main
109 +// ---------------------------------------------------------------------------
110 +
111 +async function main() {
112 + await ensureDir(OUT_DIR);
113 +
114 + const browser = await chromium.launch({ headless: true });
115 +
116 + const results = [];
117 + let passed = 0;
118 + let failed = 0;
119 +
120 + console.log(`\n🎨 SquadScope Visual Verification`);
121 + console.log(` Base URL : ${BASE_URL}`);
122 + console.log(` Output : ${OUT_DIR}`);
123 + console.log(` Matrix : ${PAGES.length} pages × ${VIEWPORTS.length} viewports × ${THEMES.length} themes = ${PAGES.length * VIEWPORTS.length * THEMES.length} screenshots\n`);
124 +
125 + for (const theme of THEMES) {
126 + for (const viewport of VIEWPORTS) {
127 + const context = await browser.newContext({
128 + viewport: { width: viewport.width, height: viewport.height },
129 + colorScheme: theme,
130 + });
131 + const page = await context.newPage();
132 +
133 + for (const pg of PAGES) {
134 + const filename = `${pg.key}-${viewport.name}-${theme}.png`;
135 + const outPath = join(OUT_DIR, filename);
136 + const url = `${BASE_URL}${pg.path}`;
137 +
138 + let status = '✅';
139 + let note = '';
140 +
141 + try {
142 + await captureScreenshot(page, url, viewport, theme, outPath);
143 + passed++;
144 + } catch (err) {
145 + status = '❌';
146 + note = err.message.split('\n')[0].slice(0, 60);
147 + failed++;
148 + }
149 +
150 + results.push({ page: pg.key, viewport: viewport.name, theme, status, note, filename });
151 + process.stdout.write(` ${status} ${filename.padEnd(45)} ${note}\n`);
152 + }
153 +
154 + await context.close();
155 + }
156 + }
157 +
158 + await browser.close();
159 +
160 + // Summary table
161 + console.log('\n─────────────────────────────────────────────────────────────');
162 + console.log(' Page Viewport Light Dark');
163 + console.log('─────────────────────────────────────────────────────────────');
164 +
165 + for (const pg of PAGES) {
166 + for (const vp of VIEWPORTS) {
167 + const light = results.find(r => r.page === pg.key && r.viewport === vp.name && r.theme === 'light');
168 + const dark = results.find(r => r.page === pg.key && r.viewport === vp.name && r.theme === 'dark');
169 + const col1 = `${pg.key}`.padEnd(20);
170 + const col2 = vp.name.padEnd(10);
171 + console.log(` ${col1}${col2} ${light?.status ?? '?'} ${dark?.status ?? '?'}`);
172 + }
173 + }
174 +
175 + console.log('─────────────────────────────────────────────────────────────');
176 + console.log(` Total: ${pad(passed + failed)} screenshots — ${pad(passed)} passed, ${pad(failed)} failed`);
177 + console.log(` Output dir: ${OUT_DIR}\n`);
178 +
179 + // Write a manifest
180 + const manifest = {
181 + generatedAt: DATE_SLUG,
182 + baseUrl: BASE_URL,
183 + outDir: OUT_DIR,
184 + results,
185 + };
186 + await writeFile(join(OUT_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
187 + console.log(` Manifest written to ${join(OUT_DIR, 'manifest.json')}\n`);
188 +
189 + if (failed > 0) process.exit(1);
190 +}
191 +
192 +main().catch(err => {
193 + console.error('Fatal error:', err);
194 + process.exit(1);
195 +});
tests/visual/playwright.config.mjs new
+101
@@ -0,0 +1,101 @@
1 +// @ts-check
2 +/**
3 + * Playwright configuration for SquadScope design visual verification.
4 + *
5 + * This config is scoped to tests/visual/ only — it is NOT the project-wide
6 + * test runner. It runs against a locally running Hugo server.
7 + *
8 + * Usage:
9 + * # Start Hugo first:
10 + * hugo server -D --bind 0.0.0.0
11 + *
12 + * # Generate / update baselines (run once on main branch):
13 + * npx playwright test --config tests/visual/playwright.config.mjs --update-snapshots
14 + *
15 + * # Run comparison (on PR branch):
16 + * npx playwright test --config tests/visual/playwright.config.mjs
17 + */
18 +
19 +import { defineConfig, devices } from '@playwright/test';
20 +
21 +export default defineConfig({
22 + testDir: '.', // relative to this config file: tests/visual/
23 + snapshotDir: 'snapshots',
24 + outputDir: '../../screenshots/playwright-output',
25 +
26 + // Retry once on CI to reduce font-rendering flakiness
27 + retries: process.env.CI ? 1 : 0,
28 +
29 + // Run tests sequentially — Hugo is on localhost, parallelism adds noise
30 + workers: 1,
31 +
32 + use: {
33 + baseURL: process.env.BASE_URL ?? 'http://localhost:1313/SquadScope',
34 + // Wait for network to settle before screenshotting
35 + actionTimeout: 15000,
36 + },
37 +
38 + expect: {
39 + toHaveScreenshot: {
40 + // Allow ~150 pixel diff for anti-aliasing and sub-pixel font rendering
41 + maxDiffPixels: 150,
42 + // Timeout for screenshot comparison
43 + timeout: 10000,
44 + },
45 + },
46 +
47 + projects: [
48 + // Desktop — light
49 + {
50 + name: 'desktop-light',
51 + use: {
52 + ...devices['Desktop Chrome'],
53 + viewport: { width: 1280, height: 800 },
54 + colorScheme: 'light',
55 + },
56 + },
57 + // Desktop — dark
58 + {
59 + name: 'desktop-dark',
60 + use: {
61 + ...devices['Desktop Chrome'],
62 + viewport: { width: 1280, height: 800 },
63 + colorScheme: 'dark',
64 + },
65 + },
66 + // Mobile — light
67 + {
68 + name: 'mobile-light',
69 + use: {
70 + ...devices['iPhone 13'],
71 + colorScheme: 'light',
72 + },
73 + },
74 + // Mobile — dark
75 + {
76 + name: 'mobile-dark',
77 + use: {
78 + ...devices['iPhone 13'],
79 + colorScheme: 'dark',
80 + },
81 + },
82 + // Wide — light (for the wide editorial layout)
83 + {
84 + name: 'wide-light',
85 + use: {
86 + ...devices['Desktop Chrome'],
87 + viewport: { width: 1920, height: 1080 },
88 + colorScheme: 'light',
89 + },
90 + },
91 + // Wide — dark
92 + {
93 + name: 'wide-dark',
94 + use: {
95 + ...devices['Desktop Chrome'],
96 + viewport: { width: 1920, height: 1080 },
97 + colorScheme: 'dark',
98 + },
99 + },
100 + ],
101 +});
tests/visual/visual.spec.mjs new
+64
@@ -0,0 +1,64 @@
1 +/**
2 + * Visual spec for SquadScope — home page
3 + *
4 + * Run with:
5 + * npx playwright test --config tests/visual/playwright.config.mjs
6 + */
7 +
8 +import { test, expect } from '@playwright/test';
9 +
10 +// CSS injected into every page to suppress dynamic / noisy elements
11 +const NOISE_SUPPRESSION_CSS = `
12 + .cost-dashboard__date,
13 + [data-cost-date],
14 + .run-date,
15 + .last-updated { visibility: hidden !important; }
16 + .dynamic-counter, .repo-count, [data-live] { visibility: hidden !important; }
17 + *, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; }
18 +`;
19 +
20 +async function settle(page) {
21 + await page.waitForLoadState('networkidle').catch(() => page.waitForTimeout(1000));
22 + await page.addStyleTag({ content: NOISE_SUPPRESSION_CSS });
23 + await page.waitForTimeout(300); // font settle
24 +}
25 +
26 +// ---------------------------------------------------------------------------
27 +// Home
28 +// ---------------------------------------------------------------------------
29 +
30 +test('home — full page', async ({ page }) => {
31 + await page.goto('/');
32 + await settle(page);
33 + await expect(page).toHaveScreenshot('home.png', { fullPage: true });
34 +});
35 +
36 +// ---------------------------------------------------------------------------
37 +// Latest weekly
38 +// ---------------------------------------------------------------------------
39 +
40 +test('weekly w22 — full page', async ({ page }) => {
41 + await page.goto('/weekly/2026/w22/');
42 + await settle(page);
43 + await expect(page).toHaveScreenshot('weekly-w22.png', { fullPage: true });
44 +});
45 +
46 +// ---------------------------------------------------------------------------
47 +// Monthly rollup
48 +// ---------------------------------------------------------------------------
49 +
50 +test('monthly may 2026 — full page', async ({ page }) => {
51 + await page.goto('/monthly/2026/05/');
52 + await settle(page);
53 + await expect(page).toHaveScreenshot('monthly-may.png', { fullPage: true });
54 +});
55 +
56 +// ---------------------------------------------------------------------------
57 +// Yearly rollup
58 +// ---------------------------------------------------------------------------
59 +
60 +test('yearly 2026 — full page', async ({ page }) => {
61 + await page.goto('/yearly/2026/');
62 + await settle(page);
63 + await expect(page).toHaveScreenshot('yearly-2026.png', { fullPage: true });
64 +});