rails_accessibility_testing 1.4.3 → 1.5.1

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +212 -53
  3. data/CHANGELOG.md +118 -0
  4. data/GUIDES/getting_started.md +105 -77
  5. data/GUIDES/system_specs_for_accessibility.md +13 -12
  6. data/README.md +150 -36
  7. data/docs_site/getting_started.md +59 -69
  8. data/exe/a11y_live_scanner +361 -0
  9. data/exe/rails_server_safe +18 -1
  10. data/lib/generators/rails_a11y/install/install_generator.rb +137 -0
  11. data/lib/generators/rails_a11y/install/templates/accessibility.yml.erb +49 -0
  12. data/lib/generators/rails_a11y/install/templates/all_pages_accessibility_spec.rb.erb +66 -0
  13. data/lib/generators/rails_a11y/install/templates/initializer.rb.erb +24 -0
  14. data/lib/rails_accessibility_testing/accessibility_helper.rb +547 -24
  15. data/lib/rails_accessibility_testing/change_detector.rb +17 -104
  16. data/lib/rails_accessibility_testing/checks/base_check.rb +56 -7
  17. data/lib/rails_accessibility_testing/checks/heading_check.rb +138 -0
  18. data/lib/rails_accessibility_testing/checks/image_alt_text_check.rb +7 -7
  19. data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +11 -1
  20. data/lib/rails_accessibility_testing/cli/command.rb +3 -1
  21. data/lib/rails_accessibility_testing/config/yaml_loader.rb +1 -1
  22. data/lib/rails_accessibility_testing/engine/rule_engine.rb +49 -5
  23. data/lib/rails_accessibility_testing/error_message_builder.rb +63 -7
  24. data/lib/rails_accessibility_testing/middleware/page_visit_logger.rb +81 -0
  25. data/lib/rails_accessibility_testing/railtie.rb +22 -0
  26. data/lib/rails_accessibility_testing/rspec_integration.rb +176 -10
  27. data/lib/rails_accessibility_testing/version.rb +1 -1
  28. data/lib/rails_accessibility_testing.rb +8 -3
  29. metadata +11 -4
  30. data/lib/generators/rails_a11y/install/generator.rb +0 -51
  31. data/lib/rails_accessibility_testing/checks/heading_hierarchy_check.rb +0 -53
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module RailsAccessibilityTesting
7
+ module Middleware
8
+ # Middleware to log page visits for live accessibility scanning
9
+ # Only active in development environment
10
+ class PageVisitLogger
11
+ def initialize(app)
12
+ @app = app
13
+ @log_file = Rails.root.join('tmp', 'a11y_page_visits.log')
14
+ FileUtils.mkdir_p(File.dirname(@log_file))
15
+ @pending_logs = []
16
+ @last_flush = Time.now
17
+ @mutex = Mutex.new
18
+
19
+ # Start background thread to flush logs periodically
20
+ @flush_thread = Thread.new do
21
+ loop do
22
+ sleep 2 # Flush every 2 seconds
23
+ flush_logs
24
+ end
25
+ end
26
+ end
27
+
28
+ def call(env)
29
+ request = ActionDispatch::Request.new(env)
30
+
31
+ # Only log GET requests for HTML pages in development
32
+ if Rails.env.development? &&
33
+ request.get? &&
34
+ request.format.html? &&
35
+ !request.path.start_with?('/assets', '/packs', '/rails', '/letter_opener')
36
+
37
+ # Add to pending logs (thread-safe)
38
+ @mutex.synchronize do
39
+ @pending_logs << {
40
+ path: request.path,
41
+ url: request.url,
42
+ timestamp: Time.now.to_f
43
+ }
44
+
45
+ # Flush immediately if we have 3+ pending logs
46
+ flush_logs if @pending_logs.length >= 3
47
+ end
48
+ end
49
+
50
+ @app.call(env)
51
+ end
52
+
53
+ private
54
+
55
+ def flush_logs
56
+ logs_to_write = []
57
+
58
+ @mutex.synchronize do
59
+ return if @pending_logs.empty?
60
+ logs_to_write = @pending_logs.dup
61
+ @pending_logs.clear
62
+ @last_flush = Time.now
63
+ end
64
+
65
+ return if logs_to_write.empty?
66
+
67
+ # Write all logs at once
68
+ File.open(@log_file, 'a') do |f|
69
+ logs_to_write.each do |log_entry|
70
+ f.puts(log_entry.to_json)
71
+ end
72
+ f.flush
73
+ end
74
+ rescue StandardError => e
75
+ # Silently fail - don't break the app if logging fails
76
+ Rails.logger.debug("Failed to flush page visit logs: #{e.message}") if defined?(Rails.logger)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Only define Railtie if Rails is available
4
+ if defined?(Rails)
5
+ module RailsAccessibilityTesting
6
+ # Railtie for Rails integration
7
+ # Makes generators available to Rails
8
+ class Railtie < Rails::Railtie
9
+ # Generators are automatically discovered by Rails
10
+ # when they're in lib/generators/ directory
11
+
12
+ # Add middleware for live scanning in development
13
+ initializer 'rails_accessibility_testing.middleware' do |app|
14
+ if Rails.env.development?
15
+ require 'rails_accessibility_testing/middleware/page_visit_logger'
16
+ app.middleware.use RailsAccessibilityTesting::Middleware::PageVisitLogger
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
@@ -27,11 +27,21 @@ module RailsAccessibilityTesting
27
27
 
28
28
  # Include accessibility helpers for system specs
29
29
  def include_helpers(config)
30
- config.include AccessibilityHelper, type: :system
30
+ config.include RailsAccessibilityTesting::AccessibilityHelper, type: :system
31
31
  end
32
32
 
33
33
  # Setup automatic accessibility checks
34
34
  def setup_automatic_checks(config)
35
+ # Use class variable to track results across all examples
36
+ @@accessibility_results = {
37
+ pages_tested: [],
38
+ total_errors: 0,
39
+ total_warnings: 0,
40
+ pages_passed: 0,
41
+ pages_failed: 0,
42
+ pages_with_warnings: 0
43
+ }
44
+
35
45
  config.after(:each, type: :system) do |example|
36
46
  # Skip if test failed or explicitly skipped
37
47
  next if example.exception
@@ -45,19 +55,175 @@ module RailsAccessibilityTesting
45
55
  next
46
56
  end
47
57
 
58
+ # Track this page test
59
+ page_result = {
60
+ path: current_path,
61
+ errors: 0,
62
+ warnings: 0,
63
+ status: :pending
64
+ }
65
+
48
66
  # Run comprehensive accessibility checks
67
+ # Note: check_comprehensive_accessibility will:
68
+ # - Raise if there are errors (test fails)
69
+ # - Print warnings if there are warnings (test passes but shows warnings)
70
+ # - Print success message if everything passes (no errors, no warnings)
71
+ # - Return hash with :errors and :warnings counts
49
72
  instance = example.example_group_instance
50
- instance.check_comprehensive_accessibility
51
73
 
52
- # If we get here without an exception, all checks passed
53
- # Note: check_comprehensive_accessibility will raise if there are errors,
54
- # so if we reach this point, checks passed successfully
55
- $stdout.puts "\n✅ All comprehensive accessibility checks passed! (11 checks)"
56
- $stdout.flush
57
- rescue StandardError => e
58
- # Accessibility check failed - set the exception so test fails
59
- example.set_exception(e)
74
+ begin
75
+ result = instance.check_comprehensive_accessibility
76
+
77
+ # Get results from return value
78
+ page_result[:errors] = result[:errors] || 0
79
+ page_result[:warnings] = result[:warnings] || 0
80
+ # Capture view_file if available from result
81
+ page_result[:view_file] = result[:page_context][:view_file] if result[:page_context] && result[:page_context][:view_file]
82
+
83
+ if page_result[:errors] > 0
84
+ page_result[:status] = :failed
85
+ @@accessibility_results[:pages_failed] += 1
86
+ @@accessibility_results[:total_errors] += page_result[:errors]
87
+ elsif page_result[:warnings] > 0
88
+ page_result[:status] = :warning
89
+ @@accessibility_results[:pages_with_warnings] += 1
90
+ @@accessibility_results[:total_warnings] += page_result[:warnings]
91
+ else
92
+ page_result[:status] = :passed
93
+ @@accessibility_results[:pages_passed] += 1
94
+ end
95
+
96
+ @@accessibility_results[:pages_tested] << page_result
97
+ rescue StandardError => e
98
+ # Accessibility check failed - extract error count from exception message or instance
99
+ errors_count = 0
100
+ warnings_count = 0
101
+
102
+ # Try to extract counts from exception message first
103
+ if e.message =~ /ACCESSIBILITY ERRORS FOUND: (\d+) error\(s\), (\d+) warning\(s\)/
104
+ errors_count = $1.to_i
105
+ warnings_count = $2.to_i
106
+ elsif e.message =~ /(\d+) issue\(s\)/
107
+ errors_count = $1.to_i
108
+ else
109
+ # Fallback: get from instance variables
110
+ errors_count = instance.instance_variable_get(:@accessibility_errors)&.length || 1
111
+ warnings_count = instance.instance_variable_get(:@accessibility_warnings)&.length || 0
112
+ end
113
+
114
+ # Ensure we have at least 1 error if exception was raised
115
+ errors_count = 1 if errors_count == 0 && e.message.include?('ACCESSIBILITY ERRORS')
116
+
117
+ page_result[:status] = :failed
118
+ page_result[:errors] = errors_count
119
+ page_result[:warnings] = warnings_count
120
+ @@accessibility_results[:pages_failed] += 1
121
+ @@accessibility_results[:total_errors] += errors_count
122
+ @@accessibility_results[:total_warnings] += warnings_count
123
+ @@accessibility_results[:pages_tested] << page_result
124
+
125
+ # Flush stdout BEFORE setting exception to ensure errors are visible
126
+ $stdout.flush
127
+ $stderr.flush
128
+
129
+ # Store error info in example metadata so it's available even if output is cleared
130
+ example.metadata[:a11y_errors] = errors_count
131
+ example.metadata[:a11y_warnings] = warnings_count
132
+ example.metadata[:a11y_failed] = true
133
+
134
+ # Set exception but don't clear output - keep errors visible
135
+ example.set_exception(e)
136
+
137
+ # Flush again after setting exception
138
+ $stdout.flush
139
+ $stderr.flush
140
+ end
141
+ end
142
+
143
+ # Show overall summary after all tests complete
144
+ config.after(:suite) do
145
+ # Show summary if we tested any pages
146
+ if @@accessibility_results && @@accessibility_results[:pages_tested].any?
147
+ show_overall_summary(@@accessibility_results)
148
+ end
149
+ end
150
+ end
151
+
152
+ # Show overall summary of all pages tested
153
+ def show_overall_summary(results)
154
+ return unless results && results[:pages_tested].any?
155
+
156
+ puts "\n" + "="*80
157
+ puts "📊 COMPREHENSIVE ACCESSIBILITY TEST REPORT"
158
+ puts "="*80
159
+ puts ""
160
+ puts "📈 Test Statistics:"
161
+ puts " Total pages tested: #{results[:pages_tested].length}"
162
+ puts " ✅ Passed (no issues): #{results[:pages_passed]} page#{'s' if results[:pages_passed] != 1}"
163
+ puts " ❌ Failed (errors): #{results[:pages_failed]} page#{'s' if results[:pages_failed] != 1}"
164
+ puts " ⚠️ Warnings only: #{results[:pages_with_warnings]} page#{'s' if results[:pages_with_warnings] != 1}"
165
+ puts ""
166
+ puts "📋 Total Issues Across All Pages:"
167
+ puts " ❌ Total errors: #{results[:total_errors]}"
168
+ puts " ⚠️ Total warnings: #{results[:total_warnings]}"
169
+ puts ""
170
+
171
+ # Show pages with errors (highest priority)
172
+ pages_with_errors = results[:pages_tested].select { |p| p[:status] == :failed }
173
+ if pages_with_errors.any?
174
+ puts "❌ Pages with Errors (#{pages_with_errors.length}):"
175
+ pages_with_errors.each do |page|
176
+ view_file = page[:view_file] || page[:path]
177
+ puts " • #{view_file}"
178
+ puts " Errors: #{page[:errors]}#{", Warnings: #{page[:warnings]}" if page[:warnings] > 0}"
179
+ puts " Path: #{page[:path]}" if page[:view_file] && page[:path] != view_file
180
+ end
181
+ puts ""
182
+ end
183
+
184
+ # Show pages with warnings only
185
+ pages_with_warnings_only = results[:pages_tested].select { |p| p[:status] == :warning }
186
+ if pages_with_warnings_only.any?
187
+ puts "⚠️ Pages with Warnings Only (#{pages_with_warnings_only.length}):"
188
+ pages_with_warnings_only.each do |page|
189
+ view_file = page[:view_file] || page[:path]
190
+ puts " • #{view_file}"
191
+ puts " Warnings: #{page[:warnings]}"
192
+ puts " Path: #{page[:path]}" if page[:view_file] && page[:path] != view_file
193
+ end
194
+ puts ""
195
+ end
196
+
197
+ # Show summary of pages that passed
198
+ pages_passed = results[:pages_tested].select { |p| p[:status] == :passed }
199
+ if pages_passed.any?
200
+ if pages_passed.length <= 15
201
+ puts "✅ Pages Passed All Checks (#{pages_passed.length}):"
202
+ pages_passed.each do |page|
203
+ puts " ✓ #{page[:path]}"
204
+ end
205
+ else
206
+ puts "✅ #{pages_passed.length} pages passed all accessibility checks"
207
+ puts " (Showing first 10):"
208
+ pages_passed.first(10).each do |page|
209
+ puts " ✓ #{page[:path]}"
210
+ end
211
+ puts " ... and #{pages_passed.length - 10} more"
212
+ end
213
+ puts ""
214
+ end
215
+
216
+ # Final summary
217
+ puts "="*80
218
+ if results[:total_errors] > 0
219
+ puts "❌ OVERALL STATUS: FAILED - #{results[:total_errors]} error#{'s' if results[:total_errors] != 1} found across #{results[:pages_failed]} page#{'s' if results[:pages_failed] != 1}"
220
+ elsif results[:total_warnings] > 0
221
+ puts "⚠️ OVERALL STATUS: PASSED WITH WARNINGS - #{results[:total_warnings]} warning#{'s' if results[:total_warnings] != 1} found"
222
+ else
223
+ puts "✅ OVERALL STATUS: PASSED - All #{results[:pages_tested].length} page#{'s' if results[:pages_tested].length != 1} passed accessibility checks!"
60
224
  end
225
+ puts "="*80
226
+ puts ""
61
227
  end
62
228
  end
63
229
  end
@@ -1,4 +1,4 @@
1
1
  module RailsAccessibilityTesting
2
- VERSION = "1.4.3"
2
+ VERSION = "1.5.1"
3
3
  end
4
4
 
@@ -5,7 +5,7 @@
5
5
  # Automatically configures accessibility testing for Rails system specs with
6
6
  # comprehensive checks and detailed error messages.
7
7
  #
8
- # @version 1.4.3
8
+ # @version 1.5.0
9
9
  # @author Regan Maharjan
10
10
  #
11
11
  # @example Basic usage
@@ -38,7 +38,7 @@ begin
38
38
  require_relative 'rails_accessibility_testing/version'
39
39
  rescue LoadError
40
40
  module RailsAccessibilityTesting
41
- VERSION = '1.4.3'
41
+ VERSION = '1.5.0'
42
42
  end
43
43
  end
44
44
 
@@ -63,7 +63,7 @@ require_relative 'rails_accessibility_testing/checks/base_check'
63
63
  require_relative 'rails_accessibility_testing/checks/form_labels_check'
64
64
  require_relative 'rails_accessibility_testing/checks/image_alt_text_check'
65
65
  require_relative 'rails_accessibility_testing/checks/interactive_elements_check'
66
- require_relative 'rails_accessibility_testing/checks/heading_hierarchy_check'
66
+ require_relative 'rails_accessibility_testing/checks/heading_check'
67
67
  require_relative 'rails_accessibility_testing/checks/keyboard_accessibility_check'
68
68
  require_relative 'rails_accessibility_testing/checks/aria_landmarks_check'
69
69
  require_relative 'rails_accessibility_testing/checks/form_errors_check'
@@ -78,6 +78,11 @@ require_relative 'rails_accessibility_testing/config/yaml_loader'
78
78
  # Load integrations
79
79
  require_relative 'rails_accessibility_testing/integration/minitest_integration'
80
80
 
81
+ # Load railtie (needed for generator discovery) - only if Rails is available
82
+ if defined?(Rails)
83
+ require_relative 'rails_accessibility_testing/railtie'
84
+ end
85
+
81
86
  # Auto-configure when RSpec is available
82
87
  if defined?(RSpec)
83
88
  RSpec.configure do |config|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_accessibility_testing
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.3
4
+ version: 1.5.1
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-19 00:00:00.000000000 Z
11
+ date: 2025-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: axe-core-capybara
@@ -75,6 +75,7 @@ email:
75
75
  executables:
76
76
  - rails_a11y
77
77
  - rails_server_safe
78
+ - a11y_live_scanner
78
79
  extensions: []
79
80
  extra_rdoc_files: []
80
81
  files:
@@ -101,9 +102,13 @@ files:
101
102
  - docs_site/contributing.md
102
103
  - docs_site/getting_started.md
103
104
  - docs_site/index.md
105
+ - exe/a11y_live_scanner
104
106
  - exe/rails_a11y
105
107
  - exe/rails_server_safe
106
- - lib/generators/rails_a11y/install/generator.rb
108
+ - lib/generators/rails_a11y/install/install_generator.rb
109
+ - lib/generators/rails_a11y/install/templates/accessibility.yml.erb
110
+ - lib/generators/rails_a11y/install/templates/all_pages_accessibility_spec.rb.erb
111
+ - lib/generators/rails_a11y/install/templates/initializer.rb.erb
107
112
  - lib/rails_accessibility_testing.rb
108
113
  - lib/rails_accessibility_testing/accessibility_helper.rb
109
114
  - lib/rails_accessibility_testing/change_detector.rb
@@ -113,7 +118,7 @@ files:
113
118
  - lib/rails_accessibility_testing/checks/duplicate_ids_check.rb
114
119
  - lib/rails_accessibility_testing/checks/form_errors_check.rb
115
120
  - lib/rails_accessibility_testing/checks/form_labels_check.rb
116
- - lib/rails_accessibility_testing/checks/heading_hierarchy_check.rb
121
+ - lib/rails_accessibility_testing/checks/heading_check.rb
117
122
  - lib/rails_accessibility_testing/checks/image_alt_text_check.rb
118
123
  - lib/rails_accessibility_testing/checks/interactive_elements_check.rb
119
124
  - lib/rails_accessibility_testing/checks/keyboard_accessibility_check.rb
@@ -127,6 +132,8 @@ files:
127
132
  - lib/rails_accessibility_testing/engine/violation_collector.rb
128
133
  - lib/rails_accessibility_testing/error_message_builder.rb
129
134
  - lib/rails_accessibility_testing/integration/minitest_integration.rb
135
+ - lib/rails_accessibility_testing/middleware/page_visit_logger.rb
136
+ - lib/rails_accessibility_testing/railtie.rb
130
137
  - lib/rails_accessibility_testing/rspec_integration.rb
131
138
  - lib/rails_accessibility_testing/shared_examples.rb
132
139
  - lib/rails_accessibility_testing/version.rb
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rails/generators/base'
4
-
5
- module RailsAccessibilityTesting
6
- module Generators
7
- # Generator to install Rails Accessibility Testing
8
- #
9
- # Creates initializer and configuration file.
10
- #
11
- # @example
12
- # rails generate rails_a11y:install
13
- #
14
- class InstallGenerator < Rails::Generators::Base
15
- source_root File.expand_path('templates', __dir__)
16
-
17
- desc "Install Rails A11y: creates initializer and configuration file"
18
-
19
- def create_initializer
20
- template 'initializer.rb.erb', 'config/initializers/rails_a11y.rb'
21
- end
22
-
23
- def create_config_file
24
- template 'accessibility.yml.erb', 'config/accessibility.yml'
25
- end
26
-
27
- def add_to_rails_helper
28
- rails_helper_path = 'spec/rails_helper.rb'
29
-
30
- if File.exist?(rails_helper_path)
31
- inject_into_file rails_helper_path,
32
- after: "require 'rspec/rails'\n" do
33
- "require 'rails_accessibility_testing'\n"
34
- end
35
- else
36
- say "⚠️ spec/rails_helper.rb not found. Please add: require 'rails_accessibility_testing'", :yellow
37
- end
38
- end
39
-
40
- def show_instructions
41
- say "\n✅ Rails Accessibility Testing installed successfully!", :green
42
- say "\nNext steps:", :yellow
43
- say " 1. Run your system specs: bundle exec rspec spec/system/"
44
- say " 2. Accessibility checks will run automatically"
45
- say " 3. Configure checks in config/accessibility.yml"
46
- say "\nFor more information, see: https://github.com/rayraycodes/rails-accessibility-testing"
47
- end
48
- end
49
- end
50
- end
51
-
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RailsAccessibilityTesting
4
- module Checks
5
- # Checks for proper heading hierarchy
6
- #
7
- # WCAG 2.1 AA: 1.3.1 Info and Relationships (Level A)
8
- #
9
- # @api private
10
- class HeadingHierarchyCheck < BaseCheck
11
- def self.rule_name
12
- :heading_hierarchy
13
- end
14
-
15
- def check
16
- violations = []
17
- headings = page.all('h1, h2, h3, h4, h5, h6', visible: true)
18
-
19
- if headings.empty?
20
- return violations # Warning only, not error
21
- end
22
-
23
- h1_count = headings.count { |h| h.tag_name == 'h1' }
24
- if h1_count == 0
25
- violations << violation(
26
- message: "Page missing H1 heading",
27
- element_context: { tag: 'page', text: 'Page has no H1 heading' },
28
- wcag_reference: "1.3.1",
29
- remediation: "Add an <h1> heading to your page:\n\n<h1>Main Page Title</h1>"
30
- )
31
- end
32
-
33
- previous_level = 0
34
- headings.each do |heading|
35
- current_level = heading.tag_name[1].to_i
36
- if current_level > previous_level + 1
37
- element_ctx = element_context(heading)
38
- violations << violation(
39
- message: "Heading hierarchy skipped (h#{previous_level} to h#{current_level})",
40
- element_context: element_ctx,
41
- wcag_reference: "1.3.1",
42
- remediation: "Fix the heading hierarchy. Don't skip levels."
43
- )
44
- end
45
- previous_level = current_level
46
- end
47
-
48
- violations
49
- end
50
- end
51
- end
52
- end
53
-