""" Tests Unitaires - Actions VWB Auteur : Dom, Alice, Kiro - 09 janvier 2026 Tests complets pour les actions VisionOnly du Visual Workflow Builder : - BaseVWBAction - VWBClickAnchorAction - VWBTypeTextAction - VWBWaitForAnchorAction Ces tests valident l'exécution des actions, la gestion d'erreurs, et l'intégration avec le ScreenCapturer. """ import unittest import time from datetime import datetime from unittest.mock import Mock, patch, MagicMock import numpy as np # Import des actions VWB import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from visual_workflow_builder.backend.actions.base_action import ( BaseVWBAction, VWBActionResult, VWBActionStatus ) from visual_workflow_builder.backend.actions.vision_ui.click_anchor import VWBClickAnchorAction from visual_workflow_builder.backend.actions.vision_ui.type_text import VWBTypeTextAction from visual_workflow_builder.backend.actions.vision_ui.wait_for_anchor import VWBWaitForAnchorAction from visual_workflow_builder.backend.contracts.error import VWBErrorType, VWBErrorSeverity from visual_workflow_builder.backend.contracts.evidence import VWBEvidenceType from visual_workflow_builder.backend.contracts.visual_anchor import ( VWBVisualAnchor, VWBVisualAnchorType, create_image_anchor ) class MockScreenCapturer: """Mock du ScreenCapturer pour les tests.""" def __init__(self, should_fail=False): self.should_fail = should_fail self.capture_count = 0 def capture(self): """Simule une capture d'écran.""" self.capture_count += 1 if self.should_fail: return None # Retourner une image factice (1920x1080 RGB) return np.random.randint(0, 255, (1080, 1920, 3), dtype=np.uint8) class ConcreteTestAction(BaseVWBAction): """Action concrète pour tester BaseVWBAction.""" def __init__(self, action_id: str, parameters: dict, should_fail=False): super().__init__( action_id=action_id, name="Test Action", description="Action de test", parameters=parameters ) self.should_fail = should_fail self.execute_core_called = False def validate_parameters(self): """Validation simple pour les tests.""" errors = [] if not self.parameters.get('required_param'): errors.append("required_param manquant") return errors def execute_core(self, step_id: str): """Exécution de test.""" self.execute_core_called = True if self.should_fail: raise ValueError("Test failure") return VWBActionResult( action_id=self.action_id, step_id=step_id, status=VWBActionStatus.SUCCESS, start_time=datetime.now(), end_time=datetime.now(), execution_time_ms=100.0, output_data={'test': 'success'}, evidence_list=[] ) class TestBaseVWBAction(unittest.TestCase): """Tests pour BaseVWBAction.""" def setUp(self): """Configuration des tests.""" self.mock_capturer = MockScreenCapturer() self.valid_parameters = {'required_param': 'test_value'} self.invalid_parameters = {} def test_action_creation(self): """Test de création d'une action.""" action = ConcreteTestAction( action_id='test_001', parameters=self.valid_parameters ) self.assertEqual(action.action_id, 'test_001') self.assertEqual(action.name, 'Test Action') self.assertEqual(action.current_status, VWBActionStatus.PENDING) self.assertIsNone(action.current_result) def test_parameter_validation_success(self): """Test de validation réussie des paramètres.""" action = ConcreteTestAction( action_id='test_001', parameters=self.valid_parameters ) errors = action.validate_parameters() self.assertEqual(len(errors), 0) def test_parameter_validation_failure(self): """Test de validation échouée des paramètres.""" action = ConcreteTestAction( action_id='test_001', parameters=self.invalid_parameters ) errors = action.validate_parameters() self.assertGreater(len(errors), 0) self.assertIn("required_param manquant", errors) def test_successful_execution(self): """Test d'exécution réussie.""" action = ConcreteTestAction( action_id='test_001', parameters=self.valid_parameters ) action.screen_capturer = self.mock_capturer result = action.execute('step_001') self.assertTrue(action.execute_core_called) self.assertEqual(result.status, VWBActionStatus.SUCCESS) self.assertEqual(result.action_id, 'test_001') self.assertEqual(result.step_id, 'step_001') self.assertGreater(result.execution_time_ms, 0) def test_execution_with_parameter_error(self): """Test d'exécution avec erreur de paramètres.""" action = ConcreteTestAction( action_id='test_001', parameters=self.invalid_parameters ) result = action.execute('step_001') self.assertFalse(action.execute_core_called) self.assertEqual(result.status, VWBActionStatus.FAILED) self.assertIsNotNone(result.error) self.assertEqual(result.error.error_type, VWBErrorType.PARAMETER_INVALID) def test_execution_with_exception(self): """Test d'exécution avec exception.""" action = ConcreteTestAction( action_id='test_001', parameters=self.valid_parameters, should_fail=True ) action.screen_capturer = self.mock_capturer result = action.execute('step_001') self.assertTrue(action.execute_core_called) self.assertEqual(result.status, VWBActionStatus.FAILED) self.assertIsNotNone(result.error) self.assertEqual(result.error.error_type, VWBErrorType.SYSTEM_ERROR) def test_retry_mechanism(self): """Test du mécanisme de retry.""" action = ConcreteTestAction( action_id='test_001', parameters={'required_param': 'test', 'retry_count': 2}, should_fail=True ) action.screen_capturer = self.mock_capturer result = action.execute('step_001') # Vérifier que execute_core a été appelé plusieurs fois self.assertTrue(action.execute_core_called) self.assertEqual(result.retry_count, 2) # Dernier essai self.assertEqual(result.status, VWBActionStatus.FAILED) class TestVWBClickAnchorAction(unittest.TestCase): """Tests pour VWBClickAnchorAction.""" def setUp(self): """Configuration des tests.""" self.mock_capturer = MockScreenCapturer() # Créer une ancre visuelle de test self.test_anchor = create_image_anchor( name="Bouton Test", reference_image_base64="fake_image_data", created_by="test_user", bounding_box={'x': 100, 'y': 200, 'width': 120, 'height': 40} ) self.valid_parameters = { 'visual_anchor': self.test_anchor, 'click_type': 'left', 'confidence_threshold': 0.8 } def test_click_action_creation(self): """Test de création d'une action de clic.""" action = VWBClickAnchorAction( action_id='click_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) self.assertEqual(action.action_id, 'click_001') self.assertEqual(action.name, 'Clic sur Ancre Visuelle') self.assertEqual(action.visual_anchor, self.test_anchor) self.assertEqual(action.click_type, 'left') def test_click_parameter_validation_success(self): """Test de validation réussie des paramètres de clic.""" action = VWBClickAnchorAction( action_id='click_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) errors = action.validate_parameters() self.assertEqual(len(errors), 0) def test_click_parameter_validation_failure(self): """Test de validation échouée des paramètres de clic.""" invalid_params = { 'visual_anchor': None, 'click_type': 'invalid', 'confidence_threshold': 1.5 } action = VWBClickAnchorAction( action_id='click_001', parameters=invalid_params ) errors = action.validate_parameters() self.assertGreater(len(errors), 0) self.assertTrue(any("Ancre visuelle requise" in error for error in errors)) self.assertTrue(any("Type de clic invalide" in error for error in errors)) def test_successful_click_execution(self): """Test d'exécution réussie du clic.""" action = VWBClickAnchorAction( action_id='click_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) result = action.execute('step_001') self.assertEqual(result.status, VWBActionStatus.SUCCESS) self.assertTrue(result.output_data['anchor_found']) self.assertGreater(result.output_data['anchor_confidence'], 0) # Vérifier qu'il y a au moins 1 evidence (peut y avoir screenshot avant + interaction) self.assertGreaterEqual(len(result.evidence_list), 1) # Vérifier qu'il y a une evidence de clic click_evidence = None for evidence in result.evidence_list: if evidence.evidence_type == VWBEvidenceType.CLICK_EVIDENCE: click_evidence = evidence break self.assertIsNotNone(click_evidence) def test_click_with_screen_capture_failure(self): """Test de clic avec échec de capture d'écran.""" failing_capturer = MockScreenCapturer(should_fail=True) action = VWBClickAnchorAction( action_id='click_001', parameters=self.valid_parameters, screen_capturer=failing_capturer ) result = action.execute('step_001') self.assertEqual(result.status, VWBActionStatus.FAILED) self.assertIsNotNone(result.error) self.assertEqual(result.error.error_type, VWBErrorType.SCREEN_CAPTURE_FAILED) def test_click_action_info(self): """Test des informations de l'action de clic.""" action = VWBClickAnchorAction( action_id='click_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) info = action.get_action_info() self.assertEqual(info['action_id'], 'click_001') self.assertEqual(info['type'], 'click_anchor') self.assertEqual(info['parameters']['anchor_name'], 'Bouton Test') self.assertEqual(info['parameters']['click_type'], 'left') class TestVWBTypeTextAction(unittest.TestCase): """Tests pour VWBTypeTextAction.""" def setUp(self): """Configuration des tests.""" self.mock_capturer = MockScreenCapturer() # Créer une ancre pour champ de saisie self.input_anchor = create_image_anchor( name="Champ Email", reference_image_base64="fake_input_image", created_by="test_user", bounding_box={'x': 200, 'y': 300, 'width': 200, 'height': 30} ) self.valid_parameters = { 'visual_anchor': self.input_anchor, 'text_to_type': 'test@example.com', 'clear_field_first': True, 'click_before_typing': True } def test_type_action_creation(self): """Test de création d'une action de saisie.""" action = VWBTypeTextAction( action_id='type_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) self.assertEqual(action.action_id, 'type_001') self.assertEqual(action.name, 'Saisie de Texte') self.assertEqual(action.text_to_type, 'test@example.com') self.assertTrue(action.clear_field_first) self.assertTrue(action.click_before_typing) def test_type_parameter_validation_success(self): """Test de validation réussie des paramètres de saisie.""" action = VWBTypeTextAction( action_id='type_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) errors = action.validate_parameters() self.assertEqual(len(errors), 0) def test_type_parameter_validation_failure(self): """Test de validation échouée des paramètres de saisie.""" invalid_params = { 'visual_anchor': None, 'text_to_type': 123, # Doit être une string 'typing_speed_ms': -1 # Doit être positif } action = VWBTypeTextAction( action_id='type_001', parameters=invalid_params ) errors = action.validate_parameters() self.assertGreater(len(errors), 0) self.assertTrue(any("Ancre visuelle requise" in error for error in errors)) self.assertTrue(any("doit être une chaîne" in error for error in errors)) def test_successful_type_execution(self): """Test d'exécution réussie de la saisie.""" action = VWBTypeTextAction( action_id='type_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) result = action.execute('step_001') self.assertEqual(result.status, VWBActionStatus.SUCCESS) self.assertEqual(result.output_data['text_typed'], 'test@example.com') self.assertTrue(result.output_data['field_cleared']) # Vérifier qu'il y a au moins 1 evidence (peut y avoir screenshot avant + interaction) self.assertGreaterEqual(len(result.evidence_list), 1) # Vérifier qu'il y a une evidence de saisie type_evidence = None for evidence in result.evidence_list: if evidence.evidence_type == VWBEvidenceType.TYPE_EVIDENCE: type_evidence = evidence break self.assertIsNotNone(type_evidence) def test_type_action_info(self): """Test des informations de l'action de saisie.""" action = VWBTypeTextAction( action_id='type_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) info = action.get_action_info() self.assertEqual(info['action_id'], 'type_001') self.assertEqual(info['type'], 'type_text') self.assertEqual(info['parameters']['text_to_type'], 'test@example.com') self.assertEqual(info['parameters']['anchor_name'], 'Champ Email') class TestVWBWaitForAnchorAction(unittest.TestCase): """Tests pour VWBWaitForAnchorAction.""" def setUp(self): """Configuration des tests.""" self.mock_capturer = MockScreenCapturer() # Créer une ancre pour l'attente self.wait_anchor = create_image_anchor( name="Loading Spinner", reference_image_base64="fake_spinner_image", created_by="test_user" ) self.valid_parameters = { 'visual_anchor': self.wait_anchor, 'wait_mode': 'appear', 'max_wait_time_ms': 5000, 'check_interval_ms': 100 } def test_wait_action_creation(self): """Test de création d'une action d'attente.""" action = VWBWaitForAnchorAction( action_id='wait_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) self.assertEqual(action.action_id, 'wait_001') self.assertEqual(action.name, 'Attente d\'Ancre Visuelle') self.assertEqual(action.wait_mode, 'appear') self.assertEqual(action.max_wait_time_ms, 5000) def test_wait_parameter_validation_success(self): """Test de validation réussie des paramètres d'attente.""" action = VWBWaitForAnchorAction( action_id='wait_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) errors = action.validate_parameters() self.assertEqual(len(errors), 0) def test_wait_parameter_validation_failure(self): """Test de validation échouée des paramètres d'attente.""" invalid_params = { 'visual_anchor': None, 'wait_mode': 'invalid_mode', 'max_wait_time_ms': -1000, 'check_interval_ms': 10000 # Plus grand que max_wait_time_ms } action = VWBWaitForAnchorAction( action_id='wait_001', parameters=invalid_params ) errors = action.validate_parameters() self.assertGreater(len(errors), 0) self.assertTrue(any("Ancre visuelle requise" in error for error in errors)) self.assertTrue(any("Mode d'attente invalide" in error for error in errors)) def test_successful_wait_execution(self): """Test d'exécution réussie de l'attente.""" # Paramètres pour une attente courte short_wait_params = self.valid_parameters.copy() short_wait_params['max_wait_time_ms'] = 1000 short_wait_params['check_interval_ms'] = 100 action = VWBWaitForAnchorAction( action_id='wait_001', parameters=short_wait_params, screen_capturer=self.mock_capturer ) result = action.execute('step_001') # Le résultat peut être SUCCESS ou TIMEOUT selon la simulation self.assertIn(result.status, [VWBActionStatus.SUCCESS, VWBActionStatus.TIMEOUT]) if result.status == VWBActionStatus.SUCCESS: self.assertTrue(result.output_data['condition_met']) self.assertGreaterEqual(len(result.evidence_list), 1) # Au moins 1 evidence # Vérifier que la première evidence est du bon type wait_evidence = None for evidence in result.evidence_list: if evidence.evidence_type == VWBEvidenceType.WAIT_EVIDENCE: wait_evidence = evidence break self.assertIsNotNone(wait_evidence) def test_wait_timeout(self): """Test de timeout de l'attente.""" # Paramètres pour un timeout rapide timeout_params = self.valid_parameters.copy() timeout_params['max_wait_time_ms'] = 200 # Très court timeout_params['check_interval_ms'] = 50 timeout_params['wait_mode'] = 'disappear' # Mode difficile à satisfaire action = VWBWaitForAnchorAction( action_id='wait_001', parameters=timeout_params, screen_capturer=self.mock_capturer ) result = action.execute('step_001') # Devrait timeout self.assertEqual(result.status, VWBActionStatus.TIMEOUT) self.assertIsNotNone(result.error) self.assertEqual(result.error.error_type, VWBErrorType.WAIT_TIMEOUT) self.assertTrue(result.output_data['timeout_reached']) def test_wait_action_info(self): """Test des informations de l'action d'attente.""" action = VWBWaitForAnchorAction( action_id='wait_001', parameters=self.valid_parameters, screen_capturer=self.mock_capturer ) info = action.get_action_info() self.assertEqual(info['action_id'], 'wait_001') self.assertEqual(info['type'], 'wait_for_anchor') self.assertEqual(info['parameters']['wait_mode'], 'appear') self.assertEqual(info['parameters']['anchor_name'], 'Loading Spinner') class TestActionsIntegration(unittest.TestCase): """Tests d'intégration entre les actions VWB.""" def setUp(self): """Configuration des tests d'intégration.""" self.mock_capturer = MockScreenCapturer() # Créer des ancres pour différents types d'actions self.button_anchor = create_image_anchor( name="Submit Button", reference_image_base64="fake_button_image", created_by="test_user" ) self.input_anchor = create_image_anchor( name="Username Field", reference_image_base64="fake_input_image", created_by="test_user" ) def test_sequential_actions_workflow(self): """Test d'un workflow séquentiel d'actions.""" # Action 1: Saisir du texte type_action = VWBTypeTextAction( action_id='type_001', parameters={ 'visual_anchor': self.input_anchor, 'text_to_type': 'testuser', 'clear_field_first': True }, screen_capturer=self.mock_capturer ) # Action 2: Cliquer sur le bouton click_action = VWBClickAnchorAction( action_id='click_001', parameters={ 'visual_anchor': self.button_anchor, 'click_type': 'left' }, screen_capturer=self.mock_capturer ) # Exécuter les actions en séquence type_result = type_action.execute('step_001') click_result = click_action.execute('step_002') # Vérifier les résultats self.assertEqual(type_result.status, VWBActionStatus.SUCCESS) self.assertEqual(click_result.status, VWBActionStatus.SUCCESS) # Vérifier que les ancres ont été utilisées self.assertEqual(self.input_anchor.usage_count, 1) self.assertEqual(self.button_anchor.usage_count, 1) def test_action_evidence_chain(self): """Test de la chaîne d'evidence entre actions.""" click_action = VWBClickAnchorAction( action_id='click_001', parameters={ 'visual_anchor': self.button_anchor, 'click_type': 'left' }, screen_capturer=self.mock_capturer ) result = click_action.execute('step_001') # Vérifier la présence d'evidence self.assertGreater(len(result.evidence_list), 0) # Vérifier les métadonnées de l'evidence evidence = result.evidence_list[0] self.assertEqual(evidence.action_id, 'click_001') self.assertEqual(evidence.step_id, 'step_001') self.assertIsNotNone(evidence.data.get('anchor_id')) def test_anchor_statistics_update(self): """Test de mise à jour des statistiques d'ancre.""" initial_usage = self.button_anchor.usage_count initial_success_rate = self.button_anchor.success_rate click_action = VWBClickAnchorAction( action_id='click_001', parameters={ 'visual_anchor': self.button_anchor, 'click_type': 'left' }, screen_capturer=self.mock_capturer ) result = click_action.execute('step_001') # Vérifier la mise à jour des statistiques self.assertEqual(self.button_anchor.usage_count, initial_usage + 1) if result.status == VWBActionStatus.SUCCESS: self.assertGreaterEqual(self.button_anchor.success_rate, initial_success_rate) if __name__ == '__main__': # Configuration des tests unittest.TestCase.maxDiff = None # Exécution des tests print("=" * 70) print(" TESTS UNITAIRES - ACTIONS 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(TestBaseVWBAction)) suite.addTests(loader.loadTestsFromTestCase(TestVWBClickAnchorAction)) suite.addTests(loader.loadTestsFromTestCase(TestVWBTypeTextAction)) suite.addTests(loader.loadTestsFromTestCase(TestVWBWaitForAnchorAction)) suite.addTests(loader.loadTestsFromTestCase(TestActionsIntegration)) # Exécuter les tests runner = unittest.TextTestRunner(verbosity=2) result = runner.run(suite) # Résumé print("\n" + "=" * 70) print(f"RÉSUMÉ DES TESTS ACTIONS 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 ACTIONS VWB SONT RÉUSSIS !") else: print("⚠️ Certains tests ont échoué - vérification nécessaire")