| 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 | // About / cost transparency |
| 38 | // --------------------------------------------------------------------------- |
| 39 | |
| 40 | test('about — full page', async ({ page }) => { |
| 41 | await page.goto('/about/'); |
| 42 | await settle(page); |
| 43 | await expect(page).toHaveScreenshot('about.png', { fullPage: true }); |
| 44 | }); |
| 45 | |
| 46 | // --------------------------------------------------------------------------- |
| 47 | // Latest weekly |
| 48 | // --------------------------------------------------------------------------- |
| 49 | |
| 50 | test('weekly w22 — full page', async ({ page }) => { |
| 51 | await page.goto('/weekly/2026/w22/'); |
| 52 | await settle(page); |
| 53 | await expect(page).toHaveScreenshot('weekly-w22.png', { fullPage: true }); |
| 54 | }); |
| 55 | |
| 56 | // --------------------------------------------------------------------------- |
| 57 | // Monthly rollup |
| 58 | // --------------------------------------------------------------------------- |
| 59 | |
| 60 | test('monthly may 2026 — full page', async ({ page }) => { |
| 61 | await page.goto('/monthly/2026/05/'); |
| 62 | await settle(page); |
| 63 | await expect(page).toHaveScreenshot('monthly-may.png', { fullPage: true }); |
| 64 | }); |
| 65 | |
| 66 | // --------------------------------------------------------------------------- |
| 67 | // Yearly rollup |
| 68 | // --------------------------------------------------------------------------- |
| 69 | |
| 70 | test('yearly 2026 — full page', async ({ page }) => { |
| 71 | await page.goto('/yearly/2026/'); |
| 72 | await settle(page); |
| 73 | await expect(page).toHaveScreenshot('yearly-2026.png', { fullPage: true }); |
| 74 | }); |
| 75 | |
| 76 | // --------------------------------------------------------------------------- |
| 77 | // Cover image / fallback visual placement (#358) |
| 78 | // --------------------------------------------------------------------------- |
| 79 | |
| 80 | test('weekly article — cover card renders above content', async ({ page }) => { |
| 81 | await page.goto('/weekly/2026/w22/'); |
| 82 | await settle(page); |
| 83 | const cover = page.locator('.article-cover'); |
| 84 | await expect(cover).toBeVisible(); |
| 85 | // Verify the cover is spatially above the article content |
| 86 | const content = page.locator('.post-content, article .content, .entry-content').first(); |
| 87 | const coverBox = await cover.boundingBox(); |
| 88 | const contentBox = await content.boundingBox(); |
| 89 | if (coverBox && contentBox) { |
| 90 | expect(coverBox.y).toBeLessThan(contentBox.y); |
| 91 | } |
| 92 | await expect(cover).toHaveScreenshot('weekly-cover-card.png'); |
| 93 | }); |
| 94 | |
| 95 | test('weekly article — cover card responsive (mobile)', async ({ page, browserName }, testInfo) => { |
| 96 | // Only run in mobile projects to avoid redundant viewport override |
| 97 | test.skip(!testInfo.project.name.includes('mobile'), 'mobile-only test'); |
| 98 | await page.goto('/weekly/2026/w22/'); |
| 99 | await settle(page); |
| 100 | const cover = page.locator('.article-cover'); |
| 101 | await expect(cover).toBeVisible(); |
| 102 | await expect(cover).toHaveScreenshot('weekly-cover-card-mobile.png'); |
| 103 | }); |