Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand accessibility testing fundamentals ๐ฏ
- Apply accessibility testing in real projects ๐๏ธ
- Debug common accessibility issues ๐
- Write type-safe accessibility tests โจ
๐ฏ Introduction
Welcome to this exciting tutorial on accessibility testing with TypeScript! ๐ In this guide, weโll explore how to test that your applications are accessible to everyone, including users with disabilities.
Youโll discover how accessibility testing (a11y) can transform your development process and make your applications truly inclusive. Whether youโre building web applications ๐, React components โ๏ธ, or Node.js APIs ๐ฅ๏ธ, understanding accessibility testing is essential for creating apps that work for everyone.
By the end of this tutorial, youโll feel confident testing and ensuring accessibility in your TypeScript projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Accessibility Testing
๐ค What is Accessibility Testing?
Accessibility testing is like having a helpful guide ๐ฆฎ that ensures your app can be used by everyone, regardless of their abilities. Think of it as building ramps alongside stairs ๐ - youโre making sure everyone can access your digital building!
In TypeScript terms, accessibility testing involves automated checks, type-safe test utilities, and structured validation of your UI components. This means you can:
- โจ Catch accessibility issues before they reach users
- ๐ Automate compliance checks with WCAG guidelines
- ๐ก๏ธ Ensure screen readers work perfectly with your app
๐ก Why Use Accessibility Testing?
Hereโs why developers love accessibility testing:
- Legal Compliance ๐: Meet ADA and WCAG requirements
- Better User Experience ๐ป: Works for everyone, not just some users
- Quality Assurance ๐: Catch issues early in development
- Market Reach ๐ง: Reach 15% more users (people with disabilities)
Real-world example: Imagine building an online store ๐. With accessibility testing, you can ensure users with visual impairments can navigate your product catalog using screen readers, and users with motor disabilities can complete checkout using only keyboard navigation.
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example using Jest and Testing Library:
// ๐ Hello, Accessibility Testing!
import { render, screen } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
// ๐จ Extend Jest matchers for accessibility
expect.extend(toHaveNoViolations);
// ๐ท๏ธ Define our component props
interface ButtonProps {
children: React.ReactNode; // ๐ถ Button content
onClick: () => void; // ๐ฏ Click handler
disabled?: boolean; // ๐ซ Disabled state
}
// ๐ฑ Simple accessible button component
const AccessibleButton: React.FC<ButtonProps> = ({
children,
onClick,
disabled = false
}) => (
<button
onClick={onClick}
disabled={disabled}
aria-label={typeof children === 'string' ? children : 'Button'}
>
{children}
</button>
);
๐ก Explanation: Notice how weโre already thinking about accessibility by adding aria-label
attributes and using semantic HTML elements!
๐ฏ Common Testing Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Basic accessibility test
describe('AccessibleButton', () => {
it('should be accessible', async () => {
const { container } = render(
<AccessibleButton onClick={() => console.log('Clicked! ๐')}>
Save Changes โจ
</AccessibleButton>
);
// ๐งช Run accessibility checks
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
// ๐จ Pattern 2: Screen reader testing
describe('Screen Reader Support', () => {
it('should provide proper labels', () => {
render(<AccessibleButton onClick={() => {}}>Delete ๐๏ธ</AccessibleButton>);
// ๐ Check if screen readers can find the button
const button = screen.getByRole('button', { name: 'Delete ๐๏ธ' });
expect(button).toBeInTheDocument();
});
});
// ๐ Pattern 3: Keyboard navigation
describe('Keyboard Navigation', () => {
it('should be focusable', () => {
render(<AccessibleButton onClick={() => {}}>Focus Me! ๐ฏ</AccessibleButton>);
const button = screen.getByRole('button');
button.focus();
expect(button).toHaveFocus();
});
});
๐ก Practical Examples
๐ Example 1: E-commerce Product Card
Letโs test a real-world component:
// ๐๏ธ Define our product type
interface Product {
id: string;
name: string;
price: number;
image: string;
alt: string; // ๐ผ๏ธ Alt text for images!
inStock: boolean;
}
// ๐จ Accessible product card component
interface ProductCardProps {
product: Product;
onAddToCart: (productId: string) => void;
}
const ProductCard: React.FC<ProductCardProps> = ({ product, onAddToCart }) => (
<article
role="article"
aria-labelledby={`product-${product.id}`}
className="product-card"
>
<img
src={product.image}
alt={product.alt}
role="img"
/>
<h3 id={`product-${product.id}`}>{product.name}</h3>
<p aria-label={`Price: ${product.price} dollars`}>
${product.price}
</p>
<button
onClick={() => onAddToCart(product.id)}
disabled={!product.inStock}
aria-describedby={`stock-${product.id}`}
>
{product.inStock ? 'Add to Cart ๐' : 'Out of Stock ๐'}
</button>
<span
id={`stock-${product.id}`}
className="sr-only"
>
{product.inStock ? 'In stock' : 'Currently unavailable'}
</span>
</article>
);
// ๐งช Comprehensive accessibility tests
describe('ProductCard Accessibility', () => {
const mockProduct: Product = {
id: '1',
name: 'TypeScript Handbook ๐',
price: 29.99,
image: '/typescript-book.jpg',
alt: 'TypeScript Handbook cover with blue background',
inStock: true
};
it('should have no accessibility violations', async () => {
const { container } = render(
<ProductCard
product={mockProduct}
onAddToCart={() => console.log('Added to cart! ๐')}
/>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should be navigable by screen readers', () => {
render(
<ProductCard
product={mockProduct}
onAddToCart={() => {}}
/>
);
// ๐ Check semantic structure
expect(screen.getByRole('article')).toBeInTheDocument();
expect(screen.getByRole('img')).toHaveAttribute('alt', mockProduct.alt);
expect(screen.getByRole('button', { name: /add to cart/i })).toBeEnabled();
});
it('should handle out of stock state accessibly', () => {
const outOfStockProduct = { ...mockProduct, inStock: false };
render(
<ProductCard
product={outOfStockProduct}
onAddToCart={() => {}}
/>
);
const button = screen.getByRole('button', { name: /out of stock/i });
expect(button).toBeDisabled();
expect(button).toHaveAttribute('aria-describedby', 'stock-1');
});
});
๐ฏ Try it yourself: Add tests for keyboard navigation and color contrast!
๐ฅ Example 2: Healthcare Form
Letโs test a critical form with proper error handling:
// ๐ฅ Patient information form
interface PatientFormProps {
onSubmit: (data: PatientData) => void;
}
interface PatientData {
firstName: string;
lastName: string;
dateOfBirth: string;
email: string;
phone: string;
}
const PatientForm: React.FC<PatientFormProps> = ({ onSubmit }) => {
const [errors, setErrors] = useState<Record<string, string>>({});
const [formData, setFormData] = useState<PatientData>({
firstName: '',
lastName: '',
dateOfBirth: '',
email: '',
phone: ''
});
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.firstName.trim()) {
newErrors.firstName = 'First name is required';
}
if (!formData.email.includes('@')) {
newErrors.email = 'Please enter a valid email address';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
onSubmit(formData);
}
};
return (
<form onSubmit={handleSubmit} noValidate>
<fieldset>
<legend>Patient Information ๐ฅ</legend>
<div className="form-group">
<label htmlFor="firstName">
First Name *
</label>
<input
id="firstName"
type="text"
value={formData.firstName}
onChange={(e) => setFormData({...formData, firstName: e.target.value})}
aria-invalid={!!errors.firstName}
aria-describedby={errors.firstName ? 'firstName-error' : undefined}
required
/>
{errors.firstName && (
<span id="firstName-error" role="alert" className="error">
โ ๏ธ {errors.firstName}
</span>
)}
</div>
<div className="form-group">
<label htmlFor="email">
Email Address *
</label>
<input
id="email"
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
required
/>
{errors.email && (
<span id="email-error" role="alert" className="error">
โ ๏ธ {errors.email}
</span>
)}
</div>
<button type="submit">
Submit Information โ
</button>
</fieldset>
</form>
);
};
// ๐งช Accessibility testing for forms
describe('PatientForm Accessibility', () => {
it('should have proper form structure', async () => {
const { container } = render(
<PatientForm onSubmit={() => {}} />
);
// ๐ Check form accessibility
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should properly label form controls', () => {
render(<PatientForm onSubmit={() => {}} />);
// ๐ท๏ธ Check labels are properly associated
expect(screen.getByLabelText('First Name *')).toBeInTheDocument();
expect(screen.getByLabelText('Email Address *')).toBeInTheDocument();
expect(screen.getByRole('group', { name: 'Patient Information ๐ฅ' })).toBeInTheDocument();
});
it('should announce errors to screen readers', async () => {
const mockSubmit = jest.fn();
render(<PatientForm onSubmit={mockSubmit} />);
// ๐ฏ Submit empty form to trigger errors
const submitButton = screen.getByRole('button', { name: /submit information/i });
await userEvent.click(submitButton);
// ๐จ Check error announcements
expect(screen.getByRole('alert')).toHaveTextContent('First name is required');
expect(screen.getByLabelText('First Name *')).toHaveAttribute('aria-invalid', 'true');
});
});
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Custom Accessibility Matchers
When youโre ready to level up, create custom test utilities:
// ๐ฏ Advanced accessibility testing utilities
interface AccessibilityTestUtils {
checkColorContrast: (element: HTMLElement) => Promise<boolean>;
checkFocusManagement: (container: HTMLElement) => Promise<boolean>;
checkAriaLabels: (container: HTMLElement) => ValidationResult;
}
// ๐ช Custom accessibility matcher
const customAccessibilityMatchers = {
toBeAccessible: async (received: HTMLElement) => {
const results = await axe(received);
const violations = results.violations;
if (violations.length === 0) {
return {
message: () => 'โ
Element is accessible!',
pass: true
};
}
return {
message: () => `
โ Accessibility violations found:
${violations.map(v => `- ${v.description}`).join('\n')}
`,
pass: false
};
}
};
// ๐ Advanced testing setup
declare global {
namespace jest {
interface Matchers<R> {
toBeAccessible(): Promise<R>;
}
}
}
expect.extend(customAccessibilityMatchers);
๐๏ธ Advanced Topic 2: Accessibility Testing Pipeline
For the brave developers who want full automation:
// ๐ Accessibility testing configuration
interface A11yConfig {
rules: {
[key: string]: { enabled: boolean; tags?: string[] };
};
tags: string[];
reporter: 'spec' | 'json' | 'html';
}
// ๐จ Custom accessibility test suite
class AccessibilityTestSuite {
private config: A11yConfig;
constructor(config: A11yConfig) {
this.config = config;
}
async runFullSuite(component: React.ComponentType): Promise<TestResults> {
const results = {
violations: [] as any[],
passes: [] as any[],
incomplete: [] as any[]
};
// ๐งช Run multiple accessibility checks
const checks = [
this.checkKeyboardNavigation(component),
this.checkScreenReader(component),
this.checkColorContrast(component),
this.checkFocusManagement(component)
];
const checkResults = await Promise.all(checks);
return {
...results,
summary: {
total: checkResults.length,
passed: checkResults.filter(r => r.passed).length,
failed: checkResults.filter(r => !r.passed).length
}
};
}
private async checkKeyboardNavigation(component: React.ComponentType) {
// ๐ฏ Implementation details...
return { passed: true, details: 'Keyboard navigation works! โจ๏ธ' };
}
private async checkScreenReader(component: React.ComponentType) {
// ๐ Implementation details...
return { passed: true, details: 'Screen reader support excellent! ๐ข' };
}
private async checkColorContrast(component: React.ComponentType) {
// ๐จ Implementation details...
return { passed: true, details: 'Color contrast meets WCAG standards! ๐' };
}
private async checkFocusManagement(component: React.ComponentType) {
// ๐ฏ Implementation details...
return { passed: true, details: 'Focus management is perfect! โจ' };
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Forgetting Alt Text
// โ Wrong way - missing alt text!
const BadImage: React.FC = () => (
<img src="/product.jpg" /> // ๐ฅ Screen readers can't describe this!
);
// โ
Correct way - always include alt text!
interface GoodImageProps {
src: string;
alt: string;
decorative?: boolean;
}
const GoodImage: React.FC<GoodImageProps> = ({ src, alt, decorative = false }) => (
<img
src={src}
alt={decorative ? '' : alt}
role={decorative ? 'presentation' : 'img'}
/>
);
// ๐งช Test it properly
describe('Image Accessibility', () => {
it('should have proper alt text', () => {
render(<GoodImage src="/product.jpg" alt="TypeScript book cover" />);
const image = screen.getByRole('img');
expect(image).toHaveAttribute('alt', 'TypeScript book cover');
});
});
๐คฏ Pitfall 2: Poor Focus Management
// โ Dangerous - focus gets lost!
const BadModal: React.FC = ({ children, onClose }) => (
<div className="modal">
{children}
<button onClick={onClose}>Close</button> // ๐ฅ Focus issues!
</div>
);
// โ
Safe - proper focus management!
const GoodModal: React.FC<{children: React.ReactNode; onClose: () => void}> = ({
children,
onClose
}) => {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// ๐ฏ Focus the modal when it opens
if (modalRef.current) {
modalRef.current.focus();
}
// ๐ Return focus when modal closes
const previouslyFocused = document.activeElement as HTMLElement;
return () => {
previouslyFocused?.focus();
};
}, []);
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
className="modal"
>
<h2 id="modal-title">Modal Title</h2>
{children}
<button onClick={onClose} aria-label="Close modal">
Close โ
</button>
</div>
);
};
๐ ๏ธ Best Practices
- ๐ฏ Test Early: Include accessibility tests in your development workflow
- ๐ Use Semantic HTML: Proper elements provide built-in accessibility
- ๐ก๏ธ Automate Testing: Use tools like axe-core and jest-axe
- ๐จ Test Real Scenarios: Test with actual screen readers and keyboard navigation
- โจ Progressive Enhancement: Start accessible, then add features
๐งช Hands-On Exercise
๐ฏ Challenge: Build an Accessible Shopping Cart
Create a fully accessible shopping cart component with TypeScript:
๐ Requirements:
- โ Add/remove items with proper announcements
- ๐ท๏ธ Screen reader support for cart totals
- ๐ค Keyboard navigation for all actions
- ๐ Live updates for cart changes
- ๐จ WCAG 2.1 AA compliance
๐ Bonus Points:
- Add focus management for add/remove actions
- Implement proper error announcements
- Create comprehensive accessibility tests
๐ก Solution
๐ Click to see solution
// ๐ฏ Our type-safe accessible shopping cart!
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
emoji: string;
}
interface CartProps {
items: CartItem[];
onUpdateQuantity: (id: string, quantity: number) => void;
onRemoveItem: (id: string) => void;
}
const AccessibleShoppingCart: React.FC<CartProps> = ({
items,
onUpdateQuantity,
onRemoveItem
}) => {
const [announcements, setAnnouncements] = useState<string>('');
const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
const announce = (message: string) => {
setAnnouncements(message);
setTimeout(() => setAnnouncements(''), 1000);
};
const handleQuantityChange = (id: string, newQuantity: number) => {
if (newQuantity === 0) {
onRemoveItem(id);
announce('Item removed from cart');
} else {
onUpdateQuantity(id, newQuantity);
announce(`Quantity updated to ${newQuantity}`);
}
};
return (
<section aria-labelledby="cart-title">
<h2 id="cart-title">Shopping Cart ๐</h2>
{/* ๐ข Screen reader announcements */}
<div aria-live="polite" aria-atomic="true" className="sr-only">
{announcements}
</div>
{/* ๐ Cart summary */}
<div role="status" aria-label={`Cart contains ${itemCount} items, total ${total.toFixed(2)} dollars`}>
<p>Items: {itemCount} | Total: ${total.toFixed(2)}</p>
</div>
{items.length === 0 ? (
<p>Your cart is empty ๐</p>
) : (
<ul role="list">
{items.map(item => (
<li key={item.id} role="listitem">
<article aria-labelledby={`item-${item.id}`}>
<h3 id={`item-${item.id}`}>
{item.emoji} {item.name}
</h3>
<p>${item.price.toFixed(2)} each</p>
<div role="group" aria-labelledby={`quantity-${item.id}`}>
<label id={`quantity-${item.id}`} htmlFor={`qty-${item.id}`}>
Quantity:
</label>
<input
id={`qty-${item.id}`}
type="number"
min="0"
value={item.quantity}
onChange={(e) => handleQuantityChange(item.id, parseInt(e.target.value))}
aria-describedby={`subtotal-${item.id}`}
/>
<span id={`subtotal-${item.id}`}>
Subtotal: ${(item.price * item.quantity).toFixed(2)}
</span>
</div>
<button
onClick={() => onRemoveItem(item.id)}
aria-label={`Remove ${item.name} from cart`}
>
Remove ๐๏ธ
</button>
</article>
</li>
))}
</ul>
)}
</section>
);
};
// ๐งช Comprehensive accessibility tests
describe('AccessibleShoppingCart', () => {
const mockItems: CartItem[] = [
{ id: '1', name: 'TypeScript Book', price: 29.99, quantity: 1, emoji: '๐' },
{ id: '2', name: 'Coffee Mug', price: 12.99, quantity: 2, emoji: 'โ' }
];
it('should have no accessibility violations', async () => {
const { container } = render(
<AccessibleShoppingCart
items={mockItems}
onUpdateQuantity={() => {}}
onRemoveItem={() => {}}
/>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should announce cart updates', async () => {
const mockUpdate = jest.fn();
render(
<AccessibleShoppingCart
items={mockItems}
onUpdateQuantity={mockUpdate}
onRemoveItem={() => {}}
/>
);
// ๐ฏ Update quantity
const quantityInput = screen.getByLabelText('Quantity:');
await userEvent.clear(quantityInput);
await userEvent.type(quantityInput, '3');
expect(screen.getByText('Quantity updated to 3')).toBeInTheDocument();
});
it('should be keyboard navigable', () => {
render(
<AccessibleShoppingCart
items={mockItems}
onUpdateQuantity={() => {}}
onRemoveItem={() => {}}
/>
);
// ๐ฏ Test keyboard navigation
const removeButton = screen.getByRole('button', { name: /remove.*from cart/i });
removeButton.focus();
expect(removeButton).toHaveFocus();
});
});
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Test accessibility with confidence using axe-core ๐ช
- โ Create accessible components that work for everyone ๐ก๏ธ
- โ Implement proper ARIA attributes and semantic HTML ๐ฏ
- โ Debug accessibility issues like a pro ๐
- โ Build inclusive applications with TypeScript! ๐
Remember: Accessibility isnโt just about compliance - itโs about creating better experiences for everyone! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered accessibility testing with TypeScript!
Hereโs what to do next:
- ๐ป Practice with the shopping cart exercise above
- ๐๏ธ Add accessibility tests to your existing projects
- ๐ Move on to our next tutorial: Performance Testing
- ๐ Share your accessible apps with the community!
Remember: Every developer who cares about accessibility is making the web better for everyone. Keep coding, keep testing, and most importantly, keep building inclusive experiences! ๐
Happy coding! ๐๐โจ