#!/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)