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:
408
tests/property/test_vwb_frontend_v2_accessibility.py
Normal file
408
tests/property/test_vwb_frontend_v2_accessibility.py
Normal file
@@ -0,0 +1,408 @@
|
||||
"""
|
||||
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"])
|
||||
Reference in New Issue
Block a user