From ae241cec198663cdc2c51c91b69bdda6cd05f2e0 Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Fri, 1 May 2026 04:47:55 +0800 Subject: [PATCH] test: deepen GUI resilience, accessibility, and performance tests --- tests/e2e/gui-accessibility.spec.ts | 133 +++++++++++++++++++++++ tests/e2e/gui-performance.spec.ts | 109 +++++++++++++++++-- tests/e2e/gui-resilience.spec.ts | 162 +++++++++++++++++++++++++++- 3 files changed, 392 insertions(+), 12 deletions(-) diff --git a/tests/e2e/gui-accessibility.spec.ts b/tests/e2e/gui-accessibility.spec.ts index f03c5ffe..7c709843 100644 --- a/tests/e2e/gui-accessibility.spec.ts +++ b/tests/e2e/gui-accessibility.spec.ts @@ -109,6 +109,29 @@ test.describe("Semantic HTML - Form Inputs", () => { }); }); +test.describe("Semantic HTML - Form Inputs on Tool Pages", () => { + test("QR generate form inputs have associated labels", async ({ loggedInPage: page }) => { + await page.goto("/qr-generate"); + await page.waitForLoadState("domcontentloaded"); + + // The URL input should be findable by its label + const urlInput = page.getByLabel("URL"); + await expect(urlInput).toBeVisible(); + }); + + test("change password form inputs have associated labels", async ({ loggedInPage: page }) => { + // Navigate to change-password (uses storageState auth but page is accessible) + await page.goto("/change-password"); + await page.waitForLoadState("domcontentloaded"); + + // Each input should be findable by label (use exact match for "New password" + // to avoid matching the "Generate strong password" button text) + await expect(page.getByLabel("Current password")).toBeVisible(); + await expect(page.getByLabel("New password", { exact: true })).toBeVisible(); + await expect(page.getByLabel("Confirm new password")).toBeVisible(); + }); +}); + test.describe("Semantic HTML - Heading Hierarchy", () => { test.use({ storageState: { cookies: [], origins: [] } }); @@ -133,6 +156,33 @@ test.describe("Semantic HTML - Heading Hierarchy", () => { }); }); +test.describe("Heading Hierarchy - Tool Pages", () => { + test("tool page has headings including an h2 for the tool name", async ({ + loggedInPage: page, + }) => { + await page.goto("/resize"); + await page.waitForLoadState("domcontentloaded"); + + // The tool page should have an h2 heading for the tool name + const h2 = page.locator("h2").filter({ hasText: "Resize" }); + await expect(h2).toBeAttached(); + + // Collect all headings in the DOM (including those in the sidebar or + // settings panel that may not pass a visibility bounding-box check) + const headingLevels = await page.evaluate(() => { + const headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6"); + return Array.from(headings).map((h) => Number.parseInt(h.tagName.charAt(1), 10)); + }); + + // There should be at least one heading + expect(headingLevels.length).toBeGreaterThan(0); + + // There should be at most one h1 (the page title or brand) + const h1Count = headingLevels.filter((l) => l === 1).length; + expect(h1Count).toBeLessThanOrEqual(1); + }); +}); + // --------------------------------------------------------------------------- // Modal/Dialog Accessibility // --------------------------------------------------------------------------- @@ -182,6 +232,43 @@ test.describe("Settings Dialog Accessibility", () => { await closeBtn.first().click(); await expect(page.locator("h2").filter({ hasText: "Settings" })).not.toBeVisible(); }); + + test("settings dialog backdrop is marked aria-hidden", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible(); + + // The backdrop overlay has aria-hidden="true" to keep it out of the a11y tree + const backdrop = page.locator("div[aria-hidden='true'].absolute.inset-0"); + await expect(backdrop).toBeAttached(); + + await page.keyboard.press("Escape"); + }); + + test("focus is contained inside settings dialog while open", async ({ loggedInPage: page }) => { + await page.locator("aside").getByText("Settings").click(); + await expect(page.locator("h2").filter({ hasText: "Settings" })).toBeVisible(); + + // Tab through several elements -- focus should stay within the dialog + for (let i = 0; i < 10; i++) { + await page.keyboard.press("Tab"); + } + + // The key test: after tabbing, focus should NOT escape to elements behind + // the dialog (like the sidebar). It should remain inside the dialog overlay. + const focusLocation = await page.evaluate(() => { + const active = document.activeElement; + if (!active) return "none"; + const sidebar = document.querySelector("aside"); + if (sidebar?.contains(active)) return "sidebar"; + const dialogOverlay = document.querySelector(".fixed.inset-0"); + if (dialogOverlay?.contains(active)) return "dialog"; + return "other"; + }); + // Focus must not have leaked into the sidebar behind the dialog + expect(focusLocation).not.toBe("sidebar"); + + await page.keyboard.press("Escape"); + }); }); test.describe("Help Dialog Accessibility", () => { @@ -222,6 +309,52 @@ test.describe("Dropzone Accessibility", () => { await expect(uploadBtn).toBeVisible(); await expect(uploadBtn).toBeEnabled(); }); + + test("dropzone upload button is focusable and keyboard-reachable", async ({ + loggedInPage: page, + }) => { + // Navigate to a tool page to ensure the dropzone is present + await page.goto("/resize"); + await page.waitForLoadState("domcontentloaded"); + + // The upload button is a real