rails_accessibility_testing 1.5.7 → 1.5.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8fee81eff828caeb719396eaf7a2e63d282b71b5490ffe192ec6600715fee9a1
4
- data.tar.gz: 418f0068868d27e49c6916ff81da722bda53e6cad97504f43a36f3b98b64ba21
3
+ metadata.gz: 9f8824e08bc8db71c7a6598998b44c6b39826f367398c8c98a1ef85366a044e2
4
+ data.tar.gz: bd9337c4b6b89deae25b1adfc9dbec92645c934703e930d5f5048c71a59117b5
5
5
  SHA512:
6
- metadata.gz: 16dd572d50a671dc94d0cfaf62b0cc6988dd009d929e9e9f98ad38df482b1653d99c3d3d80b1bf3fc956a24d94ce6bb64853d184d130f1eb18181bc031a4df2a
7
- data.tar.gz: 6cbd86e0ea02ba00c5d297ffe44c941366f17592cc81b3b02e24ca65660bde2c784eb0f024411da9bbfd3d9598d168de141ae0df19dbe225f14de83766f94c7c
6
+ metadata.gz: cd9fdfced6342b861ae5d1db4ee852b4c566c9572d62dac463fdc0d7566d37ef1201201c36c3af4d45393d24af7f0a504f76f7eaf25322e7025a577eeb3d94dc
7
+ data.tar.gz: a2672850fd85a29fb3a304243f1367215c282a34e63df58f5878d274dd31176465afaad23734699124bd6649032b68ff578f0d6a1ae4b6ec853d2f4ffd7ef23c
data/CHANGELOG.md CHANGED
@@ -5,6 +5,88 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.5.10] - 2024-12-01
9
+
10
+ ### Changed
11
+ - **Configuration flag renamed**: The `enabled` flag has been renamed to `accessibility_enabled` for better clarity. The old `enabled` key is still supported for backward compatibility.
12
+
13
+ ### Added
14
+ - **Composed Page Scanning**: The static scanner now analyzes complete page compositions (layout + view + partials) for page-level accessibility checks, eliminating false positives for heading hierarchy, ARIA landmarks, duplicate IDs, and heading issues.
15
+ - **View Composition Builder**: New comprehensive system that traces the complete page structure by finding all partials recursively across all directories in `app/views`.
16
+ - **Exhaustive Partial Detection**: Enhanced partial detection that finds partials in any subdirectory, not just specific folders. Works for deeply nested namespaces (e.g., `collections/collection_questions`).
17
+ - **ERB Content Detection**: Improved handling of ERB expressions (`<%= ... %>`) to prevent false positives for empty headings and missing accessible names on buttons.
18
+
19
+ ### Changed
20
+ - **Heading Hierarchy Checks**: Now performed on the complete composed page instead of individual files, preventing false positives when H1 is in layout or partials.
21
+ - **ARIA Landmarks Checks**: Now checks for `<main>` landmark across the entire composed page.
22
+ - **Duplicate ID Checks**: Now checks for duplicate IDs across the complete page composition.
23
+ - **Empty Heading Checks**: Now checks for empty headings across the complete composed page.
24
+ - **Partial Search**: Enhanced to traverse ALL folders in `app/views` recursively using `Dir.glob`, making it a general solution that works for any folder structure.
25
+
26
+ ### Fixed
27
+ - Fixed false positive for "Page missing MAIN landmark" when `<main>` is in the layout file.
28
+ - Fixed false positive for "Page has h2 but no h1 heading" when H1 is in a partial rendered via `render @model`.
29
+ - Fixed false positive for "Heading hierarchy skipped (h0 to h2)" when first heading is h2.
30
+ - Fixed false positive for "Duplicate ID 'ERB_CONTENT'" by filtering out ERB placeholder strings.
31
+ - Fixed partial detection for Rails shorthand patterns (`render @model`, `render collection: @models`).
32
+ - Fixed namespaced partial path resolution (e.g., `layouts/_advance_search`).
33
+ - Fixed path normalization to handle both relative and absolute paths consistently.
34
+
35
+ ### Performance
36
+ - Optimized exhaustive directory search to return first match found instead of checking all matches.
37
+
38
+ ## [Unreleased]
39
+
40
+ ### Added
41
+ - **Composed Page Scanning**: The static scanner now analyzes complete page compositions (layout + view + partials) for page-level accessibility checks, eliminating false positives for heading hierarchy, ARIA landmarks, duplicate IDs, and heading issues.
42
+ - **View Composition Builder**: New comprehensive system that traces the complete page structure by finding all partials recursively across all directories in `app/views`.
43
+ - **Exhaustive Partial Detection**: Enhanced partial detection that finds partials in any subdirectory, not just specific folders. Works for deeply nested namespaces (e.g., `collections/collection_questions`).
44
+ - **ERB Content Detection**: Improved handling of ERB expressions (`<%= ... %>`) to prevent false positives for empty headings and missing accessible names on buttons.
45
+
46
+ ### Changed
47
+ - **Heading Hierarchy Checks**: Now performed on the complete composed page instead of individual files, preventing false positives when H1 is in layout or partials.
48
+ - **ARIA Landmarks Checks**: Now checks for `<main>` landmark across the entire composed page.
49
+ - **Duplicate ID Checks**: Now checks for duplicate IDs across the complete page composition.
50
+ - **Empty Heading Checks**: Now checks for empty headings across the complete composed page.
51
+ - **Partial Search**: Enhanced to traverse ALL folders in `app/views` recursively using `Dir.glob`, making it a general solution that works for any folder structure.
52
+
53
+ ### Fixed
54
+ - Fixed false positive for "Page missing MAIN landmark" when `<main>` is in the layout file.
55
+ - Fixed false positive for "Page has h2 but no h1 heading" when H1 is in a partial rendered via `render @model`.
56
+ - Fixed false positive for "Heading hierarchy skipped (h0 to h2)" when first heading is h2.
57
+ - Fixed false positive for "Duplicate ID 'ERB_CONTENT'" by filtering out ERB placeholder strings.
58
+ - Fixed partial detection for Rails shorthand patterns (`render @model`, `render collection: @models`).
59
+ - Fixed namespaced partial path resolution (e.g., `layouts/_advance_search`).
60
+ - Fixed path normalization to handle both relative and absolute paths consistently.
61
+
62
+ ### Performance
63
+ - Optimized exhaustive directory search to return first match found instead of checking all matches.
64
+
65
+ ## [1.5.9] - 2024-12-01
66
+
67
+ ### Added
68
+ - **Force option for manual checks**: Added `force: true` parameter to `check_comprehensive_accessibility` and `check_basic_accessibility` to bypass `enabled: false` setting
69
+ - **Manual check flexibility**: You can now force manual checks to run even when accessibility checks are globally disabled
70
+
71
+ ### Improved
72
+ - **Manual check documentation**: Clarified that manual checks can be run separately in RSpec specs regardless of configuration
73
+
74
+ ## [1.5.8] - 2024-12-01
75
+
76
+ ### Fixed
77
+ - **ERB template detection**: Fixed false positives for empty headings and missing accessible names when ERB output tags (`<%= ... %>`) are present
78
+ - **Static scanning**: ERB tags are now replaced with placeholder text so checks can detect that content will be present at runtime
79
+ - **Scanner process management**: Static and live scanners now stay alive (sleep loop) when `enabled: false` instead of exiting, preventing Foreman from killing other processes
80
+
81
+ ### Added
82
+ - **Global enable/disable flag**: Added `enabled` configuration option in `accessibility.yml` to completely disable all accessibility checks (manual and automatic)
83
+ - **ERB content detection**: `ErbExtractor` now replaces `<%= ... %>` with `"ERB_CONTENT"` placeholder instead of removing it
84
+ - **ERB-aware checks**: `HeadingCheck` and `InteractiveElementsCheck` now detect ERB placeholders and skip empty checks
85
+
86
+ ### Improved
87
+ - **Static scanner behavior**: When `enabled: false`, scanner shows message and keeps running instead of exiting
88
+ - **Live scanner behavior**: When `enabled: false`, scanner shows message and keeps running instead of exiting
89
+
8
90
  ## [1.5.7] - 2024-12-01
9
91
 
10
92
  ### Fixed
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  **The RSpec + RuboCop of accessibility for Rails. Catch WCAG violations before they reach production.**
9
9
 
10
- **Current Version:** 1.5.5
10
+ **Current Version:** 1.5.9
11
11
 
12
12
  📖 **[📚 Full Documentation](https://rayraycodes.github.io/rails-accessibility-testing/)** | [💻 GitHub](https://github.com/rayraycodes/rails-accessibility-testing) | [💎 RubyGems](https://rubygems.org/gems/rails_accessibility_testing)
13
13
 
@@ -51,6 +51,13 @@ Rails Accessibility Testing fills a critical gap in the Rails testing ecosystem.
51
51
  - **Accurate error counting**: Properly tracks and displays error/warning counts
52
52
  - **Persistent output**: Errors stay visible in terminal (no clearing)
53
53
 
54
+ #### 🔍 Composed Page Scanning (NEW in 1.5.9+)
55
+ - **Complete page analysis**: Analyzes full page composition (layout + view + partials) for page-level checks
56
+ - **Eliminates false positives**: No more false positives when H1 is in layout or partials
57
+ - **Exhaustive partial finding**: Traverses ALL folders recursively to find all partials
58
+ - **Works for any structure**: General solution that works for any Rails folder structure (collections, items, profiles, loan_requests, etc.)
59
+ - **Page-level checks**: Heading hierarchy, ARIA landmarks, duplicate IDs, and empty headings checked across complete page
60
+
54
61
  #### 🔍 Smart View File Detection
55
62
  - **Intelligent matching**: Automatically finds view files even when action names don't match
56
63
  - **Controller directory scanning**: Searches all view files to find the correct template
@@ -61,6 +68,7 @@ Rails Accessibility Testing fills a critical gap in the Rails testing ecosystem.
61
68
  - **Optimized DOM queries**: Faster image alt checks without JavaScript evaluation
62
69
  - **Removed delays**: Eliminated unnecessary sleep calls in live scanner
63
70
  - **Efficient scanning**: ~25-30% faster page scans
71
+ - **Optimized directory search**: Returns first match found instead of checking all matches
64
72
 
65
73
  #### 🎨 Enhanced Developer Experience
66
74
  - **Real-time progress**: Step-by-step feedback during accessibility checks
@@ -3,7 +3,45 @@ layout: default
3
3
  title: Architecture
4
4
  ---
5
5
 
6
- # Architecture Overview
6
+ # Architecture
7
+
8
+ ## Composed Page Scanning
9
+
10
+ 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.
11
+
12
+ ### View Composition Builder
13
+
14
+ The `ViewCompositionBuilder` class traces the complete page structure:
15
+
16
+ 1. **Finds Layout File**: Identifies the layout file (defaults to `application.html.erb`)
17
+ 2. **Finds View File**: The main view file being rendered
18
+ 3. **Recursively Finds Partials**: Discovers all partials rendered in the view, including:
19
+ - Partials in the same directory
20
+ - Partials in `layouts/`, `shared/`, `application/`
21
+ - Partials in any subdirectory (exhaustive search)
22
+ - Nested partials (partials within partials)
23
+
24
+ ### Partial Detection
25
+
26
+ The gem detects all Rails render patterns:
27
+ - `render 'partial'`
28
+ - `render partial: 'partial'`
29
+ - `render @model` (Rails shorthand)
30
+ - `render collection: @models`
31
+ - `render partial: 'item', collection: @items`
32
+ - `render partial: 'form', locals: {...}`
33
+
34
+ ### Exhaustive Folder Traversal
35
+
36
+ The partial search traverses ALL folders in `app/views` recursively using `Dir.glob`, ensuring partials are found regardless of their location:
37
+ - `app/views/collections/`
38
+ - `app/views/collections/collection_questions/`
39
+ - `app/views/items/`
40
+ - `app/views/profiles/`
41
+ - `app/views/loan_requests/`
42
+ - Any other nested structure
43
+
44
+ This makes it a general solution that works for any Rails application structure. Overview
7
45
 
8
46
  This guide explains how Rails Accessibility Testing works under the hood in simple terms.
9
47
 
@@ -144,12 +144,12 @@ Rails Accessibility Testing runs **11 comprehensive checks** automatically. Thes
144
144
  1. **Form Labels** - All form inputs have associated labels
145
145
  2. **Image Alt Text** - All images have descriptive alt attributes
146
146
  3. **Interactive Elements** - Buttons, links have accessible names
147
- 4. **Heading Hierarchy** - Proper h1-h6 structure without skipping levels
147
+ 4. **Heading Hierarchy** - Proper h1-h6 structure without skipping levels (checked across complete page: layout + view + partials)
148
148
  5. **Keyboard Accessibility** - All interactive elements are keyboard accessible
149
- 6. **ARIA Landmarks** - Proper use of ARIA landmark roles
149
+ 6. **ARIA Landmarks** - Proper use of ARIA landmark roles (checked across complete page: layout + view + partials)
150
150
  7. **Form Error Associations** - Form errors are properly linked to form fields
151
151
  8. **Table Structure** - Tables have proper headers
152
- 9. **Duplicate IDs** - No duplicate ID attributes
152
+ 9. **Duplicate IDs** - No duplicate ID attributes (checked across complete page: layout + view + partials)
153
153
  10. **Skip Links** - Skip navigation links present
154
154
  11. **Color Contrast** - Text meets WCAG contrast requirements (optional, disabled by default)
155
155
 
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.5.5
10
+ **Version:** 1.5.9
11
11
 
12
12
  Rails Accessibility Testing is a comprehensive, opinionated but configurable gem that makes accessibility testing as natural as unit testing. It integrates seamlessly into your Rails workflow, catching accessibility issues as you code—not after deployment.
13
13
 
@@ -29,6 +29,28 @@ begin
29
29
  unless defined?(RailsAccessibilityTesting::AccessibilityHelper)
30
30
  raise LoadError, "RailsAccessibilityTesting::AccessibilityHelper not found after requiring gem"
31
31
  end
32
+
33
+ # Check if accessibility checks are globally disabled
34
+ begin
35
+ require 'rails_accessibility_testing/config/yaml_loader'
36
+ profile = defined?(Rails) && Rails.env.test? ? :test : :development
37
+ config = RailsAccessibilityTesting::Config::YamlLoader.load(profile: profile)
38
+ # Support both 'accessibility_enabled' (new) and 'enabled' (legacy) for backward compatibility
39
+ enabled = config.fetch('accessibility_enabled', config.fetch('enabled', true))
40
+ unless enabled
41
+ puts "⏸️ Accessibility checks are disabled (accessibility_enabled: false in config/accessibility.yml)"
42
+ puts " Set accessibility_enabled: true to enable accessibility scanning"
43
+ puts " Scanner process will remain running but will not scan files"
44
+ puts ""
45
+ # Keep process alive so Foreman doesn't kill other processes
46
+ # Sleep indefinitely until interrupted
47
+ loop do
48
+ sleep 60
49
+ end
50
+ end
51
+ rescue StandardError => e
52
+ # If config can't be loaded, continue (assume enabled)
53
+ end
32
54
  rescue LoadError => e
33
55
  $stderr.puts "❌ Error loading rails_accessibility_testing gem: #{e.message}"
34
56
  $stderr.puts " Make sure the gem is installed: bundle install"
@@ -23,6 +23,28 @@ begin
23
23
  require 'rails_accessibility_testing'
24
24
  require 'rails_accessibility_testing/static_file_scanner'
25
25
  require 'rails_accessibility_testing/file_change_tracker'
26
+
27
+ # Check if accessibility checks are globally disabled
28
+ begin
29
+ require 'rails_accessibility_testing/config/yaml_loader'
30
+ profile = defined?(Rails) && Rails.env.test? ? :test : :development
31
+ config = RailsAccessibilityTesting::Config::YamlLoader.load(profile: profile)
32
+ # Support both 'accessibility_enabled' (new) and 'enabled' (legacy) for backward compatibility
33
+ enabled = config.fetch('accessibility_enabled', config.fetch('enabled', true))
34
+ unless enabled
35
+ puts "⏸️ Accessibility checks are disabled (accessibility_enabled: false in config/accessibility.yml)"
36
+ puts " Set accessibility_enabled: true to enable accessibility scanning"
37
+ puts " Scanner process will remain running but will not scan files"
38
+ puts ""
39
+ # Keep process alive so Foreman doesn't kill other processes
40
+ # Sleep indefinitely until interrupted
41
+ loop do
42
+ sleep 60
43
+ end
44
+ end
45
+ rescue StandardError => e
46
+ # If config can't be loaded, continue (assume enabled)
47
+ end
26
48
  rescue LoadError => e
27
49
  $stderr.puts "❌ Error loading rails_accessibility_testing gem: #{e.message}"
28
50
  $stderr.puts " Make sure the gem is installed: bundle install"
@@ -3,6 +3,12 @@
3
3
  # This file configures accessibility checks for your Rails application.
4
4
  # See https://github.com/your-org/rails-a11y for full documentation.
5
5
 
6
+ # Global enable/disable flag for all accessibility checks
7
+ # Set to false to completely disable all accessibility checks (manual and automatic)
8
+ # When false, check_comprehensive_accessibility and automatic checks will be skipped
9
+ # Default: true
10
+ accessibility_enabled: true
11
+
6
12
  # WCAG compliance level (A, AA, AAA)
7
13
  wcag_level: AA
8
14
 
@@ -142,7 +142,7 @@ RSpec.describe 'All Pages Accessibility', type: :system do
142
142
  puts format_static_errors(errors, warnings)
143
143
 
144
144
  if errors.any?
145
- raise "Found #{errors.length} accessibility error#{'s' if errors.length != 1} in #{view_file}"
145
+ puts "Found #{errors.length} accessibility error#{'s' if errors.length != 1} in #{view_file}"
146
146
  end
147
147
  else
148
148
  puts "✅ #{view_file}: No errors found"
@@ -46,14 +46,34 @@ module RailsAccessibilityTesting
46
46
  # render partial: "partial_name"
47
47
  # render 'path/to/partial'
48
48
  # render partial: 'path/to/partial'
49
+ # render @model (Rails shorthand - renders _model.html.erb)
50
+ # render collection: @models (renders _model.html.erb for each)
51
+ # render partial: 'partial', locals: {...}
52
+ # render partial: 'partial', collection: @items
49
53
  # <%= render 'partial_name' %>
50
54
  # <%= render partial: 'partial_name' %>
55
+ # <%= render @model %>
56
+ # <%= render collection: @models %>
51
57
 
52
58
  patterns = [
59
+ # Standard partial renders (with or without partial: keyword)
53
60
  /render\s+(?:partial:\s*)?['"]([^'"]+)['"]/,
54
61
  /render\s+(?:partial:\s*)?:(\w+)/,
55
62
  /<%=?\s*render\s+(?:partial:\s*)?['"]([^'"]+)['"]/,
56
- /<%=?\s*render\s+(?:partial:\s*)?:(\w+)/
63
+ /<%=?\s*render\s+(?:partial:\s*)?:(\w+)/,
64
+ # Rails shorthand: render @model (renders _model.html.erb)
65
+ /render\s+@(\w+)/,
66
+ /<%=?\s*render\s+@(\w+)/,
67
+ # Collection renders: render collection: @models (renders _model.html.erb)
68
+ /render\s+collection:\s*@(\w+)/,
69
+ /<%=?\s*render\s+collection:\s*@(\w+)/,
70
+ # Partial with collection: render partial: 'item', collection: @items
71
+ /render\s+partial:\s*['"]([^'"]+)['"]\s*,\s*collection:/,
72
+ /<%=?\s*render\s+partial:\s*['"]([^'"]+)['"]\s*,\s*collection:/,
73
+ # Partial with locals: render partial: 'form', locals: {...}
74
+ # (extract just the partial name, ignore locals)
75
+ /render\s+partial:\s*['"]([^'"]+)['"]\s*,\s*locals:/,
76
+ /<%=?\s*render\s+partial:\s*['"]([^'"]+)['"]\s*,\s*locals:/
57
77
  ]
58
78
 
59
79
  patterns.each do |pattern|
@@ -77,6 +97,23 @@ module RailsAccessibilityTesting
77
97
  end
78
98
  end
79
99
 
100
+ # Also check for Rails shorthand: render @model (renders _model.html.erb)
101
+ # This is separate because it needs special handling
102
+ content.scan(/render\s+@(\w+)/) do |match|
103
+ model_name = match[0]
104
+ partial_name = model_name.underscore
105
+ partial_name = "_#{partial_name}" unless partial_name.start_with?('_')
106
+ partials << partial_name unless partials.include?(partial_name)
107
+ end
108
+
109
+ # Check for collection renders: render collection: @models
110
+ content.scan(/render\s+collection:\s*@(\w+)/) do |match|
111
+ model_name = match[0]
112
+ partial_name = model_name.underscore.singularize # collection uses singular
113
+ partial_name = "_#{partial_name}" unless partial_name.start_with?('_')
114
+ partials << partial_name unless partials.include?(partial_name)
115
+ end
116
+
80
117
  partials
81
118
  end
82
119
 
@@ -210,7 +247,12 @@ module RailsAccessibilityTesting
210
247
  end
211
248
 
212
249
  # Basic accessibility check - runs 5 basic checks
213
- def check_basic_accessibility
250
+ # @param force [Boolean] If true, bypasses the accessibility_enabled: false setting (default: false)
251
+ def check_basic_accessibility(force: false)
252
+ # Check if accessibility checks are globally disabled
253
+ # Manual calls can use force: true to bypass the accessibility_enabled: false setting
254
+ return if !force && accessibility_disabled?
255
+
214
256
  @accessibility_errors ||= []
215
257
  @accessibility_warnings ||= []
216
258
 
@@ -241,8 +283,15 @@ module RailsAccessibilityTesting
241
283
 
242
284
  # Full comprehensive check - runs all 11 checks including advanced
243
285
  # Uses the RuleEngine and checks from the checks/ folder for consistency
286
+ # @param force [Boolean] If true, bypasses the accessibility_enabled: false setting (default: false)
244
287
  # @return [Hash] Hash with :errors and :warnings counts
245
- def check_comprehensive_accessibility
288
+ def check_comprehensive_accessibility(force: false)
289
+ # Check if accessibility checks are globally disabled - do this FIRST before any output
290
+ # Manual calls can use force: true to bypass the accessibility_enabled: false setting
291
+ if !force && accessibility_disabled?
292
+ return { errors: 0, warnings: 0, page_context: {} }
293
+ end
294
+
246
295
  # Note: Page scanning cache is disabled for RSpec tests to ensure accurate error reporting
247
296
  # The cache is only used in live scanner to avoid duplicate scans
248
297
 
@@ -433,6 +482,22 @@ module RailsAccessibilityTesting
433
482
 
434
483
  private
435
484
 
485
+ # Check if accessibility checks are globally disabled via config
486
+ def accessibility_disabled?
487
+ begin
488
+ require 'rails_accessibility_testing/config/yaml_loader'
489
+ profile = defined?(Rails) && Rails.env.test? ? :test : :development
490
+ config = RailsAccessibilityTesting::Config::YamlLoader.load(profile: profile)
491
+ # Support both 'accessibility_enabled' (new) and 'enabled' (legacy) for backward compatibility
492
+ enabled = config.fetch('accessibility_enabled', config.fetch('enabled', true)) # Default to enabled if not specified
493
+ disabled = !enabled # Return true if disabled
494
+ disabled
495
+ rescue StandardError => e
496
+ # If config can't be loaded, assume enabled
497
+ false
498
+ end
499
+ end
500
+
436
501
  # Format timestamp for terminal output (shorter, more readable)
437
502
  def format_timestamp_for_terminal
438
503
  # Use just time for same-day reports, or full date if different day
@@ -23,7 +23,7 @@ module RailsAccessibilityTesting
23
23
  return violations # Warning only, not error
24
24
  end
25
25
 
26
- # Check 1: Missing H1
26
+ # Check 1: Missing H1 (page-level check - checks complete composed page)
27
27
  h1_count = headings.count { |h| h.tag_name == 'h1' }
28
28
  first_heading = headings.first
29
29
  first_heading_level = first_heading ? first_heading.tag_name[1].to_i : nil
@@ -32,30 +32,35 @@ module RailsAccessibilityTesting
32
32
  # If the first heading is h2 or higher, provide a more specific message
33
33
  if first_heading_level && first_heading_level >= 2
34
34
  element_ctx = element_context(first_heading)
35
+ # Mark as page-level check - H1 might be in layout or partial
36
+ element_ctx[:page_level_check] = true
37
+ element_ctx[:check_type] = 'heading_hierarchy'
35
38
  violations << violation(
36
- message: "Page has h#{first_heading_level} but no h1 heading",
39
+ message: "Page has h#{first_heading_level} but no h1 heading (checked complete page: layout + view + partials)",
37
40
  element_context: element_ctx,
38
41
  wcag_reference: "1.3.1",
39
- remediation: "Add an <h1> heading before the first h#{first_heading_level}:\n\n<h1>Main Page Title</h1>\n<h#{first_heading_level}>#{first_heading.text}</h#{first_heading_level}>"
42
+ remediation: "Add an <h1> heading to your page. The H1 can be in the layout, view, or a partial:\n\n<h1>Main Page Title</h1>\n<h#{first_heading_level}>#{first_heading.text}</h#{first_heading_level}>"
40
43
  )
41
44
  else
42
45
  violations << violation(
43
- message: "Page missing H1 heading",
44
- element_context: { tag: 'page', text: 'Page has no H1 heading' },
46
+ message: "Page missing H1 heading (checked complete page: layout + view + partials)",
47
+ element_context: { tag: 'page', text: 'Page has no H1 heading', page_level_check: true, check_type: 'heading_hierarchy' },
45
48
  wcag_reference: "1.3.1",
46
- remediation: "Add an <h1> heading to your page:\n\n<h1>Main Page Title</h1>"
49
+ remediation: "Add an <h1> heading to your page. The H1 can be in the layout, view, or a partial:\n\n<h1>Main Page Title</h1>"
47
50
  )
48
51
  end
49
52
  end
50
53
 
51
- # Check 2: Multiple H1s (WCAG 1.3.1)
54
+ # Check 2: Multiple H1s (WCAG 1.3.1) - page-level check
52
55
  if h1_count > 1
53
56
  # Report error for each h1 after the first one
54
57
  h1_elements = headings.select { |h| h.tag_name == 'h1' }
55
58
  h1_elements[1..-1].each do |h1|
56
59
  element_ctx = element_context(h1)
60
+ element_ctx[:page_level_check] = true
61
+ element_ctx[:check_type] = 'heading_hierarchy'
57
62
  violations << violation(
58
- message: "Page has multiple h1 headings (#{h1_count} total) - only one h1 should be used per page",
63
+ message: "Page has multiple h1 headings (#{h1_count} total) - only one h1 should be used per page (checked complete page: layout + view + partials)",
59
64
  element_context: element_ctx,
60
65
  wcag_reference: "1.3.1",
61
66
  remediation: "Use only one <h1> per page. Convert additional h1s to h2 or lower:\n\n<h1>Main Title</h1>\n<h2>Section Title</h2>"
@@ -63,14 +68,16 @@ module RailsAccessibilityTesting
63
68
  end
64
69
  end
65
70
 
66
- # Check 3: Heading hierarchy skipped levels (WCAG 1.3.1)
71
+ # Check 3: Heading hierarchy skipped levels (WCAG 1.3.1) - page-level check
67
72
  previous_level = 0
68
73
  headings.each do |heading|
69
74
  current_level = heading.tag_name[1].to_i
70
75
  if current_level > previous_level + 1
71
76
  element_ctx = element_context(heading)
77
+ element_ctx[:page_level_check] = true
78
+ element_ctx[:check_type] = 'heading_hierarchy'
72
79
  violations << violation(
73
- message: "Heading hierarchy skipped (h#{previous_level} to h#{current_level})",
80
+ message: "Heading hierarchy skipped (h#{previous_level} to h#{current_level}) - checked complete page: layout + view + partials",
74
81
  element_context: element_ctx,
75
82
  wcag_reference: "1.3.1",
76
83
  remediation: "Fix the heading hierarchy. Don't skip levels. Use h#{previous_level + 1} instead of h#{current_level}."
@@ -82,8 +89,14 @@ module RailsAccessibilityTesting
82
89
  # Check 4: Empty headings (WCAG 4.1.2)
83
90
  headings.each do |heading|
84
91
  heading_text = heading.text.strip
92
+
93
+ # Check if heading contains ERB placeholder (for static scanning)
94
+ # ErbExtractor replaces <%= ... %> with "ERB_CONTENT" so we can detect it
95
+ has_erb_content = heading_text.include?('ERB_CONTENT')
96
+
85
97
  # Check if heading is empty or only contains whitespace/formatting
86
- if heading_text.empty? || heading_text.match?(/^\s*$/)
98
+ # Skip if it contains ERB content (will be populated at runtime)
99
+ if (heading_text.empty? || heading_text.match?(/^\s*$/)) && !has_erb_content
87
100
  element_ctx = element_context(heading)
88
101
  violations << violation(
89
102
  message: "Empty heading detected (<#{heading.tag_name}>) - headings must have accessible text",
@@ -46,16 +46,24 @@ module RailsAccessibilityTesting
46
46
  aria_labelledby = element[:"aria-labelledby"]
47
47
  title = element[:title]
48
48
 
49
+ # Check if element contains ERB placeholder (for static scanning)
50
+ # ErbExtractor replaces <%= ... %> with "ERB_CONTENT" so we can detect it
51
+ # If text includes "ERB_CONTENT", it means there's ERB code that will produce content at runtime
52
+ has_erb_content = text.include?('ERB_CONTENT')
53
+
49
54
  # Check if element contains an image with alt text (common pattern for logo links)
50
55
  has_image_with_alt = false
51
- if text.empty?
56
+ if text.empty? && !has_erb_content
52
57
  # For static scanning, we can't easily check images within elements
53
58
  # This check works better in dynamic scanning with Capybara
54
59
  # For now, skip image checking in static mode
55
60
  has_image_with_alt = false
56
61
  end
57
62
 
58
- text_empty = text.empty?
63
+ # Text is empty only if it's actually empty AND doesn't contain ERB content
64
+ # If it contains ERB_CONTENT, it will have content at runtime, so it's not empty
65
+ text_empty = text.empty? && !has_erb_content
66
+
59
67
  aria_label_empty = aria_label.nil? || aria_label.to_s.strip.empty?
60
68
  aria_labelledby_empty = aria_labelledby.nil? || aria_labelledby.to_s.strip.empty?
61
69
  title_empty = title.nil? || title.to_s.strip.empty?