#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Tests de stabilité de l'interface Visual Workflow Builder Frontend V2 Auteur : Dom, Alice, Kiro - 09 janvier 2026 Ce module vérifie que les corrections apportées pour résoudre la boucle infinie de chargement sont correctement implémentées. """ import os import re import pytest from pathlib import Path # Chemin vers le frontend VWB FRONTEND_PATH = Path(__file__).parent.parent.parent / "visual_workflow_builder" / "frontend" SRC_PATH = FRONTEND_PATH / "src" class TestApiClientStability: """Tests de stabilité du client API.""" def test_api_client_initial_state_is_offline(self): """Vérifie que l'état initial du client API est 'offline'.""" api_client_path = SRC_PATH / "services" / "apiClient.ts" assert api_client_path.exists(), f"Fichier non trouvé: {api_client_path}" content = api_client_path.read_text(encoding='utf-8') # Vérifier que l'état initial est 'offline' et non 'checking' assert "connectionState: ConnectionState = 'offline'" in content, \ "L'état initial du client API doit être 'offline' pour éviter les boucles" # Vérifier qu'il n'y a pas d'état initial 'checking' assert "connectionState: ConnectionState = 'checking'" not in content, \ "L'état initial ne doit PAS être 'checking' car cela cause des re-renders" def test_api_client_no_immediate_callback_notification(self): """Vérifie que onConnectionStateChange ne notifie pas immédiatement.""" api_client_path = SRC_PATH / "services" / "apiClient.ts" content = api_client_path.read_text(encoding='utf-8') # Chercher la méthode onConnectionStateChange method_match = re.search( r'onConnectionStateChange\([^)]+\)[^{]*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}', content, re.DOTALL ) assert method_match, "Méthode onConnectionStateChange non trouvée" method_body = method_match.group(1) # Vérifier qu'il n'y a PAS d'appel immédiat au callback # Pattern: callback(this.connectionState) sans setTimeout immediate_call_pattern = r'callback\s*\(\s*this\.connectionState\s*\)' # Si le pattern est trouvé, vérifier qu'il est commenté ou dans un setTimeout if re.search(immediate_call_pattern, method_body): # Vérifier que c'est dans un commentaire lines = method_body.split('\n') for line in lines: if re.search(immediate_call_pattern, line): assert '//' in line or '/*' in line, \ "L'appel callback(this.connectionState) doit être commenté ou supprimé" def test_api_client_lazy_initialization(self): """Vérifie que l'initialisation est paresseuse (lazy).""" api_client_path = SRC_PATH / "services" / "apiClient.ts" content = api_client_path.read_text(encoding='utf-8') # Vérifier la présence du commentaire sur l'initialisation paresseuse assert "paresseuse" in content.lower() or "lazy" in content.lower(), \ "Le code doit mentionner l'initialisation paresseuse (lazy)" # Vérifier qu'il n'y a PAS d'appel automatique à initialize() à la fin du fichier # Pattern: apiClient.initialize() sans être dans une fonction lines = content.split('\n') for i, line in enumerate(lines): if 'apiClient.initialize()' in line and not line.strip().startswith('//'): # Vérifier que c'est dans une fonction ou commenté context = '\n'.join(lines[max(0, i-5):i+1]) assert 'async' in context or 'function' in context or '//' in line, \ f"Appel automatique à apiClient.initialize() trouvé ligne {i+1}" def test_api_client_async_notifications(self): """Vérifie que les notifications sont asynchrones (setTimeout).""" api_client_path = SRC_PATH / "services" / "apiClient.ts" content = api_client_path.read_text(encoding='utf-8') # Chercher la méthode setConnectionState method_match = re.search( r'setConnectionState\([^)]+\)[^{]*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}', content, re.DOTALL ) assert method_match, "Méthode setConnectionState non trouvée" method_body = method_match.group(1) # Vérifier la présence de setTimeout pour les notifications asynchrones assert 'setTimeout' in method_body, \ "Les notifications doivent être asynchrones (setTimeout) pour éviter les boucles" class TestConnectionStatusHookStability: """Tests de stabilité du hook useConnectionStatus.""" def test_hook_initial_state_is_offline(self): """Vérifie que l'état initial du hook est 'offline'.""" hook_path = SRC_PATH / "hooks" / "useConnectionStatus.ts" assert hook_path.exists(), f"Fichier non trouvé: {hook_path}" content = hook_path.read_text(encoding='utf-8') # Vérifier que l'état initial est défini comme 'offline' assert "status: 'offline'" in content, \ "L'état initial du hook doit être 'offline'" # Vérifier qu'il n'y a pas d'état initial dynamique basé sur apiClient assert "apiClient.getConnectionState()" not in content or \ "// " in content.split("apiClient.getConnectionState()")[0].split('\n')[-1], \ "L'état initial ne doit PAS être basé sur apiClient.getConnectionState()" def test_hook_uses_refs_for_callbacks(self): """Vérifie que le hook utilise des refs pour les callbacks.""" hook_path = SRC_PATH / "hooks" / "useConnectionStatus.ts" content = hook_path.read_text(encoding='utf-8') # Vérifier l'utilisation de useRef pour les callbacks assert 'useRef' in content, \ "Le hook doit utiliser useRef pour éviter les re-renders" # Vérifier que onStatusChange utilise une ref assert 'onStatusChangeRef' in content or 'Ref' in content, \ "Les callbacks doivent être stockés dans des refs" def test_hook_stable_initial_state_constant(self): """Vérifie que l'état initial est une constante stable.""" hook_path = SRC_PATH / "hooks" / "useConnectionStatus.ts" content = hook_path.read_text(encoding='utf-8') # Vérifier la présence d'une constante INITIAL_STATE assert 'INITIAL_STATE' in content, \ "L'état initial doit être une constante INITIAL_STATE définie en dehors du hook" class TestUseApiClientHookStability: """Tests de stabilité du hook useApiClient.""" def test_use_connection_state_initial_offline(self): """Vérifie que useConnectionState a un état initial 'offline'.""" hook_path = SRC_PATH / "hooks" / "useApiClient.ts" assert hook_path.exists(), f"Fichier non trouvé: {hook_path}" content = hook_path.read_text(encoding='utf-8') # Chercher la fonction useConnectionState func_match = re.search( r'export function useConnectionState\(\)[^{]*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}', content, re.DOTALL ) assert func_match, "Fonction useConnectionState non trouvée" func_body = func_match.group(1) # Vérifier que l'état initial est 'offline' assert "'offline'" in func_body, \ "useConnectionState doit avoir un état initial 'offline'" class TestWorkflowManagerStability: """Tests de stabilité du composant WorkflowManager.""" def test_workflow_manager_uses_connection_state(self): """Vérifie que WorkflowManager utilise useConnectionState.""" component_path = SRC_PATH / "components" / "WorkflowManager" / "index.tsx" assert component_path.exists(), f"Fichier non trouvé: {component_path}" content = component_path.read_text(encoding='utf-8') # Vérifier l'import de useConnectionState assert 'useConnectionState' in content, \ "WorkflowManager doit utiliser useConnectionState" def test_workflow_manager_handles_offline_mode(self): """Vérifie que WorkflowManager gère le mode hors ligne.""" component_path = SRC_PATH / "components" / "WorkflowManager" / "index.tsx" content = component_path.read_text(encoding='utf-8') # Vérifier la gestion du mode hors ligne assert 'isOffline' in content or 'offline' in content.lower(), \ "WorkflowManager doit gérer le mode hors ligne" class TestExecutorStability: """Tests de stabilité du composant Executor.""" def test_executor_uses_connection_status(self): """Vérifie que Executor utilise useConnectionStatus.""" component_path = SRC_PATH / "components" / "Executor" / "index.tsx" assert component_path.exists(), f"Fichier non trouvé: {component_path}" content = component_path.read_text(encoding='utf-8') # Vérifier l'import de useConnectionStatus assert 'useConnectionStatus' in content, \ "Executor doit utiliser useConnectionStatus" def test_executor_handles_offline_mode(self): """Vérifie que Executor gère le mode hors ligne.""" component_path = SRC_PATH / "components" / "Executor" / "index.tsx" content = component_path.read_text(encoding='utf-8') # Vérifier la gestion du mode hors ligne assert 'isOffline' in content, \ "Executor doit gérer le mode hors ligne avec isOffline" class TestTypescriptCompilation: """Tests de compilation TypeScript.""" def test_no_typescript_errors_in_api_client(self): """Vérifie qu'il n'y a pas d'erreurs TypeScript dans apiClient.ts.""" api_client_path = SRC_PATH / "services" / "apiClient.ts" content = api_client_path.read_text(encoding='utf-8') # Vérifications basiques de syntaxe TypeScript assert content.count('{') == content.count('}'), \ "Accolades non équilibrées dans apiClient.ts" assert content.count('(') == content.count(')'), \ "Parenthèses non équilibrées dans apiClient.ts" def test_no_typescript_errors_in_hooks(self): """Vérifie qu'il n'y a pas d'erreurs TypeScript dans les hooks.""" hooks_path = SRC_PATH / "hooks" for hook_file in hooks_path.glob("*.ts"): content = hook_file.read_text(encoding='utf-8') # Vérifications basiques de syntaxe TypeScript assert content.count('{') == content.count('}'), \ f"Accolades non équilibrées dans {hook_file.name}" assert content.count('(') == content.count(')'), \ f"Parenthèses non équilibrées dans {hook_file.name}" class TestFrenchDocumentation: """Tests de documentation en français.""" def test_api_client_has_french_comments(self): """Vérifie que apiClient.ts a des commentaires en français.""" api_client_path = SRC_PATH / "services" / "apiClient.ts" content = api_client_path.read_text(encoding='utf-8') # Vérifier la présence de commentaires en français french_words = ['Auteur', 'janvier', 'gestion', 'connexion', 'hors ligne'] found_french = any(word in content for word in french_words) assert found_french, \ "apiClient.ts doit avoir des commentaires en français" def test_hooks_have_french_comments(self): """Vérifie que les hooks ont des commentaires en français.""" hooks_path = SRC_PATH / "hooks" for hook_file in hooks_path.glob("*.ts"): content = hook_file.read_text(encoding='utf-8') # Vérifier la présence de commentaires en français french_words = ['Auteur', 'janvier', 'état', 'connexion'] found_french = any(word in content for word in french_words) assert found_french, \ f"{hook_file.name} doit avoir des commentaires en français" if __name__ == "__main__": pytest.main([__file__, "-v", "--tb=short"])