- 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>
282 lines
12 KiB
Python
282 lines
12 KiB
Python
#!/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"])
|