rails_accessibility_testing 1.5.10 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f8824e08bc8db71c7a6598998b44c6b39826f367398c8c98a1ef85366a044e2
4
- data.tar.gz: bd9337c4b6b89deae25b1adfc9dbec92645c934703e930d5f5048c71a59117b5
3
+ metadata.gz: 61344dc6a53b7678ea457fdfca0237fb7dfece5ef7825aaf0449fea1d187737d
4
+ data.tar.gz: c843795ff4c676607b4fde8d207a7055db5a9b128054583964843a6d41cc6ece
5
5
  SHA512:
6
- metadata.gz: cd9fdfced6342b861ae5d1db4ee852b4c566c9572d62dac463fdc0d7566d37ef1201201c36c3af4d45393d24af7f0a504f76f7eaf25322e7025a577eeb3d94dc
7
- data.tar.gz: a2672850fd85a29fb3a304243f1367215c282a34e63df58f5878d274dd31176465afaad23734699124bd6649032b68ff578f0d6a1ae4b6ec853d2f4ffd7ef23c
6
+ metadata.gz: c9e66513f4b0d427e8ae5d1dbd91d4aa1da7d6758d71993ce54cd8a1e083e241c297d76ea15dde99adab4b345165fd22d7ab7d070124fdd123dfa42eccaca049
7
+ data.tar.gz: 4b4a86455dd1e46c4f4b08c4edae141d49f533be9dd3ce86971a52ea4eb7e203b3a0c23e5730c78e04be31d36b0cfb315011201e479049302e9843e1655d74c3
data/CHANGELOG.md CHANGED
@@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [1.7.0] - 2026-02-11
11
+
12
+ ### Changed
13
+ - **Generator Templates Updated**: Improved default configuration based on production best practices
14
+ - `accessibility.yml`: Changed `accessibility_enabled` default from `true` to `false` with detailed CI/CD documentation
15
+ - `rails_a11y.rb`: Added production safety guard (`if defined?(RailsAccessibilityTesting)`) and set `auto_run_checks = false` by default
16
+ - `all_pages_accessibility_spec.rb`: Changed RSpec type from `:system` to `:accessibility` and improved error formatting with unified helper method
17
+ - Updated documentation URLs to correct GitHub Pages link
18
+ - Generator now creates `spec/accessibility/` directory instead of `spec/system/` for better organization
19
+ - **Best Practices Guide**: Added comprehensive [Best Practices guide](GUIDES/best_practices.md) documenting production-tested configuration patterns
20
+
21
+ ### Added
22
+ - **[Best Practices Guide](GUIDES/best_practices.md)**: New guide documenting recommended configuration patterns based on real-world production usage, including CI/CD safety, production guards, and improved error formatting
23
+ - **Template Updates Summary**: Added `TEMPLATE_UPDATES_SUMMARY.md` documenting all template improvements
24
+
25
+ ## [1.6.0] - 2024-12-XX
26
+
27
+ ### Added
28
+ - **Intelligent ERB Template Handling**: Enhanced ERB extractor to preserve dynamic ID structure from ERB templates
29
+ - **Dynamic ID Support**: Static scanner now correctly handles checkbox/radio inputs with ERB-generated IDs (e.g., `collection_answers_<%= question.id %>_<%= option.id %>_`)
30
+ - **Label Matching for Dynamic IDs**: Form labels check now correctly matches labels to inputs with dynamic IDs by preserving ERB placeholder structure
31
+ - **Smart Duplicate ID Detection**: Duplicate ID check now excludes dynamic IDs with ERB placeholders, preventing false positives for checkbox/radio groups in loops
32
+
33
+ ### Fixed
34
+ - Fixed false positive "Form input missing label [id: collection_answers]" for checkbox groups with dynamic IDs
35
+ - Fixed false positive "Duplicate ID 'collection_answers' found" when IDs contain ERB expressions
36
+ - Fixed missing accessible name detection for links with `href="#"` - now only flags links that truly lack accessible names (visible text, aria-label, or aria-labelledby)
37
+ - Improved `label_tag` helper conversion to handle string interpolation in ID arguments
38
+ - Enhanced raw HTML input element processing to preserve ERB structure in attributes
39
+
40
+ ### Changed
41
+ - **ERB Extractor**: Now processes raw HTML elements with ERB in attributes before removing ERB tags, preserving dynamic ID structure
42
+ - **Form Labels Check**: Updated to handle ERB placeholders in IDs when checking for associated labels
43
+ - **Duplicate IDs Check**: Now filters out IDs containing `ERB_CONTENT` placeholders from duplicate detection
44
+ - **Interactive Elements Check**: Enhanced to correctly detect accessible names for links with `href="#"`, avoiding false positives
45
+ - **RSpec Test File**: Refactored `all_pages_accessibility_spec.rb` to extract formatting helper and add proper assertions that fail tests when errors are found
46
+
47
+ ### Documentation
48
+ - Added comprehensive documentation on ERB template handling and dynamic ID processing
49
+ - Updated architecture documentation with details on how dynamic IDs are preserved
50
+ - Added examples and explanations for checkbox/radio groups with ERB-generated IDs
51
+
8
52
  ## [1.5.10] - 2024-12-01
9
53
 
10
54
  ### Changed
@@ -0,0 +1,261 @@
1
+ # Best Practices for Rails Accessibility Testing
2
+
3
+ This guide documents recommended practices for configuring and using Rails Accessibility Testing in your Rails application, based on real-world usage and production experience.
4
+
5
+ ## Configuration Best Practices
6
+
7
+ ### 1. Disable by Default for CI/CD Safety
8
+
9
+ **Recommended:** Set `accessibility_enabled: false` in `config/accessibility.yml`
10
+
11
+ ```yaml
12
+ # config/accessibility.yml
13
+ accessibility_enabled: false
14
+ ```
15
+
16
+ **Why?**
17
+ - Prevents accessibility test failures from blocking your entire CI/CD pipeline
18
+ - Allows other RSpec tests to pass even if accessibility tests fail
19
+ - Gives you control over when to run accessibility checks
20
+ - Enables manual testing: `rspec spec/accessibility/all_pages_accessibility_spec.rb`
21
+
22
+ **When to enable:**
23
+ - Set to `true` when you want accessibility tests to run automatically
24
+ - Use manual invocation for focused accessibility testing
25
+ - Enable in CI only when you're ready to enforce accessibility compliance
26
+
27
+ **Example:**
28
+ ```yaml
29
+ # Default: false
30
+ # (Set to false to allow other RSpec tests to pass in GitHub Actions CI even if accessibility tests fail.
31
+ # When true, any failing accessibility tests will cause the entire CI pipeline to fail.)
32
+ # Set to true to run accessibility checks manually: rspec spec/accessibility/all_pages_accessibility_spec.rb
33
+ accessibility_enabled: false
34
+ ```
35
+
36
+ ### 2. Production Safety Guard
37
+
38
+ **Recommended:** Wrap configuration in conditional check
39
+
40
+ ```ruby
41
+ # config/initializers/rails_a11y.rb
42
+ if defined?(RailsAccessibilityTesting)
43
+ RailsAccessibilityTesting.configure do |config|
44
+ config.auto_run_checks = false
45
+ # ... other config
46
+ end
47
+ end
48
+ ```
49
+
50
+ **Why?**
51
+ - Prevents errors if gem is not available in production
52
+ - Allows gem to be excluded from production bundle
53
+ - Safe deployment even if gem configuration is present
54
+
55
+ ### 3. Manual Control Over Automatic Checks
56
+
57
+ **Recommended:** Set `auto_run_checks = false` in initializer
58
+
59
+ ```ruby
60
+ config.auto_run_checks = false
61
+ ```
62
+
63
+ **Why?**
64
+ - Gives developers explicit control over when checks run
65
+ - Prevents unexpected test failures during development
66
+ - Allows focused accessibility testing when needed
67
+ - Use `check_comprehensive_accessibility` explicitly in specs when desired
68
+
69
+ **Alternative:** Enable per-environment
70
+ ```ruby
71
+ config.auto_run_checks = Rails.env.development? || Rails.env.test?
72
+ ```
73
+
74
+ ### 4. Use Accessibility-Specific RSpec Type
75
+
76
+ **Recommended:** Use `type: :accessibility` for accessibility specs
77
+
78
+ ```ruby
79
+ RSpec.describe 'All Pages Accessibility', type: :accessibility do
80
+ # ...
81
+ end
82
+ ```
83
+
84
+ **Why?**
85
+ - Proper RSpec integration with accessibility helpers
86
+ - Better test organization and filtering
87
+ - Clearer intent in test files
88
+
89
+ ### 5. Improved Error Formatting
90
+
91
+ **Recommended:** Use unified formatting method for better output
92
+
93
+ The generator now creates a `format_issues_by_file` helper method that:
94
+ - Groups errors and warnings by file
95
+ - Shows errors first, then warnings
96
+ - Provides better structure and readability
97
+ - Uses proper test assertions (`expect(errors).to be_empty`)
98
+
99
+ ## CI/CD Integration Best Practices
100
+
101
+ ### GitHub Actions
102
+
103
+ **Recommended approach:**
104
+
105
+ 1. **Keep accessibility disabled by default:**
106
+ ```yaml
107
+ # config/accessibility.yml
108
+ accessibility_enabled: false
109
+ ```
110
+
111
+ 2. **Run accessibility tests separately:**
112
+ ```yaml
113
+ # .github/workflows/accessibility.yml
114
+ - name: Run accessibility tests
115
+ run: |
116
+ bundle exec rspec spec/accessibility/all_pages_accessibility_spec.rb
117
+ continue-on-error: true # Don't block PRs initially
118
+ ```
119
+
120
+ 3. **Gradually enforce:**
121
+ - Start with `continue-on-error: true` to see results
122
+ - Fix existing issues
123
+ - Then set `continue-on-error: false` to enforce
124
+
125
+ ### Profile-Based Configuration
126
+
127
+ Use different profiles for different environments:
128
+
129
+ ```yaml
130
+ # config/accessibility.yml
131
+ development:
132
+ checks:
133
+ color_contrast: false # Skip expensive checks in dev
134
+
135
+ test:
136
+ checks:
137
+ # Use global settings
138
+
139
+ ci:
140
+ checks:
141
+ color_contrast: true # Full checks in CI
142
+ ```
143
+
144
+ Then set profile in CI:
145
+ ```bash
146
+ RAILS_A11Y_PROFILE=ci bundle exec rspec spec/accessibility/
147
+ ```
148
+
149
+ ## Development Workflow Best Practices
150
+
151
+ ### 1. Use Static Scanner During Development
152
+
153
+ Add to `Procfile.dev`:
154
+ ```procfile
155
+ a11y: bundle exec a11y_static_scanner
156
+ ```
157
+
158
+ **Benefits:**
159
+ - Fast feedback without browser
160
+ - Only scans changed files
161
+ - Continuous monitoring as you code
162
+ - Precise file locations and line numbers
163
+
164
+ ### 2. Manual Testing When Needed
165
+
166
+ Run accessibility tests explicitly:
167
+ ```bash
168
+ # Test all pages
169
+ rspec spec/accessibility/all_pages_accessibility_spec.rb
170
+
171
+ # Test specific page
172
+ rspec spec/system/home_page_accessibility_spec.rb
173
+ ```
174
+
175
+ ### 3. Fix Issues Incrementally
176
+
177
+ 1. **Start with critical issues** - Focus on errors first
178
+ 2. **Fix by file** - Address all issues in one file at a time
179
+ 3. **Test incrementally** - Run tests after each fix
180
+ 4. **Document exceptions** - Use `ignored_rules` with reasons
181
+
182
+ ## Configuration File Best Practices
183
+
184
+ ### Documentation URLs
185
+
186
+ Always use the correct documentation URL:
187
+ ```yaml
188
+ # See https://rayraycodes.github.io/rails-accessibility-testing/ for full documentation.
189
+ ```
190
+
191
+ ### Comprehensive Comments
192
+
193
+ Add detailed comments explaining decisions:
194
+ ```yaml
195
+ # Global enable/disable flag for all accessibility checks
196
+ # Set to false to completely disable all accessibility checks (manual and automatic)
197
+ # When false, check_comprehensive_accessibility and automatic checks will be skipped
198
+ # Default: false
199
+ # (Set to false to allow other RSpec tests to pass in GitHub Actions CI even if accessibility tests fail.
200
+ # When true, any failing accessibility tests will cause the entire CI pipeline to fail.)
201
+ # Set to true to run accessibility checks manually: rspec spec/accessibility/all_pages_accessibility_spec.rb
202
+ accessibility_enabled: false
203
+ ```
204
+
205
+ ## Test Spec Best Practices
206
+
207
+ ### 1. Use Proper Test Assertions
208
+
209
+ **Recommended:**
210
+ ```ruby
211
+ if errors.any? || warnings.any?
212
+ expect(errors).to be_empty, format_static_errors(errors, warnings)
213
+ else
214
+ puts "\n✅ #{view_file}: No errors found"
215
+ end
216
+ ```
217
+
218
+ **Why?**
219
+ - Cleaner test output
220
+ - Proper RSpec integration
221
+ - Better error messages
222
+ - Only fails on errors, not warnings
223
+
224
+ ### 2. Improved Error Formatting
225
+
226
+ Use unified formatting method:
227
+ ```ruby
228
+ def format_issues_by_file(issues_by_file, output, issue_type)
229
+ issues_by_file.each_with_index do |(file_path, file_issues), file_index|
230
+ output << "" if file_index > 0
231
+ output << "📝 #{file_path} (#{file_issues.length} #{issue_type}#{'s' if file_issues.length != 1})"
232
+ # ... format each issue
233
+ end
234
+ end
235
+ ```
236
+
237
+ **Benefits:**
238
+ - Consistent formatting
239
+ - Better readability
240
+ - Easier to maintain
241
+
242
+ ## Summary
243
+
244
+ These best practices are based on real-world production usage and help ensure:
245
+
246
+ 1. **CI/CD Safety** - Accessibility tests don't block other tests
247
+ 2. **Production Safety** - No errors if gem isn't available
248
+ 3. **Developer Control** - Explicit control over when checks run
249
+ 4. **Better UX** - Improved error formatting and test output
250
+ 5. **Incremental Adoption** - Easy to start and gradually enforce
251
+
252
+ ## Credits
253
+
254
+ These best practices were refined based on contributions from:
255
+ - **Margarita Barvinok** - Production configuration improvements
256
+ - Real-world usage in Rails applications
257
+ - Community feedback and testing
258
+
259
+ ---
260
+
261
+ **Questions?** See the main [README](../README.md) or open an issue.
@@ -280,6 +280,7 @@ Upload reports to:
280
280
  3. **Fail on violations** - Don't allow merging with issues
281
281
  4. **Report results** - Make status visible to team
282
282
  5. **Track trends** - Monitor violation counts over time
283
+ 6. **Disable by default** - Set `accessibility_enabled: false` in `config/accessibility.yml` to prevent accessibility tests from blocking other RSpec tests in CI. Enable manually when needed: `rspec spec/accessibility/all_pages_accessibility_spec.rb`
283
284
 
284
285
  ## Troubleshooting
285
286
 
data/README.md CHANGED
@@ -447,6 +447,7 @@ Complete documentation site with all guides, examples, and API reference. The do
447
447
  ### Guides
448
448
 
449
449
  - **[System Specs for Accessibility](GUIDES/system_specs_for_accessibility.md)** - ⭐ **Recommended approach** - Using system specs for reliable accessibility testing
450
+ - **[Best Practices](GUIDES/best_practices.md)** - ⭐ **Configuration recommendations** - Production-tested configuration patterns
450
451
  - **[Getting Started](GUIDES/getting_started.md)** - Quick start guide
451
452
  - **[Continuous Integration](GUIDES/continuous_integration.md)** - CI/CD setup
452
453
  - **[Writing Accessible Views](GUIDES/writing_accessible_views_in_rails.md)** - Best practices
@@ -167,6 +167,61 @@ graph TB
167
167
  style Report fill:#ffeaa7
168
168
  ```
169
169
 
170
+ ### ERB Template Handling and Dynamic IDs
171
+
172
+ The static scanner intelligently handles ERB templates with dynamic content, particularly for form inputs with dynamic IDs.
173
+
174
+ #### How Dynamic IDs Are Preserved
175
+
176
+ When the scanner encounters ERB templates with dynamic IDs, it preserves the structure instead of collapsing them:
177
+
178
+ **Example ERB Template:**
179
+ ```erb
180
+ <% question.collection_options.each do |option| %>
181
+ <input type="checkbox"
182
+ id="collection_answers_<%= question.id %>_<%= option.id %>_"
183
+ name="collection_answers[<%= question.id %>][]" />
184
+ <%= label_tag "collection_answers_#{question.id}_#{option.id}_", option.value %>
185
+ <% end %>
186
+ ```
187
+
188
+ **How It's Processed:**
189
+ - ERB expressions (`<%= question.id %>`, `<%= option.id %>`) are replaced with `ERB_CONTENT` placeholders
190
+ - The structure is preserved: `collection_answers_ERB_CONTENT_ERB_CONTENT_`
191
+ - This allows the scanner to correctly match inputs with their labels, even when IDs are dynamic
192
+
193
+ #### Label Matching with Dynamic IDs
194
+
195
+ The **Form Labels Check** correctly matches labels to inputs with dynamic IDs:
196
+ - Input: `id="collection_answers_ERB_CONTENT_ERB_CONTENT_"`
197
+ - Label: `for="collection_answers_ERB_CONTENT_ERB_CONTENT_"`
198
+ - ✅ **Match found** - The scanner recognizes these as matching pairs
199
+
200
+ #### Duplicate ID Detection
201
+
202
+ The **Duplicate IDs Check** intelligently handles dynamic IDs:
203
+ - IDs containing `ERB_CONTENT` are excluded from duplicate checking
204
+ - These represent dynamic IDs that will have different values at runtime
205
+ - Only static IDs (without ERB placeholders) are checked for duplicates
206
+ - This prevents false positives for checkbox/radio groups in loops
207
+
208
+ #### Links with href="#"
209
+
210
+ The **Interactive Elements Check** correctly handles anchor links:
211
+ - Only flags links with `href="#"` that have **no accessible name**
212
+ - An accessible name can be: visible text, `aria-label`, or `aria-labelledby`
213
+ - Links with `href="#"` that have proper labeling are **not flagged** (avoids false positives)
214
+
215
+ **Example - Valid (not flagged):**
216
+ ```erb
217
+ <%= link_to "Click me", "#", aria: { label: "Navigate to section" } %>
218
+ ```
219
+
220
+ **Example - Invalid (flagged):**
221
+ ```erb
222
+ <a href="#"></a> <!-- No text, no aria-label, no aria-labelledby -->
223
+ ```
224
+
170
225
  ---
171
226
 
172
227
  ## Configuration & Profiles
@@ -142,14 +142,20 @@ end
142
142
  Rails Accessibility Testing runs **11 comprehensive checks** automatically. These checks are WCAG 2.1 AA aligned:
143
143
 
144
144
  1. **Form Labels** - All form inputs have associated labels
145
+ - ✅ Correctly handles dynamic IDs from ERB templates (e.g., `collection_answers_<%= question.id %>_<%= option.id %>_`)
146
+ - ✅ Matches labels to inputs even when IDs contain ERB expressions
145
147
  2. **Image Alt Text** - All images have descriptive alt attributes
146
148
  3. **Interactive Elements** - Buttons, links have accessible names
149
+ - ✅ Only flags links with `href="#"` that have no accessible name (visible text, `aria-label`, or `aria-labelledby`)
150
+ - ✅ Avoids false positives for valid anchor links with proper labeling
147
151
  4. **Heading Hierarchy** - Proper h1-h6 structure without skipping levels (checked across complete page: layout + view + partials)
148
152
  5. **Keyboard Accessibility** - All interactive elements are keyboard accessible
149
153
  6. **ARIA Landmarks** - Proper use of ARIA landmark roles (checked across complete page: layout + view + partials)
150
154
  7. **Form Error Associations** - Form errors are properly linked to form fields
151
155
  8. **Table Structure** - Tables have proper headers
152
156
  9. **Duplicate IDs** - No duplicate ID attributes (checked across complete page: layout + view + partials)
157
+ - ✅ Intelligently excludes dynamic IDs with ERB placeholders from duplicate checking
158
+ - ✅ Prevents false positives for checkbox/radio groups in loops
153
159
  10. **Skip Links** - Skip navigation links present
154
160
  11. **Color Contrast** - Text meets WCAG contrast requirements (optional, disabled by default)
155
161
 
data/docs_site/index.md CHANGED
@@ -7,7 +7,7 @@ title: Home
7
7
 
8
8
  **The RSpec + RuboCop of accessibility for Rails. Catch WCAG violations before they reach production.**
9
9
 
10
- **Version:** 1.5.9
10
+ **Version:** 1.6.0
11
11
 
12
12
  Rails Accessibility Testing is a comprehensive, opinionated but configurable gem that makes accessibility testing as natural as unit testing. It integrates seamlessly into your Rails workflow, catching accessibility issues as you code—not after deployment.
13
13
 
@@ -36,6 +36,10 @@ Rails Accessibility Testing fills a critical gap in the Rails testing ecosystem.
36
36
  - **Continuous monitoring**: Watches for file changes and re-scans automatically
37
37
  - **YAML configuration**: Fully configurable via `config/accessibility.yml`
38
38
  - **Reuses existing checks**: Leverages all 11 accessibility checks via RuleEngine
39
+ - **Intelligent ERB handling**: Correctly processes dynamic IDs and ERB expressions
40
+ - Preserves structure of dynamic IDs (e.g., `collection_answers_<%= question.id %>_<%= option.id %>_`)
41
+ - Matches labels to inputs with dynamic IDs
42
+ - Excludes dynamic IDs from duplicate checking (prevents false positives)
39
43
 
40
44
  #### 🎯 Live Accessibility Scanner
41
45
  - **Real-time scanning**: Automatically scans pages as you browse during development
@@ -140,15 +144,15 @@ This will:
140
144
 
141
145
  The gem automatically runs **11 comprehensive accessibility checks**:
142
146
 
143
- 1. ✅ **Form Labels** - All form inputs have associated labels
147
+ 1. ✅ **Form Labels** - All form inputs have associated labels (handles dynamic IDs from ERB templates)
144
148
  2. ✅ **Image Alt Text** - All images have descriptive alt attributes
145
- 3. ✅ **Interactive Elements** - Buttons, links have accessible names (including links with images that have alt text)
149
+ 3. ✅ **Interactive Elements** - Buttons, links have accessible names (including links with images that have alt text; correctly handles links with `href="#"`)
146
150
  4. ✅ **Heading Hierarchy** - Proper h1-h6 structure (detects missing h1, multiple h1s, skipped levels, and h2+ without h1)
147
151
  5. ✅ **Keyboard Accessibility** - All interactive elements keyboard accessible
148
152
  6. ✅ **ARIA Landmarks** - Proper use of ARIA landmark roles
149
153
  7. ✅ **Form Error Associations** - Errors linked to form fields
150
154
  8. ✅ **Table Structure** - Tables have proper headers
151
- 9. ✅ **Duplicate IDs** - No duplicate ID attributes
155
+ 9. ✅ **Duplicate IDs** - No duplicate ID attributes (intelligently handles dynamic IDs from ERB templates)
152
156
  10. ✅ **Skip Links** - Skip navigation links present (detects various patterns)
153
157
  11. ✅ **Color Contrast** - Text meets contrast requirements (optional)
154
158
 
@@ -43,7 +43,11 @@ module RailsA11y
43
43
  end
44
44
 
45
45
  def create_all_pages_spec
46
- spec_path = 'spec/system/all_pages_accessibility_spec.rb'
46
+ # Create spec/accessibility directory if it doesn't exist
47
+ spec_dir = 'spec/accessibility'
48
+ FileUtils.mkdir_p(spec_dir) unless File.directory?(spec_dir)
49
+
50
+ spec_path = 'spec/accessibility/all_pages_accessibility_spec.rb'
47
51
 
48
52
  if File.exist?(spec_path)
49
53
  say "⚠️ #{spec_path} already exists. Skipping creation.", :yellow
@@ -113,7 +117,7 @@ module RailsA11y
113
117
  say "\n📋 Next Steps:", :yellow
114
118
  say ""
115
119
  say " 1. Run the accessibility tests:", :cyan
116
- say " bundle exec rspec spec/system/all_pages_accessibility_spec.rb"
120
+ say " bundle exec rspec spec/accessibility/all_pages_accessibility_spec.rb"
117
121
  say ""
118
122
  say " 2. For static file scanning during development:", :cyan
119
123
  say " bin/dev # Starts web server + static accessibility scanner"
@@ -1,13 +1,16 @@
1
1
  # Rails A11y Configuration
2
2
  #
3
3
  # This file configures accessibility checks for your Rails application.
4
- # See https://github.com/your-org/rails-a11y for full documentation.
4
+ # See https://rayraycodes.github.io/rails-accessibility-testing/ for full documentation.
5
5
 
6
6
  # Global enable/disable flag for all accessibility checks
7
7
  # Set to false to completely disable all accessibility checks (manual and automatic)
8
8
  # When false, check_comprehensive_accessibility and automatic checks will be skipped
9
- # Default: true
10
- accessibility_enabled: true
9
+ # Default: false
10
+ # (Set to false to allow other RSpec tests to pass in GitHub Actions CI even if accessibility tests fail.
11
+ # When true, any failing accessibility tests will cause the entire CI pipeline to fail.)
12
+ # Set to true to run accessibility checks manually: rspec spec/accessibility/all_pages_accessibility_spec.rb
13
+ accessibility_enabled: false
11
14
 
12
15
  # WCAG compliance level (A, AA, AAA)
13
16
  wcag_level: AA
@@ -1,6 +1,6 @@
1
1
  require 'rails_helper'
2
2
 
3
- RSpec.describe 'All Pages Accessibility', type: :system do
3
+ RSpec.describe 'All Pages Accessibility', type: :accessibility do
4
4
  # Test all view files for accessibility using static file scanning
5
5
  # Generated automatically by rails_a11y:install generator
6
6
 
@@ -35,7 +35,7 @@ RSpec.describe 'All Pages Accessibility', type: :system do
35
35
 
36
36
  output = []
37
37
 
38
- # Group errors by file
38
+ # Group errors and warnings by file
39
39
  errors_by_file = errors.group_by { |e| e[:file] }
40
40
  warnings_by_file = warnings.group_by { |w| w[:file] }
41
41
 
@@ -43,37 +43,8 @@ RSpec.describe 'All Pages Accessibility', type: :system do
43
43
  if errors.any?
44
44
  output << "\n" + "="*70
45
45
  output << "❌ #{errors.length} error#{'s' if errors.length != 1} found"
46
- output << "="*70
47
46
  output << ""
48
-
49
- errors_by_file.each_with_index do |(file_path, file_errors), file_index|
50
- output << "" if file_index > 0
51
-
52
- output << "📝 #{file_path} (#{file_errors.length} error#{'s' if file_errors.length != 1})"
53
-
54
- file_errors.each do |error|
55
- error_line = " • #{error[:type]}"
56
-
57
- # Add line number if available
58
- if error[:line]
59
- error_line += " [Line #{error[:line]}]"
60
- end
61
-
62
- # Add element identifier
63
- if error[:element][:id].present?
64
- error_line += " [id: #{error[:element][:id]}]"
65
- elsif error[:element][:href].present?
66
- href_display = error[:element][:href].length > 30 ? "#{error[:element][:href][0..27]}..." : error[:element][:href]
67
- error_line += " [href: #{href_display}]"
68
- elsif error[:element][:src].present?
69
- src_display = error[:element][:src].length > 30 ? "#{error[:element][:src][0..27]}..." : error[:element][:src]
70
- error_line += " [src: #{src_display}]"
71
- end
72
-
73
- output << error_line
74
- end
75
- end
76
-
47
+ format_issues_by_file(errors_by_file, output, 'error')
77
48
  output << ""
78
49
  output << "="*70
79
50
  end
@@ -85,33 +56,7 @@ RSpec.describe 'All Pages Accessibility', type: :system do
85
56
  output << "="*70
86
57
  output << ""
87
58
 
88
- warnings_by_file.each_with_index do |(file_path, file_warnings), file_index|
89
- output << "" if file_index > 0
90
-
91
- output << "📝 #{file_path} (#{file_warnings.length} warning#{'s' if file_warnings.length != 1})"
92
-
93
- file_warnings.each do |warning|
94
- warning_line = " • #{warning[:type]}"
95
-
96
- # Add line number if available
97
- if warning[:line]
98
- warning_line += " [Line #{warning[:line]}]"
99
- end
100
-
101
- # Add element identifier
102
- if warning[:element][:id].present?
103
- warning_line += " [id: #{warning[:element][:id]}]"
104
- elsif warning[:element][:href].present?
105
- href_display = warning[:element][:href].length > 30 ? "#{warning[:element][:href][0..27]}..." : warning[:element][:href]
106
- warning_line += " [href: #{href_display}]"
107
- elsif warning[:element][:src].present?
108
- src_display = warning[:element][:src].length > 30 ? "#{warning[:element][:src][0..27]}..." : warning[:element][:src]
109
- warning_line += " [src: #{src_display}]"
110
- end
111
-
112
- output << warning_line
113
- end
114
- end
59
+ format_issues_by_file(warnings_by_file, output, 'warning')
115
60
 
116
61
  output << ""
117
62
  output << "="*70
@@ -119,6 +64,36 @@ RSpec.describe 'All Pages Accessibility', type: :system do
119
64
 
120
65
  output.join("\n")
121
66
  end
67
+
68
+ def format_issues_by_file(issues_by_file, output, issue_type)
69
+ issues_by_file.each_with_index do |(file_path, file_issues), file_index|
70
+ output << "" if file_index > 0
71
+
72
+ output << "📝 #{file_path} (#{file_issues.length} #{issue_type}#{'s' if file_issues.length != 1})"
73
+
74
+ file_issues.each do |issue|
75
+ issue_line = " • #{issue[:type]}"
76
+
77
+ # Add line number if available
78
+ if issue[:line]
79
+ issue_line += " [Line #{issue[:line]}]"
80
+ end
81
+
82
+ # Add element identifier
83
+ if issue[:element][:id].present?
84
+ issue_line += " [id: #{issue[:element][:id]}]"
85
+ elsif issue[:element][:href].present?
86
+ href_display = issue[:element][:href].length > 30 ? "#{issue[:element][:href][0..27]}..." : issue[:element][:href]
87
+ issue_line += " [href: #{href_display}]"
88
+ elsif issue[:element][:src].present?
89
+ src_display = issue[:element][:src].length > 30 ? "#{issue[:element][:src][0..27]}..." : issue[:element][:src]
90
+ issue_line += " [src: #{src_display}]"
91
+ end
92
+
93
+ output << issue_line
94
+ end
95
+ end
96
+ end
122
97
 
123
98
  # Scan all view files statically
124
99
  view_files = get_all_view_files
@@ -139,13 +114,9 @@ RSpec.describe 'All Pages Accessibility', type: :system do
139
114
  warnings = result[:warnings] || []
140
115
 
141
116
  if errors.any? || warnings.any?
142
- puts format_static_errors(errors, warnings)
143
-
144
- if errors.any?
145
- puts "Found #{errors.length} accessibility error#{'s' if errors.length != 1} in #{view_file}"
146
- end
117
+ expect(errors).to be_empty, format_static_errors(errors, warnings)
147
118
  else
148
- puts "✅ #{view_file}: No errors found"
119
+ puts "\n✅ #{view_file}: No errors found"
149
120
  end
150
121
  end
151
122
  end
@@ -4,12 +4,14 @@
4
4
  #
5
5
  # Configure accessibility testing behavior for your Rails application.
6
6
  #
7
- # @see https://github.com/your-org/rails-a11y for documentation
7
+ # @see https://rayraycodes.github.io/rails-accessibility-testing/ for documentation
8
8
 
9
- RailsAccessibilityTesting.configure do |config|
10
- # Automatically run checks after system specs
11
- # Set to false to disable automatic checks
12
- config.auto_run_checks = true
9
+ # Only configure if the gem is available (not in production)
10
+ if defined?(RailsAccessibilityTesting)
11
+ RailsAccessibilityTesting.configure do |config|
12
+ # Automatically run checks after system specs
13
+ # Set to false to disable automatic checks
14
+ config.auto_run_checks = false
13
15
 
14
16
  # Logger for accessibility check output
15
17
  # Set to nil to use default logger
@@ -18,7 +20,8 @@ RailsAccessibilityTesting.configure do |config|
18
20
  # Configuration file path (relative to Rails.root)
19
21
  # config.config_path = 'config/accessibility.yml'
20
22
 
21
- # Default profile to use (development, test, ci)
22
- # config.default_profile = :test
23
+ # Default profile to use (development, test, ci)
24
+ # config.default_profile = :test
25
+ end
23
26
  end
24
27
 
@@ -6,6 +6,13 @@ module RailsAccessibilityTesting
6
6
  #
7
7
  # WCAG 2.1 AA: 4.1.1 Parsing (Level A)
8
8
  #
9
+ # @note ERB template handling:
10
+ # - IDs containing "ERB_CONTENT" placeholders are excluded from duplicate checking
11
+ # - These are dynamic IDs that will have different values at runtime
12
+ # - Example: "collection_answers_ERB_CONTENT_ERB_CONTENT_" represents a dynamic ID
13
+ # that will be unique for each checkbox/radio option when rendered
14
+ # - Static analysis cannot determine if dynamic IDs will be duplicates, so they are skipped
15
+ #
9
16
  # @api private
10
17
  class DuplicateIdsCheck < BaseCheck
11
18
  def self.rule_name
@@ -14,7 +21,11 @@ module RailsAccessibilityTesting
14
21
 
15
22
  def check
16
23
  violations = []
17
- all_ids = page.all('[id]').map { |el| el[:id] }.compact
24
+ # Collect all IDs, filtering out those with ERB_CONTENT placeholders
25
+ # IDs with ERB_CONTENT are dynamic and can't be statically verified for duplicates
26
+ # Example: "collection_answers_ERB_CONTENT_ERB_CONTENT_" - the actual IDs will be different at runtime
27
+ # because the ERB expressions will evaluate to different values
28
+ all_ids = page.all('[id]').map { |el| el[:id] }.compact.reject { |id| id.include?('ERB_CONTENT') }
18
29
  duplicates = all_ids.group_by(&:itself).select { |_k, v| v.length > 1 }.keys
19
30
 
20
31
  if duplicates.any?
@@ -6,6 +6,15 @@ module RailsAccessibilityTesting
6
6
  #
7
7
  # WCAG 2.1 AA: 1.3.1 Info and Relationships (Level A)
8
8
  #
9
+ # @note ERB template handling:
10
+ # - Dynamic IDs with ERB placeholders (e.g., "collection_answers_ERB_CONTENT_ERB_CONTENT_")
11
+ # are matched against labels with the same ERB structure
12
+ # - ErbExtractor ensures that IDs and label "for" attributes with matching ERB patterns
13
+ # will have the same "ERB_CONTENT" placeholder structure, allowing exact matching
14
+ # - This correctly handles cases like:
15
+ # <input id="collection_answers_<%= question.id %>_<%= option.id %>_">
16
+ # <%= label_tag "collection_answers_#{question.id}_#{option.id}_", option.value %>
17
+ #
9
18
  # @api private
10
19
  class FormLabelsCheck < BaseCheck
11
20
  def self.rule_name
@@ -16,11 +25,21 @@ module RailsAccessibilityTesting
16
25
  violations = []
17
26
  page_context = self.page_context
18
27
 
19
- page.all('input[type="text"], input[type="email"], input[type="password"], input[type="number"], input[type="tel"], input[type="url"], input[type="search"], input[type="date"], input[type="time"], input[type="datetime-local"], textarea, select').each do |input|
28
+ # Also check checkbox and radio inputs (they need labels too)
29
+ page.all('input[type="text"], input[type="email"], input[type="password"], input[type="number"], input[type="tel"], input[type="url"], input[type="search"], input[type="date"], input[type="time"], input[type="datetime-local"], input[type="checkbox"], input[type="radio"], textarea, select').each do |input|
20
30
  id = input[:id]
21
31
  next if id.nil? || id.to_s.strip.empty?
22
32
 
33
+ # Skip ERB_CONTENT placeholder - it's not a real ID, just a marker for dynamic content
34
+ next if id == 'ERB_CONTENT'
35
+
36
+ # Check for label with matching for attribute
37
+ # Handle ERB placeholders in IDs: ErbExtractor preserves the structure of dynamic IDs
38
+ # so "collection_answers_<%= question.id %>_<%= option.id %>_" becomes
39
+ # "collection_answers_ERB_CONTENT_ERB_CONTENT_", and label_tag with the same pattern
40
+ # will also become "collection_answers_ERB_CONTENT_ERB_CONTENT_", so they should match exactly
23
41
  has_label = page.has_css?("label[for='#{id}']", wait: false)
42
+
24
43
  aria_label = input[:"aria-label"]
25
44
  aria_labelledby = input[:"aria-labelledby"]
26
45
  has_aria_label = aria_label && !aria_label.to_s.strip.empty?
@@ -6,6 +6,12 @@ module RailsAccessibilityTesting
6
6
  #
7
7
  # WCAG 2.1 AA: 2.4.4 Link Purpose (Level A), 4.1.2 Name, Role, Value (Level A)
8
8
  #
9
+ # @note Links with href="#":
10
+ # - Only flags anchors with href="#" that have NO accessible name
11
+ # - An accessible name can be: visible text, aria-label, or aria-labelledby
12
+ # - Links with href="#" that have visible text or ARIA attributes are valid and NOT flagged
13
+ # - This avoids false positives for valid anchor links that use href="#" with proper labeling
14
+ #
9
15
  # @api private
10
16
  class InteractiveElementsCheck < BaseCheck
11
17
  def self.rule_name
@@ -45,6 +51,7 @@ module RailsAccessibilityTesting
45
51
  aria_label = element[:"aria-label"]
46
52
  aria_labelledby = element[:"aria-labelledby"]
47
53
  title = element[:title]
54
+ href = element[:href]
48
55
 
49
56
  # Check if element contains ERB placeholder (for static scanning)
50
57
  # ErbExtractor replaces <%= ... %> with "ERB_CONTENT" so we can detect it
@@ -68,12 +75,24 @@ module RailsAccessibilityTesting
68
75
  aria_labelledby_empty = aria_labelledby.nil? || aria_labelledby.to_s.strip.empty?
69
76
  title_empty = title.nil? || title.to_s.strip.empty?
70
77
 
78
+ # Only report violation if element has no accessible name
79
+ # For links with href="#", we only flag if they have no text AND no aria-label AND no aria-labelledby
80
+ # Links with href="#" that have visible text or ARIA attributes are valid and should not be flagged
71
81
  if text_empty && aria_label_empty && aria_labelledby_empty && title_empty && !has_image_with_alt
72
82
  element_ctx = element_context(element)
73
83
  tag = element.tag_name
74
84
 
85
+ # Special message for empty links with href="#"
86
+ # This rule correctly detects anchors with href="#" that have no accessible name,
87
+ # but avoids false positives when they have visible text or aria-label/aria-labelledby
88
+ message = if tag == 'a' && (href == '#' || href.to_s.strip == '#')
89
+ "Link missing accessible name [href: #]"
90
+ else
91
+ "#{tag.capitalize} missing accessible name"
92
+ end
93
+
75
94
  violations << violation(
76
- message: "#{tag.capitalize} missing accessible name",
95
+ message: message,
77
96
  element_context: element_ctx,
78
97
  wcag_reference: tag == 'a' ? "2.4.4" : "4.1.2",
79
98
  remediation: generate_remediation(tag, element_ctx)
@@ -203,6 +203,10 @@ module RailsAccessibilityTesting
203
203
  next if id.nil? || id.to_s.strip.empty?
204
204
  # Skip ERB_CONTENT placeholder - it's not a real ID
205
205
  next if id == 'ERB_CONTENT'
206
+ # Skip IDs that contain ERB_CONTENT - these are dynamic IDs that can't be statically verified
207
+ # Example: "collection_answers_ERB_CONTENT_ERB_CONTENT_" - the actual IDs will be different at runtime
208
+ # because the ERB expressions will evaluate to different values
209
+ next if id.include?('ERB_CONTENT')
206
210
 
207
211
  id_map[id] ||= []
208
212
  line = find_line_number_for_element(content, element)
@@ -4,6 +4,12 @@ module RailsAccessibilityTesting
4
4
  # Extracts HTML from ERB templates by converting Rails helpers to HTML
5
5
  # This allows static analysis of view files without rendering them
6
6
  #
7
+ # @note ERB and ID handling:
8
+ # - Dynamic IDs like "collection_answers_<%= question.id %>_<%= option.id %>_" are preserved
9
+ # - ERB expressions are replaced with "ERB_CONTENT" placeholder to maintain structure
10
+ # - This ensures IDs like "collection_answers_ERB_CONTENT_ERB_CONTENT_" are not collapsed
11
+ # - Labels with matching ERB structure will also have "ERB_CONTENT" and can be matched
12
+ #
7
13
  # @api private
8
14
  class ErbExtractor
9
15
  # Convert ERB template to HTML for static analysis
@@ -26,8 +32,57 @@ module RailsAccessibilityTesting
26
32
 
27
33
  private
28
34
 
35
+ # Convert raw HTML elements that have ERB in their attributes
36
+ # This handles cases like: <input id="collection_answers_<%= question.id %>_<%= option.id %>_">
37
+ # We need to preserve the structure of dynamic IDs so they don't get collapsed
38
+ #
39
+ # @note This must run BEFORE remove_erb_tags to preserve ERB structure in attributes
40
+ def convert_raw_html_with_erb
41
+ # Handle input elements (checkbox, radio, text, etc.) with ERB in attributes
42
+ # Pattern: <input ... id="...<%= ... %>..." ... />
43
+ @content.gsub!(/<input\s+([^>]*?)>/i) do |match|
44
+ attrs = $1
45
+ # Replace ERB in attributes with placeholder, preserving structure
46
+ # This ensures "id='collection_answers_<%= question.id %>_<%= option.id %>_'"
47
+ # becomes "id='collection_answers_ERB_CONTENT_ERB_CONTENT_'"
48
+ attrs_with_placeholders = attrs.gsub(/<%=(.*?)%>/m, 'ERB_CONTENT')
49
+ "<input #{attrs_with_placeholders}>"
50
+ end
51
+
52
+ # Handle label_tag with string interpolation in first argument: <%= label_tag "collection_answers_#{question.id}_#{option.id}_", option.value %>
53
+ # This handles string interpolation like "collection_answers_#{question.id}_#{option.id}_"
54
+ # Must match BEFORE the simple string pattern
55
+ @content.gsub!(/<%=\s*label_tag\s+["']([^"']*#\{[^}]+\}[^"']*)["'],\s*([^,]+)(?:,\s*[^%]*)?%>/) do
56
+ id_template = $1
57
+ text_expr = $2
58
+ # Replace Ruby interpolation with placeholder
59
+ id_with_placeholder = id_template.gsub(/#\{[^}]+\}/, 'ERB_CONTENT')
60
+ # Handle text expression - could be a variable, string, or method call
61
+ text_placeholder = if text_expr.strip.match?(/^["']/)
62
+ # It's a string literal
63
+ text_expr.strip.gsub(/^["']|["']$/, '')
64
+ else
65
+ # It's a variable or method call - will produce content at runtime
66
+ 'ERB_CONTENT'
67
+ end
68
+ "<label for=\"#{id_with_placeholder}\">#{text_placeholder}</label>"
69
+ end
70
+
71
+ # Handle label_tag helper with simple string: <%= label_tag "id_string", "text", options %>
72
+ # Pattern: label_tag "id_string", "text", options
73
+ @content.gsub!(/<%=\s*label_tag\s+["']([^"']+)["'],\s*["']?([^"']*?)["']?[^%]*%>/) do
74
+ id = $1
75
+ text = $2
76
+ # Replace ERB in id string with placeholder (if any)
77
+ id_with_placeholder = id.gsub(/<%=(.*?)%>/m, 'ERB_CONTENT')
78
+ "<label for=\"#{id_with_placeholder}\">#{text}</label>"
79
+ end
80
+ end
81
+
29
82
  # Convert Rails helpers to placeholder HTML
30
83
  def convert_rails_helpers
84
+ # First, handle raw HTML elements with ERB in attributes (before removing ERB tags)
85
+ convert_raw_html_with_erb
31
86
  convert_form_helpers
32
87
  convert_image_helpers
33
88
  convert_link_helpers
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAccessibilityTesting
4
- VERSION = "1.5.10"
4
+ VERSION = "1.7.0"
5
5
  end
6
6
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_accessibility_testing
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.10
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Regan Maharjan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-01 00:00:00.000000000 Z
11
+ date: 2026-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: axe-core-capybara
@@ -97,6 +97,7 @@ files:
97
97
  - CHANGELOG.md
98
98
  - CODE_OF_CONDUCT.md
99
99
  - CONTRIBUTING.md
100
+ - GUIDES/best_practices.md
100
101
  - GUIDES/continuous_integration.md
101
102
  - GUIDES/getting_started.md
102
103
  - GUIDES/system_specs_for_accessibility.md