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); }); });