""" 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"])