From aa9cb026a90fb461c5d45f72e7a91f6d4ca70197 Mon Sep 17 00:00:00 2001 From: Marcelo Paiva Date: Mon, 9 Feb 2026 13:09:43 -0500 Subject: [PATCH] Add Phase 1 accessibility infrastructure and semantic landmarks 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
landmark with id="main-content" to application layout - Add
<%= render 'shared/body_scripts' %> diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index 6d66674c..75d055b1 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -1,5 +1,5 @@ <%= render 'shared/navbar_warning' %> -
+
+ diff --git a/spec/accessibility/README.md b/spec/accessibility/README.md new file mode 100644 index 00000000..2ce60dbc --- /dev/null +++ b/spec/accessibility/README.md @@ -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 diff --git a/spec/accessibility/SETUP_NOTES.md b/spec/accessibility/SETUP_NOTES.md new file mode 100644 index 00000000..f21818d3 --- /dev/null +++ b/spec/accessibility/SETUP_NOTES.md @@ -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. diff --git a/spec/support/accessibility_helpers.rb b/spec/support/accessibility_helpers.rb new file mode 100644 index 00000000..55b96016 --- /dev/null +++ b/spec/support/accessibility_helpers.rb @@ -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