import { test, expect } from '@playwright/test'; test.describe('Accessibility', () => { test.describe('Keyboard Navigation', () => { test('can navigate with Tab key', async ({ page }) => { await page.goto('/dashboard'); // Press Tab to move focus await page.keyboard.press('Tab'); await page.waitForTimeout(100); // Something should be focused const focusedElement = await page.evaluate(() => document.activeElement?.tagName); expect(focusedElement).toBeTruthy(); }); test('can activate buttons with Enter key', async ({ page }) => { await page.goto('/devices'); // Tab to a button await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await page.waitForTimeout(100); const activeTag = await page.evaluate(() => document.activeElement?.tagName); if (activeTag === 'BUTTON' || activeTag === 'A') { // Element is focusable expect(activeTag).toMatch(/BUTTON|A/); } }); test('can activate buttons with Space key', async ({ page }) => { await page.goto('/devices'); await page.keyboard.press('Tab'); await page.waitForTimeout(100); const activeTag = await page.evaluate(() => document.activeElement?.tagName); if (activeTag === 'BUTTON') { // Buttons can be activated with space expect(activeTag).toBe('BUTTON'); } }); test('focus is visible on interactive elements', async ({ page }) => { await page.goto('/dashboard'); // Tab to first interactive element await page.keyboard.press('Tab'); await page.waitForTimeout(100); const focusedElement = page.locator(':focus'); if (await focusedElement.isVisible({ timeout: 1000 }).catch(() => false)) { await expect(focusedElement).toBeVisible(); } }); test('skip to main content link exists', async ({ page }) => { await page.goto('/dashboard'); // Look for skip link (usually visible on focus) const skipLink = page.locator('a:has-text("Skip to"), a:has-text("Skip navigation")').first(); if (await skipLink.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(skipLink).toBeVisible(); } }); }); test.describe('ARIA Labels and Roles', () => { test('navigation has proper role', async ({ page }) => { await page.goto('/dashboard'); const nav = page.locator('nav, [role="navigation"]').first(); await expect(nav).toBeVisible(); }); test('main content has proper role', async ({ page }) => { await page.goto('/dashboard'); const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); }); test('buttons have accessible names', async ({ page }) => { await page.goto('/devices'); const buttons = page.locator('button'); const count = await buttons.count(); if (count > 0) { // At least one button should have text or aria-label const firstButton = buttons.first(); const hasText = await firstButton.textContent(); const ariaLabel = await firstButton.getAttribute('aria-label'); expect(hasText || ariaLabel).toBeTruthy(); } }); test('links have descriptive text', async ({ page }) => { await page.goto('/dashboard'); const links = page.locator('a'); const count = await links.count(); if (count > 0) { const firstLink = links.first(); const text = await firstLink.textContent(); // Links should have text content if (text && text.trim().length > 0) { expect(text.trim().length).toBeGreaterThan(0); } } }); test('form inputs have labels', async ({ page }) => { await page.goto('/sites/new'); if ((await page.url()).includes('/sites/new')) { const inputs = page.locator('input[type="text"], input[type="email"]'); const count = await inputs.count(); if (count > 0) { const firstInput = inputs.first(); const id = await firstInput.getAttribute('id'); const ariaLabel = await firstInput.getAttribute('aria-label'); // Input should have either an ID (for label) or aria-label expect(id || ariaLabel).toBeTruthy(); } } }); test('alerts use proper ARIA role', async ({ page }) => { await page.goto('/dashboard'); // Look for alert/status roles const alert = page.locator('[role="alert"], [role="status"]').first(); if (await alert.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(alert).toBeVisible(); } }); }); test.describe('Color Contrast and Visual', () => { test('text is visible and readable', async ({ page }) => { await page.goto('/dashboard'); // Main content should be visible const body = page.locator('body'); await expect(body).toBeVisible(); // Text should have sufficient contrast (manual check needed for real compliance) const heading = page.locator('h1, h2').first(); if (await heading.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(heading).toBeVisible(); } }); test('icons have text alternatives', async ({ page }) => { await page.goto('/dashboard'); // Icons should have aria-label or title const icons = page.locator('svg, [data-icon]'); const count = await icons.count(); if (count > 0) { const firstIcon = icons.first(); const ariaLabel = await firstIcon.getAttribute('aria-label'); const title = await firstIcon.getAttribute('title'); const role = await firstIcon.getAttribute('role'); // Icon should have description or be decorative (role="presentation") expect(ariaLabel || title || role === 'presentation' || role === 'img').toBeTruthy(); } }); test('focus indicators are visible', async ({ page }) => { await page.goto('/dashboard'); await page.keyboard.press('Tab'); await page.waitForTimeout(200); const focused = page.locator(':focus'); if (await focused.isVisible({ timeout: 1000 }).catch(() => false)) { await expect(focused).toBeVisible(); } }); }); test.describe('Screen Reader Support', () => { test('page has descriptive title', async ({ page }) => { await page.goto('/dashboard'); const title = await page.title(); expect(title.length).toBeGreaterThan(0); expect(title).not.toBe(''); }); test('headings create proper hierarchy', async ({ page }) => { await page.goto('/dashboard'); // Should have h1 const h1 = page.locator('h1').first(); if (await h1.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(h1).toBeVisible(); } }); test('landmark regions are defined', async ({ page }) => { await page.goto('/dashboard'); // Should have main landmark const main = page.locator('main, [role="main"]').first(); await expect(main).toBeVisible(); // Should have navigation const nav = page.locator('nav, [role="navigation"]').first(); await expect(nav).toBeVisible(); }); test('loading states have aria-busy', async ({ page }) => { await page.goto('/devices'); // Look for aria-busy on loading elements const busyElement = page.locator('[aria-busy="true"]').first(); if (await busyElement.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(busyElement).toBeVisible(); } }); }); test.describe('Form Accessibility', () => { test('required fields are marked', async ({ page }) => { await page.goto('/sites/new'); if ((await page.url()).includes('/sites/new')) { const requiredInput = page.locator('input[required], input[aria-required="true"]').first(); if (await requiredInput.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(requiredInput).toBeVisible(); } } }); test('error messages are associated with fields', async ({ page }) => { await page.goto('/sites/new'); if ((await page.url()).includes('/sites/new')) { const submitButton = page.locator('button[type="submit"]').first(); if (await submitButton.isVisible({ timeout: 2000 }).catch(() => false)) { await submitButton.click(); await page.waitForTimeout(500); // Look for aria-describedby or aria-invalid const invalidInput = page.locator('[aria-invalid="true"], [aria-describedby]').first(); if (await invalidInput.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(invalidInput).toBeVisible(); } } } }); test('fieldsets have legends', async ({ page }) => { await page.goto('/sites/new'); if ((await page.url()).includes('/sites/new')) { const fieldset = page.locator('fieldset').first(); if (await fieldset.isVisible({ timeout: 2000 }).catch(() => false)) { const legend = fieldset.locator('legend').first(); if (await legend.isVisible({ timeout: 1000 }).catch(() => false)) { await expect(legend).toBeVisible(); } } } }); }); test.describe('Interactive Element Accessibility', () => { test('dialogs have proper role and focus management', async ({ page }) => { await page.goto('/devices'); // Trigger a dialog if possible const button = page.locator('button:has-text("Delete"), button:has-text("Remove")').first(); if (await button.isVisible({ timeout: 2000 }).catch(() => false)) { await button.click(); await page.waitForTimeout(300); const dialog = page.locator('[role="dialog"], [role="alertdialog"]').first(); if (await dialog.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(dialog).toBeVisible(); } } }); test('dropdowns have proper ARIA attributes', async ({ page }) => { await page.goto('/devices'); const dropdown = page.locator('[role="button"][aria-haspopup], [aria-expanded]').first(); if (await dropdown.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(dropdown).toBeVisible(); } }); test('tables have proper structure', async ({ page }) => { await page.goto('/devices'); const table = page.locator('table').first(); if (await table.isVisible({ timeout: 2000 }).catch(() => false)) { // Table should have thead and tbody const thead = table.locator('thead'); const tbody = table.locator('tbody'); if (await thead.isVisible({ timeout: 1000 }).catch(() => false)) { await expect(thead).toBeVisible(); } if (await tbody.isVisible({ timeout: 1000 }).catch(() => false)) { await expect(tbody).toBeVisible(); } } }); }); });