mirror of https://github.com/docusealco/docuseal
This commit establishes the foundation for WCAG 2.2 Level AA compliance: Infrastructure: - Add axe-core-rspec gem for automated accessibility testing - Create spec/accessibility/ directory structure - Add accessibility_helpers.rb with custom WCAG test helpers - Add comprehensive testing documentation (README.md, SETUP_NOTES.md) Semantic Landmarks (WCAG 2.4.1): - Add <main> landmark with id="main-content" to application layout - Add <nav> landmark with aria-label to navbar - Add skip navigation link for keyboard users - Skip link uses focus:translate-y-0 to appear only on keyboard focus These changes address critical barriers for screen reader and keyboard users. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>pull/599/head
parent
bb7a233863
commit
aa9cb026a9
@ -0,0 +1,201 @@
|
||||
# Accessibility Testing Framework
|
||||
|
||||
This directory contains accessibility (a11y) tests for DocuSeal, targeting **WCAG 2.2 Level AA compliance**.
|
||||
|
||||
## Overview
|
||||
|
||||
The accessibility testing framework uses:
|
||||
- **RSpec** for test structure
|
||||
- **Capybara** with **Cuprite** (headless Chrome) for browser automation
|
||||
- **axe-core-rspec** for automated WCAG validation
|
||||
- **Custom accessibility helpers** in `spec/support/accessibility_helpers.rb`
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all accessibility tests
|
||||
bundle exec rspec spec/accessibility/
|
||||
|
||||
# Run specific accessibility test file
|
||||
bundle exec rspec spec/accessibility/layouts_spec.rb
|
||||
|
||||
# Run with visible browser for debugging
|
||||
HEADLESS=false bundle exec rspec spec/accessibility/
|
||||
|
||||
# Run with coverage report
|
||||
COVERAGE=true bundle exec rspec spec/accessibility/
|
||||
```
|
||||
|
||||
## Test Categories
|
||||
|
||||
### Phase 1: Critical Barriers
|
||||
- **layouts_spec.rb** - Semantic landmarks, skip navigation
|
||||
- **images_spec.rb** - Alt text for images
|
||||
- **buttons_spec.rb** - ARIA labels for icon buttons
|
||||
- **keyboard_spec.rb** - Keyboard support for custom elements
|
||||
|
||||
### Phase 2: Forms & Input Accessibility
|
||||
- **forms_spec.rb** - Form error associations, labels
|
||||
- **modals_spec.rb** - Modal accessibility, focus traps
|
||||
- **live_regions_spec.rb** - ARIA live announcements
|
||||
|
||||
### Phase 3: Complex Interactions
|
||||
- **components_spec.rb** - Vue component accessibility
|
||||
- **tables_spec.rb** - Table headers, captions, scope
|
||||
|
||||
### Phase 4: Comprehensive Coverage
|
||||
- **wcag_compliance_spec.rb** - Full WCAG 2.2 AA validation with axe-core
|
||||
|
||||
## Custom Helpers
|
||||
|
||||
### Available Test Helpers
|
||||
|
||||
```ruby
|
||||
# Check accessible names
|
||||
expect_accessible_name('button.save', 'Save Document')
|
||||
|
||||
# Check all images have alt text
|
||||
expect_images_have_alt_text
|
||||
|
||||
# Check form inputs are labeled
|
||||
expect_form_inputs_labeled
|
||||
|
||||
# Check keyboard accessibility
|
||||
expect_keyboard_accessible('button.custom')
|
||||
|
||||
# Check semantic landmarks
|
||||
expect_semantic_landmarks
|
||||
|
||||
# Check buttons have names
|
||||
expect_buttons_have_names
|
||||
|
||||
# Check error associations
|
||||
expect_errors_associated_with_inputs('email_field', 'email_error')
|
||||
|
||||
# Check modal accessibility
|
||||
expect_accessible_modal('#my-modal')
|
||||
|
||||
# Check focus trap
|
||||
expect_focus_trap('#my-modal')
|
||||
|
||||
# Check skip navigation
|
||||
expect_skip_navigation_link
|
||||
```
|
||||
|
||||
## WCAG 2.2 Level AA Criteria
|
||||
|
||||
Our testing targets these success criteria:
|
||||
|
||||
### Level A
|
||||
- **1.1.1** Non-text Content
|
||||
- **1.3.1** Info and Relationships
|
||||
- **2.1.1** Keyboard
|
||||
- **2.1.2** No Keyboard Trap
|
||||
- **2.4.1** Bypass Blocks
|
||||
- **2.4.3** Focus Order
|
||||
- **3.3.1** Error Identification
|
||||
- **3.3.2** Labels or Instructions
|
||||
- **4.1.2** Name, Role, Value
|
||||
|
||||
### Level AA
|
||||
- **1.4.3** Contrast (Minimum)
|
||||
- **2.4.6** Headings and Labels
|
||||
- **2.4.7** Focus Visible
|
||||
- **3.2.4** Consistent Identification
|
||||
- **4.1.3** Status Messages
|
||||
|
||||
## Manual Testing
|
||||
|
||||
Automated tests catch most issues, but manual testing is required for:
|
||||
|
||||
### Screen Reader Testing
|
||||
- **NVDA** (Windows) - Free, open-source
|
||||
- **JAWS** (Windows) - Industry standard, paid
|
||||
- **VoiceOver** (Mac/iOS) - Built-in
|
||||
|
||||
Test these flows:
|
||||
1. Navigate the app with Tab/Shift+Tab only
|
||||
2. Fill out and submit forms
|
||||
3. Open and close modals
|
||||
4. Upload files via file dropzone
|
||||
5. Complete document signing flow
|
||||
|
||||
### Keyboard Navigation
|
||||
Test all interactions with:
|
||||
- **Tab** / **Shift+Tab** - Move focus
|
||||
- **Enter** / **Space** - Activate buttons/links
|
||||
- **Escape** - Close modals
|
||||
- **Arrow keys** - Navigate lists/dropdowns
|
||||
|
||||
### Browser Zoom
|
||||
- Test at 200% zoom level
|
||||
- Verify no content is cut off
|
||||
- Check mobile responsiveness
|
||||
|
||||
### Color Contrast
|
||||
- Use browser DevTools color picker
|
||||
- Verify 4.5:1 ratio for normal text
|
||||
- Verify 3:1 ratio for large text (18pt+)
|
||||
|
||||
## Validation Tools
|
||||
|
||||
### Browser Extensions
|
||||
- **axe DevTools** - Comprehensive WCAG auditing
|
||||
- **WAVE** - Visual accessibility checker
|
||||
- **Lighthouse** - Chrome DevTools built-in
|
||||
|
||||
### Command-Line Tools
|
||||
```bash
|
||||
# Run Lighthouse accessibility audit
|
||||
lighthouse http://localhost:3000 --only-categories=accessibility
|
||||
|
||||
# Run axe-core via CLI
|
||||
npx @axe-core/cli http://localhost:3000
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Add to your CI pipeline:
|
||||
|
||||
```yaml
|
||||
# .github/workflows/accessibility.yml
|
||||
name: Accessibility Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
a11y:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run accessibility tests
|
||||
run: |
|
||||
bundle install
|
||||
bundle exec rspec spec/accessibility/
|
||||
```
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
When accessibility issues are found:
|
||||
|
||||
1. **Severity**: Critical, High, Medium, Low
|
||||
2. **WCAG Criterion**: Which success criterion is violated
|
||||
3. **User Impact**: Which disability groups are affected
|
||||
4. **Location**: File path and line number
|
||||
5. **Recommendation**: Specific fix with code example
|
||||
|
||||
## Resources
|
||||
|
||||
- [WCAG 2.2 Guidelines](https://www.w3.org/WAI/WCAG22/quickref/)
|
||||
- [axe-core Rules](https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md)
|
||||
- [A11Y Project Checklist](https://www.a11yproject.com/checklist/)
|
||||
- [WebAIM Screen Reader Testing](https://webaim.org/articles/screenreader_testing/)
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features:
|
||||
1. Write accessibility tests alongside feature tests
|
||||
2. Run axe-core audit on new pages/components
|
||||
3. Test keyboard navigation manually
|
||||
4. Verify with screen reader if possible
|
||||
5. Document any accessibility considerations
|
||||
@ -0,0 +1,86 @@
|
||||
# Accessibility Testing Setup Notes
|
||||
|
||||
## Gem Installation Required
|
||||
|
||||
The `axe-core-rspec` gem has been added to the Gemfile but requires installation.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
This project requires **Ruby 4.0.1** (as specified in Gemfile).
|
||||
|
||||
Currently, the system Ruby is 2.6.10, which is incompatible. You'll need to:
|
||||
|
||||
1. **Install a Ruby version manager** (recommended: rbenv or asdf)
|
||||
|
||||
```bash
|
||||
# Using rbenv
|
||||
brew install rbenv ruby-build
|
||||
rbenv install 4.0.1
|
||||
rbenv local 4.0.1
|
||||
|
||||
# Or using asdf
|
||||
brew install asdf
|
||||
asdf plugin add ruby
|
||||
asdf install ruby 4.0.1
|
||||
asdf local ruby 4.0.1
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
|
||||
```bash
|
||||
bundle install
|
||||
yarn install
|
||||
```
|
||||
|
||||
3. **Run accessibility tests**
|
||||
|
||||
```bash
|
||||
bundle exec rspec spec/accessibility/
|
||||
```
|
||||
|
||||
## What's Been Set Up
|
||||
|
||||
✅ **Added to Gemfile (test group):**
|
||||
- `axe-core-rspec` - Automated WCAG 2.2 validation
|
||||
|
||||
✅ **Created directory structure:**
|
||||
- `spec/accessibility/` - Test files for a11y specs
|
||||
- `spec/support/` - Helper modules
|
||||
|
||||
✅ **Created accessibility helpers:**
|
||||
- `spec/support/accessibility_helpers.rb` - Custom test helpers for WCAG validation
|
||||
- Methods for checking landmarks, labels, keyboard access, modals, etc.
|
||||
|
||||
✅ **Created documentation:**
|
||||
- `spec/accessibility/README.md` - Comprehensive testing guide
|
||||
- Covers running tests, manual testing, WCAG criteria, and resources
|
||||
|
||||
## Using axe-core-rspec
|
||||
|
||||
Once the gem is installed, you can use it in your tests:
|
||||
|
||||
```ruby
|
||||
# spec/accessibility/wcag_compliance_spec.rb
|
||||
RSpec.describe 'WCAG Compliance', type: :system do
|
||||
it 'passes axe audit on home page' do
|
||||
visit root_path
|
||||
expect(page).to be_axe_clean
|
||||
end
|
||||
|
||||
it 'passes WCAG 2.2 AA on submissions page' do
|
||||
visit submissions_path
|
||||
expect(page).to be_axe_clean.according_to(:wcag2aa, :wcag22aa)
|
||||
end
|
||||
|
||||
it 'passes specific tag checks' do
|
||||
visit template_path
|
||||
expect(page).to be_axe_clean
|
||||
.excluding('.legacy-component')
|
||||
.according_to(:wcag2aa)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
After bundle install completes, the testing infrastructure will be fully operational and ready for Phase 1 fixes.
|
||||
@ -0,0 +1,142 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Accessibility testing helpers for WCAG 2.2 Level AA compliance
|
||||
module AccessibilityHelpers
|
||||
# Check if an element has proper ARIA label or text content
|
||||
def expect_accessible_name(selector, expected_name = nil)
|
||||
element = page.find(selector)
|
||||
|
||||
# Check for accessible name via aria-label, aria-labelledby, or text content
|
||||
accessible_name = element['aria-label'] ||
|
||||
find_aria_labelledby_text(element) ||
|
||||
element.text.strip
|
||||
|
||||
if expected_name
|
||||
expect(accessible_name).to eq(expected_name)
|
||||
else
|
||||
expect(accessible_name).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
# Check if images have alt text
|
||||
def expect_images_have_alt_text
|
||||
images_without_alt = page.all('img:not([alt])')
|
||||
expect(images_without_alt).to be_empty,
|
||||
"Found #{images_without_alt.count} images without alt attributes"
|
||||
end
|
||||
|
||||
# Check if form inputs have associated labels or aria-label
|
||||
def expect_form_inputs_labeled
|
||||
unlabeled_inputs = page.all('input, select, textarea').select do |input|
|
||||
next if input['type'] == 'hidden'
|
||||
next if input['type'] == 'submit'
|
||||
next if input['type'] == 'button'
|
||||
|
||||
# Check for label association, aria-label, or aria-labelledby
|
||||
has_label = input['id'] && page.has_css?("label[for='#{input['id']}']")
|
||||
has_aria = input['aria-label'] || input['aria-labelledby']
|
||||
|
||||
!has_label && !has_aria
|
||||
end
|
||||
|
||||
expect(unlabeled_inputs).to be_empty,
|
||||
"Found #{unlabeled_inputs.count} unlabeled form inputs"
|
||||
end
|
||||
|
||||
# Check if interactive elements are keyboard accessible
|
||||
def expect_keyboard_accessible(selector)
|
||||
element = page.find(selector)
|
||||
|
||||
# Interactive elements should be focusable
|
||||
expect(element['tabindex'].to_i).to be >= 0
|
||||
end
|
||||
|
||||
# Check for proper semantic landmarks
|
||||
def expect_semantic_landmarks
|
||||
expect(page).to have_css('main, [role="main"]'),
|
||||
'Page should have a main landmark'
|
||||
expect(page).to have_css('nav, [role="navigation"]'),
|
||||
'Page should have a navigation landmark'
|
||||
end
|
||||
|
||||
# Check if buttons have accessible names
|
||||
def expect_buttons_have_names
|
||||
unnamed_buttons = page.all('button').select do |button|
|
||||
text = button.text.strip
|
||||
aria_label = button['aria-label']
|
||||
aria_labelledby = button['aria-labelledby']
|
||||
|
||||
text.empty? && !aria_label && !aria_labelledby
|
||||
end
|
||||
|
||||
expect(unnamed_buttons).to be_empty,
|
||||
"Found #{unnamed_buttons.count} buttons without accessible names"
|
||||
end
|
||||
|
||||
# Check color contrast ratio (simplified check for common patterns)
|
||||
def expect_sufficient_contrast(selector)
|
||||
element = page.find(selector)
|
||||
computed_style = page.evaluate_script(
|
||||
"window.getComputedStyle(document.querySelector('#{selector}'))"
|
||||
)
|
||||
|
||||
# This is a basic check - proper contrast testing requires color parsing
|
||||
color = computed_style['color']
|
||||
background = computed_style['background-color']
|
||||
|
||||
expect(color).not_to eq('rgb(209, 213, 219)'), # text-gray-300
|
||||
'Text color has insufficient contrast'
|
||||
end
|
||||
|
||||
# Check if error messages are associated with form fields
|
||||
def expect_errors_associated_with_inputs(input_id, error_id)
|
||||
input = page.find("##{input_id}")
|
||||
expect(input['aria-describedby']).to include(error_id),
|
||||
"Input ##{input_id} should reference error ##{error_id} via aria-describedby"
|
||||
|
||||
# Error should be announced to screen readers
|
||||
error_element = page.find("##{error_id}")
|
||||
expect(error_element['role']).to eq('alert').or eq(nil)
|
||||
end
|
||||
|
||||
# Check if modal dialogs are properly configured
|
||||
def expect_accessible_modal(selector)
|
||||
modal = page.find(selector)
|
||||
|
||||
expect(modal['role']).to eq('dialog'),
|
||||
'Modal should have role="dialog"'
|
||||
expect(modal['aria-modal']).to eq('true'),
|
||||
'Modal should have aria-modal="true"'
|
||||
expect(modal['aria-labelledby']).not_to be_nil,
|
||||
'Modal should have aria-labelledby referencing its title'
|
||||
end
|
||||
|
||||
# Check if focus is trapped within modal
|
||||
def expect_focus_trap(modal_selector)
|
||||
within(modal_selector) do
|
||||
focusable = page.all('a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
||||
expect(focusable.count).to be > 0, 'Modal should contain focusable elements'
|
||||
end
|
||||
end
|
||||
|
||||
# Check for skip navigation link
|
||||
def expect_skip_navigation_link
|
||||
expect(page).to have_css('a[href="#main-content"], a[href="#main"]'),
|
||||
'Page should have a skip navigation link'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_aria_labelledby_text(element)
|
||||
labelledby_id = element['aria-labelledby']
|
||||
return nil unless labelledby_id
|
||||
|
||||
label_element = page.find("##{labelledby_id}", visible: false)
|
||||
label_element&.text&.strip
|
||||
end
|
||||
end
|
||||
|
||||
# Include helpers in system specs
|
||||
RSpec.configure do |config|
|
||||
config.include AccessibilityHelpers, type: :system
|
||||
end
|
||||
Loading…
Reference in new issue