Files
rpa_vision_v3/tests/unit/test_vwb_actions_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

708 lines
26 KiB
Python

"""
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")