From e0f235cc548f4e921dd7ecd9768109d78a409ecf Mon Sep 17 00:00:00 2001 From: Margarita Barvinok Date: Wed, 4 Feb 2026 11:28:27 -0500 Subject: [PATCH] add rails_accessibility_testing gem --- Gemfile | 2 + Gemfile.lock | 28 ++++ config/accessibility.yml | 113 ++++++++++++++++ config/initializers/rails_a11y.rb | 26 ++++ .../all_pages_accessibility_spec.rb | 124 ++++++++++++++++++ spec/rails_helper.rb | 1 + 6 files changed, 294 insertions(+) create mode 100644 config/accessibility.yml create mode 100644 config/initializers/rails_a11y.rb create mode 100644 spec/accessibility/all_pages_accessibility_spec.rb diff --git a/Gemfile b/Gemfile index 1e45513d..7da82025 100644 --- a/Gemfile +++ b/Gemfile @@ -73,6 +73,8 @@ group :development, :test do gem 'pry' gem 'pundit-matchers', '~> 3.1', '>= 3.1.2' gem 'brakeman', require: false + gem 'rails_accessibility_testing', '~> 1.6' + gem 'axe-core-capybara', '~> 4.0' end group :development, :staging do diff --git a/Gemfile.lock b/Gemfile.lock index 01bf8c4b..9a29a86a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,6 +83,17 @@ GEM annotate (3.2.0) activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) + axe-core-api (4.11.1) + dumb_delegator + ostruct + virtus + axe-core-capybara (4.11.1) + axe-core-api (= 4.11.1) + dumb_delegator + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) base64 (0.3.0) bcrypt (3.1.20) benchmark (0.4.1) @@ -106,6 +117,8 @@ GEM childprocess (5.1.0) logger (~> 1.5) coderay (1.1.3) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) concurrent-ruby (1.3.5) connection_pool (2.5.4) crass (1.0.6) @@ -115,6 +128,8 @@ GEM debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -127,6 +142,7 @@ GEM dotenv (= 3.2.0) railties (>= 6.1) drb (2.2.3) + dumb_delegator (1.1.0) erubi (1.13.1) factory_bot (6.5.0) activesupport (>= 5.0.0) @@ -146,6 +162,7 @@ GEM hashie (5.0.0) i18n (1.14.7) concurrent-ruby (~> 1.0) + ice_nine (0.11.2) image_processing (1.13.0) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) @@ -235,6 +252,7 @@ GEM omniauth (~> 2.1) ruby-saml (~> 1.18) orm_adapter (0.5.0) + ostruct (0.6.3) pg (1.5.9) pry (0.15.0) coderay (~> 1.1) @@ -285,6 +303,9 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + rails_accessibility_testing (1.6.0) + axe-core-capybara (~> 4.0) + nokogiri (>= 1.10) railties (7.1.5.2) actionpack (= 7.1.5.2) activesupport (= 7.1.5.2) @@ -350,12 +371,17 @@ GEM railties (>= 6.0.0) stringio (3.1.2) thor (1.4.0) + thread_safe (0.3.6) timeout (0.4.3) turbo-rails (2.0.11) actionpack (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + virtus (2.0.0) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) @@ -385,6 +411,7 @@ PLATFORMS DEPENDENCIES annotate (~> 3.2) + axe-core-capybara (~> 4.0) bootsnap brakeman capybara (~> 3.40) @@ -409,6 +436,7 @@ DEPENDENCIES pundit pundit-matchers (~> 3.1, >= 3.1.2) rails (~> 7.1.3, >= 7.1.3.2) + rails_accessibility_testing (~> 1.6) redis (>= 4.0.1) repost rspec-rails diff --git a/config/accessibility.yml b/config/accessibility.yml new file mode 100644 index 00000000..088afe84 --- /dev/null +++ b/config/accessibility.yml @@ -0,0 +1,113 @@ +# Rails A11y Configuration +# +# This file configures accessibility checks for your Rails application. +# See https://rayraycodes.github.io/rails-accessibility-testing/ for full documentation. + +# Global enable/disable flag for all accessibility checks +# Set to false to completely disable all accessibility checks (manual and automatic) +# When false, check_comprehensive_accessibility and automatic checks will be skipped +# Default: false +# (Set to false to allow other RSpec tests to pass in GitHub Actions CI even if accessibility tests fail. +# When true, any failing accessibility tests will cause the entire CI pipeline to fail.) +# Set to true to run accessibility checks manually: rspec spec/accessibility/all_pages_accessibility_spec.rb +accessibility_enabled: false + +# WCAG compliance level (A, AA, AAA) +wcag_level: AA + +# Summary configuration +# Control how accessibility test summaries are displayed +summary: + # Show summary at end of test suite (true/false) + show_summary: true + + # Show only errors in summary, hide warnings (true/false) + errors_only: false + + # Show fix suggestions in error messages (true/false) + # When false, only shows error type and location + show_fixes: true + + # Ignore warnings completely - only show errors (true/false) + # When true, warnings are filtered out and not displayed or counted + ignore_warnings: false + +# Scanning strategy +# 'paths' - Scan by visiting routes/paths (default) +# 'view_files' - Scan by finding view files and visiting their routes +scan_strategy: 'view_files' + +# Static scanner configuration +# Controls behavior of the static file scanner (a11y_static_scanner) +static_scanner: + # Only scan files that have changed since last scan (true/false) + # When true, uses file modification times to track changes + # When false, scans all files on every run + scan_changed_only: true + + # Check interval in seconds when running continuously + # Scanner checks for file changes every N seconds + check_interval: 3 + + # Force full scan on startup (true/false) + # When true, scans all files on first run regardless of changes + # When false, only scans changed files from the start + full_scan_on_startup: true + +# System specs configuration +# Controls behavior of accessibility checks in RSpec system specs +system_specs: + # Automatically run accessibility checks after each system spec (true/false) + # When true, checks run automatically after each `visit` in system specs + # When false, checks only run when explicitly called (e.g., check_comprehensive_accessibility) + # Default: false (set to true if you want automatic checks) + # Can be overridden per-profile (see profile sections below) + auto_run: false + +# Global check configuration +# Set to false to disable a check globally +checks: + form_labels: true + image_alt_text: true + interactive_elements: true + heading_hierarchy: true + keyboard_accessibility: true + aria_landmarks: true + form_errors: true + table_structure: true + duplicate_ids: true + skip_links: true + color_contrast: false # Disabled by default (requires JS evaluation) + +# Profile-specific configurations +# Override global settings for different environments + +development: + checks: + color_contrast: false # Skip in dev for speed + # Add other dev-specific overrides here + # system_specs: + # auto_run: true # Override global system_specs.auto_run for development + +test: + checks: + # Test environment uses global settings by default + # Add test-specific overrides here + # system_specs: + # auto_run: true # Override global system_specs.auto_run for test + +ci: + checks: + color_contrast: true # Full checks in CI + # Add CI-specific overrides here + # system_specs: + # auto_run: true # Override global system_specs.auto_run for CI + +# Ignored rules with reasons +# Use this to temporarily ignore specific rules while fixing issues +# Always include a reason and plan to fix +ignored_rules: + # - rule: form_labels + # reason: "Legacy form, scheduled for refactor in Q2" + # comment: "Will be fixed in PR #123" + diff --git a/config/initializers/rails_a11y.rb b/config/initializers/rails_a11y.rb new file mode 100644 index 00000000..4c0979d6 --- /dev/null +++ b/config/initializers/rails_a11y.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Rails A11y Configuration +# +# Configure accessibility testing behavior for your Rails application. +# +# @see https://rayraycodes.github.io/rails-accessibility-testing/ for documentation + +# Only configure if the gem is available (not in production) +if defined?(RailsAccessibilityTesting) + RailsAccessibilityTesting.configure do |config| + # Automatically run checks after system specs + # Set to false to disable automatic checks + config.auto_run_checks = false + + # Logger for accessibility check output + # Set to nil to use default logger + # config.logger = Rails.logger + + # Configuration file path (relative to Rails.root) + # config.config_path = 'config/accessibility.yml' + + # Default profile to use (development, test, ci) + # config.default_profile = :test + end +end diff --git a/spec/accessibility/all_pages_accessibility_spec.rb b/spec/accessibility/all_pages_accessibility_spec.rb new file mode 100644 index 00000000..4b278b6d --- /dev/null +++ b/spec/accessibility/all_pages_accessibility_spec.rb @@ -0,0 +1,124 @@ +require 'rails_helper' + +RSpec.describe 'All Pages Accessibility', type: :accessibility do + # Test all view files for accessibility using static file scanning + # Generated automatically by rails_a11y:install generator + + # Helper method to get all view files (non-partials) + def self.get_all_view_files + return [] unless defined?(Rails) && Rails.root + + view_dir = Rails.root.join('app', 'views') + return [] unless File.directory?(view_dir) + + extensions = %w[erb haml slim] + view_files = [] + + extensions.each do |ext| + # Find all HTML view files (exclude partials that start with _) + pattern = File.join(view_dir, '**', "*.html.#{ext}") + Dir.glob(pattern).each do |file| + # Skip partials (files starting with _) + next if File.basename(file).start_with?('_') + # Skip layout files + next if file.include?('/layouts/') + view_files << file + end + end + + view_files.sort + end + + # Format errors with file locations and line numbers + def format_static_errors(errors, warnings) + return "" if errors.empty? && warnings.empty? + + output = [] + + # Group errors and warnings by file + errors_by_file = errors.group_by { |e| e[:file] } + warnings_by_file = warnings.group_by { |w| w[:file] } + + # Show errors first + if errors.any? + output << "\n" + "="*70 + output << "āŒ #{errors.length} error#{'s' if errors.length != 1} found" + output << "" + format_issues_by_file(errors_by_file, output, 'error') + output << "" + output << "="*70 + end + + # Show warnings if any + if warnings.any? + output << "\n" + "="*70 + output << "āš ļø #{warnings.length} warning#{'s' if warnings.length != 1} found" + output << "="*70 + output << "" + + format_issues_by_file(warnings_by_file, output, 'warning') + + output << "" + output << "="*70 + end + + output.join("\n") + end + + def format_issues_by_file(issues_by_file, output, issue_type) + issues_by_file.each_with_index do |(file_path, file_issues), file_index| + output << "" if file_index > 0 + + output << "šŸ“ #{file_path} (#{file_issues.length} #{issue_type}#{'s' if file_issues.length != 1})" + + file_issues.each do |issue| + issue_line = " • #{issue[:type]}" + + # Add line number if available + if issue[:line] + issue_line += " [Line #{issue[:line]}]" + end + + # Add element identifier + if issue[:element][:id].present? + issue_line += " [id: #{issue[:element][:id]}]" + elsif issue[:element][:href].present? + href_display = issue[:element][:href].length > 30 ? "#{issue[:element][:href][0..27]}..." : issue[:element][:href] + issue_line += " [href: #{href_display}]" + elsif issue[:element][:src].present? + src_display = issue[:element][:src].length > 30 ? "#{issue[:element][:src][0..27]}..." : issue[:element][:src] + issue_line += " [src: #{src_display}]" + end + + output << issue_line + end + end + end + + # Scan all view files statically + view_files = get_all_view_files + + if view_files.empty? + it "no view files found to scan" do + skip "No view files found in app/views" + end + else + view_files.each do |view_file| + it "scans #{view_file} for accessibility issues" do + require 'rails_accessibility_testing/static_file_scanner' + + scanner = RailsAccessibilityTesting::StaticFileScanner.new(view_file) + result = scanner.scan + + errors = result[:errors] || [] + warnings = result[:warnings] || [] + + if errors.any? || warnings.any? + expect(errors).to be_empty, format_static_errors(errors, warnings) + else + puts "\nāœ… #{view_file}: No errors found" + end + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 6d54cfdb..d109f6f4 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -6,6 +6,7 @@ # Prevent database truncation if the environment is production abort("The Rails environment is running in production mode!") if Rails.env.production? require 'rspec/rails' +require 'rails_accessibility_testing' # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in