Add test IDs and accessibility to editor and preview components

Third iteration of comprehensive accessibility improvements:
- Added test IDs to CodeEditorSection (preview checkbox, editor containers, error message)
- Added test IDs and roles to ReactPreview component (loading, error, unsupported states)
- Enhanced preview components with proper ARIA labels and status roles
- Added test IDs to demo feature cards with region role
- Improved error states with data-testid for error messages
- Added aria-label attributes to preview state containers

Component updates:
- CodeEditorSection: Enable preview checkbox, editor/preview containers, error display
- ReactPreview: Loading state, error state, unsupported language state
- DemoFeatureCards: Feature cards grid with region role
- Preview state management: Added proper ARIA roles (status, alert)

These additions enable comprehensive testing of:
- Code editor workflows (editing, preview toggling, error states)
- Component preview rendering and error handling
- Different preview states (loading, error, unsupported)
- Demo feature cards navigation and display

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 18:15:42 +00:00
parent f8e94a0fae
commit 2f1cc61c2c
5 changed files with 419 additions and 16 deletions

View File

@@ -0,0 +1,357 @@
# Accessibility & Testing Improvements
## Overview
This document summarizes all accessibility enhancements and data-testid attributes added to the CodeSnippet application. These improvements make the application more accessible to screen reader users and easier to test programmatically.
---
## Navigation Components
### Navigation.tsx
- **Added:** `aria-expanded` - Indicates navigation menu state
- **Added:** `aria-controls="navigation-sidebar"` - Links button to controlled element
- **Added:** `data-testid="navigation-toggle-btn"` - Test identifier for menu toggle
- **Added:** `aria-hidden="true"` to icon - Hides decorative icon from screen readers
### NavigationSidebar.tsx
**Already had:** `data-testid` attributes on overlay and sidebar
- **Added:** `id="navigation-sidebar"` - Enables aria-controls reference
- **Added:** `role="navigation"` - Explicit semantic role
- **Added:** `aria-label="Main navigation menu"` - Descriptive label
- **Navigation items:** `data-testid={`nav-link-${item.path.replace(/\//g, '-')}`}` for dynamic test IDs
- **Sidebar close button:** `data-testid="navigation-sidebar-close-btn"`
### PageLayout.tsx
- **Added:** `data-testid="page-layout"` to main container
- **Added:** `data-testid="page-header"` to header element
- **Added:** `data-testid="logo-text"` to logo
- **Added:** `data-testid="main-content"` to main element
- **Added:** `aria-hidden="true"` to decorative grid-pattern
---
## Snippet Manager Components
### SnippetManagerRedux.tsx
- **Added:** `data-testid="snippet-manager"` to main container
- **Added:** `data-testid="snippet-manager-loading"` to loading state
### SnippetToolbar.tsx
**Already had:** Comprehensive data-testid and aria attributes
- **Enhanced:** Added `data-testid` for template menu items:
- `snippet-template-react-${template.id}`
- `snippet-template-python-${template.id}`
- `snippet-template-javascript-${template.id}`
- **Already present:**
- `data-testid="snippet-search-input"` with `aria-label`
- `data-testid="snippet-selection-mode-btn"` with `aria-pressed`
- `data-testid="snippet-create-menu-trigger"`
### SnippetCard.tsx
**Already had:** `data-testid={`snippet-card-${snippet.id}`}` and `role="article"`
### SnippetCardActions.tsx
**Already had:** Comprehensive data-testid attributes:
- `snippet-card-view-btn`
- `snippet-card-copy-btn`
- `snippet-card-edit-btn`
- `snippet-card-actions-menu` (three-dot menu)
- `snippet-card-move-submenu`
- `move-to-namespace-${namespace.id}`
- `snippet-card-delete-btn`
- All buttons have appropriate `aria-label` attributes
### SelectionControls.tsx
- **Added:** `data-testid="selection-controls"` to main container
- **Added:** `data-testid="select-all-btn"` with `aria-label`
- **Added:** `data-testid="selection-count"` to count display
- **Added:** `data-testid="bulk-move-menu-trigger"` with `aria-label`
- **Added:** `data-testid="bulk-move-menu"` to menu container
- **Added:** `data-testid={`bulk-move-to-namespace-${namespace.id}`}` for menu items
- **Added:** `aria-hidden="true"` to folder icon
### SnippetGrid.tsx
**Already had:** `data-testid="snippet-grid"` with `role="region"` and `aria-label`
### NamespaceSelector.tsx
**Already had:** Comprehensive data-testid:
- `namespace-selector-trigger`
- `namespace-selector-content`
- `namespace-option-${namespace.id}`
---
## Form Components
### SnippetFormFields.tsx
**Already had:** Excellent accessibility implementation:
- **Title input:**
- `data-testid="snippet-title-input"`
- `aria-invalid={!!errors.title}`
- `aria-describedby="title-error"` (when error present)
- **Language select:**
- `data-testid="snippet-language-select"` with `aria-label`
- `data-testid="snippet-language-options"` on content
- `data-testid={`language-option-${lang}`}` on items
- **Description textarea:**
- `data-testid="snippet-description-textarea"`
- `aria-label="Snippet description"`
### SnippetDialog.tsx
**Already had:**
- `data-testid="snippet-dialog"` on DialogContent
- `data-testid="snippet-dialog-cancel-btn"`
- `data-testid="snippet-dialog-save-btn"`
### SnippetViewer.tsx
- **Added:** `data-testid="snippet-viewer-dialog"` on DialogContent
### SnippetViewerHeader.tsx
- **Added:** `data-testid="snippet-viewer-toggle-preview-btn"` with `aria-pressed` and `aria-label`
- **Added:** `data-testid="snippet-viewer-copy-btn"` with descriptive `aria-label`
- **Added:** `data-testid="snippet-viewer-edit-btn"` with `aria-label`
- **Added:** `aria-hidden="true"` to all icons
---
## UI Primitive Components
### button.tsx
**Passes through all props** - Supports data-testid and accessibility attributes
### input.tsx
**Passes through all props** - Supports data-testid and accessibility attributes
### dialog.tsx
- **DialogOverlay:**
- **Added:** `aria-hidden="true"` - Hides scrim from accessibility tree
- **Added:** `data-testid="dialog-overlay"` - Test identifier
- **DialogContent close button:**
- **Enhanced:** `aria-label="Close dialog"` (more descriptive)
- **Added:** `data-testid="dialog-close-btn"`
### sonner.tsx (Toast component)
- **Added:** `data-testid="toast-container"` to Toaster component
- **Added:** `position="bottom-right"` for consistent positioning
- ✅ Sonner library automatically manages `aria-live` and `role="status"` on toast notifications
---
## Accessibility Patterns Implemented
### ARIA Attributes
| Attribute | Component | Purpose |
|-----------|-----------|---------|
| `aria-label` | Multiple | Provides accessible name for buttons and regions |
| `aria-expanded` | Navigation toggle | Indicates menu open/closed state |
| `aria-pressed` | Toggle buttons | Indicates pressed state for toggles |
| `aria-hidden` | Decorative elements | Hides non-semantic elements from screen readers |
| `aria-controls` | Navigation button | Links trigger to controlled element |
| `aria-modal` | Dialog | Marks dialog as modal |
| `role="dialog"` | Dialog container | Semantic dialog role |
| `aria-describedby` | Form inputs | Links errors to input fields |
| `aria-invalid` | Form inputs | Marks invalid form fields |
| `aria-label` | Regions | Descriptive labels for landmark regions |
### Semantic HTML
-`<nav>` - Navigation sections
-`<main>` - Main content area
-`<header>` - Page header
-`<footer>` - Page footer
-`<aside>` - Sidebar (if present)
-`<article>` - Snippet cards
-`role="region"` - Content grids
---
## Testing Identifiers (data-testid)
### Navigation
```
- navigation-toggle-btn
- navigation-sidebar
- navigation-sidebar-overlay
- navigation-sidebar-close-btn
- navigation-items
- nav-link-{path}
```
### Layout
```
- page-layout
- page-header
- logo-text
- main-content
- dialog-overlay
- dialog-close-btn
```
### Snippet Manager
```
- snippet-manager
- snippet-manager-loading
- snippet-search-input
- snippet-selection-mode-btn
- snippet-create-menu-trigger
- snippet-template-react-{id}
- snippet-template-python-{id}
- snippet-template-javascript-{id}
- snippet-card-{id}
- snippet-grid
```
### Snippet Actions
```
- snippet-card-view-btn
- snippet-card-copy-btn
- snippet-card-edit-btn
- snippet-card-actions-menu
- snippet-card-move-submenu
- move-to-namespace-{id}
- snippet-card-delete-btn
```
### Selection & Bulk Operations
```
- selection-controls
- select-all-btn
- selection-count
- bulk-move-menu-trigger
- bulk-move-menu
- bulk-move-to-namespace-{id}
```
### Forms
```
- snippet-title-input
- snippet-language-select
- snippet-language-options
- language-option-{lang}
- snippet-description-textarea
- snippet-dialog
- snippet-dialog-cancel-btn
- snippet-dialog-save-btn
```
### Snippet Viewer
```
- snippet-viewer-dialog
- snippet-viewer-toggle-preview-btn
- snippet-viewer-copy-btn
- snippet-viewer-edit-btn
```
### Namespaces
```
- namespace-selector-trigger
- namespace-selector-content
- namespace-option-{id}
```
### Toasts
```
- toast-container
```
---
## Best Practices Applied
### 1. **Icon Accessibility**
- All decorative icons have `aria-hidden="true"`
- Icon buttons always have `aria-label` or contextual button text
### 2. **Form Accessibility**
- Error messages linked via `aria-describedby`
- Invalid fields marked with `aria-invalid="true"`
- Labels properly associated with inputs via `htmlFor`
### 3. **Interactive Elements**
- Buttons have descriptive labels
- Toggle buttons use `aria-pressed` for state
- Dropdowns have clear trigger labels
### 4. **Screen Reader Support**
- All interactive regions have `aria-label` or `aria-labelledby`
- Decorative elements properly hidden
- Loading states announced with text
- Status messages using Sonner's built-in `aria-live="polite"`
### 5. **Testing Support**
- Semantic data-testid naming convention
- IDs include context (e.g., `snippet-card-{id}`)
- Dynamic IDs parameterized for consistency
---
## Testing Usage Examples
### Selecting Elements in Tests
```typescript
// Navigation
cy.get('[data-testid="navigation-toggle-btn"]').click()
cy.get('[data-testid="navigation-sidebar"]').should('be.visible')
// Snippet Management
cy.get('[data-testid="snippet-search-input"]').type('react')
cy.get('[data-testid="snippet-card-abc123"]').click()
cy.get('[data-testid="snippet-card-copy-btn"]').click()
// Forms
cy.get('[data-testid="snippet-title-input"]').type('New Snippet')
cy.get('[data-testid="snippet-dialog-save-btn"]').click()
// Selection
cy.get('[data-testid="select-all-btn"]').click()
cy.get('[data-testid="bulk-move-menu-trigger"]').click()
```
---
## Accessibility Testing Checklist
### Screen Reader Testing
- [ ] Test with NVDA (Windows) or JAWS
- [ ] Test with VoiceOver (macOS)
- [ ] Verify all buttons have descriptive labels
- [ ] Verify form errors are announced
### Keyboard Navigation
- [ ] All interactive elements are keyboard accessible
- [ ] Tab order is logical (automatic with semantic HTML)
- [ ] Modal dialogs trap focus
- [ ] Escape key closes modals
### Visual Accessibility
- [ ] Sufficient color contrast (WCAG AA minimum)
- [ ] Focus indicators are visible
- [ ] Text is resizable
- [ ] No information conveyed by color alone
### Automated Testing
- [ ] Use `data-testid` for E2E tests
- [ ] Run axe-core or similar for automated checks
- [ ] Test responsive design with screen readers
---
## Future Enhancements
1. **Add keyboard shortcuts** - Document and implement vim-style shortcuts
2. **Enhanced tooltips** - Add aria-describedby tooltips for complex UI
3. **Loading states** - Add aria-busy for async operations
4. **Pagination** - Add aria-label for pagination controls
5. **Code syntax highlighting** - Ensure color contrast in code blocks
6. **Animations** - Add prefers-reduced-motion support
7. **Focus management** - Restore focus after dialog close
8. **Landmark navigation** - Skip to main content link
---
## References
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
- [ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
- [Material Design 3 Accessibility](https://m3.material.io/foundations/accessible-design)
- [React Accessibility Guide](https://reactjs.org/docs/accessibility.html)

View File

@@ -4,6 +4,19 @@ import React from 'react'
// Make React globally available for components that may reference it
global.React = React
// Mock import.meta for Vite compatibility
Object.defineProperty(globalThis, 'import', {
value: {
meta: {
env: {
DEV: false,
PROD: true,
SSR: false,
},
},
},
})
// Mock Next.js router
jest.mock('next/router', () => ({
useRouter: () => ({

View File

@@ -4,8 +4,13 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
export const DemoFeatureCards = () => {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="border-primary/20">
<div
className="grid grid-cols-1 md:grid-cols-3 gap-6"
data-testid="demo-feature-cards"
role="region"
aria-label="Feature cards"
>
<Card className="border-primary/20" data-testid="feature-card-realtime">
<CardHeader>
<CardTitle className="text-lg">Real-Time Updates</CardTitle>
</CardHeader>
@@ -14,7 +19,7 @@ export const DemoFeatureCards = () => {
</CardContent>
</Card>
<Card className="border-accent/20">
<Card className="border-accent/20" data-testid="feature-card-resizable">
<CardHeader>
<CardTitle className="text-lg">Resizable Panels</CardTitle>
</CardHeader>
@@ -23,7 +28,7 @@ export const DemoFeatureCards = () => {
</CardContent>
</Card>
<Card className="border-primary/20">
<Card className="border-primary/20" data-testid="feature-card-viewmodes">
<CardHeader>
<CardTitle className="text-lg">Multiple View Modes</CardTitle>
</CardHeader>

View File

@@ -36,8 +36,14 @@ export function CodeEditorSection({
id="hasPreview"
checked={hasPreview}
onCheckedChange={(checked) => onPreviewChange(checked as boolean)}
data-testid="enable-preview-checkbox"
aria-label="Enable live preview"
/>
<Label htmlFor="hasPreview" className="text-sm font-normal cursor-pointer">
<Label
htmlFor="hasPreview"
className="text-sm font-normal cursor-pointer"
data-testid="enable-preview-label"
>
Enable live preview
</Label>
</div>
@@ -46,7 +52,10 @@ export function CodeEditorSection({
<div className="space-y-2">
<Label htmlFor="code">Code *</Label>
{hasPreview && isPreviewSupported ? (
<div className={errors.code ? 'ring-2 ring-destructive/20 rounded-md' : ''}>
<div
className={errors.code ? 'ring-2 ring-destructive/20 rounded-md' : ''}
data-testid="split-screen-editor-container"
>
<SplitScreenEditor
value={code}
onChange={onCodeChange}
@@ -61,11 +70,16 @@ export function CodeEditorSection({
className={`rounded-md border overflow-hidden ${
errors.code ? 'border-destructive ring-2 ring-destructive/20' : 'border-border'
}`}
data-testid="code-editor-container"
>
<MonacoEditor value={code} onChange={onCodeChange} language={language} height="400px" />
</div>
)}
{errors.code && <p className="text-sm text-destructive">{errors.code}</p>}
{errors.code && (
<p className="text-sm text-destructive" id="code-error" data-testid="code-error-message">
{errors.code}
</p>
)}
</div>
</>
)

View File

@@ -33,9 +33,14 @@ export function ReactPreview({ code, language, functionName, inputParameters }:
if (!['JSX', 'TSX', 'JavaScript', 'TypeScript'].includes(language)) {
return (
<div className="h-full flex items-center justify-center p-6 bg-muted/30">
<div
className="h-full flex items-center justify-center p-6 bg-muted/30"
data-testid="preview-unsupported"
role="status"
aria-label="Preview not available"
>
<div className="text-center text-muted-foreground">
<WarningCircle className="h-12 w-12 mx-auto mb-3 opacity-50" />
<WarningCircle className="h-12 w-12 mx-auto mb-3 opacity-50" aria-hidden="true" />
<p className="text-sm">Preview not available for {language}</p>
<p className="text-xs mt-1">Use JSX, TSX, JavaScript, or TypeScript</p>
</div>
@@ -45,15 +50,19 @@ export function ReactPreview({ code, language, functionName, inputParameters }:
if (error) {
return (
<div className="h-full overflow-auto p-6 bg-destructive/5">
<Alert variant="destructive" className="mb-4">
<WarningCircle className="h-4 w-4" />
<AlertDescription className="font-mono text-xs whitespace-pre-wrap">
<div
className="h-full overflow-auto p-6 bg-destructive/5"
data-testid="preview-error"
role="alert"
>
<Alert variant="destructive" className="mb-4" data-testid="preview-error-alert">
<WarningCircle className="h-4 w-4" aria-hidden="true" />
<AlertDescription className="font-mono text-xs whitespace-pre-wrap" data-testid="preview-error-message">
{error}
</AlertDescription>
</Alert>
<AIErrorHelper
error={error}
<AIErrorHelper
error={error}
context={`React component preview rendering (Language: ${language})`}
/>
</div>
@@ -62,7 +71,12 @@ export function ReactPreview({ code, language, functionName, inputParameters }:
if (!Component) {
return (
<div className="h-full flex items-center justify-center p-6 bg-muted/30">
<div
className="h-full flex items-center justify-center p-6 bg-muted/30"
data-testid="preview-loading"
role="status"
aria-label="Loading preview"
>
<div className="text-center text-muted-foreground">
<p className="text-sm">Loading preview...</p>
</div>