Files
mgeeky-decode-spam-headers/frontend/e2e/responsive.spec.ts
2026-02-18 06:58:01 +01:00

217 lines
7.1 KiB
TypeScript

import { test, expect, type Locator, type Page } 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: 900, label: "320" },
{ width: 768, height: 900, label: "768" },
{ width: 1024, height: 900, label: "1024" },
{ width: 1440, height: 900, label: "1440" },
{ width: 2560, height: 1200, label: "2560" },
];
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,
options: { requireEnabled?: boolean } = {},
) => {
const { requireEnabled = true } = options;
await locator.scrollIntoViewIfNeeded();
await expect(locator, `${label} should be visible`).toBeVisible();
if (requireEnabled) {
await expect(locator, `${label} should be enabled`).toBeEnabled();
}
await locator.click({ trial: true, force: 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 assertActionableIfPresent = async (
locator: Locator,
label: string,
viewportWidth: number,
options: { requireEnabled?: boolean } = {},
) => {
if ((await locator.count()) === 0) {
return;
}
await assertActionable(locator, label, viewportWidth, options);
};
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 ensureTestSelectorOpen = async (page: Page) => {
const testSelector = page.getByTestId("test-selector");
await testSelector.waitFor({ state: "visible" });
await testSelector.evaluate((node) => {
const details = node.querySelector("details");
if (details && !details.open) {
details.open = true;
}
});
};
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) {
return;
}
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 analyzer.goto();
await analyzer.pasteHeaders(headers);
await analyzer.clickAnalyse();
await analyzer.waitForResults();
await ensureTestSelectorOpen(page);
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 expect(reportContainer).toBeVisible();
for (const viewport of viewports) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.waitForTimeout(200);
await ensureTestSelectorOpen(page);
await assertNoHorizontalOverflow(page, viewport.label);
await assertReadableText(page, viewport.label);
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,
{ requireEnabled: false },
);
await assertActionable(
page.getByRole("button", { name: "Analyse Headers" }),
"Analyse button",
viewport.width,
{ requireEnabled: false },
);
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 assertActionableIfPresent(
page.getByTestId("report-search-input"),
"Report search input",
viewport.width,
);
await assertActionableIfPresent(
page.getByTestId("report-export-json"),
"Export JSON",
viewport.width,
);
await assertActionableIfPresent(
page.getByTestId("report-export-html"),
"Export HTML",
viewport.width,
);
await assertActionable(
page.getByRole("button", { name: "Clear Cache" }),
"Clear cache button",
viewport.width,
{ requireEnabled: false },
);
if (viewport.width <= 768) {
await assertCardsStacked(page, viewport.label, viewport.width);
}
}
});