""" Tests Unitaires - Contrats de Données VWB Auteur : Dom, Alice, Kiro - 09 janvier 2026 Tests complets pour les contrats de données du Visual Workflow Builder : - VWBActionError - VWBEvidence - VWBVisualAnchor Ces tests valident la sérialisation JSON, la validation des données, et le comportement des méthodes utilitaires. """ import unittest import json import base64 from datetime import datetime from unittest.mock import patch, MagicMock # Import des contrats VWB import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from visual_workflow_builder.backend.contracts.error import ( VWBActionError, VWBErrorType, VWBErrorSeverity, create_vwb_error ) from visual_workflow_builder.backend.contracts.evidence import ( VWBEvidence, VWBEvidenceType, create_screenshot_evidence, create_interaction_evidence ) from visual_workflow_builder.backend.contracts.visual_anchor import ( VWBVisualAnchor, VWBVisualAnchorType, create_image_anchor, create_text_anchor ) class TestVWBActionError(unittest.TestCase): """Tests pour VWBActionError.""" def setUp(self): """Configuration des tests.""" self.timestamp = datetime.now() self.error_data = { 'error_id': 'test_error_001', 'error_type': VWBErrorType.ELEMENT_NOT_FOUND, 'severity': VWBErrorSeverity.ERROR, 'message': 'Élément non trouvé', 'description': 'L\'élément bouton "Valider" n\'a pas été trouvé sur l\'écran', 'action_id': 'action_click_001', 'step_id': 'step_001', 'workflow_id': 'workflow_test', 'timestamp': self.timestamp, 'execution_time_ms': 1500.0, 'technical_details': {'screen_resolution': '1920x1080'}, 'stack_trace': 'Traceback...', 'suggestions': ['Vérifier que l\'élément est visible'], 'retry_possible': True, 'user_id': 'user_test', 'session_id': 'session_001', 'environment': 'test' } def test_error_creation(self): """Test de création d'une erreur VWB.""" error = VWBActionError(**self.error_data) self.assertEqual(error.error_id, 'test_error_001') self.assertEqual(error.error_type, VWBErrorType.ELEMENT_NOT_FOUND) self.assertEqual(error.severity, VWBErrorSeverity.ERROR) self.assertEqual(error.message, 'Élément non trouvé') self.assertEqual(error.action_id, 'action_click_001') self.assertTrue(error.retry_possible) def test_error_auto_id_generation(self): """Test de génération automatique de l'ID d'erreur.""" data = self.error_data.copy() data['error_id'] = '' error = VWBActionError(**data) self.assertTrue(error.error_id.startswith('err_action_click_001_')) self.assertGreater(len(error.error_id), 20) def test_default_suggestions_generation(self): """Test de génération des suggestions par défaut.""" data = self.error_data.copy() data['suggestions'] = [] error = VWBActionError(**data) self.assertGreater(len(error.suggestions), 0) self.assertIn('Vérifiez que l\'élément est visible à l\'écran', error.suggestions) def test_error_serialization(self): """Test de sérialisation JSON.""" error = VWBActionError(**self.error_data) # Test to_dict error_dict = error.to_dict() self.assertEqual(error_dict['error_type'], 'element_not_found') self.assertEqual(error_dict['severity'], 'error') self.assertIsInstance(error_dict['timestamp'], str) # Test to_json error_json = error.to_json() self.assertIsInstance(error_json, str) parsed = json.loads(error_json) self.assertEqual(parsed['error_type'], 'element_not_found') def test_error_deserialization(self): """Test de désérialisation JSON.""" error = VWBActionError(**self.error_data) error_dict = error.to_dict() # Test from_dict restored_error = VWBActionError.from_dict(error_dict) self.assertEqual(restored_error.error_id, error.error_id) self.assertEqual(restored_error.error_type, error.error_type) self.assertEqual(restored_error.severity, error.severity) # Test from_json error_json = error.to_json() restored_from_json = VWBActionError.from_json(error_json) self.assertEqual(restored_from_json.message, error.message) def test_is_retryable(self): """Test de la logique de retry.""" # Erreur retryable error = VWBActionError(**self.error_data) self.assertTrue(error.is_retryable()) # Erreur non retryable (paramètre invalide) data = self.error_data.copy() data['error_type'] = VWBErrorType.PARAMETER_INVALID error_non_retryable = VWBActionError(**data) self.assertFalse(error_non_retryable.is_retryable()) # Erreur fatale data = self.error_data.copy() data['severity'] = VWBErrorSeverity.FATAL error_fatal = VWBActionError(**data) self.assertFalse(error_fatal.is_retryable()) def test_user_friendly_message(self): """Test des messages conviviaux.""" error = VWBActionError(**self.error_data) friendly_msg = error.get_user_friendly_message() self.assertIn('❌', friendly_msg) self.assertIn('Élément non trouvé sur l\'écran', friendly_msg) def test_create_vwb_error_utility(self): """Test de la fonction utilitaire create_vwb_error.""" error = create_vwb_error( error_type=VWBErrorType.CLICK_FAILED, message='Clic échoué', action_id='action_001', step_id='step_001', severity=VWBErrorSeverity.WARNING ) self.assertEqual(error.error_type, VWBErrorType.CLICK_FAILED) self.assertEqual(error.severity, VWBErrorSeverity.WARNING) self.assertEqual(error.message, 'Clic échoué') class TestVWBEvidence(unittest.TestCase): """Tests pour VWBEvidence.""" def setUp(self): """Configuration des tests.""" self.timestamp = datetime.now() # Créer un screenshot base64 factice self.fake_screenshot = base64.b64encode(b'fake_image_data').decode('utf-8') self.evidence_data = { 'evidence_id': 'ev_test_001', 'evidence_type': VWBEvidenceType.SCREENSHOT_BEFORE, 'action_id': 'action_001', 'step_id': 'step_001', 'workflow_id': 'workflow_test', 'timestamp': self.timestamp, 'execution_time_ms': 500.0, 'title': 'Capture avant clic', 'description': 'Screenshot avant l\'action de clic', 'screenshot_base64': self.fake_screenshot, 'screenshot_width': 1920, 'screenshot_height': 1080, 'highlight_box': {'x': 100, 'y': 200, 'width': 150, 'height': 50}, 'data': {'confidence': 0.95}, 'success': True, 'confidence_score': 0.95, 'user_id': 'user_test', 'session_id': 'session_001', 'related_evidence_ids': [], 'parent_evidence_id': None } def test_evidence_creation(self): """Test de création d'une evidence VWB.""" evidence = VWBEvidence(**self.evidence_data) self.assertEqual(evidence.evidence_id, 'ev_test_001') self.assertEqual(evidence.evidence_type, VWBEvidenceType.SCREENSHOT_BEFORE) self.assertEqual(evidence.title, 'Capture avant clic') self.assertTrue(evidence.success) self.assertEqual(evidence.screenshot_width, 1920) def test_evidence_auto_id_generation(self): """Test de génération automatique de l'ID d'evidence.""" data = self.evidence_data.copy() data['evidence_id'] = '' evidence = VWBEvidence(**data) self.assertTrue(evidence.evidence_id.startswith('ev_action_001_')) def test_has_screenshot(self): """Test de détection de screenshot.""" evidence = VWBEvidence(**self.evidence_data) self.assertTrue(evidence.has_screenshot()) # Sans screenshot data = self.evidence_data.copy() data['screenshot_base64'] = None evidence_no_screenshot = VWBEvidence(**data) self.assertFalse(evidence_no_screenshot.has_screenshot()) def test_has_highlight(self): """Test de détection de zone surlignée.""" evidence = VWBEvidence(**self.evidence_data) self.assertTrue(evidence.has_highlight()) # Sans highlight data = self.evidence_data.copy() data['highlight_box'] = None evidence_no_highlight = VWBEvidence(**data) self.assertFalse(evidence_no_highlight.has_highlight()) def test_get_screenshot_data_url(self): """Test de génération d'URL data pour screenshot.""" evidence = VWBEvidence(**self.evidence_data) data_url = evidence.get_screenshot_data_url() self.assertIsNotNone(data_url) self.assertTrue(data_url.startswith('data:image/png;base64,')) self.assertIn(self.fake_screenshot, data_url) def test_evidence_serialization(self): """Test de sérialisation JSON.""" evidence = VWBEvidence(**self.evidence_data) # Test to_dict evidence_dict = evidence.to_dict() self.assertEqual(evidence_dict['evidence_type'], 'screenshot_before') self.assertIsInstance(evidence_dict['timestamp'], str) # Test to_json evidence_json = evidence.to_json() self.assertIsInstance(evidence_json, str) parsed = json.loads(evidence_json) self.assertEqual(parsed['evidence_type'], 'screenshot_before') def test_evidence_deserialization(self): """Test de désérialisation JSON.""" evidence = VWBEvidence(**self.evidence_data) evidence_dict = evidence.to_dict() # Test from_dict restored_evidence = VWBEvidence.from_dict(evidence_dict) self.assertEqual(restored_evidence.evidence_id, evidence.evidence_id) self.assertEqual(restored_evidence.evidence_type, evidence.evidence_type) # Test from_json evidence_json = evidence.to_json() restored_from_json = VWBEvidence.from_json(evidence_json) self.assertEqual(restored_from_json.title, evidence.title) def test_evidence_type_checks(self): """Test des vérifications de type d'evidence.""" evidence = VWBEvidence(**self.evidence_data) self.assertTrue(evidence.is_visual_evidence()) self.assertFalse(evidence.is_interaction_evidence()) # Test avec evidence d'interaction data = self.evidence_data.copy() data['evidence_type'] = VWBEvidenceType.CLICK_EVIDENCE interaction_evidence = VWBEvidence(**data) self.assertFalse(interaction_evidence.is_visual_evidence()) self.assertTrue(interaction_evidence.is_interaction_evidence()) def test_file_size_calculation(self): """Test du calcul de taille de fichier.""" evidence = VWBEvidence(**self.evidence_data) file_size = evidence.get_file_size_mb() self.assertIsInstance(file_size, float) self.assertGreater(file_size, 0) def test_create_screenshot_evidence_utility(self): """Test de la fonction utilitaire create_screenshot_evidence.""" evidence = create_screenshot_evidence( action_id='action_001', step_id='step_001', screenshot_base64=self.fake_screenshot, title='Test screenshot' ) self.assertEqual(evidence.evidence_type, VWBEvidenceType.SCREENSHOT_BEFORE) self.assertEqual(evidence.action_id, 'action_001') self.assertEqual(evidence.title, 'Test screenshot') self.assertTrue(evidence.has_screenshot()) def test_create_interaction_evidence_utility(self): """Test de la fonction utilitaire create_interaction_evidence.""" interaction_data = {'click_x': 100, 'click_y': 200} evidence = create_interaction_evidence( action_id='action_001', step_id='step_001', evidence_type=VWBEvidenceType.CLICK_EVIDENCE, title='Clic effectué', interaction_data=interaction_data ) self.assertEqual(evidence.evidence_type, VWBEvidenceType.CLICK_EVIDENCE) self.assertEqual(evidence.data, interaction_data) self.assertTrue(evidence.is_interaction_evidence()) class TestVWBVisualAnchor(unittest.TestCase): """Tests pour VWBVisualAnchor.""" def setUp(self): """Configuration des tests.""" self.timestamp = datetime.now() self.fake_image = base64.b64encode(b'fake_anchor_image').decode('utf-8') self.anchor_data = { 'anchor_id': 'anchor_test_001', 'anchor_type': VWBVisualAnchorType.IMAGE_TEMPLATE, 'name': 'Bouton Valider', 'description': 'Bouton de validation du formulaire', 'reference_image_base64': self.fake_image, 'reference_width': 120, 'reference_height': 40, 'bounding_box': {'x': 500, 'y': 300, 'width': 120, 'height': 40}, 'search_criteria': {'color_tolerance': 10}, 'confidence_threshold': 0.8, 'max_search_time_ms': 5000, 'retry_count': 3, 'visual_embedding': [0.1, 0.2, 0.3], 'embedding_model': 'clip-vit-base', 'created_by': 'user_test', 'created_at': self.timestamp, 'last_used_at': None, 'usage_count': 0, 'success_rate': 0.0, 'average_match_time_ms': 0.0, 'screen_resolution': (1920, 1080), 'application_context': 'web_browser', 'is_active': True, 'validation_hash': None } def test_anchor_creation(self): """Test de création d'une ancre VWB.""" anchor = VWBVisualAnchor(**self.anchor_data) self.assertEqual(anchor.anchor_id, 'anchor_test_001') self.assertEqual(anchor.anchor_type, VWBVisualAnchorType.IMAGE_TEMPLATE) self.assertEqual(anchor.name, 'Bouton Valider') self.assertEqual(anchor.confidence_threshold, 0.8) self.assertTrue(anchor.is_active) self.assertIsNotNone(anchor.validation_hash) def test_anchor_auto_id_generation(self): """Test de génération automatique de l'ID d'ancre.""" data = self.anchor_data.copy() data['anchor_id'] = '' anchor = VWBVisualAnchor(**data) self.assertTrue(anchor.anchor_id.startswith('anchor_bouton_valider_')) def test_has_reference_image(self): """Test de détection d'image de référence.""" anchor = VWBVisualAnchor(**self.anchor_data) self.assertTrue(anchor.has_reference_image()) # Sans image data = self.anchor_data.copy() data['reference_image_base64'] = None anchor_no_image = VWBVisualAnchor(**data) self.assertFalse(anchor_no_image.has_reference_image()) def test_has_bounding_box(self): """Test de détection de bounding box.""" anchor = VWBVisualAnchor(**self.anchor_data) self.assertTrue(anchor.has_bounding_box()) # Sans bounding box data = self.anchor_data.copy() data['bounding_box'] = None anchor_no_bbox = VWBVisualAnchor(**data) self.assertFalse(anchor_no_bbox.has_bounding_box()) def test_has_visual_embedding(self): """Test de détection d'embedding visuel.""" anchor = VWBVisualAnchor(**self.anchor_data) self.assertTrue(anchor.has_visual_embedding()) # Sans embedding data = self.anchor_data.copy() data['visual_embedding'] = None anchor_no_embedding = VWBVisualAnchor(**data) self.assertFalse(anchor_no_embedding.has_visual_embedding()) def test_update_usage_stats(self): """Test de mise à jour des statistiques d'utilisation.""" anchor = VWBVisualAnchor(**self.anchor_data) # Premier usage réussi anchor.update_usage_stats(1000.0, True) self.assertEqual(anchor.usage_count, 1) self.assertEqual(anchor.success_rate, 1.0) self.assertEqual(anchor.average_match_time_ms, 1000.0) self.assertIsNotNone(anchor.last_used_at) # Deuxième usage échoué anchor.update_usage_stats(2000.0, False) self.assertEqual(anchor.usage_count, 2) self.assertEqual(anchor.success_rate, 0.5) self.assertEqual(anchor.average_match_time_ms, 1500.0) def test_reliability_checks(self): """Test des vérifications de fiabilité.""" anchor = VWBVisualAnchor(**self.anchor_data) # Ancre neuve - pas encore fiable self.assertFalse(anchor.is_reliable()) self.assertFalse(anchor.needs_optimization()) # Simuler des usages réussis for _ in range(5): anchor.update_usage_stats(1000.0, True) self.assertTrue(anchor.is_reliable()) self.assertFalse(anchor.needs_optimization()) # Simuler des échecs for _ in range(10): anchor.update_usage_stats(8000.0, False) self.assertFalse(anchor.is_reliable()) self.assertTrue(anchor.needs_optimization()) def test_resolution_compatibility(self): """Test de compatibilité de résolution.""" anchor = VWBVisualAnchor(**self.anchor_data) # Résolution identique self.assertTrue(anchor.is_compatible_with_resolution(1920, 1080)) # Résolution proche (dans la tolérance) self.assertTrue(anchor.is_compatible_with_resolution(1900, 1070)) # Résolution trop différente self.assertFalse(anchor.is_compatible_with_resolution(1280, 720)) def test_search_area_calculation(self): """Test du calcul de zone de recherche.""" anchor = VWBVisualAnchor(**self.anchor_data) # Même résolution search_area = anchor.get_search_area(1920, 1080) self.assertEqual(search_area, anchor.bounding_box) # Résolution différente (scaling) search_area_scaled = anchor.get_search_area(960, 540) # Moitié expected = { 'x': 250, # 500 / 2 'y': 150, # 300 / 2 'width': 60, # 120 / 2 'height': 20 # 40 / 2 } self.assertEqual(search_area_scaled, expected) def test_anchor_serialization(self): """Test de sérialisation JSON.""" anchor = VWBVisualAnchor(**self.anchor_data) # Test to_dict anchor_dict = anchor.to_dict() self.assertEqual(anchor_dict['anchor_type'], 'image_template') self.assertIsInstance(anchor_dict['created_at'], str) # Test to_json anchor_json = anchor.to_json() self.assertIsInstance(anchor_json, str) parsed = json.loads(anchor_json) self.assertEqual(parsed['anchor_type'], 'image_template') def test_anchor_deserialization(self): """Test de désérialisation JSON.""" anchor = VWBVisualAnchor(**self.anchor_data) anchor_dict = anchor.to_dict() # Test from_dict restored_anchor = VWBVisualAnchor.from_dict(anchor_dict) self.assertEqual(restored_anchor.anchor_id, anchor.anchor_id) self.assertEqual(restored_anchor.anchor_type, anchor.anchor_type) # Test from_json anchor_json = anchor.to_json() restored_from_json = VWBVisualAnchor.from_json(anchor_json) self.assertEqual(restored_from_json.name, anchor.name) def test_create_image_anchor_utility(self): """Test de la fonction utilitaire create_image_anchor.""" anchor = create_image_anchor( name='Test Button', reference_image_base64=self.fake_image, created_by='user_test', confidence_threshold=0.9 ) self.assertEqual(anchor.anchor_type, VWBVisualAnchorType.IMAGE_TEMPLATE) self.assertEqual(anchor.name, 'Test Button') self.assertEqual(anchor.confidence_threshold, 0.9) self.assertTrue(anchor.has_reference_image()) def test_create_text_anchor_utility(self): """Test de la fonction utilitaire create_text_anchor.""" anchor = create_text_anchor( name='Submit Text', text_pattern='Valider', created_by='user_test' ) self.assertEqual(anchor.anchor_type, VWBVisualAnchorType.TEXT_EXACT) self.assertEqual(anchor.name, 'Submit Text') self.assertEqual(anchor.search_criteria['text_pattern'], 'Valider') class TestContractsIntegration(unittest.TestCase): """Tests d'intégration entre les contrats VWB.""" def test_error_evidence_relationship(self): """Test de relation entre erreur et evidence.""" # Créer une erreur error = create_vwb_error( error_type=VWBErrorType.ELEMENT_NOT_FOUND, message='Élément non trouvé', action_id='action_001', step_id='step_001' ) # Créer une evidence d'erreur liée evidence = create_screenshot_evidence( action_id=error.action_id, step_id=error.step_id, screenshot_base64=base64.b64encode(b'error_screenshot').decode('utf-8'), evidence_type=VWBEvidenceType.SCREENSHOT_ERROR, title='Screenshot d\'erreur' ) # Vérifier la cohérence self.assertEqual(error.action_id, evidence.action_id) self.assertEqual(error.step_id, evidence.step_id) self.assertEqual(evidence.evidence_type, VWBEvidenceType.SCREENSHOT_ERROR) def test_anchor_evidence_workflow(self): """Test de workflow ancre → evidence.""" # Créer une ancre anchor = create_image_anchor( name='Login Button', reference_image_base64=base64.b64encode(b'button_image').decode('utf-8'), created_by='user_test' ) # Simuler une utilisation réussie avec evidence evidence = create_interaction_evidence( action_id='click_login', step_id='step_001', evidence_type=VWBEvidenceType.CLICK_EVIDENCE, title='Clic sur bouton login', interaction_data={ 'anchor_id': anchor.anchor_id, 'click_coordinates': {'x': 100, 'y': 200}, 'match_confidence': 0.95 } ) # Mettre à jour les stats de l'ancre anchor.update_usage_stats(1200.0, True) # Vérifications self.assertEqual(evidence.data['anchor_id'], anchor.anchor_id) self.assertEqual(anchor.usage_count, 1) self.assertEqual(anchor.success_rate, 1.0) self.assertTrue(evidence.success) if __name__ == '__main__': # Configuration des tests unittest.TestCase.maxDiff = None # Exécution des tests print("=" * 70) print(" TESTS UNITAIRES - CONTRATS DE DONNÉES VWB") print("=" * 70) print("Auteur : Dom, Alice, Kiro - 09 janvier 2026") print("") # Créer la suite de tests loader = unittest.TestLoader() suite = unittest.TestSuite() # Ajouter les classes de tests suite.addTests(loader.loadTestsFromTestCase(TestVWBActionError)) suite.addTests(loader.loadTestsFromTestCase(TestVWBEvidence)) suite.addTests(loader.loadTestsFromTestCase(TestVWBVisualAnchor)) suite.addTests(loader.loadTestsFromTestCase(TestContractsIntegration)) # Exécuter les tests runner = unittest.TextTestRunner(verbosity=2) result = runner.run(suite) # Résumé print("\n" + "=" * 70) print(f"RÉSUMÉ DES TESTS CONTRATS VWB") print("=" * 70) print(f"Tests exécutés : {result.testsRun}") print(f"Succès : {result.testsRun - len(result.failures) - len(result.errors)}") print(f"Échecs : {len(result.failures)}") print(f"Erreurs : {len(result.errors)}") if result.failures: print("\nÉCHECS :") for test, traceback in result.failures: print(f"- {test}: {traceback}") if result.errors: print("\nERREURS :") for test, traceback in result.errors: print(f"- {test}: {traceback}") success_rate = ((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun) * 100 print(f"\nTaux de succès : {success_rate:.1f}%") if success_rate == 100.0: print("🎉 TOUS LES TESTS CONTRATS VWB SONT RÉUSSIS !") else: print("⚠️ Certains tests ont échoué - vérification nécessaire")