p19-bug-fixes
This commit is contained in:
231
e2e/tests/capabilities/accessibility/contrast.spec.ts
Normal file
231
e2e/tests/capabilities/accessibility/contrast.spec.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import {
|
||||
assertContrast,
|
||||
checkContrast,
|
||||
getComputedColors,
|
||||
WCAG_CONTRAST,
|
||||
} from "../../fixtures/accessibility";
|
||||
import { SELECTORS } from "../../fixtures/selectors";
|
||||
import { expect, test } from "../../fixtures/test";
|
||||
import { THEMES, Theme } from "../../fixtures/themes";
|
||||
|
||||
test.describe("Color Contrast Across Themes", () => {
|
||||
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
|
||||
await gotoApp();
|
||||
await waitForAppReady();
|
||||
});
|
||||
|
||||
for (const theme of THEMES) {
|
||||
test(`hero text has sufficient contrast in ${theme} theme @smoke`, async ({
|
||||
page,
|
||||
setTheme,
|
||||
waitForHero,
|
||||
}) => {
|
||||
// Set theme
|
||||
await setTheme(theme);
|
||||
|
||||
const hero = await waitForHero();
|
||||
|
||||
// Check headline contrast
|
||||
const headline = hero.locator(SELECTORS.hero.headline);
|
||||
await assertContrast(page, headline, WCAG_CONTRAST.largeText);
|
||||
|
||||
// Check summary contrast
|
||||
const summary = hero.locator(SELECTORS.hero.summary);
|
||||
await assertContrast(page, summary, WCAG_CONTRAST.normalText);
|
||||
});
|
||||
|
||||
test(`feed card text has sufficient contrast in ${theme} theme`, async ({
|
||||
page,
|
||||
setTheme,
|
||||
waitForFeed,
|
||||
}) => {
|
||||
// Set theme
|
||||
await setTheme(theme);
|
||||
|
||||
const feed = await waitForFeed();
|
||||
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
|
||||
|
||||
// Check headline contrast
|
||||
const headline = firstArticle.locator("h3");
|
||||
await assertContrast(page, headline, WCAG_CONTRAST.largeText);
|
||||
|
||||
// Check summary contrast
|
||||
const summary = firstArticle.locator(SELECTORS.feed.articleSummary);
|
||||
await assertContrast(page, summary, WCAG_CONTRAST.normalText);
|
||||
});
|
||||
|
||||
test(`modal text has sufficient contrast in ${theme} theme`, async ({
|
||||
page,
|
||||
setTheme,
|
||||
waitForHero,
|
||||
}) => {
|
||||
// Set theme
|
||||
await setTheme(theme);
|
||||
|
||||
// Open modal
|
||||
const hero = await waitForHero();
|
||||
await hero.locator(SELECTORS.hero.readButton).click();
|
||||
|
||||
const modal = page.locator(SELECTORS.summaryModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Check headline contrast
|
||||
const headline = modal.locator(SELECTORS.summaryModal.headline);
|
||||
await assertContrast(page, headline, WCAG_CONTRAST.largeText);
|
||||
|
||||
// Check body text contrast
|
||||
const bodyText = modal.locator(SELECTORS.summaryModal.summaryBody);
|
||||
await assertContrast(page, bodyText, WCAG_CONTRAST.normalText);
|
||||
|
||||
// Check TL;DR list contrast
|
||||
const tldrList = modal.locator(SELECTORS.summaryModal.tldrList);
|
||||
await assertContrast(page, tldrList, WCAG_CONTRAST.normalText);
|
||||
});
|
||||
|
||||
test(`link colors have sufficient contrast in ${theme} theme`, async ({
|
||||
page,
|
||||
setTheme,
|
||||
waitForFeed,
|
||||
}) => {
|
||||
// Set theme
|
||||
await setTheme(theme);
|
||||
|
||||
const feed = await waitForFeed();
|
||||
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
|
||||
|
||||
// Check source link contrast
|
||||
const sourceLink = firstArticle.locator(SELECTORS.feed.articleSource);
|
||||
const hasSourceLink = (await sourceLink.count()) > 0;
|
||||
|
||||
if (hasSourceLink) {
|
||||
await assertContrast(page, sourceLink, WCAG_CONTRAST.normalText);
|
||||
}
|
||||
});
|
||||
|
||||
test(`button text has sufficient contrast in ${theme} theme`, async ({
|
||||
page,
|
||||
setTheme,
|
||||
waitForHero,
|
||||
}) => {
|
||||
// Set theme
|
||||
await setTheme(theme);
|
||||
|
||||
const hero = await waitForHero();
|
||||
|
||||
// Check read button contrast
|
||||
const readButton = hero.locator(SELECTORS.hero.readButton);
|
||||
await assertContrast(page, readButton, WCAG_CONTRAST.normalText);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe("Interactive State Contrast", () => {
|
||||
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
|
||||
await gotoApp();
|
||||
await waitForAppReady();
|
||||
});
|
||||
|
||||
test("hover state maintains sufficient contrast", async ({
|
||||
page,
|
||||
waitForFeed,
|
||||
}) => {
|
||||
const feed = await waitForFeed();
|
||||
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
|
||||
const readButton = firstArticle.locator(SELECTORS.feed.articleReadButton);
|
||||
|
||||
// Get normal state colors
|
||||
const normalColors = await getComputedColors(page, readButton);
|
||||
|
||||
// Hover over button
|
||||
await readButton.hover();
|
||||
await page.waitForTimeout(300); // Wait for transition
|
||||
|
||||
// Get hover state colors
|
||||
const hoverColors = await getComputedColors(page, readButton);
|
||||
|
||||
// Both states should have sufficient contrast
|
||||
const normalRatio = checkContrast(
|
||||
normalColors.color,
|
||||
normalColors.backgroundColor,
|
||||
);
|
||||
const hoverRatio = checkContrast(
|
||||
hoverColors.color,
|
||||
hoverColors.backgroundColor,
|
||||
);
|
||||
|
||||
expect(normalRatio).toBeGreaterThanOrEqual(WCAG_CONTRAST.normalText);
|
||||
expect(hoverRatio).toBeGreaterThanOrEqual(WCAG_CONTRAST.normalText);
|
||||
});
|
||||
|
||||
test("focus state maintains sufficient contrast @smoke", async ({
|
||||
page,
|
||||
waitForFeed,
|
||||
}) => {
|
||||
const feed = await waitForFeed();
|
||||
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
|
||||
const readButton = firstArticle.locator(SELECTORS.feed.articleReadButton);
|
||||
|
||||
// Get normal state colors
|
||||
const normalColors = await getComputedColors(page, readButton);
|
||||
|
||||
// Focus the button
|
||||
await readButton.focus();
|
||||
|
||||
// Get focus state colors
|
||||
const focusColors = await getComputedColors(page, readButton);
|
||||
|
||||
// Both states should have sufficient contrast
|
||||
const normalRatio = checkContrast(
|
||||
normalColors.color,
|
||||
normalColors.backgroundColor,
|
||||
);
|
||||
const focusRatio = checkContrast(
|
||||
focusColors.color,
|
||||
focusColors.backgroundColor,
|
||||
);
|
||||
|
||||
expect(normalRatio).toBeGreaterThanOrEqual(WCAG_CONTRAST.normalText);
|
||||
expect(focusRatio).toBeGreaterThanOrEqual(WCAG_CONTRAST.normalText);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("High Contrast Theme", () => {
|
||||
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
|
||||
await gotoApp();
|
||||
await waitForAppReady();
|
||||
});
|
||||
|
||||
test("contrast theme provides enhanced visibility @smoke", async ({
|
||||
page,
|
||||
setTheme,
|
||||
waitForHero,
|
||||
waitForFeed,
|
||||
}) => {
|
||||
// Set high contrast theme
|
||||
await setTheme("contrast");
|
||||
|
||||
// Check hero
|
||||
const hero = await waitForHero();
|
||||
const headline = hero.locator(SELECTORS.hero.headline);
|
||||
const headlineColors = await getComputedColors(page, headline);
|
||||
const headlineRatio = checkContrast(
|
||||
headlineColors.color,
|
||||
headlineColors.backgroundColor,
|
||||
);
|
||||
|
||||
// High contrast should provide very strong contrast (7:1 or better)
|
||||
expect(headlineRatio).toBeGreaterThanOrEqual(7);
|
||||
|
||||
// Check feed
|
||||
const feed = await waitForFeed();
|
||||
const firstArticle = feed.locator(SELECTORS.feed.articles).first();
|
||||
const articleHeadline = firstArticle.locator("h3");
|
||||
const articleColors = await getComputedColors(page, articleHeadline);
|
||||
const articleRatio = checkContrast(
|
||||
articleColors.color,
|
||||
articleColors.backgroundColor,
|
||||
);
|
||||
|
||||
expect(articleRatio).toBeGreaterThanOrEqual(7);
|
||||
});
|
||||
});
|
||||
130
e2e/tests/capabilities/accessibility/icon-labels.spec.ts
Normal file
130
e2e/tests/capabilities/accessibility/icon-labels.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
getAccessibleName,
|
||||
hasAccessibleName,
|
||||
} from "../../fixtures/accessibility";
|
||||
import { SELECTORS } from "../../fixtures/selectors";
|
||||
import { expect, test } from "../../fixtures/test";
|
||||
|
||||
test.describe("Icon-Only Control Accessible Names @smoke", () => {
|
||||
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
|
||||
await gotoApp();
|
||||
await waitForAppReady();
|
||||
});
|
||||
|
||||
test("share buttons have accessible names", async ({ page, waitForHero }) => {
|
||||
// Open modal to access share buttons
|
||||
const hero = await waitForHero();
|
||||
await hero.locator(SELECTORS.hero.readButton).click();
|
||||
|
||||
const modal = page.locator(SELECTORS.summaryModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Check X share button
|
||||
const shareX = modal.locator(SELECTORS.summaryModal.shareX);
|
||||
await expect(shareX).toHaveAttribute("aria-label", "Share on X");
|
||||
expect(await hasAccessibleName(shareX)).toBe(true);
|
||||
|
||||
// Check WhatsApp share button
|
||||
const shareWhatsApp = modal.locator(SELECTORS.summaryModal.shareWhatsApp);
|
||||
await expect(shareWhatsApp).toHaveAttribute(
|
||||
"aria-label",
|
||||
"Share on WhatsApp",
|
||||
);
|
||||
expect(await hasAccessibleName(shareWhatsApp)).toBe(true);
|
||||
|
||||
// Check LinkedIn share button
|
||||
const shareLinkedIn = modal.locator(SELECTORS.summaryModal.shareLinkedIn);
|
||||
await expect(shareLinkedIn).toHaveAttribute(
|
||||
"aria-label",
|
||||
"Share on LinkedIn",
|
||||
);
|
||||
expect(await hasAccessibleName(shareLinkedIn)).toBe(true);
|
||||
|
||||
// Check copy link button
|
||||
const shareCopy = modal.locator(SELECTORS.summaryModal.shareCopy);
|
||||
await expect(shareCopy).toHaveAttribute("aria-label", "Copy article link");
|
||||
expect(await hasAccessibleName(shareCopy)).toBe(true);
|
||||
});
|
||||
|
||||
test("theme menu button has accessible name", async ({ page }) => {
|
||||
const themeButton = page.locator(SELECTORS.header.themeMenuButton);
|
||||
|
||||
await expect(themeButton).toHaveAttribute("aria-label", "Open theme menu");
|
||||
expect(await hasAccessibleName(themeButton)).toBe(true);
|
||||
});
|
||||
|
||||
test("back to top button has accessible name", async ({ page }) => {
|
||||
// Scroll down to make back-to-top visible
|
||||
await page.evaluate(() => window.scrollTo(0, 500));
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const backToTop = page.locator(SELECTORS.backToTop.root);
|
||||
|
||||
// Button may not be visible yet, but should have accessible name
|
||||
const hasName = await hasAccessibleName(backToTop);
|
||||
expect(hasName).toBe(true);
|
||||
|
||||
const name = await getAccessibleName(page, backToTop);
|
||||
expect(name).toContain("top");
|
||||
});
|
||||
|
||||
test("modal close button has accessible name", async ({
|
||||
page,
|
||||
waitForHero,
|
||||
}) => {
|
||||
// Open modal
|
||||
const hero = await waitForHero();
|
||||
await hero.locator(SELECTORS.hero.readButton).click();
|
||||
|
||||
const modal = page.locator(SELECTORS.summaryModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Check close button
|
||||
const closeButton = modal.locator(SELECTORS.summaryModal.closeButton);
|
||||
expect(await hasAccessibleName(closeButton)).toBe(true);
|
||||
|
||||
const name = await getAccessibleName(page, closeButton);
|
||||
expect(name?.toLowerCase()).toContain("close");
|
||||
});
|
||||
|
||||
test("policy modal close button has accessible name", async ({
|
||||
page,
|
||||
gotoApp,
|
||||
}) => {
|
||||
// Open policy modal
|
||||
await gotoApp({ policy: "terms" });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const modal = page.locator(SELECTORS.policyModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Check close button
|
||||
const closeButton = modal.locator(SELECTORS.policyModal.closeButton);
|
||||
expect(await hasAccessibleName(closeButton)).toBe(true);
|
||||
|
||||
const name = await getAccessibleName(page, closeButton);
|
||||
expect(name?.toLowerCase()).toContain("close");
|
||||
});
|
||||
|
||||
test("all interactive icons have aria-hidden on SVG", async ({
|
||||
page,
|
||||
waitForHero,
|
||||
}) => {
|
||||
// Open modal to access share buttons
|
||||
const hero = await waitForHero();
|
||||
await hero.locator(SELECTORS.hero.readButton).click();
|
||||
|
||||
const modal = page.locator(SELECTORS.summaryModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Check all SVGs in share buttons are aria-hidden
|
||||
const svgs = modal.locator(".share-icon-btn svg");
|
||||
const count = await svgs.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const svg = svgs.nth(i);
|
||||
const ariaHidden = await svg.getAttribute("aria-hidden");
|
||||
expect(ariaHidden).toBe("true");
|
||||
}
|
||||
});
|
||||
});
|
||||
264
e2e/tests/capabilities/accessibility/keyboard.spec.ts
Normal file
264
e2e/tests/capabilities/accessibility/keyboard.spec.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
173
e2e/tests/capabilities/accessibility/modal.spec.ts
Normal file
173
e2e/tests/capabilities/accessibility/modal.spec.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { SELECTORS } from "../../fixtures/selectors";
|
||||
import { expect, test } from "../../fixtures/test";
|
||||
|
||||
test.describe("Policy Modal Accessibility @smoke", () => {
|
||||
test.beforeEach(async ({ gotoApp, waitForAppReady }) => {
|
||||
await gotoApp();
|
||||
await waitForAppReady();
|
||||
});
|
||||
|
||||
test("terms modal opens and has correct ARIA attributes", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Click terms link
|
||||
const termsLink = page.locator(SELECTORS.footer.termsLink);
|
||||
await termsLink.click();
|
||||
|
||||
// Modal should be visible
|
||||
const modal = page.locator(SELECTORS.policyModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Check ARIA attributes
|
||||
await expect(modal).toHaveAttribute("role", "dialog");
|
||||
await expect(modal).toHaveAttribute("aria-modal", "true");
|
||||
|
||||
// Check aria-label
|
||||
const ariaLabel = await modal.getAttribute("aria-label");
|
||||
expect(ariaLabel).toMatch(/Terms|Attribution/);
|
||||
});
|
||||
|
||||
test("attribution modal opens and has correct ARIA attributes", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Click attribution link
|
||||
const attributionLink = page.locator(SELECTORS.footer.attributionLink);
|
||||
await attributionLink.click();
|
||||
|
||||
// Modal should be visible
|
||||
const modal = page.locator(SELECTORS.policyModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Check ARIA attributes
|
||||
await expect(modal).toHaveAttribute("role", "dialog");
|
||||
await expect(modal).toHaveAttribute("aria-modal", "true");
|
||||
});
|
||||
|
||||
test("policy modal closes with escape key @smoke", async ({ page }) => {
|
||||
// Open terms modal
|
||||
const termsLink = page.locator(SELECTORS.footer.termsLink);
|
||||
await termsLink.click();
|
||||
|
||||
const modal = page.locator(SELECTORS.policyModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Press escape
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Modal should be closed
|
||||
await expect(modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("policy modal closes with close button", async ({ page }) => {
|
||||
// Open terms modal
|
||||
const termsLink = page.locator(SELECTORS.footer.termsLink);
|
||||
await termsLink.click();
|
||||
|
||||
const modal = page.locator(SELECTORS.policyModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Click close button
|
||||
const closeButton = modal.locator(SELECTORS.policyModal.closeButton);
|
||||
await closeButton.click();
|
||||
|
||||
// Modal should be closed
|
||||
await expect(modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("policy modal closes with backdrop click", async ({ page }) => {
|
||||
// Open terms modal
|
||||
const termsLink = page.locator(SELECTORS.footer.termsLink);
|
||||
await termsLink.click();
|
||||
|
||||
const modal = page.locator(SELECTORS.policyModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Click backdrop
|
||||
const backdrop = page.locator(".fixed.inset-0.bg-black\\/70").first();
|
||||
await backdrop.click();
|
||||
|
||||
// Modal should be closed
|
||||
await page.waitForTimeout(500);
|
||||
await expect(modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("focus returns to trigger after closing policy modal @smoke", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Open terms modal
|
||||
const termsLink = page.locator(SELECTORS.footer.termsLink);
|
||||
await termsLink.click();
|
||||
|
||||
const modal = page.locator(SELECTORS.policyModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Close modal
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Focus should return to terms link
|
||||
await expect(termsLink).toBeFocused();
|
||||
});
|
||||
|
||||
test("focus is contained within policy modal", async ({ page }) => {
|
||||
// Open terms modal
|
||||
const termsLink = page.locator(SELECTORS.footer.termsLink);
|
||||
await termsLink.click();
|
||||
|
||||
const modal = page.locator(SELECTORS.policyModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Tab multiple times
|
||||
for (let i = 0; i < 10; 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);
|
||||
}
|
||||
});
|
||||
|
||||
test("policy modal content is readable", async ({ page }) => {
|
||||
// Open terms modal
|
||||
const termsLink = page.locator(SELECTORS.footer.termsLink);
|
||||
await termsLink.click();
|
||||
|
||||
const modal = page.locator(SELECTORS.policyModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Check title is present
|
||||
const title = modal.locator(SELECTORS.policyModal.termsTitle);
|
||||
await expect(title).toBeVisible();
|
||||
await expect(title).toContainText("Terms of Use");
|
||||
|
||||
// Check content is present
|
||||
const content = modal.locator(".modal-body-text");
|
||||
const paragraphs = await content.locator("p").count();
|
||||
expect(paragraphs).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("policy modal has correct heading structure", async ({ page }) => {
|
||||
// Open terms modal
|
||||
const termsLink = page.locator(SELECTORS.footer.termsLink);
|
||||
await termsLink.click();
|
||||
|
||||
const modal = page.locator(SELECTORS.policyModal.root);
|
||||
await expect(modal).toBeVisible();
|
||||
|
||||
// Should have h2 heading
|
||||
const heading = modal.locator("h2");
|
||||
await expect(heading).toBeVisible();
|
||||
|
||||
// Check heading level
|
||||
const headingLevel = await heading.evaluate((el) =>
|
||||
el.tagName.toLowerCase(),
|
||||
);
|
||||
expect(headingLevel).toBe("h2");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user