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 +4 -4
- data/CHANGELOG.md +44 -0
- data/GUIDES/best_practices.md +261 -0
- data/GUIDES/continuous_integration.md +1 -0
- data/README.md +1 -0
- data/docs_site/architecture.md +55 -0
- data/docs_site/getting_started.md +6 -0
- data/docs_site/index.md +8 -4
- data/lib/generators/rails_a11y/install/install_generator.rb +6 -2
- data/lib/generators/rails_a11y/install/templates/accessibility.yml.erb +6 -3
- data/lib/generators/rails_a11y/install/templates/all_pages_accessibility_spec.rb.erb +36 -65
- data/lib/generators/rails_a11y/install/templates/initializer.rb.erb +10 -7
- data/lib/rails_accessibility_testing/checks/duplicate_ids_check.rb +12 -1
- data/lib/rails_accessibility_testing/checks/form_labels_check.rb +20 -1
- data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +20 -1
- data/lib/rails_accessibility_testing/composed_page_scanner.rb +4 -0
- data/lib/rails_accessibility_testing/erb_extractor.rb +55 -0
- data/lib/rails_accessibility_testing/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61344dc6a53b7678ea457fdfca0237fb7dfece5ef7825aaf0449fea1d187737d
|
|
4
|
+
data.tar.gz: c843795ff4c676607b4fde8d207a7055db5a9b128054583964843a6d41cc6ece
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/docs_site/architecture.md
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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/
|
|
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.
|
|
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:
|
|
10
|
-
|
|
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: :
|
|
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
|
|
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
|
-
|
|
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.
|
|
7
|
+
# @see https://rayraycodes.github.io/rails-accessibility-testing/ for documentation
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
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.
|
|
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:
|
|
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
|