265 lines
7.0 KiB
TypeScript
265 lines
7.0 KiB
TypeScript
import { hasFocusVisible } from "../../fixtures/accessibility";
|
|
import { SELECTORS } from "../../fixtures/selectors";
|
|
import { expect, test } from "../../fixtures/test";
|
|
|
|
test.describe("Keyboard Navigation @smoke", () => {
|
|
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
|
|
await gotoApp();
|
|
await waitForAppReady();
|
|
});
|
|
|
|
test("skip link is first focusable element", async ({ page }) => {
|
|
// Press Tab to focus skip link
|
|
await page.keyboard.press("Tab");
|
|
|
|
// Skip link should be focused
|
|
const skipLink = page.locator(SELECTORS.skipLink);
|
|
await expect(skipLink).toBeFocused();
|
|
|
|
// Skip link should be visible when focused
|
|
const isVisible = await skipLink.isVisible();
|
|
expect(isVisible).toBe(true);
|
|
});
|
|
|
|
test("skip link navigates to main content", async ({ page }) => {
|
|
// Focus and activate skip link
|
|
await page.keyboard.press("Tab");
|
|
await page.keyboard.press("Enter");
|
|
|
|
// Main content should be focused
|
|
const mainContent = page.locator("#main-content");
|
|
await expect(mainContent).toBeFocused();
|
|
});
|
|
|
|
test("header controls are keyboard accessible @smoke", async ({ page }) => {
|
|
// Tab through header controls
|
|
await page.keyboard.press("Tab"); // Skip link
|
|
await page.keyboard.press("Tab"); // Logo
|
|
await page.keyboard.press("Tab"); // Language select
|
|
|
|
const languageSelect = page.locator(SELECTORS.header.languageSelect);
|
|
await expect(languageSelect).toBeFocused();
|
|
|
|
await page.keyboard.press("Tab"); // Theme menu button
|
|
|
|
const themeButton = page.locator(SELECTORS.header.themeMenuButton);
|
|
await expect(themeButton).toBeFocused();
|
|
});
|
|
|
|
test("theme menu is keyboard operable", async ({ page }) => {
|
|
// Navigate to theme button
|
|
await page.keyboard.press("Tab"); // Skip link
|
|
await page.keyboard.press("Tab"); // Logo
|
|
await page.keyboard.press("Tab"); // Language select
|
|
await page.keyboard.press("Tab"); // Theme button
|
|
|
|
// Open theme menu with Enter
|
|
await page.keyboard.press("Enter");
|
|
|
|
// Menu should be visible
|
|
const menu = page.locator(SELECTORS.themeMenu.root);
|
|
await expect(menu).toBeVisible();
|
|
|
|
// Menu items should be focusable
|
|
const menuItems = menu.locator('[role="menuitem"]');
|
|
const count = await menuItems.count();
|
|
expect(count).toBeGreaterThan(0);
|
|
|
|
// Close menu with Escape
|
|
await page.keyboard.press("Escape");
|
|
await expect(menu).not.toBeVisible();
|
|
});
|
|
|
|
test("hero read button is keyboard accessible @smoke", async ({
|
|
page,
|
|
waitForHero,
|
|
}) => {
|
|
const hero = await waitForHero();
|
|
|
|
// Navigate to hero read button
|
|
// Skip header controls first
|
|
for (let i = 0; i < 4; i++) {
|
|
await page.keyboard.press("Tab");
|
|
}
|
|
|
|
// Hero read button should be focusable
|
|
const readButton = hero.locator(SELECTORS.hero.readButton);
|
|
|
|
// Check if button is in tab order by trying to focus it
|
|
let found = false;
|
|
for (let i = 0; i < 10; i++) {
|
|
const activeElement = await page.evaluate(
|
|
() =>
|
|
document.activeElement?.textContent?.trim() ||
|
|
document.activeElement?.getAttribute("aria-label"),
|
|
);
|
|
|
|
if (activeElement?.includes("Read TL;DR")) {
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
await page.keyboard.press("Tab");
|
|
}
|
|
|
|
expect(found).toBe(true);
|
|
});
|
|
|
|
test("feed articles are keyboard navigable", async ({
|
|
page,
|
|
waitForFeed,
|
|
}) => {
|
|
const feed = await waitForFeed();
|
|
|
|
// Get first article
|
|
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
|
|
|
|
// Source link should be keyboard accessible
|
|
const sourceLink = firstArticle.locator(SELECTORS.feed.articleSource);
|
|
const hasSourceLink = (await sourceLink.count()) > 0;
|
|
|
|
if (hasSourceLink) {
|
|
// Tab to source link
|
|
let attempts = 0;
|
|
let sourceLinkFocused = false;
|
|
|
|
while (attempts < 20 && !sourceLinkFocused) {
|
|
await page.keyboard.press("Tab");
|
|
const href = await page.evaluate(() =>
|
|
document.activeElement?.getAttribute("href"),
|
|
);
|
|
|
|
if (href && href.startsWith("http")) {
|
|
sourceLinkFocused = true;
|
|
}
|
|
|
|
attempts++;
|
|
}
|
|
|
|
expect(sourceLinkFocused).toBe(true);
|
|
}
|
|
|
|
// Read button should be keyboard accessible
|
|
const readButton = firstArticle.locator(SELECTORS.feed.articleReadButton);
|
|
|
|
let attempts = 0;
|
|
let readButtonFocused = false;
|
|
|
|
while (attempts < 30 && !readButtonFocused) {
|
|
await page.keyboard.press("Tab");
|
|
const text = await page.evaluate(() =>
|
|
document.activeElement?.textContent?.trim(),
|
|
);
|
|
|
|
if (text === "Read TL;DR") {
|
|
readButtonFocused = true;
|
|
}
|
|
|
|
attempts++;
|
|
}
|
|
|
|
expect(readButtonFocused).toBe(true);
|
|
});
|
|
|
|
test("focus-visible is shown on interactive elements", async ({
|
|
page,
|
|
waitForHero,
|
|
}) => {
|
|
const hero = await waitForHero();
|
|
|
|
// Navigate to hero read button
|
|
for (let i = 0; i < 4; i++) {
|
|
await page.keyboard.press("Tab");
|
|
}
|
|
|
|
// Find focused element
|
|
const focusedElement = page.locator(":focus");
|
|
|
|
// Check that focused element has visible focus indicator
|
|
const hasVisibleFocus = await hasFocusVisible(page, focusedElement);
|
|
expect(hasVisibleFocus).toBe(true);
|
|
});
|
|
|
|
test("footer links are keyboard accessible @smoke", async ({ page }) => {
|
|
// Navigate to footer
|
|
const footer = page.locator(SELECTORS.footer.root);
|
|
await footer.scrollIntoViewIfNeeded();
|
|
|
|
// Tab through footer links
|
|
let foundFooterLink = false;
|
|
let attempts = 0;
|
|
|
|
while (attempts < 50 && !foundFooterLink) {
|
|
await page.keyboard.press("Tab");
|
|
|
|
const activeElement = await page.evaluate(() => document.activeElement);
|
|
|
|
// Check if we're in footer
|
|
const isInFooter = await page.evaluate(() => {
|
|
const active = document.activeElement;
|
|
const footer = document.querySelector("footer");
|
|
return footer?.contains(active);
|
|
});
|
|
|
|
if (isInFooter) {
|
|
foundFooterLink = true;
|
|
}
|
|
|
|
attempts++;
|
|
}
|
|
|
|
expect(foundFooterLink).toBe(true);
|
|
});
|
|
});
|
|
|
|
test.describe("Focus Management", () => {
|
|
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
|
|
await gotoApp();
|
|
await waitForAppReady();
|
|
});
|
|
|
|
test("focus moves to modal when opened @smoke", async ({
|
|
page,
|
|
waitForHero,
|
|
}) => {
|
|
const hero = await waitForHero();
|
|
|
|
// Click read button
|
|
await hero.locator(SELECTORS.hero.readButton).click();
|
|
|
|
// Modal should be visible
|
|
const modal = page.locator(SELECTORS.summaryModal.root);
|
|
await expect(modal).toBeVisible();
|
|
|
|
// Focus should be inside modal
|
|
const isFocusInModal = await page.evaluate(() => {
|
|
const modal = document.querySelector('[role="dialog"]');
|
|
const active = document.activeElement;
|
|
return modal?.contains(active);
|
|
});
|
|
|
|
expect(isFocusInModal).toBe(true);
|
|
});
|
|
|
|
test("focus is trapped within modal", async ({ page, waitForHero }) => {
|
|
const hero = await waitForHero();
|
|
|
|
// Open modal
|
|
await hero.locator(SELECTORS.hero.readButton).click();
|
|
|
|
// Tab multiple times
|
|
for (let i = 0; i < 20; i++) {
|
|
await page.keyboard.press("Tab");
|
|
|
|
// Check focus is still in modal
|
|
const isInModal = await page.evaluate(() => {
|
|
const modal = document.querySelector('[role="dialog"]');
|
|
const active = document.activeElement;
|
|
return modal?.contains(active);
|
|
});
|
|
|
|
expect(isInModal).toBe(true);
|
|
}
|
|
});
|
|
});
|