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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +212 -53
- data/CHANGELOG.md +118 -0
- data/GUIDES/getting_started.md +105 -77
- data/GUIDES/system_specs_for_accessibility.md +13 -12
- data/README.md +136 -36
- data/docs_site/getting_started.md +59 -69
- data/exe/a11y_live_scanner +361 -0
- data/exe/rails_server_safe +18 -1
- data/lib/generators/rails_a11y/install/install_generator.rb +137 -0
- data/lib/rails_accessibility_testing/accessibility_helper.rb +547 -24
- data/lib/rails_accessibility_testing/change_detector.rb +17 -104
- data/lib/rails_accessibility_testing/checks/base_check.rb +56 -7
- data/lib/rails_accessibility_testing/checks/heading_check.rb +138 -0
- data/lib/rails_accessibility_testing/checks/image_alt_text_check.rb +7 -7
- data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +11 -1
- data/lib/rails_accessibility_testing/cli/command.rb +3 -1
- data/lib/rails_accessibility_testing/config/yaml_loader.rb +1 -1
- data/lib/rails_accessibility_testing/engine/rule_engine.rb +49 -5
- data/lib/rails_accessibility_testing/error_message_builder.rb +63 -7
- data/lib/rails_accessibility_testing/middleware/page_visit_logger.rb +81 -0
- data/lib/rails_accessibility_testing/railtie.rb +22 -0
- data/lib/rails_accessibility_testing/rspec_integration.rb +176 -10
- data/lib/rails_accessibility_testing/version.rb +1 -1
- data/lib/rails_accessibility_testing.rb +8 -3
- metadata +7 -3
- data/lib/generators/rails_a11y/install/generator.rb +0 -51
- 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:**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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 |
|
|
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
|
+
|
data/exe/rails_server_safe
CHANGED
|
@@ -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.
|
|
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
|
+
|