@sc4rfurryx/proteusjs
Version:
The Modern Web Development Framework for Accessible, Responsive, and High-Performance Applications. Intelligent container queries, fluid typography, WCAG compliance, and performance optimization.
436 lines (336 loc) • 14.9 kB
text/typescript
/**
* ScreenReaderSupport Test Suite
* Comprehensive tests for screen reader compatibility
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { ScreenReaderSupport, AnnouncementConfig, LiveRegionConfig, AriaLabelConfig } from '../ScreenReaderSupport';
describe('ScreenReaderSupport', () => {
let screenReaderSupport: ScreenReaderSupport;
beforeEach(() => {
// Clean up DOM
document.body.innerHTML = '';
screenReaderSupport = new ScreenReaderSupport();
});
afterEach(() => {
screenReaderSupport.destroy();
document.body.innerHTML = '';
vi.restoreAllMocks();
});
describe('Initialization', () => {
it('should initialize with default live regions', () => {
const statusRegion = document.getElementById('proteus-live-status');
const alertRegion = document.getElementById('proteus-live-alert');
expect(statusRegion).toBeTruthy();
expect(alertRegion).toBeTruthy();
expect(statusRegion?.getAttribute('aria-live')).toBe('polite');
expect(alertRegion?.getAttribute('aria-live')).toBe('assertive');
});
it('should detect keyboard navigation', () => {
const keydownEvent = new KeyboardEvent('keydown', { key: 'Tab' });
document.dispatchEvent(keydownEvent);
expect(document.body.classList.contains('proteus-keyboard-user')).toBe(true);
const mousedownEvent = new MouseEvent('mousedown');
document.dispatchEvent(mousedownEvent);
expect(document.body.classList.contains('proteus-keyboard-user')).toBe(false);
});
});
describe('Announcements', () => {
it('should announce messages to screen readers', () => {
const message = 'Test announcement';
screenReaderSupport.announce(message);
const statusRegion = document.getElementById('proteus-live-status');
// Wait for async processing
setTimeout(() => {
expect(statusRegion?.textContent).toBe(message);
}, 100);
});
it('should handle assertive announcements', () => {
const message = 'Urgent announcement';
const config: AnnouncementConfig = { priority: 'assertive' };
screenReaderSupport.announce(message, config);
const alertRegion = document.getElementById('proteus-live-alert');
setTimeout(() => {
expect(alertRegion?.textContent).toBe(message);
}, 100);
});
it('should prevent duplicate announcements', () => {
const message = 'Duplicate test';
screenReaderSupport.announce(message);
screenReaderSupport.announce(message); // Should be ignored
const statusRegion = document.getElementById('proteus-live-status');
setTimeout(() => {
expect(statusRegion?.textContent).toBe(message);
}, 100);
});
it('should handle announcement interruption', () => {
screenReaderSupport.announce('First message');
screenReaderSupport.announce('Second message', { priority: 'polite', interrupt: true });
const statusRegion = document.getElementById('proteus-live-status');
setTimeout(() => {
expect(statusRegion?.textContent).toBe('Second message');
}, 100);
});
it('should ignore empty announcements', () => {
const statusRegion = document.getElementById('proteus-live-status');
const initialContent = statusRegion?.textContent;
screenReaderSupport.announce('');
screenReaderSupport.announce(' ');
expect(statusRegion?.textContent).toBe(initialContent);
});
});
describe('Live Regions', () => {
it('should create custom live regions', () => {
const config: LiveRegionConfig = {
type: 'status',
atomic: true,
relevant: 'additions',
busy: false
};
const region = screenReaderSupport.createLiveRegion('custom', config);
expect(region.id).toBe('proteus-live-custom');
expect(region.getAttribute('aria-live')).toBe('polite');
expect(region.getAttribute('aria-atomic')).toBe('true');
expect(region.getAttribute('aria-relevant')).toBe('additions');
expect(region.getAttribute('aria-busy')).toBe('false');
});
it('should update live region content', () => {
const region = screenReaderSupport.createLiveRegion('test', {
type: 'status',
atomic: false,
relevant: 'additions',
busy: false
});
screenReaderSupport.updateLiveRegion('test', 'Updated content');
expect(region.textContent).toBe('Updated content');
});
it('should handle different live region types', () => {
const alertRegion = screenReaderSupport.createLiveRegion('alert-test', {
type: 'alert',
atomic: true,
relevant: 'all',
busy: false
});
const logRegion = screenReaderSupport.createLiveRegion('log-test', {
type: 'log',
atomic: false,
relevant: 'additions',
busy: false
});
expect(alertRegion.getAttribute('aria-live')).toBe('assertive');
expect(alertRegion.getAttribute('role')).toBe('alert');
expect(logRegion.getAttribute('aria-live')).toBe('polite');
expect(logRegion.getAttribute('role')).toBe('status');
});
});
describe('ARIA Labels', () => {
let testElement: HTMLElement;
beforeEach(() => {
testElement = document.createElement('div');
document.body.appendChild(testElement);
});
it('should apply basic ARIA labels', () => {
const config: AriaLabelConfig = {
label: 'Test label',
role: 'button'
};
screenReaderSupport.applyAriaLabels(testElement, config);
expect(testElement.getAttribute('aria-label')).toBe('Test label');
expect(testElement.getAttribute('role')).toBe('button');
});
it('should apply state attributes', () => {
const config: AriaLabelConfig = {
expanded: true,
selected: false,
disabled: true,
hidden: false
};
screenReaderSupport.applyAriaLabels(testElement, config);
expect(testElement.getAttribute('aria-expanded')).toBe('true');
expect(testElement.getAttribute('aria-selected')).toBe('false');
expect(testElement.getAttribute('aria-disabled')).toBe('true');
expect(testElement.getAttribute('aria-hidden')).toBe('false');
});
it('should apply relationship attributes', () => {
const labelElement = document.createElement('label');
labelElement.id = 'test-label';
document.body.appendChild(labelElement);
const descElement = document.createElement('div');
descElement.id = 'test-desc';
document.body.appendChild(descElement);
const config: AriaLabelConfig = {
labelledBy: 'test-label',
describedBy: 'test-desc'
};
screenReaderSupport.applyAriaLabels(testElement, config);
expect(testElement.getAttribute('aria-labelledby')).toBe('test-label');
expect(testElement.getAttribute('aria-describedby')).toBe('test-desc');
});
});
describe('Container Announcements', () => {
it('should announce container changes', () => {
const container = document.createElement('div');
container.setAttribute('aria-label', 'Product grid');
screenReaderSupport.announceContainerChange(container, 'tablet', 768);
setTimeout(() => {
const statusRegion = document.getElementById('proteus-live-status');
expect(statusRegion?.textContent).toContain('Product grid');
expect(statusRegion?.textContent).toContain('tablet breakpoint');
expect(statusRegion?.textContent).toContain('768 pixels');
}, 600);
});
it('should announce typography changes', () => {
const textElement = document.createElement('p');
textElement.textContent = 'Sample text';
screenReaderSupport.announceTypographyChange(textElement, '18px');
setTimeout(() => {
const statusRegion = document.getElementById('proteus-live-status');
expect(statusRegion?.textContent).toContain('text size adjusted');
expect(statusRegion?.textContent).toContain('18px');
}, 400);
});
});
describe('Skip Links', () => {
it('should create skip links', () => {
const targets = [
{ id: 'main-content', label: 'Skip to main content' },
{ id: 'navigation', label: 'Skip to navigation' }
];
const skipContainer = screenReaderSupport.createSkipLinks(targets);
expect(skipContainer.getAttribute('role')).toBe('navigation');
expect(skipContainer.getAttribute('aria-label')).toBe('Skip links');
const links = skipContainer.querySelectorAll('a');
expect(links).toHaveLength(2);
expect(links[0].href).toContain('#main-content');
expect(links[0].textContent).toBe('Skip to main content');
expect(links[1].href).toContain('#navigation');
expect(links[1].textContent).toBe('Skip to navigation');
});
it('should position skip links correctly', () => {
const targets = [{ id: 'main', label: 'Skip to main' }];
const skipContainer = screenReaderSupport.createSkipLinks(targets);
const link = skipContainer.querySelector('a') as HTMLElement;
expect(link.style.position).toBe('absolute');
expect(link.style.top).toBe('-40px'); // Hidden by default
// Simulate focus
link.focus();
link.dispatchEvent(new FocusEvent('focus'));
expect(link.style.top).toBe('6px'); // Visible on focus
});
});
describe('Focus Management', () => {
it('should manage focus for dynamic content', () => {
const element = document.createElement('div');
element.textContent = 'Dynamic content';
document.body.appendChild(element);
const focusSpy = vi.spyOn(element, 'focus');
screenReaderSupport.manageFocus(element, 'Content updated');
expect(element.getAttribute('tabindex')).toBe('-1');
expect(focusSpy).toHaveBeenCalled();
setTimeout(() => {
const alertRegion = document.getElementById('proteus-live-alert');
expect(alertRegion?.textContent).toContain('Focus moved to');
expect(alertRegion?.textContent).toContain('Content updated');
}, 100);
});
it('should preserve existing tabindex', () => {
const element = document.createElement('button');
element.setAttribute('tabindex', '0');
document.body.appendChild(element);
screenReaderSupport.manageFocus(element, 'Button focused');
expect(element.getAttribute('tabindex')).toBe('0');
});
});
describe('Screen Reader Detection', () => {
it('should detect screen reader presence', () => {
// Mock screen reader indicators
Object.defineProperty(navigator, 'userAgent', {
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) NVDA/2021.1',
configurable: true
});
const newSupport = new ScreenReaderSupport();
expect(newSupport.isScreenReaderActive()).toBe(true);
newSupport.destroy();
});
it('should handle missing screen reader gracefully', () => {
expect(screenReaderSupport.isScreenReaderActive()).toBeDefined();
});
});
describe('Element Description', () => {
it('should get element description from aria-label', () => {
const element = document.createElement('div');
element.setAttribute('aria-label', 'Custom label');
screenReaderSupport.announceContainerChange(element, 'md', 600);
setTimeout(() => {
const statusRegion = document.getElementById('proteus-live-status');
expect(statusRegion?.textContent).toContain('Custom label');
}, 600);
});
it('should get element description from aria-labelledby', () => {
const labelElement = document.createElement('span');
labelElement.id = 'element-label';
labelElement.textContent = 'Labeled element';
document.body.appendChild(labelElement);
const element = document.createElement('div');
element.setAttribute('aria-labelledby', 'element-label');
screenReaderSupport.announceContainerChange(element, 'lg', 800);
setTimeout(() => {
const statusRegion = document.getElementById('proteus-live-status');
expect(statusRegion?.textContent).toContain('Labeled element');
}, 600);
});
it('should fallback to text content', () => {
const element = document.createElement('div');
element.textContent = 'Element text';
screenReaderSupport.announceContainerChange(element, 'sm', 400);
setTimeout(() => {
const statusRegion = document.getElementById('proteus-live-status');
expect(statusRegion?.textContent).toContain('Element text');
}, 600);
});
it('should fallback to tag name and class', () => {
const element = document.createElement('section');
element.className = 'content-area';
screenReaderSupport.announceContainerChange(element, 'md', 600);
setTimeout(() => {
const statusRegion = document.getElementById('proteus-live-status');
expect(statusRegion?.textContent).toContain('section with class content-area');
}, 600);
});
});
describe('Cleanup', () => {
it('should clean up resources on destroy', () => {
const customRegion = screenReaderSupport.createLiveRegion('cleanup-test', {
type: 'status',
atomic: false,
relevant: 'additions',
busy: false
});
expect(document.getElementById('proteus-live-cleanup-test')).toBeTruthy();
screenReaderSupport.destroy();
expect(document.getElementById('proteus-live-status')).toBeNull();
expect(document.getElementById('proteus-live-alert')).toBeNull();
expect(document.getElementById('proteus-live-cleanup-test')).toBeNull();
});
it('should handle multiple destroy calls gracefully', () => {
expect(() => {
screenReaderSupport.destroy();
screenReaderSupport.destroy();
}).not.toThrow();
});
});
describe('Error Handling', () => {
it('should handle invalid live region updates', () => {
expect(() => {
screenReaderSupport.updateLiveRegion('non-existent', 'content');
}).not.toThrow();
});
it('should handle invalid elements in ARIA methods', () => {
expect(() => {
screenReaderSupport.applyAriaLabels(null as any, {});
}).not.toThrow();
expect(() => {
screenReaderSupport.manageFocus(null as any, 'test');
}).not.toThrow();
});
});
});