MAESTRO: add responsive e2e checks

This commit is contained in:
Mariusz Banach
2026-02-18 06:36:31 +01:00
parent 0001ff60bf
commit f296f78030
2 changed files with 131 additions and 31 deletions

View File

@@ -77,7 +77,7 @@ All tasks in this phase are parallelizable [P] since they are independent E2E sp
- [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
- [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__/`
- [x] 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
- [x] 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 (Notes: Added viewport loop with overflow checks, actionability assertions, text overflow guards, and narrow layout card stacking validation.)
## Completion

View File

@@ -1,8 +1,10 @@
import { test, expect } from "@playwright/test";
import { test, expect, type Locator, type Page } from "@playwright/test";
import fs from "fs/promises";
import path from "path";
const headersPath = path.resolve(__dirname, "../../backend/tests/fixtures/sample_headers.txt");
import { AnalyzerPage } from "./pages/analyzer-page";
const headersPath = path.resolve(__dirname, "fixtures/sample-headers.txt");
const viewports = [
{ width: 320, height: 900, label: "320" },
@@ -12,45 +14,143 @@ const viewports = [
{ width: 2560, height: 1200, label: "2560" },
];
test("responsive layout has no horizontal overflow at key breakpoints", async ({ page }) => {
const assertNoHorizontalOverflow = async (page: Page, label: string) => {
const metrics = await page.evaluate(() => {
const docEl = document.documentElement;
const body = document.body;
return {
docScrollWidth: docEl.scrollWidth,
docClientWidth: docEl.clientWidth,
bodyScrollWidth: body.scrollWidth,
bodyClientWidth: body.clientWidth,
};
});
expect(metrics.docScrollWidth, `document overflow at ${label}px`).toBeLessThanOrEqual(
metrics.docClientWidth,
);
expect(metrics.bodyScrollWidth, `body overflow at ${label}px`).toBeLessThanOrEqual(
metrics.bodyClientWidth,
);
};
const assertActionable = async (locator: Locator, label: string, viewportWidth: number) => {
await locator.scrollIntoViewIfNeeded();
await expect(locator, `${label} should be visible`).toBeVisible();
await expect(locator, `${label} should be enabled`).toBeEnabled();
await locator.click({ trial: true });
const box = await locator.boundingBox();
expect(box, `${label} missing layout box`).not.toBeNull();
if (box) {
expect(box.x, `${label} clipped on left`).toBeGreaterThanOrEqual(0);
expect(box.x + box.width, `${label} clipped on right`).toBeLessThanOrEqual(
viewportWidth + 1,
);
}
};
const assertReadableText = async (page: Page, label: string) => {
const overflowIds = await page.evaluate(() => {
const selectors = [
'[data-testid="test-selector"]',
'[data-testid="report-container"]',
'[data-testid="report-export"]',
'[data-testid="report-search-bar"]',
];
const offenders: string[] = [];
for (const selector of selectors) {
const element = document.querySelector(selector) as HTMLElement | null;
if (!element) {
continue;
}
if (element.scrollWidth > element.clientWidth + 1) {
offenders.push(selector);
}
}
const cards = Array.from(
document.querySelectorAll('[data-testid^="test-result-card-"]'),
).slice(0, 3);
cards.forEach((card, index) => {
const element = card as HTMLElement;
if (element.scrollWidth > element.clientWidth + 1) {
offenders.push(`test-result-card-${index}`);
}
});
return offenders;
});
expect(overflowIds, `text overflow detected at ${label}px`).toEqual([]);
};
const assertCardsStacked = async (page: Page, label: string, viewportWidth: number) => {
const cards = page.locator('[data-testid^="test-result-card-"]');
const count = await cards.count();
if (count < 2) {
throw new Error("Need at least 2 report cards to validate stacking.");
}
const firstBox = await cards.nth(0).boundingBox();
const secondBox = await cards.nth(1).boundingBox();
if (!firstBox || !secondBox) {
throw new Error("Unable to read report card layout bounds.");
}
expect(
secondBox.y,
`report cards should stack vertically at ${label}px`,
).toBeGreaterThan(firstBox.y + firstBox.height - 4);
const xDelta = Math.abs(secondBox.x - firstBox.x);
expect(xDelta, `report cards should align at ${label}px`).toBeLessThanOrEqual(12);
expect(firstBox.width, `report cards should use available width at ${label}px`).toBeGreaterThan(
viewportWidth * 0.6,
);
};
test("responsive layout remains usable across key breakpoints", async ({ page }) => {
const headers = await fs.readFile(headersPath, "utf8");
const analyzer = new AnalyzerPage(page);
await page.goto("http://localhost:3100");
await analyzer.goto();
await analyzer.pasteHeaders(headers);
await analyzer.clickAnalyse();
await analyzer.waitForResults();
const headerInput = page.getByRole("textbox", { name: "Header Input" });
await headerInput.fill(headers);
await page.getByRole("button", { name: "Analyse Headers" }).click();
const firstCheckbox = page.locator('[data-testid^="test-checkbox-"]').first();
await expect(firstCheckbox).toBeVisible({ timeout: 30000 });
await expect(page.getByRole("button", { name: "Clear Cache" })).toBeEnabled();
const reportContainer = page.getByTestId("report-container");
await reportContainer.waitFor({ state: "visible", timeout: 30000 });
await expect(reportContainer).toBeVisible();
for (const viewport of viewports) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.waitForTimeout(200);
const metrics = await page.evaluate(() => {
const docEl = document.documentElement;
const body = document.body;
return {
docScrollWidth: docEl.scrollWidth,
docClientWidth: docEl.clientWidth,
bodyScrollWidth: body.scrollWidth,
bodyClientWidth: body.clientWidth,
};
});
await assertNoHorizontalOverflow(page, viewport.label);
await assertReadableText(page, viewport.label);
expect(
metrics.docScrollWidth,
`documentElement overflow at ${viewport.label}px width`,
).toBeLessThanOrEqual(metrics.docClientWidth);
await assertActionable(page.getByRole("textbox", { name: "Header Input" }), "Header input", viewport.width);
await assertActionable(page.getByRole("button", { name: "Clear header input" }), "Clear header input", viewport.width);
await assertActionable(page.getByRole("button", { name: "Analyse Headers" }), "Analyse button", viewport.width);
await assertActionable(page.getByTestId("toggle-resolve"), "DNS toggle", viewport.width);
await assertActionable(page.getByTestId("toggle-decode-all"), "Decode all toggle", viewport.width);
await assertActionable(page.getByTestId("test-search-input"), "Test search input", viewport.width);
await assertActionable(page.getByTestId("select-all-tests"), "Select all tests", viewport.width);
await assertActionable(page.getByTestId("deselect-all-tests"), "Deselect all tests", viewport.width);
await assertActionable(firstCheckbox, "First test checkbox", viewport.width);
await assertActionable(page.getByTestId("report-search-input"), "Report search input", viewport.width);
await assertActionable(page.getByTestId("report-export-json"), "Export JSON", viewport.width);
await assertActionable(page.getByTestId("report-export-html"), "Export HTML", viewport.width);
await assertActionable(page.getByRole("button", { name: "Clear Cache" }), "Clear cache button", viewport.width);
expect(
metrics.bodyScrollWidth,
`body overflow at ${viewport.label}px width`,
).toBeLessThanOrEqual(metrics.bodyClientWidth);
await expect(headerInput).toBeVisible();
await expect(reportContainer).toBeVisible();
if (viewport.width <= 768) {
await assertCardsStacked(page, viewport.label, viewport.width);
}
}
});