rails_accessibility_testing 1.4.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) 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 +136 -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/rails_accessibility_testing/accessibility_helper.rb +547 -24
  12. data/lib/rails_accessibility_testing/change_detector.rb +17 -104
  13. data/lib/rails_accessibility_testing/checks/base_check.rb +56 -7
  14. data/lib/rails_accessibility_testing/checks/heading_check.rb +138 -0
  15. data/lib/rails_accessibility_testing/checks/image_alt_text_check.rb +7 -7
  16. data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +11 -1
  17. data/lib/rails_accessibility_testing/cli/command.rb +3 -1
  18. data/lib/rails_accessibility_testing/config/yaml_loader.rb +1 -1
  19. data/lib/rails_accessibility_testing/engine/rule_engine.rb +49 -5
  20. data/lib/rails_accessibility_testing/error_message_builder.rb +63 -7
  21. data/lib/rails_accessibility_testing/middleware/page_visit_logger.rb +81 -0
  22. data/lib/rails_accessibility_testing/railtie.rb +22 -0
  23. data/lib/rails_accessibility_testing/rspec_integration.rb +176 -10
  24. data/lib/rails_accessibility_testing/version.rb +1 -1
  25. data/lib/rails_accessibility_testing.rb +8 -3
  26. metadata +7 -3
  27. data/lib/generators/rails_a11y/install/generator.rb +0 -51
  28. data/lib/rails_accessibility_testing/checks/heading_hierarchy_check.rb +0 -53
@@ -20,6 +20,7 @@ Add to your `Gemfile`:
20
20
  ```ruby
21
21
  group :development, :test do
22
22
  gem 'rails_accessibility_testing'
23
+ gem 'rspec-rails', '~> 8.0' # Required for system specs
23
24
  gem 'axe-core-capybara', '~> 4.0'
24
25
  gem 'capybara', '~> 3.40'
25
26
  gem 'selenium-webdriver', '~> 4.0'
@@ -27,7 +28,9 @@ group :development, :test do
27
28
  end
28
29
  ```
29
30
 
30
- **Important:** You must explicitly add `selenium-webdriver` to your Gemfile. It's not automatically included as a dependency.
31
+ **Important:**
32
+ - You must explicitly add `selenium-webdriver` to your Gemfile. It's not automatically included as a dependency.
33
+ - **RSpec Rails is required** - The generator creates system specs that require `rspec-rails`. If you're using Minitest, you'll need to manually create your accessibility tests.
31
34
 
32
35
  Then run:
33
36
 
@@ -46,11 +49,55 @@ rails generate rails_a11y:install
46
49
  This creates:
47
50
  - `config/initializers/rails_a11y.rb` - Configuration
48
51
  - `config/accessibility.yml` - Check settings
52
+ - `spec/system/all_pages_accessibility_spec.rb` - Comprehensive spec that tests all GET routes
49
53
  - Updates `spec/rails_helper.rb` (if using RSpec)
50
54
 
51
- ### Step 2.5: Configure Capybara Driver (Required for System Tests)
55
+ **Note:** If you already have system tests set up in your Rails application, you can skip to Step 3. If you need help configuring Capybara or installing Chrome, see the [Troubleshooting section](#troubleshooting) below.
52
56
 
53
- For system tests to work, you need to configure Capybara with a Selenium driver. Create `spec/support/driver.rb`:
57
+ ### Step 3: Run Your Tests
58
+
59
+ The generator creates `spec/system/all_pages_accessibility_spec.rb` which automatically tests all GET routes in your application.
60
+
61
+ You can also create custom system specs for specific pages:
62
+
63
+ ```ruby
64
+ # spec/system/my_page_accessibility_spec.rb
65
+ require 'rails_helper'
66
+
67
+ RSpec.describe 'My Page Accessibility', type: :system do
68
+ it 'loads the page and runs comprehensive accessibility checks' do
69
+ visit root_path
70
+
71
+ # Run comprehensive accessibility checks
72
+ # This will fail the test if any accessibility issues are found
73
+ check_comprehensive_accessibility
74
+ # ✅ If all checks pass, you'll see: "All comprehensive accessibility checks passed! (11 checks)"
75
+ end
76
+ end
77
+ ```
78
+
79
+ ### Step 5: Run Your Tests
80
+
81
+ You can run accessibility checks in several ways:
82
+
83
+ #### Option A: Run Tests Manually
84
+
85
+ ```bash
86
+ # Run all accessibility specs
87
+ bundle exec rspec spec/system/*_accessibility_spec.rb
88
+
89
+ # Or run all system specs
90
+ bundle exec rspec spec/system/
91
+ ```
92
+
93
+ Accessibility checks run automatically on every system test that visits a page.
94
+
95
+
96
+ ## Troubleshooting
97
+
98
+ ### How do I configure Capybara for system tests?
99
+
100
+ If you don't already have system tests configured, you need to set up Capybara with a Selenium driver. Create `spec/support/driver.rb`:
54
101
 
55
102
  ```ruby
56
103
  # spec/support/driver.rb
@@ -85,7 +132,7 @@ end
85
132
 
86
133
  **Note for Rails 8:** Rails 8 uses `driven_by` to configure system tests. Make sure your `spec/support/driver.rb` is loaded by `rails_helper.rb` (it should be automatically loaded if it's in the `spec/support/` directory).
87
134
 
88
- ### Step 3: Install Chrome/Chromium (Required)
135
+ ### How do I install Chrome/Chromium?
89
136
 
90
137
  System tests require Chrome or Chromium to be installed on your system:
91
138
 
@@ -109,63 +156,6 @@ Download and install Chrome from [google.com/chrome](https://www.google.com/chro
109
156
 
110
157
  The `webdrivers` gem will automatically download and manage the ChromeDriver binary for you.
111
158
 
112
- ### Step 4: Create System Specs (Recommended)
113
-
114
- Create system specs for the pages you want to test. This is the **recommended and most reliable** approach:
115
-
116
- ```ruby
117
- # spec/system/home_page_accessibility_spec.rb
118
- require 'rails_helper'
119
-
120
- RSpec.describe 'Home Page Accessibility', type: :system do
121
- it 'loads the page and runs comprehensive accessibility checks' do
122
- visit root_path
123
-
124
- # Run comprehensive accessibility checks
125
- # This will fail the test if any accessibility issues are found
126
- check_comprehensive_accessibility
127
- # ✅ If all checks pass, you'll see: "All comprehensive accessibility checks passed! (11 checks)"
128
- end
129
- end
130
- ```
131
-
132
- ### Step 5: Run Your Tests
133
-
134
- You can run accessibility checks in several ways:
135
-
136
- #### Option A: Run Tests Manually
137
-
138
- ```bash
139
- bundle exec rspec spec/system/
140
- ```
141
-
142
- Accessibility checks run automatically on every system test that visits a page.
143
-
144
- #### Option B: Run Continuously with Procfile (Recommended for Development)
145
-
146
- For continuous accessibility checking during development, add to your `Procfile.dev`:
147
-
148
- ```procfile
149
- web: bin/rails server
150
- css: bin/rails dartsass:watch
151
- a11y: while true; do bundle exec rspec spec/system/*_accessibility_spec.rb; sleep 30; done
152
- ```
153
-
154
- Then run:
155
-
156
- ```bash
157
- bin/dev
158
- ```
159
-
160
- This will:
161
- - Start your Rails server
162
- - Watch for CSS changes
163
- - **Automatically run accessibility checks every 30 seconds** on all `*_accessibility_spec.rb` files
164
-
165
- The accessibility checker will continuously monitor your pages and alert you to any issues as you develop!
166
-
167
- ## Troubleshooting
168
-
169
159
  ### Error: `uninitialized constant Selenium::WebDriver::DriverFinder`
170
160
 
171
161
  This error typically occurs when:
@@ -218,14 +208,14 @@ development:
218
208
 
219
209
  For best results, use these compatible versions:
220
210
 
221
- | Component | Recommended Version | Minimum Version |
222
- |-----------|-------------------|-----------------|
223
- | Ruby | 3.1+ | 3.0+ |
224
- | Rails | 7.1+ / 8.0+ | 6.0+ |
225
- | RSpec Rails | 6.0+ | 5.0+ |
226
- | Capybara | ~> 3.40 | 3.0+ |
227
- | selenium-webdriver | ~> 4.10 | 4.0+ |
228
- | webdrivers | ~> 5.3 | 5.0+ |
211
+ | Component | Recommended Version | Minimum Version | Required |
212
+ |-----------|-------------------|-----------------|----------|
213
+ | Ruby | 3.1+ | 3.0+ | Yes |
214
+ | Rails | 7.1+ / 8.0+ | 6.0+ | Yes |
215
+ | **RSpec Rails** | **8.0+** | **6.0+** | **Yes (for system specs)** |
216
+ | Capybara | ~> 3.40 | 3.0+ | Yes |
217
+ | selenium-webdriver | ~> 4.10 | 4.0+ | Yes |
218
+ | webdrivers | ~> 5.3 | 5.0+ | Optional |
229
219
 
230
220
  **Rails 8 Notes:**
231
221
  - Rails 8 requires `selenium-webdriver` 4.6.0+ for `DriverFinder` support
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Live accessibility scanner for development
5
+ # Monitors page visits and runs accessibility checks automatically
6
+ # Usage: Add to Procfile.dev as: a11y: bundle exec a11y_live_scanner
7
+
8
+ require 'bundler/setup'
9
+ require 'json'
10
+ require 'fileutils'
11
+ require 'timeout'
12
+
13
+ # Load Rails environment
14
+ # Find Rails root by looking for config/environment.rb
15
+ rails_root = Dir.pwd
16
+ while rails_root != '/' && !File.exist?(File.join(rails_root, 'config', 'environment.rb'))
17
+ rails_root = File.dirname(rails_root)
18
+ end
19
+
20
+ if File.exist?(File.join(rails_root, 'config', 'environment.rb'))
21
+ Dir.chdir(rails_root)
22
+ require File.join(rails_root, 'config', 'environment')
23
+ end
24
+
25
+ # Require the gem and ensure all components are loaded
26
+ begin
27
+ require 'rails_accessibility_testing'
28
+ # Verify AccessibilityHelper is available
29
+ unless defined?(RailsAccessibilityTesting::AccessibilityHelper)
30
+ raise LoadError, "RailsAccessibilityTesting::AccessibilityHelper not found after requiring gem"
31
+ end
32
+ rescue LoadError => e
33
+ $stderr.puts "❌ Error loading rails_accessibility_testing gem: #{e.message}"
34
+ $stderr.puts " Make sure the gem is installed: bundle install"
35
+ exit 1
36
+ end
37
+
38
+ require 'capybara'
39
+ require 'capybara/dsl'
40
+ require 'selenium-webdriver'
41
+
42
+ # Try to require webdrivers
43
+ begin
44
+ require 'webdrivers'
45
+ rescue LoadError
46
+ # webdrivers not available
47
+ end
48
+
49
+ # Setup Capybara for live scanning
50
+ Capybara.default_driver = :selenium_chrome_headless
51
+ Capybara.app_host = ENV.fetch('RAILS_URL', 'http://localhost:3000')
52
+ Capybara.default_max_wait_time = 5
53
+
54
+ # Configure Chrome options
55
+ browser_options = Selenium::WebDriver::Chrome::Options.new
56
+ browser_options.add_argument('--headless')
57
+ browser_options.add_argument('--window-size=1920,1080')
58
+ browser_options.add_argument('--disable-gpu')
59
+ browser_options.add_argument('--no-sandbox')
60
+
61
+ Capybara.register_driver :selenium_chrome_headless do |app|
62
+ Capybara::Selenium::Driver.new(
63
+ app,
64
+ browser: :chrome,
65
+ options: browser_options
66
+ )
67
+ end
68
+
69
+ include Capybara::DSL
70
+
71
+ # Page visit log file
72
+ def log_file
73
+ defined?(Rails) ? File.join(Rails.root, 'tmp', 'a11y_page_visits.log') : File.join(Dir.pwd, 'tmp', 'a11y_page_visits.log')
74
+ end
75
+
76
+ def scanned_pages_file
77
+ defined?(Rails) ? File.join(Rails.root, 'tmp', 'a11y_scanned_pages.json') : File.join(Dir.pwd, 'tmp', 'a11y_scanned_pages.json')
78
+ end
79
+
80
+ # Track which pages we've already scanned (to avoid duplicates)
81
+ def load_scanned_pages
82
+ return {} unless File.exist?(scanned_pages_file)
83
+ JSON.parse(File.read(scanned_pages_file))
84
+ rescue StandardError
85
+ {}
86
+ end
87
+
88
+ def save_scanned_page(path, timestamp)
89
+ scanned = load_scanned_pages
90
+ scanned[path] = timestamp
91
+ # Use atomic write to avoid triggering file watchers unnecessarily
92
+ temp_file = "#{scanned_pages_file}.tmp"
93
+ File.write(temp_file, scanned.to_json)
94
+ FileUtils.mv(temp_file, scanned_pages_file)
95
+ rescue StandardError => e
96
+ # Silently fail - don't break scanning if file write fails
97
+ end
98
+
99
+ def page_already_scanned?(path, visit_timestamp)
100
+ scanned = load_scanned_pages
101
+ return false unless scanned[path]
102
+ # Don't rescan if scanned within last 30 seconds (debounce)
103
+ last_scan_time = scanned[path].to_f
104
+ (Time.now.to_f - last_scan_time) < 30
105
+ end
106
+
107
+ # Run accessibility check on a page
108
+ def check_page(path, url = nil)
109
+ begin
110
+ # Check if scan was cancelled before starting
111
+ if $scan_cancelled
112
+ puts "⏭️ Scan cancelled - user navigated to different page"
113
+ return nil
114
+ end
115
+
116
+ # Show what we're about to scan
117
+ puts "\n" + "="*70
118
+ puts "🔍 Starting accessibility scan..."
119
+ puts "="*70
120
+ puts "📍 Path: #{path}"
121
+ puts "🔗 URL: #{url || path}"
122
+ puts "⏰ Time: #{Time.now.strftime('%H:%M:%S')}"
123
+ puts "="*70
124
+ puts ""
125
+
126
+ # Visit the page
127
+ puts " 🌐 Loading page..."
128
+ visit path
129
+
130
+ # Check for cancellation after page load
131
+ if $scan_cancelled || $current_scan_target != path
132
+ puts "⏭️ Scan cancelled - user navigated to different page"
133
+ return nil
134
+ end
135
+
136
+ # Capybara already waits for page load, no need for explicit sleep
137
+ # Only wait if page hasn't loaded (Capybara handles this automatically)
138
+
139
+ # Get page details
140
+ current_url = page.current_url rescue path
141
+ page_title = page.title rescue 'Unknown'
142
+
143
+ # Skip if redirected to sign in
144
+ if current_url.include?('sign_in') || current_url.include?('login')
145
+ puts "⏭️ Skipping #{path}: requires authentication"
146
+ puts " Redirected to: #{current_url}"
147
+ return nil
148
+ end
149
+
150
+ # Check for cancellation before running checks
151
+ if $scan_cancelled || $current_scan_target != path
152
+ puts "⏭️ Scan cancelled - user navigated to different page"
153
+ return nil
154
+ end
155
+
156
+ # Show page details
157
+ puts " ✓ Page loaded successfully"
158
+ puts " 📄 Title: #{page_title}"
159
+ puts " 🔗 Final URL: #{current_url}"
160
+ puts ""
161
+ puts " 🔍 Running accessibility checks..."
162
+ puts ""
163
+
164
+ # Run accessibility checks using Capybara page directly
165
+ # Verify AccessibilityHelper is available
166
+ unless defined?(RailsAccessibilityTesting::AccessibilityHelper)
167
+ raise "RailsAccessibilityTesting::AccessibilityHelper is not available. Make sure the gem is properly installed."
168
+ end
169
+
170
+ # Create a minimal helper instance
171
+ helper_class = Class.new do
172
+ include RailsAccessibilityTesting::AccessibilityHelper
173
+
174
+ def initialize(page)
175
+ @page = page
176
+ end
177
+
178
+ def page
179
+ @page
180
+ end
181
+ end
182
+
183
+ helper = helper_class.new(page)
184
+
185
+ # Run checks with periodic cancellation checks
186
+ # Note: The checks themselves don't support cancellation yet, but we check before/after
187
+ result = helper.check_comprehensive_accessibility
188
+
189
+ # Check for cancellation after checks complete
190
+ if $scan_cancelled || $current_scan_target != path
191
+ puts "⏭️ Scan cancelled - user navigated to different page"
192
+ return nil
193
+ end
194
+
195
+ # Show scan completion
196
+ puts ""
197
+ puts "="*70
198
+ puts "✅ Scan completed for: #{path}"
199
+ if result && result.is_a?(Hash)
200
+ puts " Errors: #{result[:errors] || 0}, Warnings: #{result[:warnings] || 0}"
201
+ end
202
+ puts "="*70
203
+ puts ""
204
+
205
+ # Return result
206
+ result
207
+ rescue StandardError => e
208
+ # Don't show error if scan was cancelled
209
+ if $scan_cancelled || $current_scan_target != path
210
+ return nil
211
+ end
212
+
213
+ puts ""
214
+ puts "="*70
215
+ puts "⚠️ Error checking #{path}"
216
+ puts " Error: #{e.message}"
217
+ puts " URL: #{url || path}"
218
+ puts "="*70
219
+ puts ""
220
+ nil
221
+ end
222
+ end
223
+
224
+ # Global variable to track current scan target (for cancellation)
225
+ $current_scan_target = nil
226
+ $scan_cancelled = false
227
+
228
+ # Main scanner loop
229
+ def run_scanner
230
+ puts "\n" + "="*70
231
+ puts "🔍 Live Accessibility Scanner Started"
232
+ puts "="*70
233
+ puts "📍 Monitoring: #{Capybara.app_host}"
234
+ puts "📝 Watching: #{log_file}"
235
+ puts "💡 Navigate to pages in your browser to see accessibility reports"
236
+ puts "⏱️ Checking for new page visits every 2 seconds..."
237
+ puts "💡 Tip: If you navigate to a new page, current scan will be cancelled"
238
+ puts "="*70
239
+ puts ""
240
+ puts "⏳ Waiting for page visits... (scanning will start automatically)"
241
+ puts ""
242
+
243
+ # Create log file if it doesn't exist
244
+ FileUtils.mkdir_p(File.dirname(log_file))
245
+ FileUtils.touch(log_file) unless File.exist?(log_file)
246
+
247
+ # Start reading from end of file
248
+ last_position = File.exist?(log_file) ? File.size(log_file) : 0
249
+ last_scan_time = {} # In-memory debounce cache
250
+
251
+ loop do
252
+ begin
253
+ # Check if log file has new content
254
+ current_size = File.exist?(log_file) ? File.size(log_file) : 0
255
+
256
+ if current_size > last_position
257
+ # Read all new lines to get the latest page visit
258
+ new_visits = []
259
+ File.open(log_file, 'r') do |f|
260
+ f.seek(last_position)
261
+ f.each_line do |line|
262
+ next if line.strip.empty?
263
+
264
+ begin
265
+ visit_data = JSON.parse(line.strip)
266
+ new_visits << visit_data
267
+ rescue JSON::ParserError => e
268
+ # Skip invalid JSON
269
+ next
270
+ end
271
+ end
272
+ end
273
+
274
+ # Process only the most recent visit (user's current page)
275
+ if new_visits.any?
276
+ begin
277
+ # Get the latest visit (last in the array)
278
+ latest_visit = new_visits.last
279
+ path = latest_visit['path']
280
+ url = latest_visit['url']
281
+ timestamp = latest_visit['timestamp'].to_f
282
+
283
+ # Debounce: Skip if we scanned this page in the last 30 seconds (in-memory check)
284
+ if last_scan_time[path] && (Time.now.to_f - last_scan_time[path]) < 30
285
+ last_position = current_size
286
+ next
287
+ end
288
+
289
+ # Skip if we've already scanned this page (from persistent storage)
290
+ if page_already_scanned?(path, timestamp)
291
+ last_position = current_size
292
+ next
293
+ end
294
+
295
+ # Cancel current scan if scanning a different page
296
+ if $current_scan_target && $current_scan_target != path
297
+ puts "\n⚠️ User navigated to new page - cancelling current scan"
298
+ puts " Previous: #{$current_scan_target}"
299
+ puts " New: #{path}"
300
+ $scan_cancelled = true
301
+ # Give a moment for cancellation to be processed
302
+ sleep 0.5
303
+ end
304
+
305
+ # Reset cancellation flag and set new target
306
+ $scan_cancelled = false
307
+ $current_scan_target = path
308
+
309
+ # Mark as scanned (both in memory and on disk)
310
+ last_scan_time[path] = Time.now.to_f
311
+ save_scanned_page(path, timestamp)
312
+
313
+ # Show that we detected a new page visit
314
+ visit_time = Time.at(timestamp)
315
+ puts "\n" + "🎯" + "="*69
316
+ puts "📥 NEW PAGE VISIT DETECTED"
317
+ puts "="*70
318
+ puts " Path: #{path}"
319
+ puts " URL: #{url}"
320
+ puts " Visited at: #{visit_time.strftime('%H:%M:%S')}"
321
+ puts " Starting scan..."
322
+ puts "="*70
323
+
324
+ # Run check with URL info
325
+ check_page(path, url)
326
+
327
+ # Clear scan target after completion
328
+ $current_scan_target = nil if $current_scan_target == path
329
+ rescue StandardError => e
330
+ puts "⚠️ Error processing visit: #{e.message}"
331
+ end
332
+ end
333
+
334
+ last_position = current_size
335
+ end
336
+
337
+ # Sleep longer to reduce file system activity and avoid triggering Rails reloads
338
+ # Check every 2 seconds - fast enough for live scanning, slow enough to not overload
339
+ # Show a subtle indicator that we're still monitoring (every 10 seconds)
340
+ if Time.now.to_i % 10 == 0
341
+ print "💚 Monitoring... (#{Time.now.strftime('%H:%M:%S')})\r"
342
+ $stdout.flush
343
+ end
344
+ sleep 2
345
+
346
+ rescue StandardError => e
347
+ puts "\n⚠️ Scanner error: #{e.message}"
348
+ puts " Continuing to monitor..."
349
+ sleep 2
350
+ end
351
+ end
352
+ end
353
+
354
+ # Run the scanner
355
+ begin
356
+ run_scanner
357
+ rescue Interrupt
358
+ puts "\n\n👋 Live scanner stopped"
359
+ exit 0
360
+ end
361
+
@@ -15,7 +15,17 @@ if [ -f "$PIDFILE" ]; then
15
15
  # Check if it's listening on the expected port
16
16
  if lsof -ti:$PORT -sTCP:LISTEN | grep -q "^$PID$" 2>/dev/null; then
17
17
  echo "Server is already running (pid: $PID, port: $PORT)"
18
+ echo "Keeping process alive for Foreman..."
19
+ # Keep the process alive by waiting indefinitely
20
+ # This prevents Foreman from shutting down all processes
21
+ while true; do
22
+ # Check if the server process is still running
23
+ if ! kill -0 "$PID" 2>/dev/null; then
24
+ echo "Server process ended. Exiting."
18
25
  exit 0
26
+ fi
27
+ sleep 5
28
+ done
19
29
  else
20
30
  # PID exists but not listening on port - stale PID file
21
31
  echo "Removing stale PID file (pid: $PID not listening on port $PORT)"
@@ -32,8 +42,15 @@ fi
32
42
  if lsof -ti:$PORT -sTCP:LISTEN >/dev/null 2>&1; then
33
43
  EXISTING_PID=$(lsof -ti:$PORT -sTCP:LISTEN | head -1)
34
44
  echo "Port $PORT is already in use by process $EXISTING_PID"
35
- echo "Server may already be running. Exiting gracefully."
45
+ echo "Server may already be running. Keeping process alive for Foreman..."
46
+ # Keep the process alive by monitoring the existing server
47
+ while true; do
48
+ if ! lsof -ti:$PORT -sTCP:LISTEN >/dev/null 2>&1; then
49
+ echo "Port $PORT is now free. Exiting so Foreman can restart."
36
50
  exit 0
51
+ fi
52
+ sleep 5
53
+ done
37
54
  fi
38
55
 
39
56
  # Start the server normally
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+ require 'fileutils'
5
+
6
+ module RailsA11y
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
+ rails_helper_content = File.read(rails_helper_path)
32
+
33
+ # Add rails_accessibility_testing require
34
+ unless rails_helper_content.include?("require 'rails_accessibility_testing'")
35
+ inject_into_file rails_helper_path,
36
+ after: "require 'rspec/rails'\n" do
37
+ "require 'rails_accessibility_testing'\n"
38
+ end
39
+ end
40
+ else
41
+ say "⚠️ spec/rails_helper.rb not found. Please add: require 'rails_accessibility_testing'", :yellow
42
+ end
43
+ end
44
+
45
+ def create_all_pages_spec
46
+ spec_path = 'spec/system/all_pages_accessibility_spec.rb'
47
+
48
+ if File.exist?(spec_path)
49
+ say "⚠️ #{spec_path} already exists. Skipping creation.", :yellow
50
+ else
51
+ template 'all_pages_accessibility_spec.rb.erb', spec_path
52
+ say "✅ Created #{spec_path}", :green
53
+ end
54
+ end
55
+
56
+ def update_procfile_dev
57
+ procfile_path = 'Procfile.dev'
58
+
59
+ if File.exist?(procfile_path)
60
+ procfile_content = File.read(procfile_path)
61
+
62
+ # Check if a11y line already exists
63
+ unless procfile_content.include?('a11y:')
64
+ # Add live scanner to Procfile.dev
65
+ a11y_line = "a11y: bundle exec a11y_live_scanner\n"
66
+ procfile_content += a11y_line
67
+ File.write(procfile_path, procfile_content)
68
+ say "✅ Added live accessibility scanner to #{procfile_path}", :green
69
+ say " 💡 Run 'bin/dev' to start live scanning as you browse pages", :cyan
70
+ else
71
+ say "⚠️ Procfile.dev already contains an a11y entry. Skipping.", :yellow
72
+ end
73
+ else
74
+ # Create Procfile.dev if it doesn't exist
75
+ procfile_content = <<~PROCFILE
76
+ web: bin/rails server
77
+ a11y: bundle exec a11y_live_scanner
78
+ PROCFILE
79
+
80
+ File.write(procfile_path, procfile_content)
81
+ say "✅ Created #{procfile_path} with live accessibility scanner", :green
82
+ end
83
+ end
84
+
85
+ def update_gitignore
86
+ gitignore_path = '.gitignore'
87
+
88
+ if File.exist?(gitignore_path)
89
+ gitignore_content = File.read(gitignore_path)
90
+
91
+ # Add tmp files for live scanner if not already present
92
+ a11y_entries = [
93
+ 'tmp/a11y_page_visits.log',
94
+ 'tmp/a11y_scanned_pages.json'
95
+ ]
96
+
97
+ a11y_entries.each do |entry|
98
+ unless gitignore_content.include?(entry)
99
+ gitignore_content += "\n#{entry}\n"
100
+ end
101
+ end
102
+
103
+ File.write(gitignore_path, gitignore_content) if gitignore_content != File.read(gitignore_path)
104
+ end
105
+ end
106
+
107
+ def show_instructions
108
+ say "\n✅ Rails Accessibility Testing installed successfully!", :green
109
+ say "\n📋 Next Steps:", :yellow
110
+ say ""
111
+ say " 1. Run the accessibility tests:", :cyan
112
+ say " bundle exec rspec spec/system/all_pages_accessibility_spec.rb"
113
+ say ""
114
+ say " 2. For live scanning during development:", :cyan
115
+ say " bin/dev # Starts web server + live accessibility scanner"
116
+ say " # Navigate to pages in your browser to see real-time reports!"
117
+ say ""
118
+ say " 3. Create custom specs for specific pages:", :cyan
119
+ say " # spec/system/my_page_accessibility_spec.rb"
120
+ say " RSpec.describe 'My Page', type: :system do"
121
+ say " it 'is accessible' do"
122
+ say " visit my_page_path"
123
+ say " check_comprehensive_accessibility"
124
+ say " end"
125
+ say " end"
126
+ say ""
127
+ say " 4. Configure which checks run in config/accessibility.yml", :cyan
128
+ say ""
129
+ say " 5. Accessibility checks run automatically after each 'visit' in system specs!", :cyan
130
+ say ""
131
+ say "📚 Documentation:", :yellow
132
+ say " • README: https://github.com/rayraycodes/rails-accessibility-testing"
133
+ say ""
134
+ end
135
+ end
136
+ end
137
+