From 2c79e89e0a23bbac6cfa305fdb36248fdf36d6a1 Mon Sep 17 00:00:00 2001 From: Mariusz Banach Date: Wed, 18 Feb 2026 06:29:48 +0100 Subject: [PATCH] MAESTRO: add visual regression e2e snapshots --- ...header-analyzer-Phase-10-Playwright-E2E.md | 2 +- frontend/e2e/visual-regression.spec.ts | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 frontend/e2e/visual-regression.spec.ts diff --git a/Auto Run Docs/SpecKit-web-header-analyzer-Phase-10-Playwright-E2E.md b/Auto Run Docs/SpecKit-web-header-analyzer-Phase-10-Playwright-E2E.md index 33706e0..fbf4991 100644 --- a/Auto Run Docs/SpecKit-web-header-analyzer-Phase-10-Playwright-E2E.md +++ b/Auto Run Docs/SpecKit-web-header-analyzer-Phase-10-Playwright-E2E.md @@ -75,7 +75,7 @@ All tasks in this phase are parallelizable [P] since they are independent E2E sp - [x] T058 [P] Create `frontend/e2e/report-interaction.spec.ts` — test US4 report features: expand all cards → collapse all → search for a term → verify filtered results → clear search → export JSON → verify downloaded file is valid JSON. Export HTML → verify downloaded file contains styled content - [x] T059 [P] Create `frontend/e2e/browser-cache.spec.ts` — test US5: complete analysis → reload page → verify headers and results restored from cache → click Clear Cache → verify input and report cleared → reload → verify empty state - [x] T060 [P] Create `frontend/e2e/rate-limiting.spec.ts` — test US6 rate limiting flow: submit requests until 429 response → verify CAPTCHA modal appears → solve CAPTCHA → verify bypass token stored → retry original request succeeds. Test that the CAPTCHA modal is keyboard accessible and visually correct -- [ ] T061 [P] Create `frontend/e2e/visual-regression.spec.ts` — screenshot-based visual testing at 4 viewports (320×568, 768×1024, 1280×720, 2560×1080). Capture: landing page (empty state), landing page with headers pasted, progress indicator active, report view with results expanded, hop chain visualisation. Use `expect(page).toHaveScreenshot()` with `animations: 'disabled'` and `mask` for dynamic content (timestamps, elapsed time). Baselines stored in `frontend/e2e/__snapshots__/` +- [x] T061 [P] Create `frontend/e2e/visual-regression.spec.ts` — screenshot-based visual testing at 4 viewports (320×568, 768×1024, 1280×720, 2560×1080). Capture: landing page (empty state), landing page with headers pasted, progress indicator active, report view with results expanded, hop chain visualisation. Use `expect(page).toHaveScreenshot()` with `animations: 'disabled'` and `mask` for dynamic content (timestamps, elapsed time). Baselines stored in `frontend/e2e/__snapshots__/` - [ ] T062 [P] Create `frontend/e2e/accessibility.spec.ts` — WCAG 2.1 AA audit using `@axe-core/playwright`. Run `AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa']).analyze()` on: landing page (empty), landing page with input, report view, CAPTCHA modal (if rate-limited). Assert zero violations. Document any necessary exceptions with justification - [ ] T063 [P] Create `frontend/e2e/responsive.spec.ts` — viewport matrix test at breakpoints 320px, 768px, 1024px, 1440px, 2560px. At each viewport: verify no horizontal scrollbar, all interactive elements visible and clickable, text readable (no overflow/clipping), report cards stack correctly on narrow viewports. Use `page.setViewportSize()` for per-test overrides diff --git a/frontend/e2e/visual-regression.spec.ts b/frontend/e2e/visual-regression.spec.ts new file mode 100644 index 0000000..23193e4 --- /dev/null +++ b/frontend/e2e/visual-regression.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from "@playwright/test"; +import fs from "fs/promises"; +import path from "path"; + +import { AnalyzerPage } from "./pages/analyzer-page"; + +const headersPath = path.resolve(__dirname, "fixtures/sample-headers.txt"); + +const viewports = [ + { width: 320, height: 568, label: "320x568" }, + { width: 768, height: 1024, label: "768x1024" }, + { width: 1280, height: 720, label: "1280x720" }, + { width: 2560, height: 1080, label: "2560x1080" }, +]; + +const baseScreenshotOptions = { + animations: "disabled" as const, + fullPage: true, +}; + +test.describe("visual regression snapshots", () => { + for (const viewport of viewports) { + test(`visual baselines at ${viewport.label}`, async ({ page }) => { + const headers = await fs.readFile(headersPath, "utf8"); + const analyzer = new AnalyzerPage(page); + + await page.setViewportSize({ width: viewport.width, height: viewport.height }); + await analyzer.goto(); + + const headerInput = page.getByRole("textbox", { name: "Header Input" }); + await headerInput.waitFor({ state: "visible" }); + + await page.evaluate(() => window.scrollTo(0, 0)); + await expect(page).toHaveScreenshot( + `landing-empty-${viewport.label}.png`, + baseScreenshotOptions, + ); + + await analyzer.pasteHeaders(headers); + await page.evaluate(() => window.scrollTo(0, 0)); + await expect(page).toHaveScreenshot( + `landing-filled-${viewport.label}.png`, + baseScreenshotOptions, + ); + + await analyzer.clickAnalyse(); + const progressIndicator = page.getByTestId("progress-indicator"); + await expect(progressIndicator).toBeVisible({ timeout: 30000 }); + + await page.waitForFunction(() => { + const node = document.querySelector('[data-testid="progress-percentage"]'); + if (!node) { + return false; + } + const value = Number((node.textContent ?? "").replace("%", "").trim()); + return Number.isFinite(value) && value >= 1; + }); + + await page.evaluate(() => window.scrollTo(0, 0)); + await expect(page).toHaveScreenshot(`progress-${viewport.label}.png`, { + ...baseScreenshotOptions, + mask: [ + page.getByTestId("progress-elapsed"), + page.getByTestId("progress-remaining"), + page.getByTestId("progress-percentage"), + page.getByTestId("progress-current-test"), + ], + }); + + await analyzer.waitForResults(); + + const resultCards = analyzer.getResultCards(); + const totalCards = await resultCards.count(); + const expandCount = Math.min(totalCards, 3); + + for (let index = 0; index < expandCount; index += 1) { + await analyzer.expandCard(index); + } + + await page.waitForTimeout(200); + await page.evaluate(() => window.scrollTo(0, 0)); + await expect(page).toHaveScreenshot( + `report-expanded-${viewport.label}.png`, + baseScreenshotOptions, + ); + + const hopChain = page.getByTestId("hop-chain-visualisation"); + await hopChain.scrollIntoViewIfNeeded(); + await expect(hopChain).toBeVisible(); + await expect(hopChain).toHaveScreenshot(`hop-chain-${viewport.label}.png`, { + animations: "disabled", + }); + }); + } +});