rails_accessibility_testing 1.5.8 → 1.6.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 +93 -0
- data/README.md +9 -1
- data/docs_site/architecture.md +94 -1
- data/docs_site/getting_started.md +9 -3
- data/docs_site/index.md +8 -4
- data/exe/a11y_live_scanner +4 -3
- data/exe/a11y_static_scanner +4 -3
- data/lib/generators/rails_a11y/install/templates/accessibility.yml.erb +2 -2
- data/lib/rails_accessibility_testing/accessibility_helper.rb +48 -6
- 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/heading_check.rb +17 -10
- data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +20 -1
- data/lib/rails_accessibility_testing/composed_page_scanner.rb +330 -0
- data/lib/rails_accessibility_testing/config/yaml_loader.rb +3 -2
- data/lib/rails_accessibility_testing/erb_extractor.rb +55 -0
- data/lib/rails_accessibility_testing/rspec_integration.rb +2 -1
- data/lib/rails_accessibility_testing/static_file_scanner.rb +47 -4
- data/lib/rails_accessibility_testing/version.rb +1 -1
- data/lib/rails_accessibility_testing/view_composition_builder.rb +313 -0
- data/lib/rails_accessibility_testing.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 232d290bc12c422c56aacad24fa16660eb12b3e106939fc6b4cac0a639740532
|
|
4
|
+
data.tar.gz: 2b5a248555626127007ef893c84bd10f8aac41e195dba54c86bebd7b1f4e1448
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 566c195bcf20d08247c58c97f11e8fe1158488d998a78eb12e293f9e70451db5076c0e314c1a383a2cdcdf5f50f9dd6c291665f2516c070cbf972de6c25ee186
|
|
7
|
+
data.tar.gz: 71e9fb76ac0b894e872397a7cc822a874acf33a32bf128c1a8ca7932a0ca283389b23534b567a5929652470239d565777de7d6a0113469eb1ac688331cbd8491
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,99 @@ 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
|
+
## [1.6.0] - 2024-12-XX
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Intelligent ERB Template Handling**: Enhanced ERB extractor to preserve dynamic ID structure from ERB templates
|
|
12
|
+
- **Dynamic ID Support**: Static scanner now correctly handles checkbox/radio inputs with ERB-generated IDs (e.g., `collection_answers_<%= question.id %>_<%= option.id %>_`)
|
|
13
|
+
- **Label Matching for Dynamic IDs**: Form labels check now correctly matches labels to inputs with dynamic IDs by preserving ERB placeholder structure
|
|
14
|
+
- **Smart Duplicate ID Detection**: Duplicate ID check now excludes dynamic IDs with ERB placeholders, preventing false positives for checkbox/radio groups in loops
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Fixed false positive "Form input missing label [id: collection_answers]" for checkbox groups with dynamic IDs
|
|
18
|
+
- Fixed false positive "Duplicate ID 'collection_answers' found" when IDs contain ERB expressions
|
|
19
|
+
- 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)
|
|
20
|
+
- Improved `label_tag` helper conversion to handle string interpolation in ID arguments
|
|
21
|
+
- Enhanced raw HTML input element processing to preserve ERB structure in attributes
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **ERB Extractor**: Now processes raw HTML elements with ERB in attributes before removing ERB tags, preserving dynamic ID structure
|
|
25
|
+
- **Form Labels Check**: Updated to handle ERB placeholders in IDs when checking for associated labels
|
|
26
|
+
- **Duplicate IDs Check**: Now filters out IDs containing `ERB_CONTENT` placeholders from duplicate detection
|
|
27
|
+
- **Interactive Elements Check**: Enhanced to correctly detect accessible names for links with `href="#"`, avoiding false positives
|
|
28
|
+
- **RSpec Test File**: Refactored `all_pages_accessibility_spec.rb` to extract formatting helper and add proper assertions that fail tests when errors are found
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
- Added comprehensive documentation on ERB template handling and dynamic ID processing
|
|
32
|
+
- Updated architecture documentation with details on how dynamic IDs are preserved
|
|
33
|
+
- Added examples and explanations for checkbox/radio groups with ERB-generated IDs
|
|
34
|
+
|
|
35
|
+
## [1.5.10] - 2024-12-01
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- **Configuration flag renamed**: The `enabled` flag has been renamed to `accessibility_enabled` for better clarity. The old `enabled` key is still supported for backward compatibility.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
- **Composed Page Scanning**: The static scanner now analyzes complete page compositions (layout + view + partials) for page-level accessibility checks, eliminating false positives for heading hierarchy, ARIA landmarks, duplicate IDs, and heading issues.
|
|
42
|
+
- **View Composition Builder**: New comprehensive system that traces the complete page structure by finding all partials recursively across all directories in `app/views`.
|
|
43
|
+
- **Exhaustive Partial Detection**: Enhanced partial detection that finds partials in any subdirectory, not just specific folders. Works for deeply nested namespaces (e.g., `collections/collection_questions`).
|
|
44
|
+
- **ERB Content Detection**: Improved handling of ERB expressions (`<%= ... %>`) to prevent false positives for empty headings and missing accessible names on buttons.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- **Heading Hierarchy Checks**: Now performed on the complete composed page instead of individual files, preventing false positives when H1 is in layout or partials.
|
|
48
|
+
- **ARIA Landmarks Checks**: Now checks for `<main>` landmark across the entire composed page.
|
|
49
|
+
- **Duplicate ID Checks**: Now checks for duplicate IDs across the complete page composition.
|
|
50
|
+
- **Empty Heading Checks**: Now checks for empty headings across the complete composed page.
|
|
51
|
+
- **Partial Search**: Enhanced to traverse ALL folders in `app/views` recursively using `Dir.glob`, making it a general solution that works for any folder structure.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- Fixed false positive for "Page missing MAIN landmark" when `<main>` is in the layout file.
|
|
55
|
+
- Fixed false positive for "Page has h2 but no h1 heading" when H1 is in a partial rendered via `render @model`.
|
|
56
|
+
- Fixed false positive for "Heading hierarchy skipped (h0 to h2)" when first heading is h2.
|
|
57
|
+
- Fixed false positive for "Duplicate ID 'ERB_CONTENT'" by filtering out ERB placeholder strings.
|
|
58
|
+
- Fixed partial detection for Rails shorthand patterns (`render @model`, `render collection: @models`).
|
|
59
|
+
- Fixed namespaced partial path resolution (e.g., `layouts/_advance_search`).
|
|
60
|
+
- Fixed path normalization to handle both relative and absolute paths consistently.
|
|
61
|
+
|
|
62
|
+
### Performance
|
|
63
|
+
- Optimized exhaustive directory search to return first match found instead of checking all matches.
|
|
64
|
+
|
|
65
|
+
## [Unreleased]
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
- **Composed Page Scanning**: The static scanner now analyzes complete page compositions (layout + view + partials) for page-level accessibility checks, eliminating false positives for heading hierarchy, ARIA landmarks, duplicate IDs, and heading issues.
|
|
69
|
+
- **View Composition Builder**: New comprehensive system that traces the complete page structure by finding all partials recursively across all directories in `app/views`.
|
|
70
|
+
- **Exhaustive Partial Detection**: Enhanced partial detection that finds partials in any subdirectory, not just specific folders. Works for deeply nested namespaces (e.g., `collections/collection_questions`).
|
|
71
|
+
- **ERB Content Detection**: Improved handling of ERB expressions (`<%= ... %>`) to prevent false positives for empty headings and missing accessible names on buttons.
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
- **Heading Hierarchy Checks**: Now performed on the complete composed page instead of individual files, preventing false positives when H1 is in layout or partials.
|
|
75
|
+
- **ARIA Landmarks Checks**: Now checks for `<main>` landmark across the entire composed page.
|
|
76
|
+
- **Duplicate ID Checks**: Now checks for duplicate IDs across the complete page composition.
|
|
77
|
+
- **Empty Heading Checks**: Now checks for empty headings across the complete composed page.
|
|
78
|
+
- **Partial Search**: Enhanced to traverse ALL folders in `app/views` recursively using `Dir.glob`, making it a general solution that works for any folder structure.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
- Fixed false positive for "Page missing MAIN landmark" when `<main>` is in the layout file.
|
|
82
|
+
- Fixed false positive for "Page has h2 but no h1 heading" when H1 is in a partial rendered via `render @model`.
|
|
83
|
+
- Fixed false positive for "Heading hierarchy skipped (h0 to h2)" when first heading is h2.
|
|
84
|
+
- Fixed false positive for "Duplicate ID 'ERB_CONTENT'" by filtering out ERB placeholder strings.
|
|
85
|
+
- Fixed partial detection for Rails shorthand patterns (`render @model`, `render collection: @models`).
|
|
86
|
+
- Fixed namespaced partial path resolution (e.g., `layouts/_advance_search`).
|
|
87
|
+
- Fixed path normalization to handle both relative and absolute paths consistently.
|
|
88
|
+
|
|
89
|
+
### Performance
|
|
90
|
+
- Optimized exhaustive directory search to return first match found instead of checking all matches.
|
|
91
|
+
|
|
92
|
+
## [1.5.9] - 2024-12-01
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
- **Force option for manual checks**: Added `force: true` parameter to `check_comprehensive_accessibility` and `check_basic_accessibility` to bypass `enabled: false` setting
|
|
96
|
+
- **Manual check flexibility**: You can now force manual checks to run even when accessibility checks are globally disabled
|
|
97
|
+
|
|
98
|
+
### Improved
|
|
99
|
+
- **Manual check documentation**: Clarified that manual checks can be run separately in RSpec specs regardless of configuration
|
|
100
|
+
|
|
8
101
|
## [1.5.8] - 2024-12-01
|
|
9
102
|
|
|
10
103
|
### Fixed
|
data/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
**The RSpec + RuboCop of accessibility for Rails. Catch WCAG violations before they reach production.**
|
|
9
9
|
|
|
10
|
-
**Current Version:** 1.5.
|
|
10
|
+
**Current Version:** 1.5.9
|
|
11
11
|
|
|
12
12
|
📖 **[📚 Full Documentation](https://rayraycodes.github.io/rails-accessibility-testing/)** | [💻 GitHub](https://github.com/rayraycodes/rails-accessibility-testing) | [💎 RubyGems](https://rubygems.org/gems/rails_accessibility_testing)
|
|
13
13
|
|
|
@@ -51,6 +51,13 @@ Rails Accessibility Testing fills a critical gap in the Rails testing ecosystem.
|
|
|
51
51
|
- **Accurate error counting**: Properly tracks and displays error/warning counts
|
|
52
52
|
- **Persistent output**: Errors stay visible in terminal (no clearing)
|
|
53
53
|
|
|
54
|
+
#### 🔍 Composed Page Scanning (NEW in 1.5.9+)
|
|
55
|
+
- **Complete page analysis**: Analyzes full page composition (layout + view + partials) for page-level checks
|
|
56
|
+
- **Eliminates false positives**: No more false positives when H1 is in layout or partials
|
|
57
|
+
- **Exhaustive partial finding**: Traverses ALL folders recursively to find all partials
|
|
58
|
+
- **Works for any structure**: General solution that works for any Rails folder structure (collections, items, profiles, loan_requests, etc.)
|
|
59
|
+
- **Page-level checks**: Heading hierarchy, ARIA landmarks, duplicate IDs, and empty headings checked across complete page
|
|
60
|
+
|
|
54
61
|
#### 🔍 Smart View File Detection
|
|
55
62
|
- **Intelligent matching**: Automatically finds view files even when action names don't match
|
|
56
63
|
- **Controller directory scanning**: Searches all view files to find the correct template
|
|
@@ -61,6 +68,7 @@ Rails Accessibility Testing fills a critical gap in the Rails testing ecosystem.
|
|
|
61
68
|
- **Optimized DOM queries**: Faster image alt checks without JavaScript evaluation
|
|
62
69
|
- **Removed delays**: Eliminated unnecessary sleep calls in live scanner
|
|
63
70
|
- **Efficient scanning**: ~25-30% faster page scans
|
|
71
|
+
- **Optimized directory search**: Returns first match found instead of checking all matches
|
|
64
72
|
|
|
65
73
|
#### 🎨 Enhanced Developer Experience
|
|
66
74
|
- **Real-time progress**: Step-by-step feedback during accessibility checks
|
data/docs_site/architecture.md
CHANGED
|
@@ -3,7 +3,45 @@ layout: default
|
|
|
3
3
|
title: Architecture
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Architecture
|
|
6
|
+
# Architecture
|
|
7
|
+
|
|
8
|
+
## Composed Page Scanning
|
|
9
|
+
|
|
10
|
+
The gem now uses **composed page scanning** for page-level accessibility checks. This ensures that checks like heading hierarchy, ARIA landmarks, and duplicate IDs are evaluated against the complete rendered page (layout + view + partials), not individual files.
|
|
11
|
+
|
|
12
|
+
### View Composition Builder
|
|
13
|
+
|
|
14
|
+
The `ViewCompositionBuilder` class traces the complete page structure:
|
|
15
|
+
|
|
16
|
+
1. **Finds Layout File**: Identifies the layout file (defaults to `application.html.erb`)
|
|
17
|
+
2. **Finds View File**: The main view file being rendered
|
|
18
|
+
3. **Recursively Finds Partials**: Discovers all partials rendered in the view, including:
|
|
19
|
+
- Partials in the same directory
|
|
20
|
+
- Partials in `layouts/`, `shared/`, `application/`
|
|
21
|
+
- Partials in any subdirectory (exhaustive search)
|
|
22
|
+
- Nested partials (partials within partials)
|
|
23
|
+
|
|
24
|
+
### Partial Detection
|
|
25
|
+
|
|
26
|
+
The gem detects all Rails render patterns:
|
|
27
|
+
- `render 'partial'`
|
|
28
|
+
- `render partial: 'partial'`
|
|
29
|
+
- `render @model` (Rails shorthand)
|
|
30
|
+
- `render collection: @models`
|
|
31
|
+
- `render partial: 'item', collection: @items`
|
|
32
|
+
- `render partial: 'form', locals: {...}`
|
|
33
|
+
|
|
34
|
+
### Exhaustive Folder Traversal
|
|
35
|
+
|
|
36
|
+
The partial search traverses ALL folders in `app/views` recursively using `Dir.glob`, ensuring partials are found regardless of their location:
|
|
37
|
+
- `app/views/collections/`
|
|
38
|
+
- `app/views/collections/collection_questions/`
|
|
39
|
+
- `app/views/items/`
|
|
40
|
+
- `app/views/profiles/`
|
|
41
|
+
- `app/views/loan_requests/`
|
|
42
|
+
- Any other nested structure
|
|
43
|
+
|
|
44
|
+
This makes it a general solution that works for any Rails application structure. Overview
|
|
7
45
|
|
|
8
46
|
This guide explains how Rails Accessibility Testing works under the hood in simple terms.
|
|
9
47
|
|
|
@@ -129,6 +167,61 @@ graph TB
|
|
|
129
167
|
style Report fill:#ffeaa7
|
|
130
168
|
```
|
|
131
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
|
+
|
|
132
225
|
---
|
|
133
226
|
|
|
134
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
|
|
147
|
-
|
|
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
|
|
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
|
-
6. **ARIA Landmarks** - Proper use of ARIA landmark roles
|
|
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
|
-
9. **Duplicate IDs** - No duplicate ID attributes
|
|
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
|
|
data/exe/a11y_live_scanner
CHANGED
|
@@ -35,10 +35,11 @@ begin
|
|
|
35
35
|
require 'rails_accessibility_testing/config/yaml_loader'
|
|
36
36
|
profile = defined?(Rails) && Rails.env.test? ? :test : :development
|
|
37
37
|
config = RailsAccessibilityTesting::Config::YamlLoader.load(profile: profile)
|
|
38
|
-
|
|
38
|
+
# Support both 'accessibility_enabled' (new) and 'enabled' (legacy) for backward compatibility
|
|
39
|
+
enabled = config.fetch('accessibility_enabled', config.fetch('enabled', true))
|
|
39
40
|
unless enabled
|
|
40
|
-
puts "⏸️ Accessibility checks are disabled (
|
|
41
|
-
puts " Set
|
|
41
|
+
puts "⏸️ Accessibility checks are disabled (accessibility_enabled: false in config/accessibility.yml)"
|
|
42
|
+
puts " Set accessibility_enabled: true to enable accessibility scanning"
|
|
42
43
|
puts " Scanner process will remain running but will not scan files"
|
|
43
44
|
puts ""
|
|
44
45
|
# Keep process alive so Foreman doesn't kill other processes
|
data/exe/a11y_static_scanner
CHANGED
|
@@ -29,10 +29,11 @@ begin
|
|
|
29
29
|
require 'rails_accessibility_testing/config/yaml_loader'
|
|
30
30
|
profile = defined?(Rails) && Rails.env.test? ? :test : :development
|
|
31
31
|
config = RailsAccessibilityTesting::Config::YamlLoader.load(profile: profile)
|
|
32
|
-
|
|
32
|
+
# Support both 'accessibility_enabled' (new) and 'enabled' (legacy) for backward compatibility
|
|
33
|
+
enabled = config.fetch('accessibility_enabled', config.fetch('enabled', true))
|
|
33
34
|
unless enabled
|
|
34
|
-
puts "⏸️ Accessibility checks are disabled (
|
|
35
|
-
puts " Set
|
|
35
|
+
puts "⏸️ Accessibility checks are disabled (accessibility_enabled: false in config/accessibility.yml)"
|
|
36
|
+
puts " Set accessibility_enabled: true to enable accessibility scanning"
|
|
36
37
|
puts " Scanner process will remain running but will not scan files"
|
|
37
38
|
puts ""
|
|
38
39
|
# Keep process alive so Foreman doesn't kill other processes
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
# This file configures accessibility checks for your Rails application.
|
|
4
4
|
# See https://github.com/your-org/rails-a11y for full documentation.
|
|
5
5
|
|
|
6
|
-
# Global enable/disable flag
|
|
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
9
|
# Default: true
|
|
10
|
-
|
|
10
|
+
accessibility_enabled: true
|
|
11
11
|
|
|
12
12
|
# WCAG compliance level (A, AA, AAA)
|
|
13
13
|
wcag_level: AA
|
|
@@ -46,14 +46,34 @@ module RailsAccessibilityTesting
|
|
|
46
46
|
# render partial: "partial_name"
|
|
47
47
|
# render 'path/to/partial'
|
|
48
48
|
# render partial: 'path/to/partial'
|
|
49
|
+
# render @model (Rails shorthand - renders _model.html.erb)
|
|
50
|
+
# render collection: @models (renders _model.html.erb for each)
|
|
51
|
+
# render partial: 'partial', locals: {...}
|
|
52
|
+
# render partial: 'partial', collection: @items
|
|
49
53
|
# <%= render 'partial_name' %>
|
|
50
54
|
# <%= render partial: 'partial_name' %>
|
|
55
|
+
# <%= render @model %>
|
|
56
|
+
# <%= render collection: @models %>
|
|
51
57
|
|
|
52
58
|
patterns = [
|
|
59
|
+
# Standard partial renders (with or without partial: keyword)
|
|
53
60
|
/render\s+(?:partial:\s*)?['"]([^'"]+)['"]/,
|
|
54
61
|
/render\s+(?:partial:\s*)?:(\w+)/,
|
|
55
62
|
/<%=?\s*render\s+(?:partial:\s*)?['"]([^'"]+)['"]/,
|
|
56
|
-
/<%=?\s*render\s+(?:partial:\s*)?:(\w+)
|
|
63
|
+
/<%=?\s*render\s+(?:partial:\s*)?:(\w+)/,
|
|
64
|
+
# Rails shorthand: render @model (renders _model.html.erb)
|
|
65
|
+
/render\s+@(\w+)/,
|
|
66
|
+
/<%=?\s*render\s+@(\w+)/,
|
|
67
|
+
# Collection renders: render collection: @models (renders _model.html.erb)
|
|
68
|
+
/render\s+collection:\s*@(\w+)/,
|
|
69
|
+
/<%=?\s*render\s+collection:\s*@(\w+)/,
|
|
70
|
+
# Partial with collection: render partial: 'item', collection: @items
|
|
71
|
+
/render\s+partial:\s*['"]([^'"]+)['"]\s*,\s*collection:/,
|
|
72
|
+
/<%=?\s*render\s+partial:\s*['"]([^'"]+)['"]\s*,\s*collection:/,
|
|
73
|
+
# Partial with locals: render partial: 'form', locals: {...}
|
|
74
|
+
# (extract just the partial name, ignore locals)
|
|
75
|
+
/render\s+partial:\s*['"]([^'"]+)['"]\s*,\s*locals:/,
|
|
76
|
+
/<%=?\s*render\s+partial:\s*['"]([^'"]+)['"]\s*,\s*locals:/
|
|
57
77
|
]
|
|
58
78
|
|
|
59
79
|
patterns.each do |pattern|
|
|
@@ -77,6 +97,23 @@ module RailsAccessibilityTesting
|
|
|
77
97
|
end
|
|
78
98
|
end
|
|
79
99
|
|
|
100
|
+
# Also check for Rails shorthand: render @model (renders _model.html.erb)
|
|
101
|
+
# This is separate because it needs special handling
|
|
102
|
+
content.scan(/render\s+@(\w+)/) do |match|
|
|
103
|
+
model_name = match[0]
|
|
104
|
+
partial_name = model_name.underscore
|
|
105
|
+
partial_name = "_#{partial_name}" unless partial_name.start_with?('_')
|
|
106
|
+
partials << partial_name unless partials.include?(partial_name)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Check for collection renders: render collection: @models
|
|
110
|
+
content.scan(/render\s+collection:\s*@(\w+)/) do |match|
|
|
111
|
+
model_name = match[0]
|
|
112
|
+
partial_name = model_name.underscore.singularize # collection uses singular
|
|
113
|
+
partial_name = "_#{partial_name}" unless partial_name.start_with?('_')
|
|
114
|
+
partials << partial_name unless partials.include?(partial_name)
|
|
115
|
+
end
|
|
116
|
+
|
|
80
117
|
partials
|
|
81
118
|
end
|
|
82
119
|
|
|
@@ -210,9 +247,11 @@ module RailsAccessibilityTesting
|
|
|
210
247
|
end
|
|
211
248
|
|
|
212
249
|
# Basic accessibility check - runs 5 basic checks
|
|
213
|
-
|
|
250
|
+
# @param force [Boolean] If true, bypasses the accessibility_enabled: false setting (default: false)
|
|
251
|
+
def check_basic_accessibility(force: false)
|
|
214
252
|
# Check if accessibility checks are globally disabled
|
|
215
|
-
|
|
253
|
+
# Manual calls can use force: true to bypass the accessibility_enabled: false setting
|
|
254
|
+
return if !force && accessibility_disabled?
|
|
216
255
|
|
|
217
256
|
@accessibility_errors ||= []
|
|
218
257
|
@accessibility_warnings ||= []
|
|
@@ -244,10 +283,12 @@ module RailsAccessibilityTesting
|
|
|
244
283
|
|
|
245
284
|
# Full comprehensive check - runs all 11 checks including advanced
|
|
246
285
|
# Uses the RuleEngine and checks from the checks/ folder for consistency
|
|
286
|
+
# @param force [Boolean] If true, bypasses the accessibility_enabled: false setting (default: false)
|
|
247
287
|
# @return [Hash] Hash with :errors and :warnings counts
|
|
248
|
-
def check_comprehensive_accessibility
|
|
288
|
+
def check_comprehensive_accessibility(force: false)
|
|
249
289
|
# Check if accessibility checks are globally disabled - do this FIRST before any output
|
|
250
|
-
|
|
290
|
+
# Manual calls can use force: true to bypass the accessibility_enabled: false setting
|
|
291
|
+
if !force && accessibility_disabled?
|
|
251
292
|
return { errors: 0, warnings: 0, page_context: {} }
|
|
252
293
|
end
|
|
253
294
|
|
|
@@ -447,7 +488,8 @@ module RailsAccessibilityTesting
|
|
|
447
488
|
require 'rails_accessibility_testing/config/yaml_loader'
|
|
448
489
|
profile = defined?(Rails) && Rails.env.test? ? :test : :development
|
|
449
490
|
config = RailsAccessibilityTesting::Config::YamlLoader.load(profile: profile)
|
|
450
|
-
|
|
491
|
+
# Support both 'accessibility_enabled' (new) and 'enabled' (legacy) for backward compatibility
|
|
492
|
+
enabled = config.fetch('accessibility_enabled', config.fetch('enabled', true)) # Default to enabled if not specified
|
|
451
493
|
disabled = !enabled # Return true if disabled
|
|
452
494
|
disabled
|
|
453
495
|
rescue StandardError => e
|
|
@@ -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?
|
|
@@ -23,7 +23,7 @@ module RailsAccessibilityTesting
|
|
|
23
23
|
return violations # Warning only, not error
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
# Check 1: Missing H1
|
|
26
|
+
# Check 1: Missing H1 (page-level check - checks complete composed page)
|
|
27
27
|
h1_count = headings.count { |h| h.tag_name == 'h1' }
|
|
28
28
|
first_heading = headings.first
|
|
29
29
|
first_heading_level = first_heading ? first_heading.tag_name[1].to_i : nil
|
|
@@ -32,30 +32,35 @@ module RailsAccessibilityTesting
|
|
|
32
32
|
# If the first heading is h2 or higher, provide a more specific message
|
|
33
33
|
if first_heading_level && first_heading_level >= 2
|
|
34
34
|
element_ctx = element_context(first_heading)
|
|
35
|
+
# Mark as page-level check - H1 might be in layout or partial
|
|
36
|
+
element_ctx[:page_level_check] = true
|
|
37
|
+
element_ctx[:check_type] = 'heading_hierarchy'
|
|
35
38
|
violations << violation(
|
|
36
|
-
message: "Page has h#{first_heading_level} but no h1 heading",
|
|
39
|
+
message: "Page has h#{first_heading_level} but no h1 heading (checked complete page: layout + view + partials)",
|
|
37
40
|
element_context: element_ctx,
|
|
38
41
|
wcag_reference: "1.3.1",
|
|
39
|
-
remediation: "Add an <h1> heading
|
|
42
|
+
remediation: "Add an <h1> heading to your page. The H1 can be in the layout, view, or a partial:\n\n<h1>Main Page Title</h1>\n<h#{first_heading_level}>#{first_heading.text}</h#{first_heading_level}>"
|
|
40
43
|
)
|
|
41
44
|
else
|
|
42
45
|
violations << violation(
|
|
43
|
-
message: "Page missing H1 heading",
|
|
44
|
-
element_context: { tag: 'page', text: 'Page has no H1 heading' },
|
|
46
|
+
message: "Page missing H1 heading (checked complete page: layout + view + partials)",
|
|
47
|
+
element_context: { tag: 'page', text: 'Page has no H1 heading', page_level_check: true, check_type: 'heading_hierarchy' },
|
|
45
48
|
wcag_reference: "1.3.1",
|
|
46
|
-
remediation: "Add an <h1> heading to your page:\n\n<h1>Main Page Title</h1>"
|
|
49
|
+
remediation: "Add an <h1> heading to your page. The H1 can be in the layout, view, or a partial:\n\n<h1>Main Page Title</h1>"
|
|
47
50
|
)
|
|
48
51
|
end
|
|
49
52
|
end
|
|
50
53
|
|
|
51
|
-
# Check 2: Multiple H1s (WCAG 1.3.1)
|
|
54
|
+
# Check 2: Multiple H1s (WCAG 1.3.1) - page-level check
|
|
52
55
|
if h1_count > 1
|
|
53
56
|
# Report error for each h1 after the first one
|
|
54
57
|
h1_elements = headings.select { |h| h.tag_name == 'h1' }
|
|
55
58
|
h1_elements[1..-1].each do |h1|
|
|
56
59
|
element_ctx = element_context(h1)
|
|
60
|
+
element_ctx[:page_level_check] = true
|
|
61
|
+
element_ctx[:check_type] = 'heading_hierarchy'
|
|
57
62
|
violations << violation(
|
|
58
|
-
message: "Page has multiple h1 headings (#{h1_count} total) - only one h1 should be used per page",
|
|
63
|
+
message: "Page has multiple h1 headings (#{h1_count} total) - only one h1 should be used per page (checked complete page: layout + view + partials)",
|
|
59
64
|
element_context: element_ctx,
|
|
60
65
|
wcag_reference: "1.3.1",
|
|
61
66
|
remediation: "Use only one <h1> per page. Convert additional h1s to h2 or lower:\n\n<h1>Main Title</h1>\n<h2>Section Title</h2>"
|
|
@@ -63,14 +68,16 @@ module RailsAccessibilityTesting
|
|
|
63
68
|
end
|
|
64
69
|
end
|
|
65
70
|
|
|
66
|
-
# Check 3: Heading hierarchy skipped levels (WCAG 1.3.1)
|
|
71
|
+
# Check 3: Heading hierarchy skipped levels (WCAG 1.3.1) - page-level check
|
|
67
72
|
previous_level = 0
|
|
68
73
|
headings.each do |heading|
|
|
69
74
|
current_level = heading.tag_name[1].to_i
|
|
70
75
|
if current_level > previous_level + 1
|
|
71
76
|
element_ctx = element_context(heading)
|
|
77
|
+
element_ctx[:page_level_check] = true
|
|
78
|
+
element_ctx[:check_type] = 'heading_hierarchy'
|
|
72
79
|
violations << violation(
|
|
73
|
-
message: "Heading hierarchy skipped (h#{previous_level} to h#{current_level})",
|
|
80
|
+
message: "Heading hierarchy skipped (h#{previous_level} to h#{current_level}) - checked complete page: layout + view + partials",
|
|
74
81
|
element_context: element_ctx,
|
|
75
82
|
wcag_reference: "1.3.1",
|
|
76
83
|
remediation: "Fix the heading hierarchy. Don't skip levels. Use h#{previous_level + 1} instead of h#{current_level}."
|