Files
Cass Agent f85dcbe20b style(js): prettier wrap pages assets, e2e tests, and CSS
Application pages JS, service worker, CSS, and the e2e/unit test tree
were rewritten by the same formatter pass as the vendor files: quotes,
wrapping, trailing commas, and import sort. Viewer behavior is
unchanged.

- src/pages_assets/*.js, styles.css, sw.js, virtual-list.test.html
- tests/e2e, tests/html_export, tests/pages-*.mjs, tests/performance
- tests/accessibility, tests/playwright.config.ts

Co-authored-by: Grok <noreply@x.ai>
2026-09-21 10:07:26 -04:00

279 lines
8.9 KiB
TypeScript

import {
expect,
focusFirstKeyboardReachableElement,
gotoFile,
test,
waitForPageReady,
} from "../setup/test-utils";
test.describe("Keyboard Accessibility", () => {
test("can tab through interactive elements", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
// Start tabbing
const focusedElements: string[] = [];
for (let i = 0; i < 20; i++) {
await page.keyboard.press("Tab");
const tagName = await page.evaluate(() => document.activeElement?.tagName || "NONE");
focusedElements.push(tagName);
}
// Should have visited some interactive elements
const interactiveElements = focusedElements.filter((tag) =>
["BUTTON", "INPUT", "A", "DETAILS", "SUMMARY"].includes(tag),
);
expect(interactiveElements.length).toBeGreaterThan(0);
});
test("focus is visible on interactive elements", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
const focused = await focusFirstKeyboardReachableElement(page);
expect(focused).toBe(true);
// Check if focus indicator is visible
const hasFocusStyles = await page.evaluate(() => {
const el = document.activeElement;
if (!el || el === document.body) return false;
const styles = window.getComputedStyle(el);
const outline = styles.outline;
const outlineStyle = styles.outlineStyle;
const outlineWidth = parseFloat(styles.outlineWidth || "0");
const boxShadow = styles.boxShadow;
// Should have visible focus indicator
return (
(outline !== "none" &&
outline !== "0px none" &&
outlineStyle !== "none" &&
outlineWidth > 0) ||
el.matches(":focus-visible") ||
(boxShadow !== "none" && boxShadow.includes("rgb"))
);
});
expect(hasFocusStyles).toBe(true);
});
test("Escape closes modals/popups", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
// Try to open something that might be closeable
const searchInput = page.locator('#search-input, input[type="search"]');
if ((await searchInput.count()) > 0) {
await searchInput.first().focus();
await searchInput.first().fill("test");
// Press Escape
await page.keyboard.press("Escape");
const value = await searchInput.first().inputValue();
const stillFocused = await searchInput
.first()
.evaluate((el) => el === document.activeElement);
expect(value === "" || !stillFocused).toBe(true);
}
});
test("Enter/Space activates buttons", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
// Find a button
const button = page.locator("button").first();
const buttonExists = (await button.count()) > 0;
if (buttonExists) {
await button.focus();
await expect(button).toBeFocused();
await button.evaluate((el) => {
(el as HTMLElement & { __cassActivationCount?: number }).__cassActivationCount = 0;
el.addEventListener(
"click",
() => {
(el as HTMLElement & { __cassActivationCount?: number }).__cassActivationCount =
((el as HTMLElement & { __cassActivationCount?: number }).__cassActivationCount ??
0) + 1;
},
{ once: false },
);
});
// Press Enter
await page.keyboard.press("Enter");
await page.waitForTimeout(200);
const activationCount = await button.evaluate(
(el) => (el as HTMLElement & { __cassActivationCount?: number }).__cassActivationCount ?? 0,
);
expect(activationCount).toBeGreaterThan(0);
}
});
test("arrow keys work in appropriate contexts", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
// This tests that arrow keys don't break anything
await page.keyboard.press("ArrowDown");
await page.keyboard.press("ArrowUp");
await page.keyboard.press("ArrowLeft");
await page.keyboard.press("ArrowRight");
// Page should still be functional
const messageCount = await page.locator(".message").count();
expect(messageCount).toBeGreaterThan(0);
});
});
test.describe("Screen Reader Accessibility", () => {
test("page has proper heading structure", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
// Should have at least one h1
const h1Count = await page.locator("h1").count();
expect(h1Count).toBeGreaterThanOrEqual(1);
// Heading levels should not skip (h1 -> h3 without h2)
const headings = await page.evaluate(() => {
const headingEls = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
return Array.from(headingEls).map((el) => parseInt(el.tagName[1]));
});
if (headings.length > 1) {
for (let i = 1; i < headings.length; i++) {
const diff = headings[i] - headings[i - 1];
// Should not skip more than 1 level going down
expect(diff).toBeLessThanOrEqual(1);
}
}
});
test("images have alt text", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
const images = page.locator("img");
const imageCount = await images.count();
for (let i = 0; i < imageCount; i++) {
const alt = await images.nth(i).getAttribute("alt");
// Should have alt attribute (can be empty for decorative)
expect(alt !== null).toBe(true);
}
});
test("interactive elements have accessible names", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
const buttons = page.locator("button");
const buttonCount = await buttons.count();
for (let i = 0; i < Math.min(buttonCount, 10); i++) {
const button = buttons.nth(i);
// Get accessible name
const accessibleName = await button.evaluate((el) => {
return (
el.getAttribute("aria-label") || el.getAttribute("title") || el.textContent?.trim() || ""
);
});
// Should have some accessible name
expect(accessibleName.length).toBeGreaterThan(0);
}
});
test("main content has proper landmark", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
// Should have main landmark
const main = page.locator('main, [role="main"]');
await expect(main.first()).toBeAttached();
});
});
test.describe("Color Contrast", () => {
test("text has sufficient contrast", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
// Get text and background colors
const colors = await page.evaluate(() => {
const body = document.body;
const style = window.getComputedStyle(body);
return {
textColor: style.color,
bgColor: style.backgroundColor,
};
});
// Basic check that colors are different
expect(colors.textColor).not.toBe(colors.bgColor);
// Log for manual verification
console.log(`Text: ${colors.textColor}, Background: ${colors.bgColor}`);
});
test("both themes have readable text", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
// Check current theme
const theme1Colors = await page.evaluate(() => ({
theme: document.documentElement.getAttribute("data-theme"),
text: window.getComputedStyle(document.body).color,
bg: window.getComputedStyle(document.body).backgroundColor,
}));
// Toggle theme (use force to bypass stability check)
const toggleBtn = page.locator('#theme-toggle, [data-action="toggle-theme"], .theme-toggle');
if ((await toggleBtn.count()) > 0) {
await toggleBtn.first().click({ force: true });
await page.waitForTimeout(300);
const theme2Colors = await page.evaluate(() => ({
theme: document.documentElement.getAttribute("data-theme"),
text: window.getComputedStyle(document.body).color,
bg: window.getComputedStyle(document.body).backgroundColor,
}));
// Both themes should have distinct text and background
expect(theme1Colors.text).not.toBe(theme1Colors.bg);
expect(theme2Colors.text).not.toBe(theme2Colors.bg);
}
});
});