Files
rpa_vision_v3/tests/unit/test_vwb_palette_extension_09jan2026.py
Dom a27b74cf22 v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- 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>
2026-01-29 11:23:51 +01:00

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)