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