Loading...
Loading...
Compare original and translation side by side
// tests/accessibility/homepage.a11y.test.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Homepage Accessibility', () => {
test('should not have any automatically detectable WCAG A or AA violations', async ({
page,
}) => {
await page.goto('/');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('navigation should be accessible', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.include('nav')
.analyze();
expect(results.violations).toEqual([]);
});
test('form should be accessible', async ({ page }) => {
await page.goto('/contact');
const results = await new AxeBuilder({ page })
.include('form')
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
// Additional form checks
const inputs = await page.locator('input, select, textarea').all();
for (const input of inputs) {
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
// Every input should have an associated label
if (id) {
const label = await page.locator(`label[for="${id}"]`).count();
expect(
label > 0 || ariaLabel || ariaLabelledBy,
`Input with id="${id}" has no associated label`
).toBeTruthy();
}
}
});
test('images should have alt text', async ({ page }) => {
await page.goto('/');
const images = await page.locator('img').all();
for (const img of images) {
const alt = await img.getAttribute('alt');
const role = await img.getAttribute('role');
const ariaLabel = await img.getAttribute('aria-label');
// Decorative images should have empty alt or role="presentation"
// Content images must have descriptive alt text
expect(
alt !== null || role === 'presentation' || ariaLabel,
'Image missing alt text'
).toBeTruthy();
}
});
test('color contrast should meet AA standards', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['cat.color'])
.analyze();
expect(results.violations).toEqual([]);
});
});// tests/accessibility/homepage.a11y.test.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Homepage Accessibility', () => {
test('should not have any automatically detectable WCAG A or AA violations', async ({
page,
}) => {
await page.goto('/');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('navigation should be accessible', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.include('nav')
.analyze();
expect(results.violations).toEqual([]);
});
test('form should be accessible', async ({ page }) => {
await page.goto('/contact');
const results = await new AxeBuilder({ page })
.include('form')
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
expect(results.violations).toEqual([]);
// Additional form checks
const inputs = await page.locator('input, select, textarea').all();
for (const input of inputs) {
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
// Every input should have an associated label
if (id) {
const label = await page.locator(`label[for="${id}"]`).count();
expect(
label > 0 || ariaLabel || ariaLabelledBy,
`Input with id="${id}" has no associated label`
).toBeTruthy();
}
}
});
test('images should have alt text', async ({ page }) => {
await page.goto('/');
const images = await page.locator('img').all();
for (const img of images) {
const alt = await img.getAttribute('alt');
const role = await img.getAttribute('role');
const ariaLabel = await img.getAttribute('aria-label');
// Decorative images should have empty alt or role="presentation"
// Content images must have descriptive alt text
expect(
alt !== null || role === 'presentation' || ariaLabel,
'Image missing alt text'
).toBeTruthy();
}
});
test('color contrast should meet AA standards', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['cat.color'])
.analyze();
expect(results.violations).toEqual([]);
});
});// tests/accessibility/keyboard-navigation.test.ts
import { test, expect } from '@playwright/test';
test.describe('Keyboard Navigation', () => {
test('should navigate through focusable elements with Tab', async ({
page,
}) => {
await page.goto('/');
// Get all focusable elements
const focusableSelectors =
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
const focusableElements = await page.locator(focusableSelectors).all();
// Tab through all elements
for (let i = 0; i < focusableElements.length; i++) {
await page.keyboard.press('Tab');
const focusedElement = await page.evaluate(() => {
return {
tagName: document.activeElement?.tagName,
id: document.activeElement?.id,
className: document.activeElement?.className,
};
});
expect(focusedElement.tagName).toBeTruthy();
}
});
test('should skip navigation with skip link', async ({ page }) => {
await page.goto('/');
// Tab to skip link (usually first focusable element)
await page.keyboard.press('Tab');
const skipLink = await page.locator('.skip-link');
await expect(skipLink).toBeFocused();
// Activate skip link
await page.keyboard.press('Enter');
// Focus should be on main content
const focusedElement = await page.evaluate(() => {
return document.activeElement?.id;
});
expect(focusedElement).toBe('main-content');
});
test('modal should trap focus', async ({ page }) => {
await page.goto('/');
// Open modal
await page.click('[data-testid="open-modal"]');
await page.waitForSelector('[role="dialog"]');
const modal = page.locator('[role="dialog"]');
const focusableInModal = modal.locator(
'a[href], button, input, select, textarea'
);
const count = await focusableInModal.count();
// Tab through all elements in modal
for (let i = 0; i < count + 2; i++) {
await page.keyboard.press('Tab');
}
// Focus should still be within modal
const focusedElement = await page.evaluate(() => {
const modal = document.querySelector('[role="dialog"]');
return modal?.contains(document.activeElement);
});
expect(focusedElement).toBe(true);
});
test('dropdown menu should be keyboard accessible', async ({ page }) => {
await page.goto('/');
// Navigate to dropdown trigger
await page.keyboard.press('Tab');
const dropdown = page.locator('[data-testid="dropdown-menu"]');
await dropdown.focus();
// Open dropdown with Enter
await page.keyboard.press('Enter');
// Menu should be visible
const menu = page.locator('[role="menu"]');
await expect(menu).toBeVisible();
// Navigate menu items with arrow keys
await page.keyboard.press('ArrowDown');
const firstItem = menu.locator('[role="menuitem"]').first();
await expect(firstItem).toBeFocused();
await page.keyboard.press('ArrowDown');
const secondItem = menu.locator('[role="menuitem"]').nth(1);
await expect(secondItem).toBeFocused();
// Escape should close menu
await page.keyboard.press('Escape');
await expect(menu).not.toBeVisible();
await expect(dropdown).toBeFocused();
});
test('form can be completed using keyboard only', async ({ page }) => {
await page.goto('/contact');
// Tab to first field
await page.keyboard.press('Tab');
await page.keyboard.type('John Doe');
// Tab to email field
await page.keyboard.press('Tab');
await page.keyboard.type('john@example.com');
// Tab to message
await page.keyboard.press('Tab');
await page.keyboard.type('Test message');
// Tab to submit and activate
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
// Check form was submitted
await expect(page.locator('.success-message')).toBeVisible();
});
});// tests/accessibility/keyboard-navigation.test.ts
import { test, expect } from '@playwright/test';
test.describe('Keyboard Navigation', () => {
test('should navigate through focusable elements with Tab', async ({
page,
}) => {
await page.goto('/');
// Get all focusable elements
const focusableSelectors =
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
const focusableElements = await page.locator(focusableSelectors).all();
// Tab through all elements
for (let i = 0; i < focusableElements.length; i++) {
await page.keyboard.press('Tab');
const focusedElement = await page.evaluate(() => {
return {
tagName: document.activeElement?.tagName,
id: document.activeElement?.id,
className: document.activeElement?.className,
};
});
expect(focusedElement.tagName).toBeTruthy();
}
});
test('should skip navigation with skip link', async ({ page }) => {
await page.goto('/');
// Tab to skip link (usually first focusable element)
await page.keyboard.press('Tab');
const skipLink = await page.locator('.skip-link');
await expect(skipLink).toBeFocused();
// Activate skip link
await page.keyboard.press('Enter');
// Focus should be on main content
const focusedElement = await page.evaluate(() => {
return document.activeElement?.id;
});
expect(focusedElement).toBe('main-content');
});
test('modal should trap focus', async ({ page }) => {
await page.goto('/');
// Open modal
await page.click('[data-testid="open-modal"]');
await page.waitForSelector('[role="dialog"]');
const modal = page.locator('[role="dialog"]');
const focusableInModal = modal.locator(
'a[href], button, input, select, textarea'
);
const count = await focusableInModal.count();
// Tab through all elements in modal
for (let i = 0; i < count + 2; i++) {
await page.keyboard.press('Tab');
}
// Focus should still be within modal
const focusedElement = await page.evaluate(() => {
const modal = document.querySelector('[role="dialog"]');
return modal?.contains(document.activeElement);
});
expect(focusedElement).toBe(true);
});
test('dropdown menu should be keyboard accessible', async ({ page }) => {
await page.goto('/');
// Navigate to dropdown trigger
await page.keyboard.press('Tab');
const dropdown = page.locator('[data-testid="dropdown-menu"]');
await dropdown.focus();
// Open dropdown with Enter
await page.keyboard.press('Enter');
// Menu should be visible
const menu = page.locator('[role="menu"]');
await expect(menu).toBeVisible();
// Navigate menu items with arrow keys
await page.keyboard.press('ArrowDown');
const firstItem = menu.locator('[role="menuitem"]').first();
await expect(firstItem).toBeFocused();
await page.keyboard.press('ArrowDown');
const secondItem = menu.locator('[role="menuitem"]').nth(1);
await expect(secondItem).toBeFocused();
// Escape should close menu
await page.keyboard.press('Escape');
await expect(menu).not.toBeVisible();
await expect(dropdown).toBeFocused();
});
test('form can be completed using keyboard only', async ({ page }) => {
await page.goto('/contact');
// Tab to first field
await page.keyboard.press('Tab');
await page.keyboard.type('John Doe');
// Tab to email field
await page.keyboard.press('Tab');
await page.keyboard.type('john@example.com');
// Tab to message
await page.keyboard.press('Tab');
await page.keyboard.type('Test message');
// Tab to submit and activate
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
// Check form was submitted
await expect(page.locator('.success-message')).toBeVisible();
});
});// tests/accessibility/aria.test.ts
import { test, expect } from '@playwright/test';
test.describe('ARIA Attributes', () => {
test('buttons should have accessible names', async ({ page }) => {
await page.goto('/');
const buttons = await page.locator('button').all();
for (const button of buttons) {
const text = await button.textContent();
const ariaLabel = await button.getAttribute('aria-label');
const ariaLabelledBy = await button.getAttribute('aria-labelledby');
expect(
text?.trim() || ariaLabel || ariaLabelledBy,
'Button has no accessible name'
).toBeTruthy();
}
});
test('icons should have aria-hidden or labels', async ({ page }) => {
await page.goto('/');
const icons = await page
.locator('[class*="icon"], svg[class*="icon"]')
.all();
for (const icon of icons) {
const ariaHidden = await icon.getAttribute('aria-hidden');
const ariaLabel = await icon.getAttribute('aria-label');
const title = await icon.locator('title').count();
// Icon should be hidden from screen readers OR have a label
expect(
ariaHidden === 'true' || ariaLabel || title > 0,
'Icon without aria-hidden or accessible name'
).toBeTruthy();
}
});
test('custom widgets should have correct roles', async ({ page }) => {
await page.goto('/components');
// Tab widget
const tablist = page.locator('[role="tablist"]');
await expect(tablist).toHaveCount(1);
const tabs = tablist.locator('[role="tab"]');
const tabpanels = page.locator('[role="tabpanel"]');
expect(await tabs.count()).toBeGreaterThan(0);
expect(await tabs.count()).toBe(await tabpanels.count());
// Check aria-selected
const selectedTab = tabs.locator('[aria-selected="true"]');
await expect(selectedTab).toHaveCount(1);
// Check tab associations
const firstTab = tabs.first();
const ariaControls = await firstTab.getAttribute('aria-controls');
const associatedPanel = page.locator(`[id="${ariaControls}"]`);
await expect(associatedPanel).toHaveCount(1);
});
test('live regions announce changes', async ({ page }) => {
await page.goto('/');
// Find live region
const liveRegion = page.locator('[role="status"], [aria-live]');
// Trigger update
await page.click('[data-testid="load-data"]');
// Wait for content
await liveRegion.waitFor({ state: 'visible' });
const ariaLive = await liveRegion.getAttribute('aria-live');
expect(['polite', 'assertive']).toContain(ariaLive);
});
});// tests/accessibility/aria.test.ts
import { test, expect } from '@playwright/test';
test.describe('ARIA Attributes', () => {
test('buttons should have accessible names', async ({ page }) => {
await page.goto('/');
const buttons = await page.locator('button').all();
for (const button of buttons) {
const text = await button.textContent();
const ariaLabel = await button.getAttribute('aria-label');
const ariaLabelledBy = await button.getAttribute('aria-labelledby');
expect(
text?.trim() || ariaLabel || ariaLabelledBy,
'Button has no accessible name'
).toBeTruthy();
}
});
test('icons should have aria-hidden or labels', async ({ page }) => {
await page.goto('/');
const icons = await page
.locator('[class*="icon"], svg[class*="icon"]')
.all();
for (const icon of icons) {
const ariaHidden = await icon.getAttribute('aria-hidden');
const ariaLabel = await icon.getAttribute('aria-label');
const title = await icon.locator('title').count();
// Icon should be hidden from screen readers OR have a label
expect(
ariaHidden === 'true' || ariaLabel || title > 0,
'Icon without aria-hidden or accessible name'
).toBeTruthy();
}
});
test('custom widgets should have correct roles', async ({ page }) => {
await page.goto('/components');
// Tab widget
const tablist = page.locator('[role="tablist"]');
await expect(tablist).toHaveCount(1);
const tabs = tablist.locator('[role="tab"]');
const tabpanels = page.locator('[role="tabpanel"]');
expect(await tabs.count()).toBeGreaterThan(0);
expect(await tabs.count()).toBe(await tabpanels.count());
// Check aria-selected
const selectedTab = tabs.locator('[aria-selected="true"]');
await expect(selectedTab).toHaveCount(1);
// Check tab associations
const firstTab = tabs.first();
const ariaControls = await firstTab.getAttribute('aria-controls');
const associatedPanel = page.locator(`[id="${ariaControls}"]`);
await expect(associatedPanel).toHaveCount(1);
});
test('live regions announce changes', async ({ page }) => {
await page.goto('/');
// Find live region
const liveRegion = page.locator('[role="status"], [aria-live]');
// Trigger update
await page.click('[data-testid="load-data"]');
// Wait for content
await liveRegion.waitFor({ state: 'visible' });
const ariaLive = await liveRegion.getAttribute('aria-live');
expect(['polite', 'assertive']).toContain(ariaLive);
});
});// tests/components/Button.a11y.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { Button } from '../Button';
expect.extend(toHaveNoViolations);
describe('Button Accessibility', () => {
test('should not have accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('icon button should have aria-label', async () => {
const { container } = render(
<Button aria-label="Close">
<CloseIcon />
</Button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('disabled button should be accessible', async () => {
const { container } = render(<Button disabled>Disabled</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});// tests/components/Button.a11y.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { Button } from '../Button';
expect.extend(toHaveNoViolations);
describe('Button Accessibility', () => {
test('should not have accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('icon button should have aria-label', async () => {
const { container } = render(
<Button aria-label="Close">
<CloseIcon />
</Button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('disabled button should be accessible', async () => {
const { container } = render(<Button disabled>Disabled</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});// cypress/e2e/accessibility.cy.js
describe('Accessibility Tests', () => {
beforeEach(() => {
cy.visit('/');
cy.injectAxe();
});
it('has no detectable a11y violations on load', () => {
cy.checkA11y();
});
it('navigation is accessible', () => {
cy.checkA11y('nav');
});
it('focuses on first error when form submission fails', () => {
cy.get('form').within(() => {
cy.get('[type="submit"]').click();
});
cy.focused().should('have.attr', 'aria-invalid', 'true');
});
it('modal has correct focus management', () => {
cy.get('[data-cy="open-modal"]').click();
// Focus should be in modal
cy.get('[role="dialog"]').should('exist');
cy.focused().parents('[role="dialog"]').should('exist');
// Close modal with Escape
cy.get('body').type('{esc}');
cy.get('[role="dialog"]').should('not.exist');
// Focus returns to trigger
cy.get('[data-cy="open-modal"]').should('have.focus');
});
});// cypress/e2e/accessibility.cy.js
describe('Accessibility Tests', () => {
beforeEach(() => {
cy.visit('/');
cy.injectAxe();
});
it('has no detectable a11y violations on load', () => {
cy.checkA11y();
});
it('navigation is accessible', () => {
cy.checkA11y('nav');
});
it('focuses on first error when form submission fails', () => {
cy.get('form').within(() => {
cy.get('[type="submit"]').click();
});
cy.focused().should('have.attr', 'aria-invalid', 'true');
});
it('modal has correct focus management', () => {
cy.get('[data-cy="open-modal"]').click();
// Focus should be in modal
cy.get('[role="dialog"]').should('exist');
cy.focused().parents('[role="dialog"]').should('exist');
// Close modal with Escape
cy.get('body').type('{esc}');
cy.get('[role="dialog"]').should('not.exist');
// Focus returns to trigger
cy.get('[data-cy="open-modal"]').should('have.focus');
});
});undefinedundefineddef test_homepage_accessibility(self, driver):
"""Test homepage for WCAG violations."""
driver.get('http://localhost:3000')
axe = Axe(driver)
axe.inject()
# Run axe accessibility tests
results = axe.run()
# Assert no violations
assert len(results['violations']) == 0, \
axe.report(results['violations'])
def test_form_accessibility(self, driver):
"""Test form accessibility."""
driver.get('http://localhost:3000/contact')
axe = Axe(driver)
axe.inject()
# Run with specific tags
results = axe.run(options={
'runOnly': {
'type': 'tag',
'values': ['wcag2a', 'wcag2aa', 'wcag21aa']
}
})
violations = results['violations']
assert len(violations) == 0, \
f"Found {len(violations)} accessibility violations"
def test_keyboard_navigation(self, driver):
"""Test keyboard navigation."""
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
driver.get('http://localhost:3000')
body = driver.find_element(By.TAG_NAME, 'body')
# Tab through focusable elements
for _ in range(10):
body.send_keys(Keys.TAB)
active = driver.switch_to.active_element
tag_name = active.tag_name
# Focused element should be interactive
assert tag_name in ['a', 'button', 'input', 'select', 'textarea'], \
f"Unexpected focused element: {tag_name}"undefineddef test_homepage_accessibility(self, driver):
"""Test homepage for WCAG violations."""
driver.get('http://localhost:3000')
axe = Axe(driver)
axe.inject()
# Run axe accessibility tests
results = axe.run()
# Assert no violations
assert len(results['violations']) == 0, \
axe.report(results['violations'])
def test_form_accessibility(self, driver):
"""Test form accessibility."""
driver.get('http://localhost:3000/contact')
axe = Axe(driver)
axe.inject()
# Run with specific tags
results = axe.run(options={
'runOnly': {
'type': 'tag',
'values': ['wcag2a', 'wcag2aa', 'wcag21aa']
}
})
violations = results['violations']
assert len(violations) == 0, \
f"Found {len(violations)} accessibility violations"
def test_keyboard_navigation(self, driver):
"""Test keyboard navigation."""
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
driver.get('http://localhost:3000')
body = driver.find_element(By.TAG_NAME, 'body')
# Tab through focusable elements
for _ in range(10):
body.send_keys(Keys.TAB)
active = driver.switch_to.active_element
tag_name = active.tag_name
# Focused element should be interactive
assert tag_name in ['a', 'button', 'input', 'select', 'textarea'], \
f"Unexpected focused element: {tag_name}"undefined