Add secret management guide, client-side performance monitoring, and search accessibility enhancements

- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
This commit is contained in:
pacnpal
2025-12-23 16:41:42 -05:00
parent ae31e889d7
commit edcd8f2076
155 changed files with 22046 additions and 4645 deletions

View File

@@ -0,0 +1,2 @@
# Accessibility Tests Package
# Contains automated WCAG 2.1 AA compliance tests

View File

@@ -0,0 +1,384 @@
"""
Automated WCAG 2.1 AA Compliance Tests
This module provides automated accessibility testing using axe-core via Selenium.
Tests verify WCAG 2.1 AA compliance across key pages of the ThrillWiki application.
Requirements:
- selenium
- axe-selenium-python
- Chrome browser with ChromeDriver
Installation:
pip install selenium axe-selenium-python
Usage:
python manage.py test backend.tests.accessibility
Note: These tests require a running server and browser. They are skipped if
dependencies are not installed or if running in CI without browser support.
"""
import os
import unittest
from django.test import TestCase, LiveServerTestCase, override_settings
from django.urls import reverse
from django.contrib.auth import get_user_model
# Check if selenium and axe are available
try:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
HAS_SELENIUM = True
except ImportError:
HAS_SELENIUM = False
try:
from axe_selenium_python import Axe
HAS_AXE = True
except ImportError:
HAS_AXE = False
User = get_user_model()
def skip_if_no_browser():
"""Decorator to skip tests if browser dependencies are not available."""
if not HAS_SELENIUM:
return unittest.skip("Selenium not installed")
if not HAS_AXE:
return unittest.skip("axe-selenium-python not installed")
if os.environ.get('CI') and not os.environ.get('BROWSER_TESTS'):
return unittest.skip("Browser tests disabled in CI")
return lambda func: func
class AccessibilityTestMixin:
"""Mixin providing common accessibility testing utilities."""
def run_axe_audit(self, url_name=None, url=None):
"""
Run axe accessibility audit on a page.
Args:
url_name: Django URL name to reverse
url: Full URL (alternative to url_name)
Returns:
dict: Axe results containing violations and passes
"""
if url_name:
url = f'{self.live_server_url}{reverse(url_name)}'
elif not url:
raise ValueError("Either url_name or url must be provided")
self.driver.get(url)
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
)
axe = Axe(self.driver)
axe.inject()
results = axe.run()
return results
def assert_no_critical_violations(self, results, page_name="page"):
"""
Assert no critical or serious accessibility violations.
Args:
results: Axe audit results
page_name: Name of page for error messages
"""
critical_violations = [
v for v in results.get('violations', [])
if v.get('impact') in ('critical', 'serious')
]
if critical_violations:
violation_details = "\n".join([
f"- {v['id']}: {v['description']} (impact: {v['impact']})"
for v in critical_violations
])
self.fail(
f"Critical accessibility violations found on {page_name}:\n"
f"{violation_details}"
)
def assert_wcag_aa_compliant(self, results, page_name="page"):
"""
Assert WCAG 2.1 AA compliance (no violations at all).
Args:
results: Axe audit results
page_name: Name of page for error messages
"""
violations = results.get('violations', [])
if violations:
violation_details = "\n".join([
f"- {v['id']}: {v['description']} (impact: {v['impact']})"
for v in violations
])
self.fail(
f"WCAG 2.1 AA violations found on {page_name}:\n"
f"{violation_details}"
)
@skip_if_no_browser()
@override_settings(DEBUG=True)
class WCAGComplianceTests(AccessibilityTestMixin, LiveServerTestCase):
"""
Automated WCAG 2.1 AA compliance tests for ThrillWiki.
These tests use axe-core via Selenium to automatically detect
accessibility issues across key pages.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Configure Chrome for headless testing
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--window-size=1920,1080')
try:
cls.driver = webdriver.Chrome(options=chrome_options)
cls.driver.implicitly_wait(10)
except Exception as e:
raise unittest.SkipTest(f"Chrome WebDriver not available: {e}")
@classmethod
def tearDownClass(cls):
if hasattr(cls, 'driver'):
cls.driver.quit()
super().tearDownClass()
def test_homepage_accessibility(self):
"""Test homepage WCAG 2.1 AA compliance."""
results = self.run_axe_audit(url_name='home')
self.assert_no_critical_violations(results, "homepage")
def test_park_list_accessibility(self):
"""Test park list page WCAG 2.1 AA compliance."""
results = self.run_axe_audit(url_name='parks:park_list')
self.assert_no_critical_violations(results, "park list")
def test_ride_list_accessibility(self):
"""Test ride list page WCAG 2.1 AA compliance."""
results = self.run_axe_audit(url_name='rides:global_ride_list')
self.assert_no_critical_violations(results, "ride list")
def test_manufacturer_list_accessibility(self):
"""Test manufacturer list page WCAG 2.1 AA compliance."""
results = self.run_axe_audit(url_name='rides:manufacturer_list')
self.assert_no_critical_violations(results, "manufacturer list")
def test_login_page_accessibility(self):
"""Test login page WCAG 2.1 AA compliance."""
results = self.run_axe_audit(url_name='account_login')
self.assert_no_critical_violations(results, "login page")
def test_signup_page_accessibility(self):
"""Test signup page WCAG 2.1 AA compliance."""
results = self.run_axe_audit(url_name='account_signup')
self.assert_no_critical_violations(results, "signup page")
class HTMLAccessibilityTests(TestCase):
"""
Unit tests for accessibility features in rendered HTML.
These tests don't require a browser and verify that templates
render correct accessibility attributes.
"""
def test_homepage_has_main_landmark(self):
"""Verify homepage has a main landmark."""
response = self.client.get(reverse('home'))
self.assertContains(response, '<main')
def test_homepage_has_skip_link(self):
"""Verify homepage has skip to content link."""
response = self.client.get(reverse('home'))
# Skip link should be present
self.assertContains(response, 'skip', msg_prefix="Should have skip link")
def test_navigation_has_aria_labels(self):
"""Verify navigation elements have ARIA labels."""
response = self.client.get(reverse('home'))
# Header should have aria-label or be wrapped in nav
self.assertTrue(
b'aria-label' in response.content or b'<nav' in response.content,
"Navigation should have ARIA attributes"
)
def test_images_have_alt_text(self):
"""Verify images have alt attributes."""
response = self.client.get(reverse('home'))
content = response.content.decode('utf-8')
# Find all img tags
import re
img_tags = re.findall(r'<img[^>]*>', content)
for img in img_tags:
self.assertIn(
'alt=',
img,
f"Image missing alt attribute: {img[:100]}"
)
def test_form_fields_have_labels(self):
"""Verify form fields have associated labels."""
response = self.client.get(reverse('account_login'))
content = response.content.decode('utf-8')
# Find input elements (excluding hidden and submit)
import re
inputs = re.findall(
r'<input[^>]*type=["\'](?!hidden|submit)[^"\']*["\'][^>]*>',
content
)
for inp in inputs:
# Each input should have id attribute for label association
self.assertTrue(
'id=' in inp or 'aria-label' in inp,
f"Input missing id or aria-label: {inp[:100]}"
)
def test_buttons_are_accessible(self):
"""Verify buttons have accessible names."""
response = self.client.get(reverse('home'))
content = response.content.decode('utf-8')
import re
# Find button elements
buttons = re.findall(r'<button[^>]*>.*?</button>', content, re.DOTALL)
for button in buttons:
# Button should have text content or aria-label
has_text = bool(re.search(r'>([^<]+)<', button))
has_aria = 'aria-label' in button
self.assertTrue(
has_text or has_aria,
f"Button missing accessible name: {button[:100]}"
)
class KeyboardNavigationTests(TestCase):
"""
Tests for keyboard navigation requirements.
These tests verify that interactive elements are keyboard accessible.
"""
def test_interactive_elements_are_focusable(self):
"""Verify interactive elements don't have tabindex=-1."""
response = self.client.get(reverse('home'))
content = response.content.decode('utf-8')
# Links and buttons should not have tabindex=-1 (unless intentionally hidden)
import re
problematic = re.findall(
r'<(a|button)[^>]*tabindex=["\']?-1["\']?[^>]*>',
content
)
# Filter out elements that are legitimately hidden
for elem in problematic:
self.assertIn(
'aria-hidden',
elem,
f"Interactive element has tabindex=-1 without aria-hidden: {elem}"
)
def test_modals_have_escape_handler(self):
"""Verify modal templates include escape key handling."""
from django.template.loader import get_template
template = get_template('components/modals/modal_inner.html')
source = template.template.source
self.assertIn(
'escape',
source.lower(),
"Modal should handle Escape key"
)
def test_dropdowns_have_keyboard_support(self):
"""Verify dropdown menus support keyboard navigation."""
response = self.client.get(reverse('home'))
content = response.content.decode('utf-8')
# Check for aria-expanded on dropdown triggers
if 'dropdown' in content.lower() or 'menu' in content.lower():
self.assertIn(
'aria-expanded',
content,
"Dropdown should have aria-expanded attribute"
)
class ARIAAttributeTests(TestCase):
"""
Tests for correct ARIA attribute usage.
"""
def test_modal_has_dialog_role(self):
"""Verify modal has role=dialog."""
from django.template.loader import get_template
template = get_template('components/modals/modal_inner.html')
source = template.template.source
self.assertIn(
'role="dialog"',
source,
"Modal should have role=dialog"
)
def test_modal_has_aria_modal(self):
"""Verify modal has aria-modal=true."""
from django.template.loader import get_template
template = get_template('components/modals/modal_inner.html')
source = template.template.source
self.assertIn(
'aria-modal="true"',
source,
"Modal should have aria-modal=true"
)
def test_breadcrumb_has_navigation_role(self):
"""Verify breadcrumbs use nav element with aria-label."""
from django.template.loader import get_template
template = get_template('components/navigation/breadcrumbs.html')
source = template.template.source
self.assertIn(
'<nav',
source,
"Breadcrumbs should use nav element"
)
self.assertIn(
'aria-label',
source,
"Breadcrumbs nav should have aria-label"
)