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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +376 -1
  3. data/CHANGELOG.md +50 -1
  4. data/GUIDES/getting_started.md +40 -5
  5. data/GUIDES/system_specs_for_accessibility.md +12 -4
  6. data/README.md +52 -8
  7. data/docs_site/Gemfile.lock +89 -0
  8. data/docs_site/_config.yml +9 -0
  9. data/docs_site/_includes/header.html +1 -0
  10. data/docs_site/_layouts/default.html +754 -15
  11. data/docs_site/architecture.md +533 -0
  12. data/docs_site/index.md +2 -1
  13. data/exe/a11y_live_scanner +10 -39
  14. data/exe/a11y_static_scanner +333 -0
  15. data/lib/generators/rails_a11y/install/install_generator.rb +19 -30
  16. data/lib/generators/rails_a11y/install/templates/accessibility.yml.erb +39 -0
  17. data/lib/generators/rails_a11y/install/templates/all_pages_accessibility_spec.rb.erb +132 -45
  18. data/lib/rails_accessibility_testing/accessibility_helper.rb +131 -126
  19. data/lib/rails_accessibility_testing/checks/base_check.rb +14 -5
  20. data/lib/rails_accessibility_testing/checks/form_errors_check.rb +1 -1
  21. data/lib/rails_accessibility_testing/checks/form_labels_check.rb +6 -4
  22. data/lib/rails_accessibility_testing/checks/heading_check.rb +7 -15
  23. data/lib/rails_accessibility_testing/checks/image_alt_text_check.rb +1 -1
  24. data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +12 -8
  25. data/lib/rails_accessibility_testing/config/yaml_loader.rb +20 -0
  26. data/lib/rails_accessibility_testing/erb_extractor.rb +141 -0
  27. data/lib/rails_accessibility_testing/error_message_builder.rb +11 -6
  28. data/lib/rails_accessibility_testing/file_change_tracker.rb +95 -0
  29. data/lib/rails_accessibility_testing/line_number_finder.rb +61 -0
  30. data/lib/rails_accessibility_testing/rspec_integration.rb +74 -33
  31. data/lib/rails_accessibility_testing/shared_examples.rb +2 -0
  32. data/lib/rails_accessibility_testing/static_file_scanner.rb +80 -0
  33. data/lib/rails_accessibility_testing/static_page_adapter.rb +116 -0
  34. data/lib/rails_accessibility_testing/static_scanning.rb +61 -0
  35. data/lib/rails_accessibility_testing/version.rb +3 -1
  36. data/lib/rails_accessibility_testing/violation_converter.rb +80 -0
  37. data/lib/rails_accessibility_testing.rb +9 -1
  38. 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 live scanner to Procfile.dev
74
- a11y_line = "a11y: bundle exec a11y_live_scanner\n"
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 live accessibility scanner to #{procfile_path}", :green
78
- say " 💡 Run 'bin/dev' to start live scanning as you browse pages", :cyan
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
- say "⚠️ Procfile.dev already contains an a11y entry. Skipping.", :yellow
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 a11y_live_scanner
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 live accessibility scanner", :green
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 start live scanning as you browse pages", :cyan
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
- gitignore_path = '.gitignore'
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 live scanning during development:", :cyan
130
- say " bin/dev # Starts web server + live accessibility scanner"
131
- say " # Navigate to pages in your browser to see real-time reports!"
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 GET routes for accessibility
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 testable routes
8
- def self.get_testable_routes
9
- return [] unless defined?(Rails) && Rails.application
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.application.routes.routes.select do |route|
12
- next false unless route.verb.to_s.include?('GET')
13
-
14
- path_spec = route.path.spec.to_s
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
- # Exclude API routes, internal Rails routes, and format-specific routes
17
- next false if path_spec =~ /\.(json|xml|js)/
18
- next false if path_spec =~ /rails/
19
- next false if path_spec =~ /active_storage/
20
- next false if path_spec =~ /action_cable/
21
- next false if path_spec =~ /letter_opener/
22
- next false if path_spec =~ /up/
23
- next false if path_spec =~ /recede_historical_location/
24
- next false if path_spec =~ /resume_historical_location/
25
- next false if path_spec =~ /refresh_historical_location/
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
- # Skip routes with complex requirements
28
- next false if path_spec.include?('destroy') || path_spec.include?('delete')
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
- # Skip routes with multiple required params (too complex to test automatically)
31
- param_count = path_spec.scan(/\(:(\w+)\)/).length
32
- next false if param_count > 1
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
- true
116
+ output << ""
117
+ output << "="*70
35
118
  end
119
+
120
+ output.join("\n")
36
121
  end
37
122
 
38
- # Test each route
39
- get_testable_routes.each do |route|
40
- path = RailsAccessibilityTesting::ChangeDetector.route_to_path(route)
41
- next unless path
42
-
43
- it "checks accessibility for #{path}" do
44
- begin
45
- visit path
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
- # Wait for page to load
48
- sleep 0.5
138
+ errors = result[:errors] || []
139
+ warnings = result[:warnings] || []
49
140
 
50
- # Skip if redirected to sign in (requires authentication)
51
- if page.current_url.include?('sign_in') || page.current_url.include?('login')
52
- skip "Skipping #{path}: requires authentication"
53
- elsif page.has_content?('Error') || page.has_content?('404') || page.has_content?('Not Found')
54
- skip "Skipping #{path}: page not found or error"
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
- # Page loaded successfully, run accessibility checks
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
-