rails_accessibility_testing 1.5.2 → 1.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +376 -1
- data/CHANGELOG.md +76 -0
- data/GUIDES/getting_started.md +40 -5
- data/GUIDES/system_specs_for_accessibility.md +12 -4
- data/README.md +52 -8
- data/docs_site/Gemfile.lock +89 -0
- data/docs_site/_config.yml +9 -0
- data/docs_site/_includes/header.html +1 -0
- data/docs_site/_layouts/default.html +754 -15
- data/docs_site/architecture.md +533 -0
- data/docs_site/index.md +2 -1
- data/exe/a11y_live_scanner +10 -39
- data/exe/a11y_static_scanner +333 -0
- data/lib/generators/rails_a11y/install/install_generator.rb +19 -30
- data/lib/generators/rails_a11y/install/templates/accessibility.yml.erb +39 -0
- data/lib/generators/rails_a11y/install/templates/all_pages_accessibility_spec.rb.erb +132 -45
- data/lib/rails_accessibility_testing/accessibility_helper.rb +131 -126
- data/lib/rails_accessibility_testing/checks/base_check.rb +14 -5
- data/lib/rails_accessibility_testing/checks/form_errors_check.rb +1 -1
- data/lib/rails_accessibility_testing/checks/form_labels_check.rb +6 -4
- data/lib/rails_accessibility_testing/checks/heading_check.rb +7 -15
- data/lib/rails_accessibility_testing/checks/image_alt_text_check.rb +1 -1
- data/lib/rails_accessibility_testing/checks/interactive_elements_check.rb +12 -8
- data/lib/rails_accessibility_testing/config/yaml_loader.rb +20 -0
- data/lib/rails_accessibility_testing/erb_extractor.rb +141 -0
- data/lib/rails_accessibility_testing/error_message_builder.rb +11 -6
- data/lib/rails_accessibility_testing/file_change_tracker.rb +95 -0
- data/lib/rails_accessibility_testing/line_number_finder.rb +61 -0
- data/lib/rails_accessibility_testing/rspec_integration.rb +74 -33
- data/lib/rails_accessibility_testing/shared_examples.rb +2 -0
- data/lib/rails_accessibility_testing/static_file_scanner.rb +80 -0
- data/lib/rails_accessibility_testing/static_page_adapter.rb +116 -0
- data/lib/rails_accessibility_testing/static_scanning.rb +61 -0
- data/lib/rails_accessibility_testing/version.rb +3 -1
- data/lib/rails_accessibility_testing/violation_converter.rb +80 -0
- data/lib/rails_accessibility_testing.rb +9 -1
- metadata +26 -2
|
@@ -213,16 +213,16 @@ module RailsAccessibilityTesting
|
|
|
213
213
|
@accessibility_warnings = []
|
|
214
214
|
@in_comprehensive_check = true
|
|
215
215
|
|
|
216
|
-
# Show page being checked
|
|
216
|
+
# Show page being checked - simplified header
|
|
217
217
|
page_path = safe_page_path || 'current page'
|
|
218
218
|
page_url = safe_page_url || 'current URL'
|
|
219
|
+
view_file = determine_view_file(page_path, page_url, {})
|
|
220
|
+
|
|
219
221
|
puts "\n" + "="*70
|
|
220
|
-
puts "🔍 Scanning
|
|
221
|
-
puts "="*70
|
|
222
|
-
puts "📍 Page: #{page_path}"
|
|
223
|
-
puts "🔗 URL: #{page_url}"
|
|
222
|
+
puts "🔍 Scanning: #{view_file || page_path}"
|
|
224
223
|
puts "="*70
|
|
225
|
-
|
|
224
|
+
print " Running checks"
|
|
225
|
+
$stdout.flush
|
|
226
226
|
|
|
227
227
|
# Use RuleEngine to run checks from checks/ folder
|
|
228
228
|
begin
|
|
@@ -234,23 +234,26 @@ module RailsAccessibilityTesting
|
|
|
234
234
|
path: safe_page_path
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
# Progress callback for real-time feedback
|
|
237
|
+
# Progress callback for real-time feedback - show dots only
|
|
238
238
|
progress_callback = lambda do |check_number, total_checks, check_name, status, data = nil|
|
|
239
239
|
case status
|
|
240
240
|
when :start
|
|
241
|
-
print "
|
|
241
|
+
print "."
|
|
242
242
|
$stdout.flush
|
|
243
243
|
when :passed
|
|
244
|
-
|
|
244
|
+
# Already printed dot, no need to print anything
|
|
245
245
|
when :found_issues
|
|
246
|
-
|
|
246
|
+
# Already printed dot, no need to print anything
|
|
247
247
|
when :error
|
|
248
|
-
|
|
248
|
+
# Already printed dot, no need to print anything
|
|
249
249
|
end
|
|
250
250
|
end
|
|
251
251
|
|
|
252
252
|
violations = engine.check(page, context: context, progress_callback: progress_callback)
|
|
253
253
|
|
|
254
|
+
# Print newline after dots
|
|
255
|
+
puts ""
|
|
256
|
+
|
|
254
257
|
# Convert violations to our error/warning format
|
|
255
258
|
violations.each do |violation|
|
|
256
259
|
element_context = violation.element_context || {}
|
|
@@ -306,33 +309,60 @@ module RailsAccessibilityTesting
|
|
|
306
309
|
puts error_output
|
|
307
310
|
$stdout.flush # Flush immediately to ensure errors are visible
|
|
308
311
|
|
|
309
|
-
#
|
|
310
|
-
|
|
312
|
+
# Check config for errors_only flag
|
|
313
|
+
errors_only = false
|
|
314
|
+
begin
|
|
315
|
+
require 'rails_accessibility_testing/config/yaml_loader'
|
|
316
|
+
profile = defined?(Rails) && Rails.env.test? ? :test : :development
|
|
317
|
+
config = RailsAccessibilityTesting::Config::YamlLoader.load(profile: profile)
|
|
318
|
+
summary_config = config['summary'] || {}
|
|
319
|
+
errors_only = summary_config.fetch('errors_only', false)
|
|
320
|
+
rescue StandardError
|
|
321
|
+
# Use default if config can't be loaded
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Check if we're in live scanner mode (detect by checking caller)
|
|
325
|
+
is_live_scanner = caller.any? { |line| line.include?('a11y_live_scanner') }
|
|
326
|
+
|
|
327
|
+
# Show warnings after errors (if any) - but skip if errors_only or live scanner
|
|
328
|
+
if @accessibility_warnings.any? && !is_live_scanner && !errors_only
|
|
311
329
|
warning_output = format_all_warnings(@accessibility_warnings)
|
|
312
330
|
puts warning_output
|
|
313
331
|
$stdout.flush
|
|
314
332
|
end
|
|
315
333
|
|
|
316
|
-
#
|
|
317
|
-
puts "
|
|
318
|
-
puts "
|
|
319
|
-
puts " #{warning_count} warning#{'s' if warning_count != 1}" if warning_count > 0
|
|
320
|
-
puts "="*70
|
|
334
|
+
# Compact summary
|
|
335
|
+
puts "📊 #{error_count} error#{'s' if error_count != 1}"
|
|
336
|
+
puts " #{warning_count} warning#{'s' if warning_count != 1}" if warning_count > 0 && !is_live_scanner && !errors_only
|
|
321
337
|
$stdout.flush
|
|
322
338
|
|
|
323
339
|
# Raise to fail the test (errors already formatted above)
|
|
324
340
|
# Include error and warning counts in message so they can be extracted even if exception is caught
|
|
325
341
|
raise "ACCESSIBILITY ERRORS FOUND: #{error_count} error(s), #{warning_count} warning(s) - see details above"
|
|
326
342
|
elsif @accessibility_warnings.any?
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
343
|
+
# Check config for errors_only flag
|
|
344
|
+
errors_only = false
|
|
345
|
+
begin
|
|
346
|
+
require 'rails_accessibility_testing/config/yaml_loader'
|
|
347
|
+
profile = defined?(Rails) && Rails.env.test? ? :test : :development
|
|
348
|
+
config = RailsAccessibilityTesting::Config::YamlLoader.load(profile: profile)
|
|
349
|
+
summary_config = config['summary'] || {}
|
|
350
|
+
errors_only = summary_config.fetch('errors_only', false)
|
|
351
|
+
rescue StandardError
|
|
352
|
+
# Use default if config can't be loaded
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Check if we're in live scanner mode - skip warnings in live scanner
|
|
356
|
+
is_live_scanner = caller.any? { |line| line.include?('a11y_live_scanner') }
|
|
357
|
+
|
|
358
|
+
if !is_live_scanner && !errors_only
|
|
359
|
+
# Only warnings, no errors - show warnings and indicate test passed with warnings
|
|
360
|
+
puts format_all_warnings(@accessibility_warnings)
|
|
361
|
+
puts "📊 #{@accessibility_warnings.length} warning#{'s' if @accessibility_warnings.length != 1}"
|
|
362
|
+
else
|
|
363
|
+
# Live scanner or errors_only - just show success
|
|
364
|
+
puts "✅ No errors"
|
|
365
|
+
end
|
|
336
366
|
else
|
|
337
367
|
# All checks passed with no errors and no warnings - show success message
|
|
338
368
|
puts "📊 SUMMARY: All checks passed!"
|
|
@@ -396,141 +426,103 @@ module RailsAccessibilityTesting
|
|
|
396
426
|
}
|
|
397
427
|
end
|
|
398
428
|
|
|
399
|
-
# Format all collected errors
|
|
429
|
+
# Format all collected errors - compact format grouped by file
|
|
400
430
|
def format_all_errors(errors)
|
|
401
431
|
return "" if errors.empty?
|
|
402
432
|
|
|
403
|
-
|
|
433
|
+
# Group errors by view file
|
|
434
|
+
errors_by_file = errors.group_by { |e| e[:page_context][:view_file] || e[:page_context][:path] || 'unknown' }
|
|
404
435
|
|
|
405
436
|
output = []
|
|
406
437
|
output << "\n" + "="*70
|
|
407
|
-
output << "❌
|
|
438
|
+
output << "❌ #{errors.length} error#{'s' if errors.length != 1} found"
|
|
408
439
|
output << "="*70
|
|
409
440
|
output << ""
|
|
410
|
-
output << "📋 SUMMARY OF ISSUES:"
|
|
411
|
-
output << ""
|
|
412
441
|
|
|
413
|
-
#
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
element_context = error[:element_context]
|
|
442
|
+
# Show errors grouped by file with clear spacing
|
|
443
|
+
errors_by_file.each_with_index do |(file_path, file_errors), file_index|
|
|
444
|
+
# Add spacing between files (except first)
|
|
445
|
+
output << "" if file_index > 0
|
|
418
446
|
|
|
419
|
-
#
|
|
420
|
-
|
|
447
|
+
# File header
|
|
448
|
+
output << "📝 #{file_path} (#{file_errors.length} error#{'s' if file_errors.length != 1})"
|
|
421
449
|
|
|
422
|
-
#
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
450
|
+
# List errors for this file
|
|
451
|
+
file_errors.each_with_index do |error, index|
|
|
452
|
+
error_type = error[:error_type]
|
|
453
|
+
element_context = error[:element_context]
|
|
454
|
+
|
|
455
|
+
# Compact error line
|
|
456
|
+
error_line = " • #{error_type}"
|
|
457
|
+
|
|
458
|
+
# Add element identifier if available (compact)
|
|
459
|
+
if element_context[:id].present?
|
|
460
|
+
error_line += " [id: #{element_context[:id]}]"
|
|
461
|
+
elsif element_context[:href].present?
|
|
462
|
+
href_display = element_context[:href].length > 30 ? "#{element_context[:href][0..27]}..." : element_context[:href]
|
|
463
|
+
error_line += " [href: #{href_display}]"
|
|
464
|
+
elsif element_context[:src].present?
|
|
465
|
+
src_display = element_context[:src].length > 30 ? "#{element_context[:src][0..27]}..." : element_context[:src]
|
|
466
|
+
error_line += " [src: #{src_display}]"
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
output << error_line
|
|
441
470
|
end
|
|
442
|
-
|
|
443
|
-
output << summary
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
output << ""
|
|
447
|
-
output << "="*70
|
|
448
|
-
output << "📝 DETAILED ERROR DESCRIPTIONS:"
|
|
449
|
-
output << "="*70
|
|
450
|
-
output << ""
|
|
451
|
-
|
|
452
|
-
# Detailed descriptions at bottom
|
|
453
|
-
errors.each_with_index do |error, index|
|
|
454
|
-
output << "\n" + "-"*70
|
|
455
|
-
output << "ERROR #{index + 1} of #{errors.length}:"
|
|
456
|
-
output << "-"*70
|
|
457
|
-
output << error[:message]
|
|
458
471
|
end
|
|
459
472
|
|
|
460
473
|
output << ""
|
|
461
474
|
output << "="*70
|
|
462
|
-
output << "💡 Fix all issues above, then re-run the accessibility checks"
|
|
463
|
-
output << "="*70
|
|
464
475
|
output << ""
|
|
465
476
|
|
|
466
477
|
output.join("\n")
|
|
467
478
|
end
|
|
468
479
|
|
|
469
|
-
# Format all collected warnings
|
|
480
|
+
# Format all collected warnings - compact format grouped by file
|
|
470
481
|
def format_all_warnings(warnings)
|
|
471
482
|
return "" if warnings.empty?
|
|
472
483
|
|
|
473
|
-
|
|
484
|
+
# Group warnings by view file
|
|
485
|
+
warnings_by_file = warnings.group_by { |w| w[:page_context][:view_file] || w[:page_context][:path] || 'unknown' }
|
|
474
486
|
|
|
475
487
|
output = []
|
|
476
488
|
output << "\n" + "="*70
|
|
477
|
-
output << "⚠️
|
|
489
|
+
output << "⚠️ #{warnings.length} warning#{'s' if warnings.length != 1} found"
|
|
478
490
|
output << "="*70
|
|
479
491
|
output << ""
|
|
480
|
-
output << "📋 SUMMARY OF WARNINGS:"
|
|
481
|
-
output << ""
|
|
482
492
|
|
|
483
|
-
#
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
element_context = warning[:element_context]
|
|
488
|
-
|
|
489
|
-
# Build summary line - prioritize view file
|
|
490
|
-
summary = " #{index + 1}. #{warning_type}"
|
|
493
|
+
# Show warnings grouped by file with clear spacing
|
|
494
|
+
warnings_by_file.each_with_index do |(file_path, file_warnings), file_index|
|
|
495
|
+
# Add spacing between files (except first)
|
|
496
|
+
output << "" if file_index > 0
|
|
491
497
|
|
|
492
|
-
#
|
|
493
|
-
if
|
|
494
|
-
summary += "\n 📝 File: #{page_context[:view_file]}"
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
# Add element identifier if available
|
|
498
|
-
if element_context[:id].present?
|
|
499
|
-
summary += "\n 🔍 Element: [id: #{element_context[:id]}]"
|
|
500
|
-
elsif element_context[:href].present?
|
|
501
|
-
href_display = element_context[:href].length > 40 ? "#{element_context[:href][0..37]}..." : element_context[:href]
|
|
502
|
-
summary += "\n 🔍 Element: [href: #{href_display}]"
|
|
503
|
-
elsif element_context[:src].present?
|
|
504
|
-
src_display = element_context[:src].length > 40 ? "#{element_context[:src][0..37]}..." : element_context[:src]
|
|
505
|
-
summary += "\n 🔍 Element: [src: #{src_display}]"
|
|
506
|
-
end
|
|
498
|
+
# File header
|
|
499
|
+
output << "📝 #{file_path} (#{file_warnings.length} warning#{'s' if file_warnings.length != 1})"
|
|
507
500
|
|
|
508
|
-
#
|
|
509
|
-
|
|
510
|
-
|
|
501
|
+
# List warnings for this file
|
|
502
|
+
file_warnings.each_with_index do |warning, index|
|
|
503
|
+
warning_type = warning[:warning_type]
|
|
504
|
+
element_context = warning[:element_context]
|
|
505
|
+
|
|
506
|
+
# Compact warning line
|
|
507
|
+
warning_line = " • #{warning_type}"
|
|
508
|
+
|
|
509
|
+
# Add element identifier if available (compact)
|
|
510
|
+
if element_context[:id].present?
|
|
511
|
+
warning_line += " [id: #{element_context[:id]}]"
|
|
512
|
+
elsif element_context[:href].present?
|
|
513
|
+
href_display = element_context[:href].length > 30 ? "#{element_context[:href][0..27]}..." : element_context[:href]
|
|
514
|
+
warning_line += " [href: #{href_display}]"
|
|
515
|
+
elsif element_context[:src].present?
|
|
516
|
+
src_display = element_context[:src].length > 30 ? "#{element_context[:src][0..27]}..." : element_context[:src]
|
|
517
|
+
warning_line += " [src: #{src_display}]"
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
output << warning_line
|
|
511
521
|
end
|
|
512
|
-
|
|
513
|
-
output << summary
|
|
514
|
-
end
|
|
515
|
-
|
|
516
|
-
output << ""
|
|
517
|
-
output << "="*70
|
|
518
|
-
output << "📝 DETAILED WARNING DESCRIPTIONS:"
|
|
519
|
-
output << "="*70
|
|
520
|
-
output << ""
|
|
521
|
-
|
|
522
|
-
# Detailed descriptions at bottom
|
|
523
|
-
warnings.each_with_index do |warning, index|
|
|
524
|
-
output << "\n" + "-"*70
|
|
525
|
-
output << "WARNING #{index + 1} of #{warnings.length}:"
|
|
526
|
-
output << "-"*70
|
|
527
|
-
output << warning[:message]
|
|
528
522
|
end
|
|
529
523
|
|
|
530
524
|
output << ""
|
|
531
525
|
output << "="*70
|
|
532
|
-
output << "💡 Consider addressing these warnings to improve accessibility"
|
|
533
|
-
output << "="*70
|
|
534
526
|
output << ""
|
|
535
527
|
|
|
536
528
|
output.join("\n")
|
|
@@ -542,10 +534,23 @@ module RailsAccessibilityTesting
|
|
|
542
534
|
# @param page_context [Hash] Context about the page being tested
|
|
543
535
|
# @return [String] Formatted error message
|
|
544
536
|
def build_error_message(error_type, element_context, page_context)
|
|
537
|
+
# Check config for show_fixes setting
|
|
538
|
+
show_fixes = true
|
|
539
|
+
begin
|
|
540
|
+
require 'rails_accessibility_testing/config/yaml_loader'
|
|
541
|
+
profile = defined?(Rails) && Rails.env.test? ? :test : :development
|
|
542
|
+
config = RailsAccessibilityTesting::Config::YamlLoader.load(profile: profile)
|
|
543
|
+
summary_config = config['summary'] || {}
|
|
544
|
+
show_fixes = summary_config.fetch('show_fixes', true)
|
|
545
|
+
rescue StandardError
|
|
546
|
+
# Use default if config can't be loaded
|
|
547
|
+
end
|
|
548
|
+
|
|
545
549
|
RailsAccessibilityTesting::ErrorMessageBuilder.build(
|
|
546
550
|
error_type: error_type,
|
|
547
551
|
element_context: element_context,
|
|
548
|
-
page_context: page_context
|
|
552
|
+
page_context: page_context,
|
|
553
|
+
show_fixes: show_fixes
|
|
549
554
|
)
|
|
550
555
|
end
|
|
551
556
|
|
|
@@ -80,11 +80,20 @@ module RailsAccessibilityTesting
|
|
|
80
80
|
# @param element_context [Hash] Optional element context to help find partials
|
|
81
81
|
# @return [Hash]
|
|
82
82
|
def page_context(element_context = nil)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
# For static scanning, use view_file from context if available
|
|
84
|
+
if @context && @context[:view_file]
|
|
85
|
+
{
|
|
86
|
+
url: nil,
|
|
87
|
+
path: nil,
|
|
88
|
+
view_file: @context[:view_file]
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
{
|
|
92
|
+
url: safe_page_url,
|
|
93
|
+
path: safe_page_path,
|
|
94
|
+
view_file: determine_view_file(element_context)
|
|
95
|
+
}
|
|
96
|
+
end
|
|
88
97
|
end
|
|
89
98
|
|
|
90
99
|
# Get element context from Capybara element
|
|
@@ -17,7 +17,7 @@ module RailsAccessibilityTesting
|
|
|
17
17
|
|
|
18
18
|
page.all('.field_with_errors input, .field_with_errors textarea, .field_with_errors select, .is-invalid, [aria-invalid="true"]').each do |input|
|
|
19
19
|
id = input[:id]
|
|
20
|
-
next if id.
|
|
20
|
+
next if id.nil? || id.to_s.strip.empty?
|
|
21
21
|
|
|
22
22
|
has_error_message = page.has_css?("[aria-describedby*='#{id}'], .field_with_errors label[for='#{id}'] + .error, .field_with_errors label[for='#{id}'] + .invalid-feedback", wait: false)
|
|
23
23
|
|
|
@@ -18,13 +18,15 @@ module RailsAccessibilityTesting
|
|
|
18
18
|
|
|
19
19
|
page.all('input[type="text"], input[type="email"], input[type="password"], input[type="number"], input[type="tel"], input[type="url"], input[type="search"], input[type="date"], input[type="time"], input[type="datetime-local"], textarea, select').each do |input|
|
|
20
20
|
id = input[:id]
|
|
21
|
-
next if id.
|
|
21
|
+
next if id.nil? || id.to_s.strip.empty?
|
|
22
22
|
|
|
23
23
|
has_label = page.has_css?("label[for='#{id}']", wait: false)
|
|
24
|
-
aria_label = input[:"aria-label"]
|
|
25
|
-
aria_labelledby = input[:"aria-labelledby"]
|
|
24
|
+
aria_label = input[:"aria-label"]
|
|
25
|
+
aria_labelledby = input[:"aria-labelledby"]
|
|
26
|
+
has_aria_label = aria_label && !aria_label.to_s.strip.empty?
|
|
27
|
+
has_aria_labelledby = aria_labelledby && !aria_labelledby.to_s.strip.empty?
|
|
26
28
|
|
|
27
|
-
unless has_label ||
|
|
29
|
+
unless has_label || has_aria_label || has_aria_labelledby
|
|
28
30
|
element_ctx = element_context(input)
|
|
29
31
|
element_ctx[:input_type] = input[:type] || input.tag_name
|
|
30
32
|
|
|
@@ -95,22 +95,14 @@ module RailsAccessibilityTesting
|
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
# Check 5: Headings with only images (no alt text or empty alt) (WCAG 4.1.2)
|
|
98
|
+
# Note: For static scanning, we check if heading text is empty
|
|
99
|
+
# Full image checking within headings requires more complex DOM traversal
|
|
100
|
+
# This is a simplified check - full implementation would require xpath queries
|
|
98
101
|
headings.each do |heading|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
images_without_alt = images.select { |img| img[:alt].nil? || img[:alt].strip.empty? }
|
|
104
|
-
if images_without_alt.any?
|
|
105
|
-
element_ctx = element_context(heading)
|
|
106
|
-
violations << violation(
|
|
107
|
-
message: "Heading contains images without alt text - headings must have accessible text",
|
|
108
|
-
element_context: element_ctx,
|
|
109
|
-
wcag_reference: "4.1.2",
|
|
110
|
-
remediation: "Add alt text to images in the heading, or add visible text alongside the images:\n\n<h1><img src=\"image.jpg\" alt=\"Descriptive text\">Heading Text</h1>"
|
|
111
|
-
)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
102
|
+
heading_text = heading.text.strip
|
|
103
|
+
# If heading has no text, it might be image-only (but we can't easily check images within heading in static mode)
|
|
104
|
+
# This check is primarily for dynamic scanning where we can traverse the DOM
|
|
105
|
+
# For static scanning, we rely on the empty heading check above
|
|
114
106
|
end
|
|
115
107
|
|
|
116
108
|
# Check 6: Headings used only for styling (WCAG 2.4.6)
|
|
@@ -31,7 +31,7 @@ module RailsAccessibilityTesting
|
|
|
31
31
|
wcag_reference: "1.1.1",
|
|
32
32
|
remediation: generate_remediation(element_ctx)
|
|
33
33
|
)
|
|
34
|
-
elsif alt_value.
|
|
34
|
+
elsif (alt_value.nil? || alt_value.to_s.strip.empty?) && has_alt_attribute
|
|
35
35
|
# Image has alt attribute but it's empty - warn about this
|
|
36
36
|
# Empty alt is valid for decorative images, but we should check if it's actually decorative
|
|
37
37
|
element_ctx = element_context(img)
|
|
@@ -18,22 +18,26 @@ module RailsAccessibilityTesting
|
|
|
18
18
|
page.all('button, a[href], [role="button"], [role="link"]').each do |element|
|
|
19
19
|
next unless element.visible?
|
|
20
20
|
|
|
21
|
-
text = element.text.strip
|
|
21
|
+
text = element.text.to_s.strip
|
|
22
22
|
aria_label = element[:"aria-label"]
|
|
23
23
|
aria_labelledby = element[:"aria-labelledby"]
|
|
24
24
|
title = element[:title]
|
|
25
25
|
|
|
26
26
|
# Check if element contains an image with alt text (common pattern for logo links)
|
|
27
27
|
has_image_with_alt = false
|
|
28
|
-
if text.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
end
|
|
28
|
+
if text.empty?
|
|
29
|
+
# For static scanning, we can't easily check images within elements
|
|
30
|
+
# This check works better in dynamic scanning with Capybara
|
|
31
|
+
# For now, skip image checking in static mode
|
|
32
|
+
has_image_with_alt = false
|
|
34
33
|
end
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
text_empty = text.empty?
|
|
36
|
+
aria_label_empty = aria_label.nil? || aria_label.to_s.strip.empty?
|
|
37
|
+
aria_labelledby_empty = aria_labelledby.nil? || aria_labelledby.to_s.strip.empty?
|
|
38
|
+
title_empty = title.nil? || title.to_s.strip.empty?
|
|
39
|
+
|
|
40
|
+
if text_empty && aria_label_empty && aria_labelledby_empty && title_empty && !has_image_with_alt
|
|
37
41
|
element_ctx = element_context(element)
|
|
38
42
|
tag = element.tag_name
|
|
39
43
|
|
|
@@ -68,8 +68,21 @@ module RailsAccessibilityTesting
|
|
|
68
68
|
|
|
69
69
|
merged_checks = checks.merge(profile_checks)
|
|
70
70
|
|
|
71
|
+
# Deep merge summary configuration
|
|
72
|
+
base_summary = base_config['summary'] || {}
|
|
73
|
+
profile_summary = profile_config['summary'] || {}
|
|
74
|
+
merged_summary = base_summary.merge(profile_summary)
|
|
75
|
+
|
|
76
|
+
# Merge static_scanner config
|
|
77
|
+
base_static_scanner = base_config['static_scanner'] || {}
|
|
78
|
+
profile_static_scanner = profile_config['static_scanner'] || {}
|
|
79
|
+
merged_static_scanner = base_static_scanner.merge(profile_static_scanner)
|
|
80
|
+
|
|
71
81
|
base_config.merge(
|
|
72
82
|
'checks' => merged_checks,
|
|
83
|
+
'summary' => merged_summary,
|
|
84
|
+
'scan_strategy' => profile_config['scan_strategy'] || base_config['scan_strategy'] || 'paths',
|
|
85
|
+
'static_scanner' => merged_static_scanner,
|
|
73
86
|
'profile' => profile.to_s,
|
|
74
87
|
'ignored_rules' => parse_ignored_rules(parsed, profile)
|
|
75
88
|
)
|
|
@@ -105,6 +118,13 @@ module RailsAccessibilityTesting
|
|
|
105
118
|
{
|
|
106
119
|
'wcag_level' => 'AA',
|
|
107
120
|
'checks' => default_checks,
|
|
121
|
+
'summary' => {
|
|
122
|
+
'show_summary' => true,
|
|
123
|
+
'errors_only' => false,
|
|
124
|
+
'show_fixes' => true,
|
|
125
|
+
'ignore_warnings' => false
|
|
126
|
+
},
|
|
127
|
+
'scan_strategy' => 'paths',
|
|
108
128
|
'ignored_rules' => [],
|
|
109
129
|
'profile' => 'test'
|
|
110
130
|
}
|