From 03a3995fc5406b369252dfe28d0ad1680b9bab80 Mon Sep 17 00:00:00 2001 From: Mariusz Banach Date: Wed, 18 Feb 2026 06:19:00 +0100 Subject: [PATCH] MAESTRO: add report interaction e2e spec --- ...header-analyzer-Phase-10-Playwright-E2E.md | 2 +- frontend/e2e/report-interaction.spec.ts | 116 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 frontend/e2e/report-interaction.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 6c79514..d2e2b08 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 @@ -72,7 +72,7 @@ All tasks in this phase are parallelizable [P] since they are independent E2E sp - [x] T055 [P] Create `frontend/e2e/paste-and-analyse.spec.ts` — test US1+US3+US4 primary flow: paste sample headers → click Analyse → verify progress indicator appears with test names → verify report renders with severity-coloured cards → expand/collapse a card → verify hop chain visualisation rendered. Also test Ctrl+Enter keyboard shortcut triggers analysis. Assert on UI effects of SSE progress (progress bar increments, test name updates) - [x] T056 [P] Create `frontend/e2e/file-drop.spec.ts` — test US1 file drop flow: dispatch synthetic DataTransfer+drop events on drop zone with `.eml` fixture → verify textarea auto-populates with header content → click Analyse → verify report renders. Test rejection of unsupported file types (e.g., `.pdf`) - [x] T057 [P] Create `frontend/e2e/test-selection.spec.ts` — test US2: open test selector → verify 106+ tests listed → click Deselect All → select 3 specific tests → analyse → verify only 3 results in report. Test search/filter narrows visible tests. Test DNS and decode-all toggle states persist through analysis -- [ ] 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] 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 - [ ] 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 - [ ] 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__/` diff --git a/frontend/e2e/report-interaction.spec.ts b/frontend/e2e/report-interaction.spec.ts new file mode 100644 index 0000000..5c5accf --- /dev/null +++ b/frontend/e2e/report-interaction.spec.ts @@ -0,0 +1,116 @@ +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 parseCount = (value: string | null): { match: number; total: number } => { + if (!value) { + return { match: 0, total: 0 }; + } + const [matchRaw, totalRaw] = value.split("/").map((part) => part.trim()); + const match = Number(matchRaw); + const total = Number(totalRaw); + return { + match: Number.isFinite(match) ? match : 0, + total: Number.isFinite(total) ? total : 0, + }; +}; + +const deriveSearchQuery = (value: string | null): string => { + const trimmed = (value ?? "").trim(); + if (!trimmed) { + return ""; + } + return trimmed.length > 120 ? trimmed.slice(0, 120).trim() : trimmed; +}; + +test("report interactions allow expand/collapse, search, and export", async ({ page }) => { + const headers = await fs.readFile(headersPath, "utf8"); + const analyzer = new AnalyzerPage(page); + + await analyzer.goto(); + await analyzer.pasteHeaders(headers); + await analyzer.clickAnalyse(); + await analyzer.waitForResults(); + + const resultCards = analyzer.getResultCards(); + const totalCount = await resultCards.count(); + expect(totalCount).toBeGreaterThan(0); + + for (let index = 0; index < totalCount; index += 1) { + await analyzer.expandCard(index); + } + + const toggleLocator = page.locator('[data-testid^="test-result-toggle-"]'); + await expect.poll(async () => { + return toggleLocator.evaluateAll((nodes) => { + return nodes.filter((node) => node.getAttribute("aria-expanded") === "true").length; + }); + }).toBe(totalCount); + + for (let index = 0; index < totalCount; index += 1) { + await analyzer.collapseCard(index); + } + + await expect.poll(async () => { + return toggleLocator.evaluateAll((nodes) => { + return nodes.filter((node) => node.getAttribute("aria-expanded") === "true").length; + }); + }).toBe(0); + + const firstTestName = await resultCards.first().locator("button span").first().textContent(); + const query = deriveSearchQuery(firstTestName); + if (!query) { + throw new Error("Unable to derive report search query."); + } + + const searchInput = page.getByTestId("report-search-input"); + const searchCount = page.getByTestId("report-search-count"); + const initialCounts = parseCount(await searchCount.textContent()); + expect(initialCounts.match).toBe(totalCount); + expect(initialCounts.total).toBe(totalCount); + + await searchInput.fill(query); + + await expect.poll(async () => resultCards.count()).toBeGreaterThan(0); + const filteredCount = await resultCards.count(); + const filteredCounts = parseCount(await searchCount.textContent()); + + expect(filteredCounts.total).toBeGreaterThan(0); + expect(filteredCounts.match).toBe(filteredCount); + expect(filteredCounts.total).toBe(totalCount); + if (totalCount > 1) { + expect(filteredCount).toBeLessThan(totalCount); + expect(filteredCounts.match).not.toBe(initialCounts.match); + } + + await page.getByRole("button", { name: "Clear search" }).click(); + + await expect.poll(async () => resultCards.count()).toBe(totalCount); + const clearedCounts = parseCount(await searchCount.textContent()); + expect(clearedCounts.match).toBe(totalCount); + + const jsonDownload = await analyzer.exportJson(); + expect(jsonDownload.suggestedFilename()).toBe("analysis-report.json"); + const jsonPath = await jsonDownload.path(); + if (!jsonPath) { + throw new Error("JSON export did not provide a download path."); + } + const jsonContents = await fs.readFile(jsonPath, "utf8"); + const parsed = JSON.parse(jsonContents) as { metadata?: Record }; + expect(parsed.metadata).toBeTruthy(); + + const htmlDownload = await analyzer.exportHtml(); + expect(htmlDownload.suggestedFilename()).toBe("analysis-report.html"); + const htmlPath = await htmlDownload.path(); + if (!htmlPath) { + throw new Error("HTML export did not provide a download path."); + } + const htmlContents = await fs.readFile(htmlPath, "utf8"); + expect(htmlContents).toContain("Email Header Analysis Report"); + expect(htmlContents).toContain("