rails_accessibility_testing 1.5.5 → 1.5.6
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/ARCHITECTURE.md +336 -71
- data/CHANGELOG.md +17 -0
- data/GUIDES/getting_started.md +46 -177
- data/README.md +4 -0
- data/docs_site/_config.yml +3 -0
- data/docs_site/_layouts/default.html +95 -588
- data/docs_site/architecture.md +98 -469
- data/docs_site/ci_integration.md +87 -32
- data/docs_site/configuration.md +119 -51
- data/docs_site/contributing.md +166 -6
- data/docs_site/favicon.svg +31 -0
- data/docs_site/getting_started.md +188 -66
- data/docs_site/index.md +136 -21
- data/lib/generators/rails_a11y/install/templates/accessibility.yml.erb +16 -0
- data/lib/rails_accessibility_testing/accessibility_helper.rb +86 -16
- data/lib/rails_accessibility_testing/checks/base_check.rb +32 -5
- data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +23 -0
- data/lib/rails_accessibility_testing/config/yaml_loader.rb +9 -0
- data/lib/rails_accessibility_testing/error_message_builder.rb +28 -0
- data/lib/rails_accessibility_testing/rspec_integration.rb +25 -1
- data/lib/rails_accessibility_testing/version.rb +1 -1
- metadata +3 -2
|
@@ -115,6 +115,43 @@ module RailsAccessibilityTesting
|
|
|
115
115
|
|
|
116
116
|
nil
|
|
117
117
|
end
|
|
118
|
+
|
|
119
|
+
# Find partial in layouts directory based on element context
|
|
120
|
+
def find_partial_in_layouts(element_context)
|
|
121
|
+
return nil unless element_context
|
|
122
|
+
|
|
123
|
+
extensions = %w[erb haml slim]
|
|
124
|
+
id = element_context[:id].to_s
|
|
125
|
+
classes = element_context[:classes].to_s
|
|
126
|
+
|
|
127
|
+
# Common layout partial names
|
|
128
|
+
partial_names = []
|
|
129
|
+
partial_names << id.split('-').first if id.present?
|
|
130
|
+
partial_names << id.split('_').first if id.present?
|
|
131
|
+
|
|
132
|
+
if classes.present?
|
|
133
|
+
classes.split(/\s+/).each do |cls|
|
|
134
|
+
partial_names << cls.split('-').first
|
|
135
|
+
partial_names << cls.split('_').first
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Check all partials in layouts directory
|
|
140
|
+
extensions.each do |ext|
|
|
141
|
+
# First try specific names
|
|
142
|
+
partial_names.uniq.each do |partial_name|
|
|
143
|
+
next if partial_name.blank?
|
|
144
|
+
partial_path = "app/views/layouts/_#{partial_name}.html.#{ext}"
|
|
145
|
+
return partial_path if File.exist?(partial_path)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# If no match, scan all layout partials and try to match by content
|
|
149
|
+
layout_partials = Dir.glob("app/views/layouts/_*.html.#{ext}")
|
|
150
|
+
# Could add content-based matching here if needed
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
118
155
|
end
|
|
119
156
|
|
|
120
157
|
# Include PartialDetection in AccessibilityHelper so methods are available
|
|
@@ -598,6 +635,16 @@ module RailsAccessibilityTesting
|
|
|
598
635
|
end
|
|
599
636
|
|
|
600
637
|
# Determine likely view file based on Rails path and element context
|
|
638
|
+
#
|
|
639
|
+
# Priority order (based on Rails HTML structure):
|
|
640
|
+
# 1. View file (yield content) - most common location for page-specific issues
|
|
641
|
+
# 2. Partials rendered in the view file
|
|
642
|
+
# 3. Layout partials (navbar, footer) - site-wide components
|
|
643
|
+
# 4. Layout file (application.html.erb) - wrapper structure
|
|
644
|
+
#
|
|
645
|
+
# This ensures errors are attributed to the correct file:
|
|
646
|
+
# - Page-specific issues → view files (yield content)
|
|
647
|
+
# - Site-wide issues → layout partials
|
|
601
648
|
def determine_view_file(path, url, element_context = nil)
|
|
602
649
|
return nil unless path
|
|
603
650
|
|
|
@@ -610,10 +657,12 @@ module RailsAccessibilityTesting
|
|
|
610
657
|
controller = route[:controller]
|
|
611
658
|
action = route[:action]
|
|
612
659
|
|
|
613
|
-
# Try to find the exact view file
|
|
660
|
+
# Priority 1: Try to find the exact view file (yield content)
|
|
661
|
+
# This is where most page-specific accessibility issues occur
|
|
614
662
|
view_file = find_view_file_for_controller_action(controller, action)
|
|
615
663
|
|
|
616
|
-
#
|
|
664
|
+
# Priority 2: Check for partials rendered in the view file
|
|
665
|
+
# These are page-specific partials (e.g., _form, _item_card)
|
|
617
666
|
if view_file && element_context
|
|
618
667
|
# Scan the view file for rendered partials
|
|
619
668
|
partials_in_view = find_partials_in_view_file(view_file)
|
|
@@ -623,23 +672,25 @@ module RailsAccessibilityTesting
|
|
|
623
672
|
return partial_file if partial_file
|
|
624
673
|
end
|
|
625
674
|
|
|
626
|
-
#
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
# Also check layout partials
|
|
634
|
-
layout_partial = find_partial_in_layouts(element_context)
|
|
635
|
-
return layout_partial if layout_partial
|
|
636
|
-
end
|
|
675
|
+
# Priority 3: Check if element is in a layout partial (navbar, footer, etc.)
|
|
676
|
+
# These are site-wide components rendered in the layout
|
|
677
|
+
if element_context && element_in_layout?(element_context)
|
|
678
|
+
# Check layout partials first (more specific than layout file)
|
|
679
|
+
layout_partial = find_partial_in_layouts(element_context)
|
|
680
|
+
return layout_partial if layout_partial
|
|
637
681
|
|
|
638
|
-
#
|
|
682
|
+
# Then check layout file itself
|
|
683
|
+
layout_file = find_layout_file
|
|
684
|
+
return layout_file if layout_file
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Priority 4: Check other partials (shared partials, etc.)
|
|
688
|
+
if element_context
|
|
639
689
|
partial_file = find_partial_for_element(controller, element_context)
|
|
640
690
|
return partial_file if partial_file
|
|
641
691
|
end
|
|
642
692
|
|
|
693
|
+
# Return view file if found (yield content - most common case)
|
|
643
694
|
return view_file if view_file
|
|
644
695
|
rescue StandardError => e
|
|
645
696
|
# Fall through to path-based detection
|
|
@@ -713,6 +764,15 @@ module RailsAccessibilityTesting
|
|
|
713
764
|
end
|
|
714
765
|
|
|
715
766
|
# Check if element is likely in a layout (navbar, footer, etc.)
|
|
767
|
+
# vs yield content (main area)
|
|
768
|
+
#
|
|
769
|
+
# Based on Rails HTML structure:
|
|
770
|
+
# - Layout elements: navbar, footer, header (outside <main>)
|
|
771
|
+
# - Yield content: elements inside <main id="maincontent"> (view files)
|
|
772
|
+
#
|
|
773
|
+
# This helps correctly attribute errors:
|
|
774
|
+
# - Layout issues → layout partials/files
|
|
775
|
+
# - Page-specific issues → view files (yield content)
|
|
716
776
|
def element_in_layout?(element_context)
|
|
717
777
|
return false unless element_context
|
|
718
778
|
|
|
@@ -720,8 +780,18 @@ module RailsAccessibilityTesting
|
|
|
720
780
|
parent = element_context[:parent]
|
|
721
781
|
return false unless parent
|
|
722
782
|
|
|
723
|
-
#
|
|
724
|
-
|
|
783
|
+
# If element is inside <main>, it's likely yield content (view file), not layout
|
|
784
|
+
# Check if parent or ancestor is main element
|
|
785
|
+
parent_tag = parent[:tag].to_s.downcase
|
|
786
|
+
parent_id = parent[:id].to_s.downcase
|
|
787
|
+
|
|
788
|
+
# Elements inside <main> are yield content, not layout
|
|
789
|
+
if parent_tag == 'main' || parent_id.include?('maincontent') || parent_id.include?('main-content')
|
|
790
|
+
return false
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# Common layout class/id patterns (navbar, footer, header)
|
|
794
|
+
layout_indicators = ['navbar', 'nav', 'footer', 'header', 'main-nav', 'sidebar', 'skip']
|
|
725
795
|
|
|
726
796
|
classes = parent[:classes].to_s.downcase
|
|
727
797
|
id = parent[:id].to_s.downcase
|
|
@@ -137,8 +137,12 @@ module RailsAccessibilityTesting
|
|
|
137
137
|
nil
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
-
# Determine likely view file (simplified version)
|
|
141
|
-
#
|
|
140
|
+
# Determine likely view file (simplified version for checks)
|
|
141
|
+
# Uses same priority logic as AccessibilityHelper:
|
|
142
|
+
# 1. View file (yield content) - most common
|
|
143
|
+
# 2. Partials in view file
|
|
144
|
+
# 3. Layout partials
|
|
145
|
+
# 4. Layout file
|
|
142
146
|
def determine_view_file(element_context = nil)
|
|
143
147
|
return nil unless safe_page_path
|
|
144
148
|
|
|
@@ -150,20 +154,43 @@ module RailsAccessibilityTesting
|
|
|
150
154
|
controller = route[:controller]
|
|
151
155
|
action = route[:action]
|
|
152
156
|
|
|
157
|
+
# Priority 1: View file (yield content)
|
|
153
158
|
view_file = find_view_file_for_controller_action(controller, action)
|
|
154
159
|
|
|
155
|
-
#
|
|
160
|
+
# Priority 2: Partials rendered in the view file
|
|
156
161
|
if view_file && element_context
|
|
157
|
-
# Scan the view file for rendered partials
|
|
158
162
|
partials_in_view = find_partials_in_view_file(view_file)
|
|
159
163
|
|
|
160
|
-
# Check if element matches any partial in the view
|
|
161
164
|
if partials_in_view.any?
|
|
162
165
|
partial_file = find_partial_for_element_in_list(controller, element_context, partials_in_view)
|
|
163
166
|
return partial_file if partial_file
|
|
164
167
|
end
|
|
165
168
|
end
|
|
166
169
|
|
|
170
|
+
# Priority 3: Layout partials (if element is in layout area)
|
|
171
|
+
if element_context
|
|
172
|
+
# Use the same element_in_layout? logic from AccessibilityHelper
|
|
173
|
+
# Check if element is likely in layout (navbar, footer, etc.)
|
|
174
|
+
parent = element_context[:parent]
|
|
175
|
+
if parent
|
|
176
|
+
parent_tag = parent[:tag].to_s.downcase
|
|
177
|
+
parent_id = parent[:id].to_s.downcase
|
|
178
|
+
|
|
179
|
+
# Skip if inside <main> (yield content)
|
|
180
|
+
unless parent_tag == 'main' || parent_id.include?('maincontent') || parent_id.include?('main-content')
|
|
181
|
+
layout_indicators = ['navbar', 'nav', 'footer', 'header', 'main-nav', 'sidebar', 'skip']
|
|
182
|
+
classes = parent[:classes].to_s.downcase
|
|
183
|
+
id = parent[:id].to_s.downcase
|
|
184
|
+
|
|
185
|
+
if layout_indicators.any? { |indicator| classes.include?(indicator) || id.include?(indicator) }
|
|
186
|
+
layout_partial = find_partial_in_layouts(element_context)
|
|
187
|
+
return layout_partial if layout_partial
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Return view file (yield content) - most common case
|
|
167
194
|
view_file
|
|
168
195
|
rescue StandardError
|
|
169
196
|
nil
|
|
@@ -18,6 +18,29 @@ module RailsAccessibilityTesting
|
|
|
18
18
|
page.all('button, a[href], [role="button"], [role="link"]').each do |element|
|
|
19
19
|
next unless element.visible?
|
|
20
20
|
|
|
21
|
+
# Check for headings inside button elements (accessibility violation)
|
|
22
|
+
# Headings should not be nested inside interactive elements
|
|
23
|
+
if element.tag_name == 'button' || element[:role] == 'button'
|
|
24
|
+
headings_inside = element.all('h1, h2, h3, h4, h5, h6', visible: true)
|
|
25
|
+
if headings_inside.any?
|
|
26
|
+
headings_inside.each do |heading|
|
|
27
|
+
element_ctx = element_context(element)
|
|
28
|
+
heading_level = heading.tag_name
|
|
29
|
+
element_ctx[:nested_heading] = heading_level
|
|
30
|
+
element_ctx[:heading_text] = heading.text.strip
|
|
31
|
+
|
|
32
|
+
violations << violation(
|
|
33
|
+
message: "Button contains #{heading_level.upcase} heading - headings should not be nested inside buttons",
|
|
34
|
+
element_context: element_ctx,
|
|
35
|
+
wcag_reference: "4.1.2",
|
|
36
|
+
remediation: "Remove the heading from inside the button. Use plain text or aria-label instead:\n\n" \
|
|
37
|
+
"<button aria-label='#{heading.text.strip}'>#{heading.text.strip}</button>\n\n" \
|
|
38
|
+
"Or use a div/span with appropriate styling instead of a heading element."
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
21
44
|
text = element.text.to_s.strip
|
|
22
45
|
aria_label = element[:"aria-label"]
|
|
23
46
|
aria_labelledby = element[:"aria-labelledby"]
|
|
@@ -78,11 +78,17 @@ module RailsAccessibilityTesting
|
|
|
78
78
|
profile_static_scanner = profile_config['static_scanner'] || {}
|
|
79
79
|
merged_static_scanner = base_static_scanner.merge(profile_static_scanner)
|
|
80
80
|
|
|
81
|
+
# Merge system_specs config
|
|
82
|
+
base_system_specs = base_config['system_specs'] || {}
|
|
83
|
+
profile_system_specs = profile_config['system_specs'] || {}
|
|
84
|
+
merged_system_specs = base_system_specs.merge(profile_system_specs)
|
|
85
|
+
|
|
81
86
|
base_config.merge(
|
|
82
87
|
'checks' => merged_checks,
|
|
83
88
|
'summary' => merged_summary,
|
|
84
89
|
'scan_strategy' => profile_config['scan_strategy'] || base_config['scan_strategy'] || 'paths',
|
|
85
90
|
'static_scanner' => merged_static_scanner,
|
|
91
|
+
'system_specs' => merged_system_specs,
|
|
86
92
|
'profile' => profile.to_s,
|
|
87
93
|
'ignored_rules' => parse_ignored_rules(parsed, profile)
|
|
88
94
|
)
|
|
@@ -125,6 +131,9 @@ module RailsAccessibilityTesting
|
|
|
125
131
|
'ignore_warnings' => false
|
|
126
132
|
},
|
|
127
133
|
'scan_strategy' => 'paths',
|
|
134
|
+
'system_specs' => {
|
|
135
|
+
'auto_run' => false # Run accessibility checks automatically in system specs (default: false)
|
|
136
|
+
},
|
|
128
137
|
'ignored_rules' => [],
|
|
129
138
|
'profile' => 'test'
|
|
130
139
|
}
|
|
@@ -138,6 +138,8 @@ module RailsAccessibilityTesting
|
|
|
138
138
|
custom_element_remediation(error_type, element_context)
|
|
139
139
|
when /Duplicate IDs found/i
|
|
140
140
|
duplicate_ids_remediation(element_context)
|
|
141
|
+
when /Button contains.*heading|heading.*inside.*button/i
|
|
142
|
+
button_heading_remediation(error_type, element_context)
|
|
141
143
|
else
|
|
142
144
|
# Fallback: try to match on key phrases even if format is slightly different
|
|
143
145
|
case error_type.to_s
|
|
@@ -169,6 +171,8 @@ module RailsAccessibilityTesting
|
|
|
169
171
|
custom_element_remediation(error_type, element_context)
|
|
170
172
|
when /duplicate.*id/i
|
|
171
173
|
duplicate_ids_remediation(element_context)
|
|
174
|
+
when /button.*heading|heading.*button/i
|
|
175
|
+
button_heading_remediation(error_type, element_context)
|
|
172
176
|
else
|
|
173
177
|
" Please review the element details above and fix the accessibility issue."
|
|
174
178
|
end
|
|
@@ -406,6 +410,30 @@ module RailsAccessibilityTesting
|
|
|
406
410
|
remediation
|
|
407
411
|
end
|
|
408
412
|
|
|
413
|
+
def button_heading_remediation(error_type, element_context)
|
|
414
|
+
heading_level = element_context[:nested_heading] || 'H2'
|
|
415
|
+
heading_text = element_context[:heading_text] || 'Section Title'
|
|
416
|
+
|
|
417
|
+
remediation = " Remove heading from inside button:\n\n"
|
|
418
|
+
remediation += " ❌ Current (Bad):\n"
|
|
419
|
+
remediation += " <button>\n"
|
|
420
|
+
remediation += " <#{heading_level.downcase}>#{heading_text}</#{heading_level.downcase}>\n"
|
|
421
|
+
remediation += " </button>\n\n"
|
|
422
|
+
remediation += " ✅ Solution 1: Use plain text with aria-label:\n"
|
|
423
|
+
remediation += " <button aria-label=\"#{heading_text}\">#{heading_text}</button>\n\n"
|
|
424
|
+
remediation += " ✅ Solution 2: Use span/div with CSS styling:\n"
|
|
425
|
+
remediation += " <button>\n"
|
|
426
|
+
remediation += " <span class=\"button-title\">#{heading_text}</span>\n"
|
|
427
|
+
remediation += " </button>\n\n"
|
|
428
|
+
remediation += " ✅ Solution 3: Separate heading and button:\n"
|
|
429
|
+
remediation += " <#{heading_level.downcase}>#{heading_text}</#{heading_level.downcase}>\n"
|
|
430
|
+
remediation += " <button>Action</button>\n\n"
|
|
431
|
+
remediation += " 💡 Best Practice: Headings should not be nested inside buttons.\n"
|
|
432
|
+
remediation += " Buttons are interactive elements, headings are structural.\n"
|
|
433
|
+
remediation += " Use plain text or aria-label for button labels.\n"
|
|
434
|
+
remediation
|
|
435
|
+
end
|
|
436
|
+
|
|
409
437
|
def footer
|
|
410
438
|
"📖 WCAG Reference: #{WCAG_REFERENCE}\n#{SEPARATOR}"
|
|
411
439
|
end
|
|
@@ -9,7 +9,31 @@ module RailsAccessibilityTesting
|
|
|
9
9
|
enable_spec_type_inference(config)
|
|
10
10
|
include_matchers(config)
|
|
11
11
|
include_helpers(config)
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
# Check YAML config first, then fall back to initializer config
|
|
14
|
+
auto_run = should_auto_run_checks?
|
|
15
|
+
setup_automatic_checks(config) if auto_run
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Determine if checks should run automatically
|
|
19
|
+
# Checks YAML config first, then falls back to initializer config
|
|
20
|
+
def should_auto_run_checks?
|
|
21
|
+
# Try to load from YAML config
|
|
22
|
+
begin
|
|
23
|
+
require 'rails_accessibility_testing/config/yaml_loader'
|
|
24
|
+
profile = defined?(Rails) && Rails.env.test? ? :test : :development
|
|
25
|
+
yaml_config = Config::YamlLoader.load(profile: profile)
|
|
26
|
+
|
|
27
|
+
# Check if system_specs.auto_run is explicitly set in YAML
|
|
28
|
+
if yaml_config['system_specs'] && yaml_config['system_specs'].key?('auto_run')
|
|
29
|
+
return yaml_config['system_specs']['auto_run']
|
|
30
|
+
end
|
|
31
|
+
rescue StandardError
|
|
32
|
+
# If YAML loading fails, fall through to initializer config
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Fall back to initializer configuration
|
|
36
|
+
RailsAccessibilityTesting.config.auto_run_checks
|
|
13
37
|
end
|
|
14
38
|
|
|
15
39
|
private
|
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.
|
|
4
|
+
version: 1.5.6
|
|
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-
|
|
11
|
+
date: 2025-12-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: axe-core-capybara
|
|
@@ -116,6 +116,7 @@ files:
|
|
|
116
116
|
- docs_site/ci_integration.md
|
|
117
117
|
- docs_site/configuration.md
|
|
118
118
|
- docs_site/contributing.md
|
|
119
|
+
- docs_site/favicon.svg
|
|
119
120
|
- docs_site/getting_started.md
|
|
120
121
|
- docs_site/index.md
|
|
121
122
|
- exe/a11y_live_scanner
|