Rails Accessibility Testing

The RSpec + RuboCop of accessibility for Rails

Architecture

Composed Page Scanning

The gem now uses composed page scanning for page-level accessibility checks. This ensures that checks like heading hierarchy, ARIA landmarks, and duplicate IDs are evaluated against the complete rendered page (layout + view + partials), not individual files.

View Composition Builder

The ViewCompositionBuilder class traces the complete page structure:

  1. Finds Layout File: Identifies the layout file (defaults to application.html.erb)
  2. Finds View File: The main view file being rendered
  3. Recursively Finds Partials: Discovers all partials rendered in the view, including:
    • Partials in the same directory
    • Partials in layouts/, shared/, application/
    • Partials in any subdirectory (exhaustive search)
    • Nested partials (partials within partials)

Partial Detection

The gem detects all Rails render patterns:

Exhaustive Folder Traversal

The partial search traverses ALL folders in app/views recursively using Dir.glob, ensuring partials are found regardless of their location:

This makes it a general solution that works for any Rails application structure. Overview

This guide explains how Rails Accessibility Testing works under the hood in simple terms.


The Big Picture

The gem sits between your Rails Application and standard accessibility tools (like axe-core). It acts as a bridge that automatically checks your code for issues.

Core Concept

  1. You run a test (or the static scanner watches your files).
  2. The Gem activates and scans the content.
  3. 11 Checks run to find accessibility violations.
  4. Errors are reported with the exact file and line number to fix.
graph LR
    A[Your Code] --> B[Rails A11y Gem]
    B --> C[Compliance Checks]
    C --> D[Clear Error Report]
    
    style A fill:#ff6b6b
    style B fill:#4ecdc4
    style C fill:#45b7d1
    style D fill:#ffeaa7

Two Ways to Scan

The gem provides two main ways to check your application. Both share the same “brain” (the Rule Engine) but work differently.

1. System Tests (Browser-Based)

This is the most accurate method. It runs while your standard Rails system tests (RSpec/Minitest) are executing.

sequenceDiagram
    participant Test as Your Test
    participant Gem as Rails A11y
    participant Browser as Browser
    
    Test->>Browser: visit('/page')
    Browser->>Gem: "Page is ready!"
    Gem->>Browser: "Scan this page now"
    Browser->>Gem: "Found 2 errors"
    Gem->>Test: Fails test with details

2. Static Scanner (File-Based)

This is the fast method for development. It scans your source code files directly without opening a browser.

graph LR
    A[ERB File] -->|Read| B[Converter]
    B -->|HTML| C[Scanner]
    C -->|Errors| D[Console Output]
    
    style A fill:#ff6b6b
    style C fill:#4ecdc4

Key Components

Here are the main parts of the gem and what they do:

🧠 The Rule Engine

The “brain” of the operation. It:

🕵️ The Checks

There are 11 specialized agents, each looking for a specific type of problem:

  1. Form Labels (Do inputs have labels?)
  2. Image Alt Text (Do images have descriptions?)
  3. Headings (Is the document structure logical?)
  4. Color Contrast (Is text readable?)
  5. …and 7 others.

📍 View Detector

This smart component figures out where the error came from.

⚡ Performance System

To keep your tests fast, the gem uses:


How Static Scanning Works (Simplified)

The static scanner is a “pipeline” that transforms your Ruby code into something we can test.

  1. Watch: It watches your file system for changes.
  2. Extract: When you save a file, it pulls out the HTML and standardizes Rails helpers (like converting link_to to <a href...>).
  3. Test: It runs the standard accessibility checks on this “virtual” page.
  4. Report: If it finds issues, it maps them back to the line number in your original file.
graph TB
    User[You Save a File] --> Watcher[File Watcher]
    Watcher --> Extractor[HTML Extractor]
    Extractor --> Checker[Accessibility Checks]
    Checker --> Report[Error Message]
    
    style User fill:#ff6b6b
    style Checker fill:#4ecdc4
    style Report fill:#ffeaa7

ERB Template Handling and Dynamic IDs

The static scanner intelligently handles ERB templates with dynamic content, particularly for form inputs with dynamic IDs.

How Dynamic IDs Are Preserved

When the scanner encounters ERB templates with dynamic IDs, it preserves the structure instead of collapsing them:

Example ERB Template:

<% question.collection_options.each do |option| %>
  <input type="checkbox" 
         id="collection_answers_<%= question.id %>_<%= option.id %>_"
         name="collection_answers[<%= question.id %>][]" />
  <%= label_tag "collection_answers_#{question.id}_#{option.id}_", option.value %>
<% end %>

How It’s Processed:

Label Matching with Dynamic IDs

The Form Labels Check correctly matches labels to inputs with dynamic IDs:

Duplicate ID Detection

The Duplicate IDs Check intelligently handles dynamic IDs:

The Interactive Elements Check correctly handles anchor links:

Example - Valid (not flagged):

<%= link_to "Click me", "#", aria: { label: "Navigate to section" } %>

Example - Invalid (flagged):

<a href="#"></a>  <!-- No text, no aria-label, no aria-labelledby -->

Configuration & Profiles

You can change how the gem behaves using Profiles.

Configuration happens in config/accessibility.yml.


Directory Structure

Quick map of where important things live in the gem:


This architecture is designed to be invisible when it works, and helpful when it finds problems.