rails_accessibility_testing 1.4.2 → 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 +135 -0
- data/GUIDES/getting_started.md +171 -9
- data/GUIDES/system_specs_for_accessibility.md +13 -12
- data/README.md +139 -36
- data/docs_site/getting_started.md +142 -18
- data/docs_site/index.md +1 -1
- 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 +8 -4
- data/lib/generators/rails_a11y/install/generator.rb +0 -51
- data/lib/rails_accessibility_testing/checks/heading_hierarchy_check.rb +0 -53
|
@@ -20,10 +20,18 @@ 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'
|
|
25
|
+
gem 'capybara', '~> 3.40'
|
|
26
|
+
gem 'selenium-webdriver', '~> 4.0'
|
|
27
|
+
gem 'webdrivers', '~> 5.0' # Optional but recommended for automatic driver management
|
|
24
28
|
end
|
|
25
29
|
```
|
|
26
30
|
|
|
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.
|
|
34
|
+
|
|
27
35
|
Then run:
|
|
28
36
|
|
|
29
37
|
```bash
|
|
@@ -41,17 +49,22 @@ rails generate rails_a11y:install
|
|
|
41
49
|
This creates:
|
|
42
50
|
- `config/initializers/rails_a11y.rb` - Configuration
|
|
43
51
|
- `config/accessibility.yml` - Check settings
|
|
52
|
+
- `spec/system/all_pages_accessibility_spec.rb` - Comprehensive spec that tests all GET routes
|
|
44
53
|
- Updates `spec/rails_helper.rb` (if using RSpec)
|
|
45
54
|
|
|
46
|
-
|
|
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.
|
|
56
|
+
|
|
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.
|
|
47
60
|
|
|
48
|
-
|
|
61
|
+
You can also create custom system specs for specific pages:
|
|
49
62
|
|
|
50
63
|
```ruby
|
|
51
|
-
# spec/system/
|
|
64
|
+
# spec/system/my_page_accessibility_spec.rb
|
|
52
65
|
require 'rails_helper'
|
|
53
66
|
|
|
54
|
-
RSpec.describe '
|
|
67
|
+
RSpec.describe 'My Page Accessibility', type: :system do
|
|
55
68
|
it 'loads the page and runs comprehensive accessibility checks' do
|
|
56
69
|
visit root_path
|
|
57
70
|
|
|
@@ -63,40 +76,151 @@ RSpec.describe 'Home Page Accessibility', type: :system do
|
|
|
63
76
|
end
|
|
64
77
|
```
|
|
65
78
|
|
|
66
|
-
### Step
|
|
79
|
+
### Step 5: Run Your Tests
|
|
67
80
|
|
|
68
81
|
You can run accessibility checks in several ways:
|
|
69
82
|
|
|
70
83
|
#### Option A: Run Tests Manually
|
|
71
84
|
|
|
72
85
|
```bash
|
|
86
|
+
# Run all accessibility specs
|
|
87
|
+
bundle exec rspec spec/system/*_accessibility_spec.rb
|
|
88
|
+
|
|
89
|
+
# Or run all system specs
|
|
73
90
|
bundle exec rspec spec/system/
|
|
74
91
|
```
|
|
75
92
|
|
|
76
93
|
Accessibility checks run automatically on every system test that visits a page.
|
|
77
94
|
|
|
78
|
-
#### Option B: Run Continuously with Procfile (Recommended for Development)
|
|
79
95
|
|
|
80
|
-
|
|
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`:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# spec/support/driver.rb
|
|
104
|
+
require 'selenium-webdriver'
|
|
105
|
+
require 'capybara/rails'
|
|
106
|
+
require 'capybara/rspec'
|
|
107
|
+
|
|
108
|
+
# Configure Chrome options
|
|
109
|
+
browser_options = Selenium::WebDriver::Chrome::Options.new
|
|
110
|
+
browser_options.add_argument('--window-size=1920,1080')
|
|
111
|
+
browser_options.add_argument('--headless') unless ENV['SHOW_TEST_BROWSER']
|
|
112
|
+
|
|
113
|
+
# Register the driver
|
|
114
|
+
Capybara.register_driver :selenium_chrome_headless do |app|
|
|
115
|
+
Capybara::Selenium::Driver.new(
|
|
116
|
+
app,
|
|
117
|
+
browser: :chrome,
|
|
118
|
+
options: browser_options
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Set as default JavaScript driver
|
|
123
|
+
Capybara.javascript_driver = :selenium_chrome_headless
|
|
81
124
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
125
|
+
# Configure RSpec to use the driver for system tests
|
|
126
|
+
RSpec.configure do |config|
|
|
127
|
+
config.before(:each, type: :system) do
|
|
128
|
+
driven_by :selenium_chrome_headless
|
|
129
|
+
end
|
|
130
|
+
end
|
|
86
131
|
```
|
|
87
132
|
|
|
88
|
-
|
|
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).
|
|
134
|
+
|
|
135
|
+
### How do I install Chrome/Chromium?
|
|
136
|
+
|
|
137
|
+
System tests require Chrome or Chromium to be installed on your system:
|
|
138
|
+
|
|
139
|
+
**macOS:**
|
|
140
|
+
```bash
|
|
141
|
+
brew install --cask google-chrome
|
|
142
|
+
# or for Chromium:
|
|
143
|
+
brew install --cask chromium
|
|
144
|
+
```
|
|
89
145
|
|
|
146
|
+
**Linux (Ubuntu/Debian):**
|
|
90
147
|
```bash
|
|
91
|
-
|
|
148
|
+
sudo apt-get update
|
|
149
|
+
sudo apt-get install -y google-chrome-stable
|
|
150
|
+
# or for Chromium:
|
|
151
|
+
sudo apt-get install -y chromium-browser
|
|
92
152
|
```
|
|
93
153
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
154
|
+
**Windows:**
|
|
155
|
+
Download and install Chrome from [google.com/chrome](https://www.google.com/chrome/)
|
|
156
|
+
|
|
157
|
+
The `webdrivers` gem will automatically download and manage the ChromeDriver binary for you.
|
|
158
|
+
|
|
159
|
+
### Error: `uninitialized constant Selenium::WebDriver::DriverFinder`
|
|
160
|
+
|
|
161
|
+
This error typically occurs when:
|
|
162
|
+
1. **Missing selenium-webdriver gem** - Make sure you've added `gem 'selenium-webdriver', '~> 4.0'` to your Gemfile
|
|
163
|
+
2. **Version incompatibility** - Ensure you're using compatible versions:
|
|
164
|
+
- `selenium-webdriver` ~> 4.0 (4.6.0+ recommended for Rails 8)
|
|
165
|
+
- `webdrivers` ~> 5.0 (if using webdrivers)
|
|
166
|
+
- `capybara` ~> 3.40
|
|
167
|
+
|
|
168
|
+
**Solution:**
|
|
169
|
+
```bash
|
|
170
|
+
# Update your Gemfile
|
|
171
|
+
gem 'selenium-webdriver', '~> 4.10'
|
|
172
|
+
gem 'webdrivers', '~> 5.3'
|
|
173
|
+
gem 'capybara', '~> 3.40'
|
|
174
|
+
|
|
175
|
+
# Then run
|
|
176
|
+
bundle update selenium-webdriver webdrivers capybara
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Error: Chrome/ChromeDriver not found
|
|
180
|
+
|
|
181
|
+
**Solution:**
|
|
182
|
+
1. Make sure Chrome is installed (see Step 2.5 above)
|
|
183
|
+
2. If using `webdrivers` gem, it should auto-download ChromeDriver. If not:
|
|
184
|
+
```bash
|
|
185
|
+
bundle exec webdrivers chrome
|
|
186
|
+
```
|
|
187
|
+
3. For manual installation, download from [ChromeDriver downloads](https://chromedriver.chromium.org/downloads)
|
|
188
|
+
|
|
189
|
+
### System tests not running
|
|
190
|
+
|
|
191
|
+
**Check:**
|
|
192
|
+
1. Your spec has `type: :system` metadata
|
|
193
|
+
2. `spec/support/driver.rb` exists and is properly configured
|
|
194
|
+
3. `spec/rails_helper.rb` loads support files (should be automatic)
|
|
195
|
+
4. Chrome is installed and accessible
|
|
196
|
+
|
|
197
|
+
### Tests are slow
|
|
198
|
+
|
|
199
|
+
Disable expensive checks in development:
|
|
200
|
+
```yaml
|
|
201
|
+
# config/accessibility.yml
|
|
202
|
+
development:
|
|
203
|
+
checks:
|
|
204
|
+
color_contrast: false # Disable expensive color contrast checks
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Version Compatibility
|
|
208
|
+
|
|
209
|
+
For best results, use these compatible versions:
|
|
210
|
+
|
|
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 |
|
|
98
219
|
|
|
99
|
-
|
|
220
|
+
**Rails 8 Notes:**
|
|
221
|
+
- Rails 8 requires `selenium-webdriver` 4.6.0+ for `DriverFinder` support
|
|
222
|
+
- Make sure your `driven_by` configuration is in `spec/support/driver.rb`
|
|
223
|
+
- Rails 8 system tests use `driven_by` instead of direct Capybara configuration
|
|
100
224
|
|
|
101
225
|
## Learn More
|
|
102
226
|
|
data/docs_site/index.md
CHANGED
|
@@ -7,7 +7,7 @@ title: Home
|
|
|
7
7
|
|
|
8
8
|
**The RSpec + RuboCop of accessibility for Rails. Catch WCAG violations before they reach production.**
|
|
9
9
|
|
|
10
|
-
**Version:** 1.4.
|
|
10
|
+
**Version:** 1.4.3
|
|
11
11
|
|
|
12
12
|
Rails Accessibility Testing is a comprehensive accessibility testing gem that makes accessibility testing as natural as unit testing. It integrates seamlessly into your Rails workflow, catching WCAG 2.1 AA violations as you code—not after deployment.
|
|
13
13
|
|
|
@@ -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
|