30 / 74 · 10 Visual & Accessibility Testing · ARIA Labels and Color Contrast← prev⊞ allnext →☰ Read as one page
6.2Automated ARIA Testing
Testing Alt Text Quality
// tests/accessibility/aria-validation.spec.ts
import { test, expect } from '@playwright/test';
test('all images have alt text', async ({ page }) => {
await page.goto('/products');
const images = await page.locator('img').all();
const violations: string[] = [];
for (const img of images) {
const alt = await img.getAttribute('alt');
const src = await img.getAttribute('src');
const role = await img.getAttribute('role');
// Decorative images should have alt="" and role="presentation"
// Content images must have descriptive alt text
if (role === 'presentation' || role === 'none') {
// Decorative: alt must be empty string (not missing)
if (alt !== '') {
violations.push(`Decorative image ${src} should have alt="", got alt="${alt}"`);
}
} else {
// Content image: must have meaningful alt
if (!alt || alt.trim().length === 0) {
violations.push(`Image ${src} has no alt text`);
} else if (alt.match(/^(image|photo|picture|img|icon)\b/i)) {
violations.push(`Image ${src} has non-descriptive alt: "${alt}"`);
} else if (src && alt === src.split('/').pop()) {
violations.push(`Image ${src} uses filename as alt: "${alt}"`);
}
}
}
expect(violations).toEqual([]);
});
Testing Focus Indicators
test('interactive elements have visible focus indicators', async ({ page }) => {
await page.goto('/');
// Tab through all focusable elements
const focusableSelector = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
const focusableCount = await page.locator(focusableSelector).count();
const violations: string[] = [];
for (let i = 0; i < Math.min(focusableCount, 50); i++) {
await page.keyboard.press('Tab');
// Get the currently focused element
const focused = page.locator(':focus');
const isVisible = await focused.isVisible();
if (isVisible) {
// Check that focus indicator is visible
const outlineStyle = await focused.evaluate((el) => {
const styles = window.getComputedStyle(el);
return {
outline: styles.outline,
outlineWidth: styles.outlineWidth,
outlineColor: styles.outlineColor,
boxShadow: styles.boxShadow,
border: styles.border,
tag: el.tagName,
text: el.textContent?.trim().substring(0, 30),
};
});
// Focus must be visible via outline, box-shadow, or border change
const hasVisibleFocus =
outlineStyle.outlineWidth !== '0px' ||
outlineStyle.boxShadow !== 'none' ||
outlineStyle.outline !== 'none';
if (!hasVisibleFocus) {
violations.push(
`${outlineStyle.tag} "${outlineStyle.text}" has no visible focus indicator`
);
}
}
}
expect(violations).toEqual([]);
});