v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40) - Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard) - Ollama GPU fonctionnel - Self-healing interactif - Dashboard confiance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
426
visual_workflow_builder/test_accessibility.py
Normal file
426
visual_workflow_builder/test_accessibility.py
Normal file
@@ -0,0 +1,426 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests d'accessibilité pour le Visual Workflow Builder
|
||||
Valide les fonctionnalités ARIA, navigation clavier et support screen readers
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import requests
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
from axe_selenium_python import Axe
|
||||
|
||||
def setup_driver():
|
||||
"""Configure le driver Selenium avec les options d'accessibilité"""
|
||||
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')
|
||||
|
||||
# Options pour simuler les technologies d'assistance
|
||||
chrome_options.add_argument('--force-prefers-reduced-motion')
|
||||
chrome_options.add_argument('--enable-accessibility-logging')
|
||||
|
||||
try:
|
||||
driver = webdriver.Chrome(options=chrome_options)
|
||||
return driver
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors de la configuration du driver: {e}")
|
||||
return None
|
||||
|
||||
def wait_for_server(url, timeout=30):
|
||||
"""Attend que le serveur soit disponible"""
|
||||
print(f"🔄 Attente du serveur {url}...")
|
||||
start_time = time.time()
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
try:
|
||||
response = requests.get(url, timeout=5)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Serveur disponible")
|
||||
return True
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
time.sleep(1)
|
||||
|
||||
print(f"❌ Serveur non disponible après {timeout}s")
|
||||
return False
|
||||
|
||||
def test_aria_attributes(driver):
|
||||
"""Test des attributs ARIA"""
|
||||
print("\n🧪 Test des attributs ARIA...")
|
||||
|
||||
tests_passed = 0
|
||||
total_tests = 0
|
||||
|
||||
# Test 1: Canvas principal
|
||||
total_tests += 1
|
||||
try:
|
||||
canvas = driver.find_element(By.CSS_SELECTOR, '[role="application"]')
|
||||
aria_label = canvas.get_attribute('aria-label')
|
||||
if aria_label and 'canvas' in aria_label.lower():
|
||||
print("✅ Canvas a un aria-label approprié")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Canvas manque d'aria-label approprié")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test canvas ARIA: {e}")
|
||||
|
||||
# Test 2: Palette
|
||||
total_tests += 1
|
||||
try:
|
||||
palette = driver.find_element(By.CSS_SELECTOR, '[role="region"]')
|
||||
aria_label = palette.get_attribute('aria-label')
|
||||
if aria_label and ('palette' in aria_label.lower() or 'boîte' in aria_label.lower()):
|
||||
print("✅ Palette a un aria-label approprié")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Palette manque d'aria-label approprié")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test palette ARIA: {e}")
|
||||
|
||||
# Test 3: Boutons avec aria-pressed
|
||||
total_tests += 1
|
||||
try:
|
||||
buttons = driver.find_elements(By.CSS_SELECTOR, 'button[aria-pressed]')
|
||||
if len(buttons) > 0:
|
||||
print(f"✅ {len(buttons)} boutons avec aria-pressed trouvés")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Aucun bouton avec aria-pressed trouvé")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test boutons ARIA: {e}")
|
||||
|
||||
# Test 4: Éléments avec aria-live
|
||||
total_tests += 1
|
||||
try:
|
||||
live_regions = driver.find_elements(By.CSS_SELECTOR, '[aria-live]')
|
||||
if len(live_regions) > 0:
|
||||
print(f"✅ {len(live_regions)} régions live trouvées")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Aucune région aria-live trouvée")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test régions live: {e}")
|
||||
|
||||
return tests_passed, total_tests
|
||||
|
||||
def test_keyboard_navigation(driver):
|
||||
"""Test de la navigation clavier"""
|
||||
print("\n⌨️ Test de la navigation clavier...")
|
||||
|
||||
tests_passed = 0
|
||||
total_tests = 0
|
||||
|
||||
# Test 1: Navigation avec Tab
|
||||
total_tests += 1
|
||||
try:
|
||||
body = driver.find_element(By.TAG_NAME, 'body')
|
||||
body.send_keys(Keys.TAB)
|
||||
active_element = driver.switch_to.active_element
|
||||
if active_element.tag_name in ['button', 'input', 'a'] or active_element.get_attribute('tabindex'):
|
||||
print("✅ Navigation Tab fonctionne")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Navigation Tab ne fonctionne pas correctement")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test navigation Tab: {e}")
|
||||
|
||||
# Test 2: Raccourcis clavier
|
||||
total_tests += 1
|
||||
try:
|
||||
# Test Alt+H pour high contrast
|
||||
body = driver.find_element(By.TAG_NAME, 'body')
|
||||
body.send_keys(Keys.ALT + 'h')
|
||||
time.sleep(0.5)
|
||||
|
||||
# Vérifier si le mode high contrast est activé
|
||||
high_contrast = driver.execute_script(
|
||||
"return document.body.classList.contains('high-contrast')"
|
||||
)
|
||||
if high_contrast:
|
||||
print("✅ Raccourci Alt+H fonctionne")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Raccourci Alt+H ne fonctionne pas")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test raccourcis: {e}")
|
||||
|
||||
# Test 3: Navigation dans la palette
|
||||
total_tests += 1
|
||||
try:
|
||||
# Chercher un élément de palette focusable
|
||||
palette_items = driver.find_elements(By.CSS_SELECTOR, '.node-item[tabindex]')
|
||||
if len(palette_items) > 0:
|
||||
palette_items[0].click()
|
||||
palette_items[0].send_keys(Keys.ARROW_DOWN)
|
||||
time.sleep(0.2)
|
||||
print("✅ Navigation flèches dans palette fonctionne")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Aucun élément de palette navigable trouvé")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test navigation palette: {e}")
|
||||
|
||||
return tests_passed, total_tests
|
||||
|
||||
def test_screen_reader_support(driver):
|
||||
"""Test du support des lecteurs d'écran"""
|
||||
print("\n🔊 Test du support des lecteurs d'écran...")
|
||||
|
||||
tests_passed = 0
|
||||
total_tests = 0
|
||||
|
||||
# Test 1: Éléments sr-only
|
||||
total_tests += 1
|
||||
try:
|
||||
sr_only_elements = driver.find_elements(By.CSS_SELECTOR, '.sr-only')
|
||||
if len(sr_only_elements) > 0:
|
||||
print(f"✅ {len(sr_only_elements)} éléments sr-only trouvés")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Aucun élément sr-only trouvé")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test sr-only: {e}")
|
||||
|
||||
# Test 2: Skip links
|
||||
total_tests += 1
|
||||
try:
|
||||
skip_links = driver.find_elements(By.CSS_SELECTOR, '.skip-link')
|
||||
if len(skip_links) > 0:
|
||||
print(f"✅ {len(skip_links)} skip links trouvés")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Aucun skip link trouvé")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test skip links: {e}")
|
||||
|
||||
# Test 3: Landmarks
|
||||
total_tests += 1
|
||||
try:
|
||||
landmarks = driver.find_elements(By.CSS_SELECTOR, '[role="main"], [role="navigation"], [role="region"], [role="banner"]')
|
||||
if len(landmarks) > 0:
|
||||
print(f"✅ {len(landmarks)} landmarks trouvés")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Aucun landmark trouvé")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test landmarks: {e}")
|
||||
|
||||
# Test 4: Headings structure
|
||||
total_tests += 1
|
||||
try:
|
||||
headings = driver.find_elements(By.CSS_SELECTOR, 'h1, h2, h3, h4, h5, h6')
|
||||
if len(headings) > 0:
|
||||
print(f"✅ {len(headings)} headings trouvés")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Aucun heading trouvé")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test headings: {e}")
|
||||
|
||||
return tests_passed, total_tests
|
||||
|
||||
def test_high_contrast_mode(driver):
|
||||
"""Test du mode high contrast"""
|
||||
print("\n🎨 Test du mode high contrast...")
|
||||
|
||||
tests_passed = 0
|
||||
total_tests = 0
|
||||
|
||||
# Test 1: Activation du mode high contrast
|
||||
total_tests += 1
|
||||
try:
|
||||
# Chercher le bouton de toggle high contrast
|
||||
contrast_button = driver.find_element(By.CSS_SELECTOR, '[aria-label*="contraste"], [title*="contraste"]')
|
||||
contrast_button.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Vérifier si le mode est activé
|
||||
high_contrast = driver.execute_script(
|
||||
"return document.body.classList.contains('high-contrast')"
|
||||
)
|
||||
if high_contrast:
|
||||
print("✅ Mode high contrast activé")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Mode high contrast non activé")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur activation high contrast: {e}")
|
||||
|
||||
# Test 2: Styles high contrast appliqués
|
||||
total_tests += 1
|
||||
try:
|
||||
# Vérifier que les styles high contrast sont appliqués
|
||||
elements_with_contrast = driver.find_elements(By.CSS_SELECTOR, '.high-contrast *')
|
||||
if len(elements_with_contrast) > 0:
|
||||
print("✅ Styles high contrast appliqués")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Styles high contrast non appliqués")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur vérification styles: {e}")
|
||||
|
||||
return tests_passed, total_tests
|
||||
|
||||
def test_focus_management(driver):
|
||||
"""Test de la gestion du focus"""
|
||||
print("\n🎯 Test de la gestion du focus...")
|
||||
|
||||
tests_passed = 0
|
||||
total_tests = 0
|
||||
|
||||
# Test 1: Focus visible
|
||||
total_tests += 1
|
||||
try:
|
||||
# Naviguer avec Tab et vérifier le focus
|
||||
body = driver.find_element(By.TAG_NAME, 'body')
|
||||
body.send_keys(Keys.TAB)
|
||||
|
||||
active_element = driver.switch_to.active_element
|
||||
outline = driver.execute_script(
|
||||
"return window.getComputedStyle(arguments[0]).outline",
|
||||
active_element
|
||||
)
|
||||
|
||||
if outline and outline != 'none':
|
||||
print("✅ Focus visible présent")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("❌ Focus visible manquant")
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test focus visible: {e}")
|
||||
|
||||
# Test 2: Focus trap dans les modales
|
||||
total_tests += 1
|
||||
try:
|
||||
# Chercher des modales ou popups
|
||||
modals = driver.find_elements(By.CSS_SELECTOR, '[role="dialog"], .modal, .popup')
|
||||
if len(modals) == 0:
|
||||
print("ℹ️ Aucune modale trouvée (test skippé)")
|
||||
tests_passed += 1 # Pas d'erreur si pas de modale
|
||||
else:
|
||||
print("✅ Modales détectées pour test focus trap")
|
||||
tests_passed += 1
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur test focus trap: {e}")
|
||||
|
||||
return tests_passed, total_tests
|
||||
|
||||
def run_axe_audit(driver):
|
||||
"""Exécute un audit d'accessibilité avec axe-core"""
|
||||
print("\n🔍 Audit d'accessibilité avec axe-core...")
|
||||
|
||||
try:
|
||||
axe = Axe(driver)
|
||||
results = axe.run()
|
||||
|
||||
violations = results.get('violations', [])
|
||||
passes = results.get('passes', [])
|
||||
|
||||
print(f"✅ {len(passes)} règles respectées")
|
||||
print(f"❌ {len(violations)} violations trouvées")
|
||||
|
||||
if violations:
|
||||
print("\n📋 Détail des violations:")
|
||||
for violation in violations[:5]: # Limiter à 5 pour la lisibilité
|
||||
print(f" • {violation['id']}: {violation['description']}")
|
||||
print(f" Impact: {violation['impact']}")
|
||||
print(f" Éléments affectés: {len(violation['nodes'])}")
|
||||
|
||||
return len(violations) == 0, len(passes), len(violations)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur audit axe: {e}")
|
||||
return False, 0, 0
|
||||
|
||||
def main():
|
||||
"""Fonction principale des tests d'accessibilité"""
|
||||
print("🚀 Démarrage des tests d'accessibilité du Visual Workflow Builder")
|
||||
print("=" * 70)
|
||||
|
||||
# Vérifier que le serveur est démarré
|
||||
if not wait_for_server("http://localhost:3000"):
|
||||
print("❌ Impossible de se connecter au serveur frontend")
|
||||
return False
|
||||
|
||||
# Configurer le driver
|
||||
driver = setup_driver()
|
||||
if not driver:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Charger l'application
|
||||
print("🌐 Chargement de l'application...")
|
||||
driver.get("http://localhost:3000")
|
||||
|
||||
# Attendre que l'application soit chargée
|
||||
WebDriverWait(driver, 10).until(
|
||||
EC.presence_of_element_located((By.TAG_NAME, "body"))
|
||||
)
|
||||
|
||||
total_passed = 0
|
||||
total_tests = 0
|
||||
|
||||
# Exécuter les tests
|
||||
passed, tests = test_aria_attributes(driver)
|
||||
total_passed += passed
|
||||
total_tests += tests
|
||||
|
||||
passed, tests = test_keyboard_navigation(driver)
|
||||
total_passed += passed
|
||||
total_tests += tests
|
||||
|
||||
passed, tests = test_screen_reader_support(driver)
|
||||
total_passed += passed
|
||||
total_tests += tests
|
||||
|
||||
passed, tests = test_high_contrast_mode(driver)
|
||||
total_passed += passed
|
||||
total_tests += tests
|
||||
|
||||
passed, tests = test_focus_management(driver)
|
||||
total_passed += passed
|
||||
total_tests += tests
|
||||
|
||||
# Audit axe-core
|
||||
axe_passed, axe_passes, axe_violations = run_axe_audit(driver)
|
||||
|
||||
# Résultats finaux
|
||||
print("\n" + "=" * 70)
|
||||
print("📊 RÉSULTATS DES TESTS D'ACCESSIBILITÉ")
|
||||
print("=" * 70)
|
||||
print(f"Tests manuels: {total_passed}/{total_tests} passés ({total_passed/total_tests*100:.1f}%)")
|
||||
print(f"Audit axe-core: {axe_passes} règles respectées, {axe_violations} violations")
|
||||
|
||||
success_rate = total_passed / total_tests if total_tests > 0 else 0
|
||||
overall_success = success_rate >= 0.8 and axe_violations <= 5
|
||||
|
||||
if overall_success:
|
||||
print("✅ TESTS D'ACCESSIBILITÉ RÉUSSIS")
|
||||
print("🎉 L'application respecte les standards d'accessibilité")
|
||||
else:
|
||||
print("❌ TESTS D'ACCESSIBILITÉ ÉCHOUÉS")
|
||||
print("⚠️ Des améliorations d'accessibilité sont nécessaires")
|
||||
|
||||
return overall_success
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur lors des tests: {e}")
|
||||
return False
|
||||
|
||||
finally:
|
||||
driver.quit()
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
Reference in New Issue
Block a user