- 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>
597 lines
25 KiB
Python
597 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests unitaires pour l'extension de la Palette VWB avec actions du catalogue
|
|
Auteur : Dom, Alice, Kiro - 09 janvier 2026
|
|
|
|
Ce module teste l'intégration des actions du catalogue VisionOnly dans la Palette VWB,
|
|
incluant le chargement dynamique, la recherche unifiée, et l'affichage des catégories.
|
|
"""
|
|
|
|
import pytest
|
|
import asyncio
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch, AsyncMock
|
|
|
|
# Configuration du chemin pour les imports
|
|
import sys
|
|
sys.path.append(str(Path(__file__).parent.parent.parent))
|
|
|
|
from visual_workflow_builder.backend.actions.registry import VWBActionRegistry
|
|
from visual_workflow_builder.backend.contracts.visual_anchor import VWBVisualAnchor
|
|
from visual_workflow_builder.backend.contracts.evidence import VWBEvidence
|
|
from visual_workflow_builder.backend.contracts.error import VWBActionError
|
|
|
|
|
|
class TestVWBPaletteExtension:
|
|
"""Tests pour l'extension de la Palette VWB avec le catalogue d'actions"""
|
|
|
|
def setup_method(self):
|
|
"""Configuration avant chaque test"""
|
|
self.registry = VWBActionRegistry()
|
|
self.test_actions = [
|
|
{
|
|
'id': 'click_anchor',
|
|
'name': 'Cliquer sur Ancre Visuelle',
|
|
'description': 'Cliquer sur un élément identifié visuellement',
|
|
'category': 'vision_ui',
|
|
'icon': '🖱️',
|
|
'parameters': {
|
|
'visual_anchor': {
|
|
'type': 'VWBVisualAnchor',
|
|
'required': True,
|
|
'description': 'Ancre visuelle à cliquer'
|
|
},
|
|
'click_type': {
|
|
'type': 'string',
|
|
'required': False,
|
|
'default': 'left',
|
|
'options': ['left', 'right', 'double'],
|
|
'description': 'Type de clic à effectuer'
|
|
}
|
|
},
|
|
'examples': [
|
|
{
|
|
'name': 'Clic simple sur bouton',
|
|
'description': 'Cliquer sur un bouton avec reconnaissance visuelle',
|
|
'parameters': {
|
|
'visual_anchor': {
|
|
'anchor_type': 'button',
|
|
'description': 'Bouton de validation'
|
|
},
|
|
'click_type': 'left'
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
'id': 'type_text',
|
|
'name': 'Saisir Texte',
|
|
'description': 'Saisir du texte dans un champ identifié visuellement',
|
|
'category': 'vision_ui',
|
|
'icon': '⌨️',
|
|
'parameters': {
|
|
'visual_anchor': {
|
|
'type': 'VWBVisualAnchor',
|
|
'required': True,
|
|
'description': 'Champ de saisie cible'
|
|
},
|
|
'text': {
|
|
'type': 'string',
|
|
'required': True,
|
|
'description': 'Texte à saisir'
|
|
}
|
|
},
|
|
'examples': [
|
|
{
|
|
'name': 'Saisie dans formulaire',
|
|
'description': 'Saisir du texte dans un champ de formulaire',
|
|
'parameters': {
|
|
'visual_anchor': {
|
|
'anchor_type': 'input',
|
|
'description': 'Champ nom utilisateur'
|
|
},
|
|
'text': 'utilisateur@exemple.com'
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
'id': 'wait_for_anchor',
|
|
'name': 'Attendre Ancre Visuelle',
|
|
'description': 'Attendre qu\'un élément visuel apparaisse',
|
|
'category': 'control',
|
|
'icon': '⏳',
|
|
'parameters': {
|
|
'visual_anchor': {
|
|
'type': 'VWBVisualAnchor',
|
|
'required': True,
|
|
'description': 'Élément à attendre'
|
|
},
|
|
'timeout_ms': {
|
|
'type': 'number',
|
|
'required': False,
|
|
'default': 10000,
|
|
'min': 1000,
|
|
'max': 60000,
|
|
'description': 'Délai d\'attente en millisecondes'
|
|
}
|
|
},
|
|
'examples': [
|
|
{
|
|
'name': 'Attendre chargement page',
|
|
'description': 'Attendre qu\'un élément de la page soit visible',
|
|
'parameters': {
|
|
'visual_anchor': {
|
|
'anchor_type': 'text',
|
|
'description': 'Texte "Chargement terminé"'
|
|
},
|
|
'timeout_ms': 15000
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
|
|
def test_catalog_actions_structure(self):
|
|
"""Test de la structure des actions du catalogue"""
|
|
print("🧪 Test de la structure des actions du catalogue...")
|
|
|
|
for action in self.test_actions:
|
|
# Vérifier les champs obligatoires
|
|
assert 'id' in action, f"Action {action.get('name', 'inconnue')} manque l'ID"
|
|
assert 'name' in action, f"Action {action['id']} manque le nom"
|
|
assert 'description' in action, f"Action {action['id']} manque la description"
|
|
assert 'category' in action, f"Action {action['id']} manque la catégorie"
|
|
assert 'icon' in action, f"Action {action['id']} manque l'icône"
|
|
assert 'parameters' in action, f"Action {action['id']} manque les paramètres"
|
|
assert 'examples' in action, f"Action {action['id']} manque les exemples"
|
|
|
|
# Vérifier les catégories valides
|
|
valid_categories = ['vision_ui', 'control', 'data', 'navigation', 'validation']
|
|
assert action['category'] in valid_categories, f"Catégorie invalide: {action['category']}"
|
|
|
|
# Vérifier la structure des paramètres
|
|
for param_name, param_config in action['parameters'].items():
|
|
assert 'type' in param_config, f"Paramètre {param_name} manque le type"
|
|
assert 'required' in param_config, f"Paramètre {param_name} manque required"
|
|
assert 'description' in param_config, f"Paramètre {param_name} manque la description"
|
|
|
|
# Vérifier la structure des exemples
|
|
for example in action['examples']:
|
|
assert 'name' in example, f"Exemple manque le nom dans {action['id']}"
|
|
assert 'description' in example, f"Exemple manque la description dans {action['id']}"
|
|
assert 'parameters' in example, f"Exemple manque les paramètres dans {action['id']}"
|
|
|
|
print(f"✅ Structure validée pour {len(self.test_actions)} actions")
|
|
|
|
def test_category_metadata_mapping(self):
|
|
"""Test du mapping des métadonnées de catégories"""
|
|
print("🧪 Test du mapping des métadonnées de catégories...")
|
|
|
|
# Métadonnées attendues pour chaque catégorie
|
|
expected_metadata = {
|
|
'vision_ui': {
|
|
'name': 'Vision UI',
|
|
'description': 'Actions d\'interaction visuelle avec l\'interface utilisateur',
|
|
'icon': '🖱️',
|
|
'color': '#2196f3'
|
|
},
|
|
'control': {
|
|
'name': 'Contrôle Vision',
|
|
'description': 'Actions de contrôle et synchronisation visuelles',
|
|
'icon': '⏳',
|
|
'color': '#ff9800'
|
|
},
|
|
'data': {
|
|
'name': 'Données Vision',
|
|
'description': 'Actions de manipulation de données avec vision',
|
|
'icon': '📊',
|
|
'color': '#4caf50'
|
|
}
|
|
}
|
|
|
|
# Grouper les actions par catégorie
|
|
actions_by_category = {}
|
|
for action in self.test_actions:
|
|
category = action['category']
|
|
if category not in actions_by_category:
|
|
actions_by_category[category] = []
|
|
actions_by_category[category].append(action)
|
|
|
|
# Vérifier chaque catégorie
|
|
for category_id, actions in actions_by_category.items():
|
|
assert category_id in expected_metadata, f"Catégorie non mappée: {category_id}"
|
|
|
|
metadata = expected_metadata[category_id]
|
|
assert len(metadata['name']) > 0, f"Nom vide pour catégorie {category_id}"
|
|
assert len(metadata['description']) > 0, f"Description vide pour catégorie {category_id}"
|
|
assert len(metadata['icon']) > 0, f"Icône vide pour catégorie {category_id}"
|
|
assert metadata['color'].startswith('#'), f"Couleur invalide pour catégorie {category_id}"
|
|
|
|
print(f" ✅ Catégorie {category_id}: {len(actions)} actions, métadonnées OK")
|
|
|
|
print(f"✅ Mapping validé pour {len(actions_by_category)} catégories")
|
|
|
|
def test_step_template_conversion(self):
|
|
"""Test de la conversion des actions du catalogue en StepTemplate"""
|
|
print("🧪 Test de la conversion en StepTemplate...")
|
|
|
|
for action in self.test_actions:
|
|
# Simuler la conversion (logique du frontend)
|
|
required_parameters = [
|
|
name for name, param in action['parameters'].items()
|
|
if param['required']
|
|
]
|
|
|
|
default_parameters = {
|
|
name: param['default']
|
|
for name, param in action['parameters'].items()
|
|
if 'default' in param
|
|
}
|
|
|
|
step_template = {
|
|
'id': action['id'],
|
|
'type': action['id'], # Utiliser l'ID comme type pour les actions du catalogue
|
|
'name': action['name'],
|
|
'description': action['description'],
|
|
'icon': action['icon'],
|
|
'defaultParameters': default_parameters,
|
|
'requiredParameters': required_parameters
|
|
}
|
|
|
|
# Vérifier la structure du StepTemplate
|
|
assert step_template['id'] == action['id']
|
|
assert step_template['name'] == action['name']
|
|
assert step_template['description'] == action['description']
|
|
assert step_template['icon'] == action['icon']
|
|
assert isinstance(step_template['defaultParameters'], dict)
|
|
assert isinstance(step_template['requiredParameters'], list)
|
|
|
|
# Vérifier que les paramètres requis sont corrects
|
|
for param_name in step_template['requiredParameters']:
|
|
assert param_name in action['parameters']
|
|
assert action['parameters'][param_name]['required'] is True
|
|
|
|
# Vérifier que les paramètres par défaut sont corrects
|
|
for param_name, default_value in step_template['defaultParameters'].items():
|
|
assert param_name in action['parameters']
|
|
assert action['parameters'][param_name].get('default') == default_value
|
|
|
|
print(f" ✅ Conversion OK pour {action['name']}")
|
|
|
|
print(f"✅ Conversion validée pour {len(self.test_actions)} actions")
|
|
|
|
def test_drag_data_format(self):
|
|
"""Test du format des données de drag & drop"""
|
|
print("🧪 Test du format des données de drag & drop...")
|
|
|
|
for action in self.test_actions:
|
|
# Format pour les actions du catalogue
|
|
catalog_drag_data = f"catalog:{action['id']}"
|
|
|
|
# Vérifier le format
|
|
assert catalog_drag_data.startswith('catalog:')
|
|
assert catalog_drag_data.endswith(action['id'])
|
|
|
|
# Vérifier que c'est différent des actions par défaut
|
|
default_drag_data = action['id'] # Format pour actions par défaut
|
|
assert catalog_drag_data != default_drag_data
|
|
|
|
print(f" ✅ Format drag OK pour {action['name']}: {catalog_drag_data}")
|
|
|
|
print("✅ Format de drag & drop validé")
|
|
|
|
def test_search_functionality(self):
|
|
"""Test de la fonctionnalité de recherche unifiée"""
|
|
print("🧪 Test de la fonctionnalité de recherche...")
|
|
|
|
# Termes de recherche à tester
|
|
search_tests = [
|
|
{
|
|
'term': 'clic',
|
|
'expected_matches': ['click_anchor'],
|
|
'description': 'Recherche par nom partiel'
|
|
},
|
|
{
|
|
'term': 'visuel',
|
|
'expected_matches': ['click_anchor', 'type_text', 'wait_for_anchor'],
|
|
'description': 'Recherche par description'
|
|
},
|
|
{
|
|
'term': 'anchor',
|
|
'expected_matches': ['click_anchor', 'wait_for_anchor'],
|
|
'description': 'Recherche par type/ID'
|
|
},
|
|
{
|
|
'term': 'inexistant',
|
|
'expected_matches': [],
|
|
'description': 'Recherche sans résultat'
|
|
}
|
|
]
|
|
|
|
for test in search_tests:
|
|
term = test['term'].lower()
|
|
matches = []
|
|
|
|
# Simuler la logique de recherche du frontend
|
|
for action in self.test_actions:
|
|
if (term in action['name'].lower() or
|
|
term in action['description'].lower() or
|
|
term in action['id'].lower()):
|
|
matches.append(action['id'])
|
|
|
|
# Vérifier les résultats
|
|
expected = test['expected_matches']
|
|
assert len(matches) == len(expected), f"Nombre de résultats incorrect pour '{term}'"
|
|
|
|
for expected_id in expected:
|
|
assert expected_id in matches, f"Action {expected_id} manquante pour '{term}'"
|
|
|
|
print(f" ✅ {test['description']}: '{term}' -> {len(matches)} résultats")
|
|
|
|
print("✅ Fonctionnalité de recherche validée")
|
|
|
|
def test_visual_indicators(self):
|
|
"""Test des indicateurs visuels pour les actions du catalogue"""
|
|
print("🧪 Test des indicateurs visuels...")
|
|
|
|
# Vérifier les éléments visuels attendus
|
|
visual_elements = {
|
|
'category_background': '#f8f9ff', # Fond des catégories catalogue
|
|
'step_background': '#f0f4ff', # Fond des étapes catalogue
|
|
'step_hover': '#e3f2fd', # Survol des étapes catalogue
|
|
'border_color': '#2196f3', # Bordure gauche des étapes
|
|
'chip_color': 'primary', # Couleur des chips "VisionOnly"
|
|
'vision_label_color': '#2196f3' # Couleur du label "VISION"
|
|
}
|
|
|
|
for element, expected_value in visual_elements.items():
|
|
# Vérifier que les valeurs sont définies et cohérentes
|
|
assert expected_value is not None, f"Valeur manquante pour {element}"
|
|
|
|
if element.endswith('_color') and expected_value.startswith('#'):
|
|
# Vérifier le format hexadécimal des couleurs
|
|
assert len(expected_value) == 7, f"Format couleur invalide: {expected_value}"
|
|
assert all(c in '0123456789abcdefABCDEF' for c in expected_value[1:]), f"Couleur invalide: {expected_value}"
|
|
|
|
print(f" ✅ Indicateur {element}: {expected_value}")
|
|
|
|
print("✅ Indicateurs visuels validés")
|
|
|
|
def test_tooltip_content(self):
|
|
"""Test du contenu des tooltips enrichis"""
|
|
print("🧪 Test du contenu des tooltips...")
|
|
|
|
for action in self.test_actions:
|
|
# Vérifier les éléments du tooltip
|
|
tooltip_elements = {
|
|
'name': action['name'],
|
|
'description': action['description'],
|
|
'required_params': [
|
|
name for name, param in action['parameters'].items()
|
|
if param['required']
|
|
],
|
|
'vision_indicator': '🎯 Action avec reconnaissance visuelle automatique'
|
|
}
|
|
|
|
# Vérifier le nom
|
|
assert len(tooltip_elements['name']) > 0, f"Nom vide pour {action['id']}"
|
|
|
|
# Vérifier la description
|
|
assert len(tooltip_elements['description']) > 0, f"Description vide pour {action['id']}"
|
|
|
|
# Vérifier les paramètres requis
|
|
assert len(tooltip_elements['required_params']) > 0, f"Aucun paramètre requis pour {action['id']}"
|
|
|
|
# Vérifier l'indicateur vision
|
|
assert 'reconnaissance visuelle' in tooltip_elements['vision_indicator']
|
|
|
|
print(f" ✅ Tooltip OK pour {action['name']}")
|
|
|
|
print("✅ Contenu des tooltips validé")
|
|
|
|
def test_performance_considerations(self):
|
|
"""Test des considérations de performance"""
|
|
print("🧪 Test des considérations de performance...")
|
|
|
|
# Simuler un grand nombre d'actions
|
|
large_action_set = self.test_actions * 10 # 30 actions
|
|
|
|
# Test de temps de filtrage
|
|
start_time = time.time()
|
|
|
|
search_term = 'clic'
|
|
filtered_actions = [
|
|
action for action in large_action_set
|
|
if (search_term.lower() in action['name'].lower() or
|
|
search_term.lower() in action['description'].lower())
|
|
]
|
|
|
|
filter_time = time.time() - start_time
|
|
|
|
# Vérifier que le filtrage est rapide (< 10ms pour 30 actions)
|
|
assert filter_time < 0.01, f"Filtrage trop lent: {filter_time:.3f}s"
|
|
|
|
# Test de conversion en masse
|
|
start_time = time.time()
|
|
|
|
step_templates = []
|
|
for action in large_action_set:
|
|
step_template = {
|
|
'id': action['id'],
|
|
'type': action['id'],
|
|
'name': action['name'],
|
|
'description': action['description'],
|
|
'icon': action['icon']
|
|
}
|
|
step_templates.append(step_template)
|
|
|
|
conversion_time = time.time() - start_time
|
|
|
|
# Vérifier que la conversion est rapide (< 5ms pour 30 actions)
|
|
assert conversion_time < 0.005, f"Conversion trop lente: {conversion_time:.3f}s"
|
|
|
|
print(f" ✅ Filtrage de {len(large_action_set)} actions: {filter_time:.3f}s")
|
|
print(f" ✅ Conversion de {len(large_action_set)} actions: {conversion_time:.3f}s")
|
|
print("✅ Performance validée")
|
|
|
|
def test_error_handling(self):
|
|
"""Test de la gestion d'erreurs"""
|
|
print("🧪 Test de la gestion d'erreurs...")
|
|
|
|
# Test avec action malformée
|
|
malformed_action = {
|
|
'id': 'malformed',
|
|
'name': 'Action Malformée',
|
|
# Manque description, category, etc.
|
|
}
|
|
|
|
# Vérifier que les champs manquants sont détectés
|
|
required_fields = ['description', 'category', 'icon', 'parameters', 'examples']
|
|
missing_fields = [field for field in required_fields if field not in malformed_action]
|
|
|
|
assert len(missing_fields) > 0, "Aucun champ manquant détecté"
|
|
print(f" ✅ Champs manquants détectés: {missing_fields}")
|
|
|
|
# Test avec catégorie invalide
|
|
invalid_category_action = {
|
|
'id': 'invalid_cat',
|
|
'name': 'Catégorie Invalide',
|
|
'description': 'Test',
|
|
'category': 'invalid_category',
|
|
'icon': '❌',
|
|
'parameters': {},
|
|
'examples': []
|
|
}
|
|
|
|
valid_categories = ['vision_ui', 'control', 'data', 'navigation', 'validation']
|
|
assert invalid_category_action['category'] not in valid_categories
|
|
print(f" ✅ Catégorie invalide détectée: {invalid_category_action['category']}")
|
|
|
|
# Test avec paramètre malformé
|
|
malformed_param_action = {
|
|
'id': 'malformed_param',
|
|
'name': 'Paramètre Malformé',
|
|
'description': 'Test',
|
|
'category': 'vision_ui',
|
|
'icon': '❌',
|
|
'parameters': {
|
|
'bad_param': {
|
|
# Manque 'type', 'required', 'description'
|
|
'value': 'test'
|
|
}
|
|
},
|
|
'examples': []
|
|
}
|
|
|
|
param = malformed_param_action['parameters']['bad_param']
|
|
param_required_fields = ['type', 'required', 'description']
|
|
param_missing_fields = [field for field in param_required_fields if field not in param]
|
|
|
|
assert len(param_missing_fields) > 0, "Aucun champ de paramètre manquant détecté"
|
|
print(f" ✅ Champs de paramètre manquants détectés: {param_missing_fields}")
|
|
|
|
print("✅ Gestion d'erreurs validée")
|
|
|
|
def test_integration_compatibility(self):
|
|
"""Test de la compatibilité avec l'intégration VWB existante"""
|
|
print("🧪 Test de la compatibilité d'intégration...")
|
|
|
|
# Vérifier que les actions du catalogue n'interfèrent pas avec les actions par défaut
|
|
default_categories = [
|
|
'actions-web',
|
|
'logique',
|
|
'donnees',
|
|
'controle'
|
|
]
|
|
|
|
catalog_categories = [
|
|
'catalog_vision_ui',
|
|
'catalog_control',
|
|
'catalog_data'
|
|
]
|
|
|
|
# Vérifier qu'il n'y a pas de collision d'IDs
|
|
for default_cat in default_categories:
|
|
for catalog_cat in catalog_categories:
|
|
assert default_cat != catalog_cat, f"Collision d'ID de catégorie: {default_cat}"
|
|
|
|
print(f" ✅ Pas de collision entre {len(default_categories)} catégories par défaut et {len(catalog_categories)} catégories catalogue")
|
|
|
|
# Vérifier que les types d'actions sont distincts
|
|
default_step_types = ['click', 'type', 'wait', 'condition', 'extract', 'scroll', 'navigate', 'screenshot']
|
|
catalog_step_types = [action['id'] for action in self.test_actions]
|
|
|
|
# Vérifier qu'il n'y a pas de collision de types
|
|
for default_type in default_step_types:
|
|
for catalog_type in catalog_step_types:
|
|
assert default_type != catalog_type, f"Collision de type d'étape: {default_type}"
|
|
|
|
print(f" ✅ Pas de collision entre {len(default_step_types)} types par défaut et {len(catalog_step_types)} types catalogue")
|
|
|
|
# Vérifier la compatibilité des formats de données
|
|
for action in self.test_actions:
|
|
# Format de drag pour actions catalogue
|
|
catalog_drag = f"catalog:{action['id']}"
|
|
|
|
# Vérifier que le format est parsable
|
|
assert ':' in catalog_drag
|
|
parts = catalog_drag.split(':')
|
|
assert len(parts) == 2
|
|
assert parts[0] == 'catalog'
|
|
assert parts[1] == action['id']
|
|
|
|
print(" ✅ Format de données compatible")
|
|
print("✅ Compatibilité d'intégration validée")
|
|
|
|
|
|
def run_tests():
|
|
"""Exécuter tous les tests de l'extension Palette VWB"""
|
|
print("🚀 Démarrage des tests d'extension Palette VWB...")
|
|
print("=" * 60)
|
|
|
|
test_instance = TestVWBPaletteExtension()
|
|
test_instance.setup_method()
|
|
|
|
tests = [
|
|
test_instance.test_catalog_actions_structure,
|
|
test_instance.test_category_metadata_mapping,
|
|
test_instance.test_step_template_conversion,
|
|
test_instance.test_drag_data_format,
|
|
test_instance.test_search_functionality,
|
|
test_instance.test_visual_indicators,
|
|
test_instance.test_tooltip_content,
|
|
test_instance.test_performance_considerations,
|
|
test_instance.test_error_handling,
|
|
test_instance.test_integration_compatibility,
|
|
]
|
|
|
|
passed = 0
|
|
failed = 0
|
|
|
|
for test in tests:
|
|
try:
|
|
test()
|
|
passed += 1
|
|
print()
|
|
except Exception as e:
|
|
print(f"❌ ÉCHEC: {e}")
|
|
failed += 1
|
|
print()
|
|
|
|
print("=" * 60)
|
|
print(f"📊 RÉSULTATS: {passed} réussis, {failed} échoués")
|
|
|
|
if failed == 0:
|
|
print("🎉 TOUS LES TESTS RÉUSSIS - Extension Palette VWB validée !")
|
|
return True
|
|
else:
|
|
print("⚠️ CERTAINS TESTS ONT ÉCHOUÉ - Corrections nécessaires")
|
|
return False
|
|
|
|
|
|
if __name__ == "__main__":
|
|
success = run_tests()
|
|
exit(0 if success else 1) |