Files
rpa_vision_v3/tests/property/test_vwb_frontend_v2_accessibility.py
Dom a27b74cf22 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>
2026-01-29 11:23:51 +01:00

408 lines
16 KiB
Python

"""
Tests de Propriété - Accessibilité VWB Frontend V2
Auteur : Dom, Alice, Kiro - 08 janvier 2026
Ce module teste les fonctionnalités d'accessibilité du Visual Workflow Builder Frontend V2,
incluant la navigation au clavier, la conformité WCAG 2.1 et la responsivité.
"""
import pytest
from hypothesis import given, strategies as st, assume, settings
import json
import os
import re
from typing import Dict, List, Set, Any
from pathlib import Path
class TestAccessibilityProperties:
"""Tests de propriétés d'accessibilité"""
@pytest.fixture(autouse=True)
def setup(self):
"""Configuration initiale des tests"""
self.frontend_path = Path("visual_workflow_builder/frontend/src")
self.components_path = self.frontend_path / "components"
self.hooks_path = self.frontend_path / "hooks"
# Raccourcis clavier obligatoires
self.required_keyboard_shortcuts = {
'Tab': 'Navigation vers l\'élément suivant',
'Shift+Tab': 'Navigation vers l\'élément précédent',
'Enter': 'Activation de l\'élément',
'Space': 'Activation alternative',
'Escape': 'Annulation ou fermeture',
'ArrowUp': 'Navigation vers le haut',
'ArrowDown': 'Navigation vers le bas',
'ArrowLeft': 'Navigation vers la gauche',
'ArrowRight': 'Navigation vers la droite',
}
# Attributs ARIA obligatoires
self.required_aria_attributes = {
'aria-label', 'aria-labelledby', 'aria-describedby',
'aria-expanded', 'aria-hidden', 'aria-live',
'role', 'tabindex'
}
# Ratios de contraste minimum (WCAG 2.1 AA)
self.min_contrast_ratios = {
'normal_text': 4.5,
'large_text': 3.0,
'ui_components': 3.0,
}
def get_typescript_files(self) -> List[Path]:
"""Récupère tous les fichiers TypeScript du frontend"""
tsx_files = list(self.frontend_path.rglob("*.tsx"))
ts_files = list(self.frontend_path.rglob("*.ts"))
return tsx_files + ts_files
def extract_keyboard_handlers(self, file_path: Path) -> List[str]:
"""Extrait les gestionnaires d'événements clavier d'un fichier"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Patterns pour détecter les gestionnaires clavier
patterns = [
r'onKeyDown\s*=\s*{([^}]+)}',
r'onKeyUp\s*=\s*{([^}]+)}',
r'onKeyPress\s*=\s*{([^}]+)}',
r'addEventListener\([\'"]keydown[\'"]',
r'addEventListener\([\'"]keyup[\'"]',
r'addEventListener\([\'"]keypress[\'"]',
r'useKeyboardNavigation\(',
r'handleKeyDown',
r'handleKeyUp',
r'event\.key\s*===',
]
handlers = []
for pattern in patterns:
matches = re.findall(pattern, content, re.IGNORECASE)
handlers.extend(matches)
return handlers
except Exception as e:
print(f"Erreur lors de la lecture de {file_path}: {e}")
return []
def extract_aria_attributes(self, file_path: Path) -> List[str]:
"""Extrait les attributs ARIA d'un fichier"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Pattern pour détecter les attributs ARIA
aria_pattern = r'(aria-[a-z-]+|role|tabindex)\s*='
matches = re.findall(aria_pattern, content, re.IGNORECASE)
return list(set(matches)) # Supprimer les doublons
except Exception as e:
print(f"Erreur lors de la lecture de {file_path}: {e}")
return []
def check_responsive_breakpoints(self, file_path: Path) -> Dict[str, Any]:
"""Vérifie la présence de breakpoints responsifs"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Patterns pour détecter la responsivité
responsive_patterns = [
r'useMediaQuery\(',
r'theme\.breakpoints\.',
r'@media\s*\(',
r'xs|sm|md|lg|xl', # Breakpoints Material-UI
r'isMobile|isTablet|isDesktop',
r'useResponsiveLayout',
]
responsive_features = []
for pattern in responsive_patterns:
if re.search(pattern, content, re.IGNORECASE):
responsive_features.append(pattern)
return {
'has_responsive_features': len(responsive_features) > 0,
'responsive_patterns_found': responsive_features,
'responsive_score': len(responsive_features)
}
except Exception as e:
print(f"Erreur lors de la lecture de {file_path}: {e}")
return {'has_responsive_features': False, 'responsive_patterns_found': [], 'responsive_score': 0}
@given(st.sampled_from(['components', 'hooks']))
@settings(max_examples=10, deadline=5000)
def test_property_26_keyboard_navigation_completeness(self, directory_type):
"""
Propriété 26 : Navigation Clavier Complète
Pour tout composant interactif dans les composants ou hooks,
il doit exister des gestionnaires d'événements clavier appropriés
pour assurer une navigation complète au clavier.
**Valide : Exigences 11.1, 11.3**
"""
# Sélectionner le répertoire à tester
if directory_type == 'components':
base_path = self.components_path
else:
base_path = self.hooks_path
if not base_path.exists():
pytest.skip(f"Répertoire {base_path} non trouvé")
# Récupérer les fichiers TypeScript
ts_files = list(base_path.rglob("*.tsx")) + list(base_path.rglob("*.ts"))
if not ts_files:
pytest.skip(f"Aucun fichier TypeScript trouvé dans {base_path}")
# Vérifier la présence de gestionnaires clavier
files_with_keyboard_support = 0
total_interactive_files = 0
for file_path in ts_files:
# Ignorer les fichiers de types et utilitaires
if 'types' in str(file_path) or 'utils' in str(file_path):
continue
handlers = self.extract_keyboard_handlers(file_path)
# Considérer comme interactif si contient des éléments UI
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
interactive_indicators = [
'Button', 'TextField', 'Select', 'Checkbox', 'Radio',
'onClick', 'onFocus', 'onBlur', 'tabIndex', 'role=',
'Canvas', 'Dialog', 'Menu'
]
is_interactive = any(indicator in content for indicator in interactive_indicators)
if is_interactive:
total_interactive_files += 1
if handlers:
files_with_keyboard_support += 1
# Au moins 70% des fichiers interactifs doivent avoir un support clavier
if total_interactive_files > 0:
keyboard_support_ratio = files_with_keyboard_support / total_interactive_files
if keyboard_support_ratio < 0.7:
pytest.fail(
f"Propriété 26 violée : Seulement {files_with_keyboard_support}/{total_interactive_files} "
f"({keyboard_support_ratio:.1%}) des composants interactifs ont un support clavier. "
f"Minimum requis : 70%"
)
def test_property_27_wcag_compliance(self):
"""
Propriété 27 : Conformité Accessibilité
L'application doit respecter les standards WCAG 2.1 niveau AA
en incluant les attributs ARIA appropriés et les bonnes pratiques d'accessibilité.
**Valide : Exigences 11.2**
"""
# Vérifier la présence du fournisseur d'accessibilité
accessibility_provider_file = self.components_path / "AccessibilityProvider" / "index.tsx"
if not accessibility_provider_file.exists():
pytest.fail("Fournisseur d'accessibilité manquant")
# Vérifier le contenu du fournisseur d'accessibilité
with open(accessibility_provider_file, 'r', encoding='utf-8') as f:
provider_content = f.read()
# Vérifications WCAG essentielles
wcag_requirements = [
'aria-live', # Annonces aux lecteurs d'écran
'aria-label', # Étiquettes accessibles
'role', # Rôles sémantiques
'tabindex', # Navigation au clavier
'focus', # Gestion du focus
'prefers-reduced-motion', # Respect des préférences utilisateur
'prefers-contrast', # Support du contraste élevé
]
missing_requirements = []
for requirement in wcag_requirements:
if requirement not in provider_content.lower():
missing_requirements.append(requirement)
if missing_requirements:
pytest.fail(
f"Propriété 27 violée : Exigences WCAG manquantes : {missing_requirements}"
)
# Vérifier la présence d'attributs ARIA dans les composants
component_files = list(self.components_path.rglob("*.tsx"))
files_with_aria = 0
for file_path in component_files:
aria_attributes = self.extract_aria_attributes(file_path)
if aria_attributes:
files_with_aria += 1
# Au moins 50% des composants doivent avoir des attributs ARIA
if len(component_files) > 0:
aria_ratio = files_with_aria / len(component_files)
if aria_ratio < 0.5:
pytest.fail(
f"Propriété 27 violée : Seulement {files_with_aria}/{len(component_files)} "
f"({aria_ratio:.1%}) des composants ont des attributs ARIA. Minimum requis : 50%"
)
@given(st.sampled_from(['xs', 'sm', 'md', 'lg', 'xl']))
@settings(max_examples=5, deadline=3000)
def test_property_28_responsive_screen_adaptation(self, breakpoint):
"""
Propriété 28 : Responsivité Écrans
Pour tout breakpoint de taille d'écran (xs, sm, md, lg, xl),
l'interface doit s'adapter correctement et rester utilisable.
**Valide : Exigences 11.4**
"""
# Vérifier la présence du hook de responsivité
responsive_hook_file = self.hooks_path / "useResponsiveLayout.ts"
if not responsive_hook_file.exists():
pytest.fail("Hook de responsivité manquant")
# Vérifier le contenu du hook
with open(responsive_hook_file, 'r', encoding='utf-8') as f:
hook_content = f.read()
# Vérifier que le breakpoint est supporté
if breakpoint not in hook_content:
pytest.fail(f"Breakpoint {breakpoint} non supporté dans le hook de responsivité")
# Vérifier les configurations responsives essentielles
responsive_configs = [
'paletteWidth',
'propertiesWidth',
'variablesHeight',
'showMinimap',
'canvasMinHeight',
'buttonSize',
]
missing_configs = []
for config in responsive_configs:
if config not in hook_content:
missing_configs.append(config)
if missing_configs:
pytest.fail(
f"Propriété 28 violée : Configurations responsives manquantes : {missing_configs}"
)
# Vérifier l'utilisation de la responsivité dans l'App principal
app_file = self.frontend_path / "App.tsx"
if app_file.exists():
with open(app_file, 'r', encoding='utf-8') as f:
app_content = f.read()
if 'useResponsiveLayout' not in app_content:
pytest.fail("Hook de responsivité non utilisé dans l'App principal")
if 'getResponsiveStyles' not in app_content:
pytest.fail("Styles responsifs non appliqués dans l'App principal")
def test_keyboard_shortcuts_completeness(self):
"""
Test de complétude des raccourcis clavier
Vérifie que tous les raccourcis clavier essentiels sont implémentés
et documentés correctement.
"""
# Vérifier la présence du hook de navigation clavier
keyboard_hook_file = self.hooks_path / "useKeyboardNavigation.ts"
if not keyboard_hook_file.exists():
pytest.fail("Hook de navigation clavier manquant")
# Vérifier le contenu du hook
with open(keyboard_hook_file, 'r', encoding='utf-8') as f:
hook_content = f.read()
# Vérifier la présence des raccourcis essentiels
essential_shortcuts = [
'Tab', # Navigation
'ArrowUp', # Déplacement
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'Delete', # Suppression
'Escape', # Annulation
'Enter', # Activation
'Ctrl+Z', # Annuler (détecté par 'ctrlKey: true' + 'z')
'Ctrl+S', # Sauvegarder
]
missing_shortcuts = []
for shortcut in essential_shortcuts:
# Adapter la recherche selon le format du raccourci
if 'Ctrl+' in shortcut:
key = shortcut.split('+')[1].lower()
if f"key: '{key}'" not in hook_content or 'ctrlKey: true' not in hook_content:
missing_shortcuts.append(shortcut)
else:
if f"key: '{shortcut}'" not in hook_content:
missing_shortcuts.append(shortcut)
if missing_shortcuts:
pytest.fail(
f"Raccourcis clavier essentiels manquants : {missing_shortcuts}"
)
# Vérifier la présence du composant d'aide aux raccourcis
shortcuts_component_file = self.components_path / "KeyboardShortcuts" / "index.tsx"
if not shortcuts_component_file.exists():
pytest.fail("Composant d'aide aux raccourcis clavier manquant")
def test_accessibility_provider_integration(self):
"""
Test d'intégration du fournisseur d'accessibilité
Vérifie que le fournisseur d'accessibilité est correctement intégré
dans l'application principale.
"""
app_file = self.frontend_path / "App.tsx"
if not app_file.exists():
pytest.fail("Fichier App.tsx manquant")
with open(app_file, 'r', encoding='utf-8') as f:
app_content = f.read()
# Vérifications d'intégration
integration_checks = [
'AccessibilityProvider', # Import et utilisation
'useKeyboardNavigation', # Hook de navigation
'useResponsiveLayout', # Hook de responsivité
'KeyboardShortcuts', # Composant de raccourcis
]
missing_integrations = []
for check in integration_checks:
if check not in app_content:
missing_integrations.append(check)
if missing_integrations:
pytest.fail(
f"Intégrations d'accessibilité manquantes dans App.tsx : {missing_integrations}"
)
if __name__ == "__main__":
# Exécution des tests en mode standalone
pytest.main([__file__, "-v", "--tb=short"])