rails_accessibility_testing 1.5.3 → 1.5.4
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 +376 -1
- data/CHANGELOG.md +50 -1
- data/GUIDES/getting_started.md +40 -5
- data/GUIDES/system_specs_for_accessibility.md +12 -4
- data/README.md +52 -8
- data/docs_site/Gemfile.lock +89 -0
- data/docs_site/_config.yml +9 -0
- data/docs_site/_includes/header.html +1 -0
- data/docs_site/_layouts/default.html +754 -15
- data/docs_site/architecture.md +533 -0
- data/docs_site/index.md +2 -1
- data/exe/a11y_live_scanner +10 -39
- data/exe/a11y_static_scanner +333 -0
- data/lib/generators/rails_a11y/install/install_generator.rb +19 -30
- data/lib/generators/rails_a11y/install/templates/accessibility.yml.erb +39 -0
- data/lib/generators/rails_a11y/install/templates/all_pages_accessibility_spec.rb.erb +132 -45
- data/lib/rails_accessibility_testing/accessibility_helper.rb +131 -126
- data/lib/rails_accessibility_testing/checks/base_check.rb +14 -5
- data/lib/rails_accessibility_testing/checks/form_errors_check.rb +1 -1
- data/lib/rails_accessibility_testing/checks/form_labels_check.rb +6 -4
- data/lib/rails_accessibility_testing/checks/heading_check.rb +7 -15
- data/lib/rails_accessibility_testing/checks/image_alt_text_check.rb +1 -1
- data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +12 -8
- data/lib/rails_accessibility_testing/config/yaml_loader.rb +20 -0
- data/lib/rails_accessibility_testing/erb_extractor.rb +141 -0
- data/lib/rails_accessibility_testing/error_message_builder.rb +11 -6
- data/lib/rails_accessibility_testing/file_change_tracker.rb +95 -0
- data/lib/rails_accessibility_testing/line_number_finder.rb +61 -0
- data/lib/rails_accessibility_testing/rspec_integration.rb +74 -33
- data/lib/rails_accessibility_testing/shared_examples.rb +2 -0
- data/lib/rails_accessibility_testing/static_file_scanner.rb +80 -0
- data/lib/rails_accessibility_testing/static_page_adapter.rb +116 -0
- data/lib/rails_accessibility_testing/static_scanning.rb +61 -0
- data/lib/rails_accessibility_testing/version.rb +3 -1
- data/lib/rails_accessibility_testing/violation_converter.rb +80 -0
- data/lib/rails_accessibility_testing.rb +9 -1
- metadata +26 -2
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Static accessibility scanner for development
|
|
5
|
+
# Scans all view files and shows accessibility errors
|
|
6
|
+
# Usage: Add to Procfile.dev as: a11y: bundle exec a11y_static_scanner
|
|
7
|
+
|
|
8
|
+
require 'bundler/setup'
|
|
9
|
+
|
|
10
|
+
# Load Rails environment
|
|
11
|
+
rails_root = Dir.pwd
|
|
12
|
+
while rails_root != '/' && !File.exist?(File.join(rails_root, 'config', 'environment.rb'))
|
|
13
|
+
rails_root = File.dirname(rails_root)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if File.exist?(File.join(rails_root, 'config', 'environment.rb'))
|
|
17
|
+
Dir.chdir(rails_root)
|
|
18
|
+
require File.join(rails_root, 'config', 'environment')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Require the gem
|
|
22
|
+
begin
|
|
23
|
+
require 'rails_accessibility_testing'
|
|
24
|
+
require 'rails_accessibility_testing/static_file_scanner'
|
|
25
|
+
require 'rails_accessibility_testing/file_change_tracker'
|
|
26
|
+
rescue LoadError => e
|
|
27
|
+
$stderr.puts "❌ Error loading rails_accessibility_testing gem: #{e.message}"
|
|
28
|
+
$stderr.puts " Make sure the gem is installed: bundle install"
|
|
29
|
+
exit 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Format errors with file locations and line numbers
|
|
33
|
+
def format_errors(errors, warnings)
|
|
34
|
+
return "" if errors.empty? && warnings.empty?
|
|
35
|
+
|
|
36
|
+
output = []
|
|
37
|
+
|
|
38
|
+
# Group errors by file
|
|
39
|
+
errors_by_file = errors.group_by { |e| e[:file] }
|
|
40
|
+
warnings_by_file = warnings.group_by { |w| w[:file] }
|
|
41
|
+
|
|
42
|
+
# Show errors first
|
|
43
|
+
if errors.any?
|
|
44
|
+
output << "\n" + "="*70
|
|
45
|
+
output << "❌ #{errors.length} error#{'s' if errors.length != 1} found"
|
|
46
|
+
output << "="*70
|
|
47
|
+
output << ""
|
|
48
|
+
|
|
49
|
+
errors_by_file.each_with_index do |(file_path, file_errors), file_index|
|
|
50
|
+
output << "" if file_index > 0
|
|
51
|
+
|
|
52
|
+
# Show relative path
|
|
53
|
+
relative_path = file_path.sub(%r{^.*app/views/}, 'app/views/')
|
|
54
|
+
output << "📝 #{relative_path} (#{file_errors.length} error#{'s' if file_errors.length != 1})"
|
|
55
|
+
|
|
56
|
+
file_errors.each do |error|
|
|
57
|
+
error_line = " • #{error[:type]}"
|
|
58
|
+
|
|
59
|
+
# Add line number if available
|
|
60
|
+
if error[:line]
|
|
61
|
+
error_line += " [Line #{error[:line]}]"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Add element identifier
|
|
65
|
+
element_id = error[:element][:id]
|
|
66
|
+
element_href = error[:element][:href]
|
|
67
|
+
element_src = error[:element][:src]
|
|
68
|
+
|
|
69
|
+
if element_id && !element_id.to_s.strip.empty?
|
|
70
|
+
error_line += " [id: #{element_id}]"
|
|
71
|
+
elsif element_href && !element_href.to_s.strip.empty?
|
|
72
|
+
href_display = element_href.length > 30 ? "#{element_href[0..27]}..." : element_href
|
|
73
|
+
error_line += " [href: #{href_display}]"
|
|
74
|
+
elsif element_src && !element_src.to_s.strip.empty?
|
|
75
|
+
src_display = element_src.length > 30 ? "#{element_src[0..27]}..." : element_src
|
|
76
|
+
error_line += " [src: #{src_display}]"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
output << error_line
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
output << ""
|
|
84
|
+
output << "="*70
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Show warnings if any
|
|
88
|
+
if warnings.any?
|
|
89
|
+
output << "\n" + "="*70
|
|
90
|
+
output << "⚠️ #{warnings.length} warning#{'s' if warnings.length != 1} found"
|
|
91
|
+
output << "="*70
|
|
92
|
+
output << ""
|
|
93
|
+
|
|
94
|
+
warnings_by_file.each_with_index do |(file_path, file_warnings), file_index|
|
|
95
|
+
output << "" if file_index > 0
|
|
96
|
+
|
|
97
|
+
# Show relative path
|
|
98
|
+
relative_path = file_path.sub(%r{^.*app/views/}, 'app/views/')
|
|
99
|
+
output << "📝 #{relative_path} (#{file_warnings.length} warning#{'s' if file_warnings.length != 1})"
|
|
100
|
+
|
|
101
|
+
file_warnings.each do |warning|
|
|
102
|
+
warning_line = " • #{warning[:type]}"
|
|
103
|
+
|
|
104
|
+
# Add line number if available
|
|
105
|
+
if warning[:line]
|
|
106
|
+
warning_line += " [Line #{warning[:line]}]"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Add element identifier
|
|
110
|
+
element_id = warning[:element][:id]
|
|
111
|
+
element_href = warning[:element][:href]
|
|
112
|
+
element_src = warning[:element][:src]
|
|
113
|
+
|
|
114
|
+
if element_id && !element_id.to_s.strip.empty?
|
|
115
|
+
warning_line += " [id: #{element_id}]"
|
|
116
|
+
elsif element_href && !element_href.to_s.strip.empty?
|
|
117
|
+
href_display = element_href.length > 30 ? "#{element_href[0..27]}..." : element_href
|
|
118
|
+
warning_line += " [href: #{href_display}]"
|
|
119
|
+
elsif element_src && !element_src.to_s.strip.empty?
|
|
120
|
+
src_display = element_src.length > 30 ? "#{element_src[0..27]}..." : element_src
|
|
121
|
+
warning_line += " [src: #{src_display}]"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
output << warning_line
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
output << ""
|
|
129
|
+
output << "="*70
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
output.join("\n")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get all view files
|
|
136
|
+
def get_all_view_files
|
|
137
|
+
return [] unless defined?(Rails) && Rails.root
|
|
138
|
+
|
|
139
|
+
view_dir = Rails.root.join('app', 'views')
|
|
140
|
+
return [] unless File.directory?(view_dir)
|
|
141
|
+
|
|
142
|
+
extensions = %w[erb haml slim]
|
|
143
|
+
view_files = []
|
|
144
|
+
|
|
145
|
+
extensions.each do |ext|
|
|
146
|
+
# Find all HTML view files (exclude partials that start with _)
|
|
147
|
+
pattern = File.join(view_dir, '**', "*.html.#{ext}")
|
|
148
|
+
Dir.glob(pattern).each do |file|
|
|
149
|
+
# Skip partials (files starting with _)
|
|
150
|
+
next if File.basename(file).start_with?('_')
|
|
151
|
+
# Skip layout files
|
|
152
|
+
next if file.include?('/layouts/')
|
|
153
|
+
view_files << file
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
view_files.sort
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Main scanner
|
|
161
|
+
begin
|
|
162
|
+
puts "\n" + "="*70
|
|
163
|
+
puts "🔍 Static Accessibility Scanner"
|
|
164
|
+
puts "="*70
|
|
165
|
+
puts "📁 Scanning all view files..."
|
|
166
|
+
puts ""
|
|
167
|
+
|
|
168
|
+
view_files = get_all_view_files
|
|
169
|
+
|
|
170
|
+
if view_files.empty?
|
|
171
|
+
puts "⚠️ No view files found to scan"
|
|
172
|
+
exit 0
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
puts "📋 Found #{view_files.length} view file#{'s' if view_files.length != 1} to scan"
|
|
176
|
+
puts ""
|
|
177
|
+
|
|
178
|
+
# Load config
|
|
179
|
+
begin
|
|
180
|
+
config = RailsAccessibilityTesting::Config::YamlLoader.load(profile: :development)
|
|
181
|
+
ignore_warnings = config.dig('summary', 'ignore_warnings') || false
|
|
182
|
+
static_config = config['static_scanner'] || {}
|
|
183
|
+
scan_changed_only = static_config.fetch('scan_changed_only', true)
|
|
184
|
+
check_interval = static_config.fetch('check_interval', 3)
|
|
185
|
+
full_scan_on_startup = static_config.fetch('full_scan_on_startup', true)
|
|
186
|
+
rescue StandardError
|
|
187
|
+
ignore_warnings = false
|
|
188
|
+
scan_changed_only = true
|
|
189
|
+
check_interval = 3
|
|
190
|
+
full_scan_on_startup = true
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Determine which files to scan
|
|
194
|
+
files_to_scan = []
|
|
195
|
+
|
|
196
|
+
if scan_changed_only && !full_scan_on_startup
|
|
197
|
+
# Only scan changed files from the start (no full scan on startup)
|
|
198
|
+
changed = RailsAccessibilityTesting::FileChangeTracker.changed_files(view_files)
|
|
199
|
+
if changed.empty?
|
|
200
|
+
puts "✅ No files changed since last scan"
|
|
201
|
+
puts "🔄 Scanner running continuously - watching for file changes..."
|
|
202
|
+
puts " Press Ctrl-C to stop"
|
|
203
|
+
puts ""
|
|
204
|
+
|
|
205
|
+
# Just watch for changes
|
|
206
|
+
loop do
|
|
207
|
+
sleep check_interval
|
|
208
|
+
changed = RailsAccessibilityTesting::FileChangeTracker.changed_files(view_files)
|
|
209
|
+
if changed.any?
|
|
210
|
+
puts "\n📝 #{changed.length} file#{'s' if changed.length != 1} changed - scanning..."
|
|
211
|
+
files_to_scan = changed
|
|
212
|
+
break
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
else
|
|
216
|
+
puts "📝 Found #{changed.length} changed file#{'s' if changed.length != 1} to scan"
|
|
217
|
+
files_to_scan = changed
|
|
218
|
+
end
|
|
219
|
+
elsif scan_changed_only && full_scan_on_startup
|
|
220
|
+
# Always scan all files on startup when full_scan_on_startup is true
|
|
221
|
+
# This ensures every new instance of bin/dev runs a full scan
|
|
222
|
+
puts "📋 Scanning all #{view_files.length} file#{'s' if view_files.length != 1} on startup"
|
|
223
|
+
files_to_scan = view_files
|
|
224
|
+
else
|
|
225
|
+
# Scan all files every time (scan_changed_only is false)
|
|
226
|
+
puts "📋 Scanning all #{view_files.length} file#{'s' if view_files.length != 1}"
|
|
227
|
+
files_to_scan = view_files
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Scan files
|
|
231
|
+
all_errors = []
|
|
232
|
+
all_warnings = []
|
|
233
|
+
|
|
234
|
+
if files_to_scan.any?
|
|
235
|
+
files_to_scan.each do |view_file|
|
|
236
|
+
scanner = RailsAccessibilityTesting::StaticFileScanner.new(view_file)
|
|
237
|
+
result = scanner.scan
|
|
238
|
+
|
|
239
|
+
errors = result[:errors] || []
|
|
240
|
+
warnings = result[:warnings] || []
|
|
241
|
+
|
|
242
|
+
all_errors.concat(errors)
|
|
243
|
+
all_warnings.concat(warnings) unless ignore_warnings
|
|
244
|
+
|
|
245
|
+
print "."
|
|
246
|
+
$stdout.flush
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Update state with scanned files
|
|
250
|
+
RailsAccessibilityTesting::FileChangeTracker.update_state(files_to_scan)
|
|
251
|
+
|
|
252
|
+
puts ""
|
|
253
|
+
puts ""
|
|
254
|
+
|
|
255
|
+
# Show results
|
|
256
|
+
if all_errors.any? || (all_warnings.any? && !ignore_warnings)
|
|
257
|
+
puts format_errors(all_errors, ignore_warnings ? [] : all_warnings)
|
|
258
|
+
puts ""
|
|
259
|
+
puts "💡 Fix the errors above - scanner will re-run automatically"
|
|
260
|
+
puts ""
|
|
261
|
+
else
|
|
262
|
+
puts "✅ All #{files_to_scan.length} scanned file#{'s' if files_to_scan.length != 1} passed accessibility checks!"
|
|
263
|
+
puts ""
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Keep scanner running continuously - re-scan files when they change
|
|
268
|
+
if scan_changed_only
|
|
269
|
+
# Always show waiting message after scanning
|
|
270
|
+
puts "🔄 Scanner running continuously - watching for file changes..."
|
|
271
|
+
puts " Press Ctrl-C to stop"
|
|
272
|
+
puts ""
|
|
273
|
+
|
|
274
|
+
loop do
|
|
275
|
+
sleep check_interval
|
|
276
|
+
|
|
277
|
+
# Check for changed files using FileChangeTracker
|
|
278
|
+
changed = RailsAccessibilityTesting::FileChangeTracker.changed_files(view_files)
|
|
279
|
+
|
|
280
|
+
if changed.any?
|
|
281
|
+
puts "\n📝 #{changed.length} file#{'s' if changed.length != 1} changed - re-scanning..."
|
|
282
|
+
puts ""
|
|
283
|
+
|
|
284
|
+
# Scan only changed files
|
|
285
|
+
all_errors = []
|
|
286
|
+
all_warnings = []
|
|
287
|
+
|
|
288
|
+
changed.each do |view_file|
|
|
289
|
+
scanner = RailsAccessibilityTesting::StaticFileScanner.new(view_file)
|
|
290
|
+
result = scanner.scan
|
|
291
|
+
|
|
292
|
+
errors = result[:errors] || []
|
|
293
|
+
warnings = result[:warnings] || []
|
|
294
|
+
|
|
295
|
+
all_errors.concat(errors)
|
|
296
|
+
all_warnings.concat(warnings) unless ignore_warnings
|
|
297
|
+
|
|
298
|
+
print "."
|
|
299
|
+
$stdout.flush
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Update state
|
|
303
|
+
RailsAccessibilityTesting::FileChangeTracker.update_state(changed)
|
|
304
|
+
|
|
305
|
+
puts ""
|
|
306
|
+
puts ""
|
|
307
|
+
|
|
308
|
+
# Show results
|
|
309
|
+
if all_errors.any? || (all_warnings.any? && !ignore_warnings)
|
|
310
|
+
puts format_errors(all_errors, ignore_warnings ? [] : all_warnings)
|
|
311
|
+
puts ""
|
|
312
|
+
puts "💡 Fix the errors above - scanner will re-run automatically"
|
|
313
|
+
puts ""
|
|
314
|
+
else
|
|
315
|
+
puts "✅ All #{changed.length} scanned file#{'s' if changed.length != 1} passed accessibility checks!"
|
|
316
|
+
puts ""
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
else
|
|
321
|
+
# Not watching for changes - exit after scan
|
|
322
|
+
puts "✅ Scan complete"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
rescue Interrupt
|
|
326
|
+
puts "\n\n👋 Scanner stopped"
|
|
327
|
+
exit 0
|
|
328
|
+
rescue StandardError => e
|
|
329
|
+
puts "\n\n❌ Error: #{e.message}"
|
|
330
|
+
puts e.backtrace.first(5).join("\n")
|
|
331
|
+
exit 1
|
|
332
|
+
end
|
|
333
|
+
|
|
@@ -70,14 +70,21 @@ module RailsA11y
|
|
|
70
70
|
|
|
71
71
|
# Check if a11y line already exists
|
|
72
72
|
unless procfile_content.include?('a11y:')
|
|
73
|
-
# Add
|
|
74
|
-
a11y_line = "a11y: bundle exec
|
|
73
|
+
# Add static scanner to Procfile.dev
|
|
74
|
+
a11y_line = "a11y: bundle exec a11y_static_scanner\n"
|
|
75
75
|
procfile_content += a11y_line
|
|
76
76
|
modified = true
|
|
77
|
-
say "✅ Added
|
|
78
|
-
say " 💡 Run 'bin/dev' to
|
|
77
|
+
say "✅ Added static accessibility scanner to #{procfile_path}", :green
|
|
78
|
+
say " 💡 Run 'bin/dev' to scan all view files and show errors", :cyan
|
|
79
79
|
else
|
|
80
|
-
|
|
80
|
+
# Update existing a11y line if it's using live scanner
|
|
81
|
+
if procfile_content.include?('a11y_live_scanner')
|
|
82
|
+
procfile_content.gsub!(/a11y:.*a11y_live_scanner/, 'a11y: bundle exec a11y_static_scanner')
|
|
83
|
+
modified = true
|
|
84
|
+
say "✅ Updated a11y scanner to static file scanner in #{procfile_path}", :green
|
|
85
|
+
else
|
|
86
|
+
say "⚠️ Procfile.dev already contains an a11y entry. Skipping.", :yellow
|
|
87
|
+
end
|
|
81
88
|
end
|
|
82
89
|
|
|
83
90
|
# Save if we made changes
|
|
@@ -87,36 +94,18 @@ module RailsA11y
|
|
|
87
94
|
# Use rails_server_safe to prevent Foreman termination issues
|
|
88
95
|
procfile_content = <<~PROCFILE
|
|
89
96
|
web: bundle exec rails_server_safe
|
|
90
|
-
a11y: bundle exec
|
|
97
|
+
a11y: bundle exec a11y_static_scanner
|
|
91
98
|
PROCFILE
|
|
92
99
|
|
|
93
100
|
File.write(procfile_path, procfile_content)
|
|
94
|
-
say "✅ Created #{procfile_path} with
|
|
101
|
+
say "✅ Created #{procfile_path} with static accessibility scanner", :green
|
|
95
102
|
say " 💡 Using rails_server_safe to prevent Foreman process termination", :cyan
|
|
96
|
-
say " 💡 Run 'bin/dev' to
|
|
103
|
+
say " 💡 Run 'bin/dev' to scan all view files and show errors", :cyan
|
|
97
104
|
end
|
|
98
105
|
end
|
|
99
106
|
|
|
100
107
|
def update_gitignore
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if File.exist?(gitignore_path)
|
|
104
|
-
gitignore_content = File.read(gitignore_path)
|
|
105
|
-
|
|
106
|
-
# Add tmp files for live scanner if not already present
|
|
107
|
-
a11y_entries = [
|
|
108
|
-
'tmp/a11y_page_visits.log',
|
|
109
|
-
'tmp/a11y_scanned_pages.json'
|
|
110
|
-
]
|
|
111
|
-
|
|
112
|
-
a11y_entries.each do |entry|
|
|
113
|
-
unless gitignore_content.include?(entry)
|
|
114
|
-
gitignore_content += "\n#{entry}\n"
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
File.write(gitignore_path, gitignore_content) if gitignore_content != File.read(gitignore_path)
|
|
119
|
-
end
|
|
108
|
+
# No longer needed - static scanner doesn't create tmp files
|
|
120
109
|
end
|
|
121
110
|
|
|
122
111
|
def show_instructions
|
|
@@ -126,9 +115,9 @@ module RailsA11y
|
|
|
126
115
|
say " 1. Run the accessibility tests:", :cyan
|
|
127
116
|
say " bundle exec rspec spec/system/all_pages_accessibility_spec.rb"
|
|
128
117
|
say ""
|
|
129
|
-
say " 2. For
|
|
130
|
-
say " bin/dev # Starts web server +
|
|
131
|
-
say " #
|
|
118
|
+
say " 2. For static file scanning during development:", :cyan
|
|
119
|
+
say " bin/dev # Starts web server + static accessibility scanner"
|
|
120
|
+
say " # Scans all view files and shows errors automatically!"
|
|
132
121
|
say ""
|
|
133
122
|
say " 3. Create custom specs for specific pages:", :cyan
|
|
134
123
|
say " # spec/system/my_page_accessibility_spec.rb"
|
|
@@ -6,6 +6,45 @@
|
|
|
6
6
|
# WCAG compliance level (A, AA, AAA)
|
|
7
7
|
wcag_level: AA
|
|
8
8
|
|
|
9
|
+
# Summary configuration
|
|
10
|
+
# Control how accessibility test summaries are displayed
|
|
11
|
+
summary:
|
|
12
|
+
# Show summary at end of test suite (true/false)
|
|
13
|
+
show_summary: true
|
|
14
|
+
|
|
15
|
+
# Show only errors in summary, hide warnings (true/false)
|
|
16
|
+
errors_only: false
|
|
17
|
+
|
|
18
|
+
# Show fix suggestions in error messages (true/false)
|
|
19
|
+
# When false, only shows error type and location
|
|
20
|
+
show_fixes: true
|
|
21
|
+
|
|
22
|
+
# Ignore warnings completely - only show errors (true/false)
|
|
23
|
+
# When true, warnings are filtered out and not displayed or counted
|
|
24
|
+
ignore_warnings: false
|
|
25
|
+
|
|
26
|
+
# Scanning strategy
|
|
27
|
+
# 'paths' - Scan by visiting routes/paths (default)
|
|
28
|
+
# 'view_files' - Scan by finding view files and visiting their routes
|
|
29
|
+
scan_strategy: 'view_files'
|
|
30
|
+
|
|
31
|
+
# Static scanner configuration
|
|
32
|
+
# Controls behavior of the static file scanner (a11y_static_scanner)
|
|
33
|
+
static_scanner:
|
|
34
|
+
# Only scan files that have changed since last scan (true/false)
|
|
35
|
+
# When true, uses file modification times to track changes
|
|
36
|
+
# When false, scans all files on every run
|
|
37
|
+
scan_changed_only: true
|
|
38
|
+
|
|
39
|
+
# Check interval in seconds when running continuously
|
|
40
|
+
# Scanner checks for file changes every N seconds
|
|
41
|
+
check_interval: 3
|
|
42
|
+
|
|
43
|
+
# Force full scan on startup (true/false)
|
|
44
|
+
# When true, scans all files on first run regardless of changes
|
|
45
|
+
# When false, only scans changed files from the start
|
|
46
|
+
full_scan_on_startup: true
|
|
47
|
+
|
|
9
48
|
# Global check configuration
|
|
10
49
|
# Set to false to disable a check globally
|
|
11
50
|
checks:
|
|
@@ -1,66 +1,153 @@
|
|
|
1
1
|
require 'rails_helper'
|
|
2
2
|
|
|
3
3
|
RSpec.describe 'All Pages Accessibility', type: :system do
|
|
4
|
-
# Test all
|
|
4
|
+
# Test all view files for accessibility using static file scanning
|
|
5
5
|
# Generated automatically by rails_a11y:install generator
|
|
6
6
|
|
|
7
|
-
# Helper method to get all
|
|
8
|
-
def self.
|
|
9
|
-
return [] unless defined?(Rails) && Rails.
|
|
7
|
+
# Helper method to get all view files (non-partials)
|
|
8
|
+
def self.get_all_view_files
|
|
9
|
+
return [] unless defined?(Rails) && Rails.root
|
|
10
10
|
|
|
11
|
-
Rails.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
view_dir = Rails.root.join('app', 'views')
|
|
12
|
+
return [] unless File.directory?(view_dir)
|
|
13
|
+
|
|
14
|
+
extensions = %w[erb haml slim]
|
|
15
|
+
view_files = []
|
|
16
|
+
|
|
17
|
+
extensions.each do |ext|
|
|
18
|
+
# Find all HTML view files (exclude partials that start with _)
|
|
19
|
+
pattern = File.join(view_dir, '**', "*.html.#{ext}")
|
|
20
|
+
Dir.glob(pattern).each do |file|
|
|
21
|
+
# Skip partials (files starting with _)
|
|
22
|
+
next if File.basename(file).start_with?('_')
|
|
23
|
+
# Skip layout files
|
|
24
|
+
next if file.include?('/layouts/')
|
|
25
|
+
view_files << file
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
view_files.sort
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Format errors with file locations and line numbers
|
|
33
|
+
def format_static_errors(errors, warnings)
|
|
34
|
+
return "" if errors.empty? && warnings.empty?
|
|
35
|
+
|
|
36
|
+
output = []
|
|
37
|
+
|
|
38
|
+
# Group errors by file
|
|
39
|
+
errors_by_file = errors.group_by { |e| e[:file] }
|
|
40
|
+
warnings_by_file = warnings.group_by { |w| w[:file] }
|
|
41
|
+
|
|
42
|
+
# Show errors first
|
|
43
|
+
if errors.any?
|
|
44
|
+
output << "\n" + "="*70
|
|
45
|
+
output << "❌ #{errors.length} error#{'s' if errors.length != 1} found"
|
|
46
|
+
output << "="*70
|
|
47
|
+
output << ""
|
|
15
48
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
26
76
|
|
|
27
|
-
|
|
28
|
-
|
|
77
|
+
output << ""
|
|
78
|
+
output << "="*70
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Show warnings if any
|
|
82
|
+
if warnings.any?
|
|
83
|
+
output << "\n" + "="*70
|
|
84
|
+
output << "⚠️ #{warnings.length} warning#{'s' if warnings.length != 1} found"
|
|
85
|
+
output << "="*70
|
|
86
|
+
output << ""
|
|
29
87
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
88
|
+
warnings_by_file.each_with_index do |(file_path, file_warnings), file_index|
|
|
89
|
+
output << "" if file_index > 0
|
|
90
|
+
|
|
91
|
+
output << "📝 #{file_path} (#{file_warnings.length} warning#{'s' if file_warnings.length != 1})"
|
|
92
|
+
|
|
93
|
+
file_warnings.each do |warning|
|
|
94
|
+
warning_line = " • #{warning[:type]}"
|
|
95
|
+
|
|
96
|
+
# Add line number if available
|
|
97
|
+
if warning[:line]
|
|
98
|
+
warning_line += " [Line #{warning[:line]}]"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Add element identifier
|
|
102
|
+
if warning[:element][:id].present?
|
|
103
|
+
warning_line += " [id: #{warning[:element][:id]}]"
|
|
104
|
+
elsif warning[:element][:href].present?
|
|
105
|
+
href_display = warning[:element][:href].length > 30 ? "#{warning[:element][:href][0..27]}..." : warning[:element][:href]
|
|
106
|
+
warning_line += " [href: #{href_display}]"
|
|
107
|
+
elsif warning[:element][:src].present?
|
|
108
|
+
src_display = warning[:element][:src].length > 30 ? "#{warning[:element][:src][0..27]}..." : warning[:element][:src]
|
|
109
|
+
warning_line += " [src: #{src_display}]"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
output << warning_line
|
|
113
|
+
end
|
|
114
|
+
end
|
|
33
115
|
|
|
34
|
-
|
|
116
|
+
output << ""
|
|
117
|
+
output << "="*70
|
|
35
118
|
end
|
|
119
|
+
|
|
120
|
+
output.join("\n")
|
|
36
121
|
end
|
|
37
122
|
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
123
|
+
# Scan all view files statically
|
|
124
|
+
view_files = get_all_view_files
|
|
125
|
+
|
|
126
|
+
if view_files.empty?
|
|
127
|
+
it "no view files found to scan" do
|
|
128
|
+
skip "No view files found in app/views"
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
view_files.each do |view_file|
|
|
132
|
+
it "scans #{view_file} for accessibility issues" do
|
|
133
|
+
require 'rails_accessibility_testing/static_file_scanner'
|
|
134
|
+
|
|
135
|
+
scanner = RailsAccessibilityTesting::StaticFileScanner.new(view_file)
|
|
136
|
+
result = scanner.scan
|
|
46
137
|
|
|
47
|
-
|
|
48
|
-
|
|
138
|
+
errors = result[:errors] || []
|
|
139
|
+
warnings = result[:warnings] || []
|
|
49
140
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
141
|
+
if errors.any? || warnings.any?
|
|
142
|
+
puts format_static_errors(errors, warnings)
|
|
143
|
+
|
|
144
|
+
if errors.any?
|
|
145
|
+
raise "Found #{errors.length} accessibility error#{'s' if errors.length != 1} in #{view_file}"
|
|
146
|
+
end
|
|
55
147
|
else
|
|
56
|
-
|
|
57
|
-
check_comprehensive_accessibility
|
|
148
|
+
puts "✅ #{view_file}: No errors found"
|
|
58
149
|
end
|
|
59
|
-
rescue => e
|
|
60
|
-
# Skip routes that can't be accessed
|
|
61
|
-
skip "Skipping #{path}: #{e.message}"
|
|
62
150
|
end
|
|
63
151
|
end
|
|
64
152
|
end
|
|
65
153
|
end
|
|
66
|
-
|