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

@@ -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);
}
}
});