You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/spec/support/accessibility_helpers.rb

143 lines
4.7 KiB

# frozen_string_literal: true
# Accessibility testing helpers for WCAG 2.2 Level AA compliance
module AccessibilityHelpers
# Check if an element has proper ARIA label or text content
def expect_accessible_name(selector, expected_name = nil)
element = page.find(selector)
# Check for accessible name via aria-label, aria-labelledby, or text content
accessible_name = element['aria-label'] ||
find_aria_labelledby_text(element) ||
element.text.strip
if expected_name
expect(accessible_name).to eq(expected_name)
else
expect(accessible_name).not_to be_empty
end
end
# Check if images have alt text
def expect_images_have_alt_text
images_without_alt = page.all('img:not([alt])')
expect(images_without_alt).to be_empty,
"Found #{images_without_alt.count} images without alt attributes"
end
# Check if form inputs have associated labels or aria-label
def expect_form_inputs_labeled
unlabeled_inputs = page.all('input, select, textarea').select do |input|
next if input['type'] == 'hidden'
next if input['type'] == 'submit'
next if input['type'] == 'button'
# Check for label association, aria-label, or aria-labelledby
has_label = input['id'] && page.has_css?("label[for='#{input['id']}']")
has_aria = input['aria-label'] || input['aria-labelledby']
!has_label && !has_aria
end
expect(unlabeled_inputs).to be_empty,
"Found #{unlabeled_inputs.count} unlabeled form inputs"
end
# Check if interactive elements are keyboard accessible
def expect_keyboard_accessible(selector)
element = page.find(selector)
# Interactive elements should be focusable
expect(element['tabindex'].to_i).to be >= 0
end
# Check for proper semantic landmarks
def expect_semantic_landmarks
expect(page).to have_css('main, [role="main"]'),
'Page should have a main landmark'
expect(page).to have_css('nav, [role="navigation"]'),
'Page should have a navigation landmark'
end
# Check if buttons have accessible names
def expect_buttons_have_names
unnamed_buttons = page.all('button').select do |button|
text = button.text.strip
aria_label = button['aria-label']
aria_labelledby = button['aria-labelledby']
text.empty? && !aria_label && !aria_labelledby
end
expect(unnamed_buttons).to be_empty,
"Found #{unnamed_buttons.count} buttons without accessible names"
end
# Check color contrast ratio (simplified check for common patterns)
def expect_sufficient_contrast(selector)
element = page.find(selector)
computed_style = page.evaluate_script(
"window.getComputedStyle(document.querySelector('#{selector}'))"
)
# This is a basic check - proper contrast testing requires color parsing
color = computed_style['color']
background = computed_style['background-color']
expect(color).not_to eq('rgb(209, 213, 219)'), # text-gray-300
'Text color has insufficient contrast'
end
# Check if error messages are associated with form fields
def expect_errors_associated_with_inputs(input_id, error_id)
input = page.find("##{input_id}")
expect(input['aria-describedby']).to include(error_id),
"Input ##{input_id} should reference error ##{error_id} via aria-describedby"
# Error should be announced to screen readers
error_element = page.find("##{error_id}")
expect(error_element['role']).to eq('alert').or eq(nil)
end
# Check if modal dialogs are properly configured
def expect_accessible_modal(selector)
modal = page.find(selector)
expect(modal['role']).to eq('dialog'),
'Modal should have role="dialog"'
expect(modal['aria-modal']).to eq('true'),
'Modal should have aria-modal="true"'
expect(modal['aria-labelledby']).not_to be_nil,
'Modal should have aria-labelledby referencing its title'
end
# Check if focus is trapped within modal
def expect_focus_trap(modal_selector)
within(modal_selector) do
focusable = page.all('a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])')
expect(focusable.count).to be > 0, 'Modal should contain focusable elements'
end
end
# Check for skip navigation link
def expect_skip_navigation_link
expect(page).to have_css('a[href="#main-content"], a[href="#main"]'),
'Page should have a skip navigation link'
end
private
def find_aria_labelledby_text(element)
labelledby_id = element['aria-labelledby']
return nil unless labelledby_id
label_element = page.find("##{labelledby_id}", visible: false)
label_element&.text&.strip
end
end
# Include helpers in system specs
RSpec.configure do |config|
config.include AccessibilityHelpers, type: :system
end