- 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>
408 lines
16 KiB
Python
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"]) |