Modern QA2026Automated ARIA Testing — tiles
Log inJoin
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([]);
});