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

309 lines
12 KiB
Python

"""
Tests pour Fiche #3 - Context Hints dans Résolution Composite
Auteur: Dom, Alice Kiro - 15 décembre 2024
Objectif: Valider que context_hints est maintenant pris en compte dans la résolution composite
"""
import pytest
from unittest.mock import Mock, MagicMock
from dataclasses import dataclass
from typing import Dict, Any, Optional
from core.execution.target_resolver import TargetResolver, ResolvedTarget, ResolutionStrategy
from core.models.ui_element import UIElement, UIElementEmbeddings, VisualFeatures
from core.models.screen_state import ScreenState
@dataclass
class MockTargetSpec:
"""Mock TargetSpec pour les tests"""
by_role: Optional[str] = None
by_text: Optional[str] = None
by_position: Optional[tuple] = None
context_hints: Optional[Dict[str, Any]] = None
selection_policy: Optional[str] = "first"
class TestTargetResolverCompositeHints:
"""Tests pour la Fiche #3 - Context Hints dans résolution composite"""
def setup_method(self):
"""Setup pour chaque test"""
self.resolver = TargetResolver()
# Créer des embeddings et features par défaut
default_embeddings = UIElementEmbeddings()
default_visual_features = VisualFeatures(
dominant_color="#ffffff",
has_icon=False,
shape="rectangle",
size_category="medium"
)
# Créer des éléments UI de test
self.username_label = UIElement(
element_id="username_label",
type="label",
role="label",
bbox=(100, 100, 80, 20), # (x, y, w, h)
center=(140, 110),
label="Username",
label_confidence=0.9,
embeddings=default_embeddings,
visual_features=default_visual_features,
confidence=0.9
)
self.username_input = UIElement(
element_id="username_input",
type="text_input",
role="form_input",
bbox=(100, 130, 200, 30), # En dessous du label
center=(200, 145),
label="",
label_confidence=0.8,
embeddings=default_embeddings,
visual_features=default_visual_features,
confidence=0.95
)
self.password_input = UIElement(
element_id="password_input",
type="text_input",
role="form_input",
bbox=(100, 180, 200, 30), # Plus bas
center=(200, 195),
label="",
label_confidence=0.8,
embeddings=default_embeddings,
visual_features=default_visual_features,
confidence=0.95
)
self.submit_button = UIElement(
element_id="submit_button",
type="button",
role="primary_action",
bbox=(320, 130, 80, 30), # À droite de l'input
center=(360, 145),
label="Submit",
label_confidence=0.9,
embeddings=default_embeddings,
visual_features=default_visual_features,
confidence=0.9
)
self.ui_elements = [
self.username_label,
self.username_input,
self.password_input,
self.submit_button
]
# Mock screen state
self.screen_state = Mock(spec=ScreenState)
self.screen_state.ui_elements = self.ui_elements
self.screen_state.screen_state_id = "test_screen"
def test_fiche3_context_hints_triggers_composite_mode(self):
"""
Test Fiche #3: Vérifier que context_hints déclenche le mode composite
Avant: by_role="input" + context_hints ne déclenchait pas composite
Après: by_role="input" + context_hints déclenche composite
"""
# Spec avec role + context_hints (devrait déclencher composite)
target_spec = MockTargetSpec(
by_role="text_input",
context_hints={"below_text": "Username"}
)
# Vérifier que c'est maintenant considéré comme composite
is_composite = self.resolver._is_composite_spec(target_spec)
assert is_composite, "by_role + context_hints devrait déclencher le mode composite"
def test_fiche3_composite_resolution_with_context_hints(self):
"""
Test Fiche #3: Résolution composite avec context_hints
Doit trouver l'input qui est en dessous du label "Username"
"""
target_spec = MockTargetSpec(
by_role="text_input",
context_hints={"below_text": "Username"}
)
# Mock de la méthode _get_ui_elements
self.resolver._get_ui_elements = Mock(return_value=self.ui_elements)
# Résoudre
result = self.resolver.resolve_target(target_spec, self.screen_state)
# Vérifications
assert result is not None, "Devrait trouver un élément"
assert result.element.element_id == "username_input", f"Devrait trouver username_input, trouvé: {result.element.element_id}"
assert result.strategy_used == ResolutionStrategy.COMPOSITE.value, "Devrait utiliser la stratégie composite"
# Vérifier les détails de résolution
details = result.resolution_details
assert "context_hints" in details["criteria_used"], "context_hints devrait être dans criteria_used"
assert details["criteria_used"]["context_hints"]["below_text"] == "Username"
def test_fiche3_context_hints_below_text_filtering(self):
"""
Test du filtrage below_text dans _apply_context_hints_to_candidates
"""
candidates = [self.username_input, self.password_input] # Les deux inputs
context_hints = {"below_text": "Username"}
scores = {elem.element_id: 1.0 for elem in candidates}
# Appliquer les context hints
filtered = self.resolver._apply_context_hints_to_candidates(
candidates, context_hints, self.ui_elements, scores
)
# Vérifications
assert len(filtered) == 2, f"Devrait garder les 2 inputs (tous en dessous), trouvé: {len(filtered)}"
assert self.username_input in filtered, "username_input devrait être gardé"
assert self.password_input in filtered, "password_input devrait être gardé"
def test_fiche3_context_hints_right_of_text_filtering(self):
"""
Test du filtrage right_of_text
"""
candidates = [self.username_input, self.submit_button]
context_hints = {"right_of_text": "Username"}
scores = {elem.element_id: 1.0 for elem in candidates}
# Appliquer les context hints
filtered = self.resolver._apply_context_hints_to_candidates(
candidates, context_hints, self.ui_elements, scores
)
# Le submit button est à droite du label Username
assert len(filtered) == 1, f"Devrait garder 1 élément, trouvé: {len(filtered)}"
assert self.submit_button in filtered, "submit_button devrait être gardé (à droite)"
def test_fiche3_context_hints_near_text_filtering(self):
"""
Test du filtrage near_text avec distance
"""
candidates = [self.username_input, self.password_input]
context_hints = {"near_text": "Username", "max_distance": 100}
scores = {elem.element_id: 1.0 for elem in candidates}
# Appliquer les context hints
filtered = self.resolver._apply_context_hints_to_candidates(
candidates, context_hints, self.ui_elements, scores
)
# username_input est plus proche que password_input
assert len(filtered) == 1, f"Devrait garder 1 élément proche, trouvé: {len(filtered)}"
assert self.username_input in filtered, "username_input devrait être gardé (plus proche)"
def test_fiche3_cache_key_includes_context_hints(self):
"""
Test que la clé de cache inclut maintenant context_hints
"""
target_spec1 = MockTargetSpec(
by_role="text_input",
context_hints={"below_text": "Username"}
)
target_spec2 = MockTargetSpec(
by_role="text_input",
context_hints={"below_text": "Password"}
)
target_spec3 = MockTargetSpec(
by_role="text_input"
# Pas de context_hints
)
# Générer les clés de cache
key1 = self.resolver._make_cache_key(target_spec1, self.screen_state)
key2 = self.resolver._make_cache_key(target_spec2, self.screen_state)
key3 = self.resolver._make_cache_key(target_spec3, self.screen_state)
# Vérifications
assert key1 != key2, "Clés différentes pour context_hints différents"
assert key1 != key3, "Clés différentes avec/sans context_hints"
assert key2 != key3, "Clés différentes avec/sans context_hints"
# Vérifier que context_hints est dans la clé
assert "Username" in key1, "Username devrait être dans la clé de cache"
assert "Password" in key2, "Password devrait être dans la clé de cache"
def test_fiche3_error_handling_in_context_hints(self):
"""
Test de la gestion d'erreurs dans _apply_context_hints_to_candidates
"""
candidates = [self.username_input]
# Context hints avec données invalides
invalid_context_hints = {
"below_text": None, # Invalide
"within_region": [1, 2, 3], # Pas assez d'éléments
"near_text": 123 # Type invalide
}
scores = {elem.element_id: 1.0 for elem in candidates}
# Ne devrait pas planter
filtered = self.resolver._apply_context_hints_to_candidates(
candidates, invalid_context_hints, self.ui_elements, scores
)
# Devrait retourner les candidats originaux en cas d'erreur
assert filtered == candidates, "Devrait retourner candidats originaux si erreur"
def test_fiche3_multiple_context_hints_combined(self):
"""
Test de combinaison de plusieurs context_hints
"""
target_spec = MockTargetSpec(
by_role="text_input",
context_hints={
"below_text": "Username",
"near_text": "Username",
"max_distance": 100
}
)
# Mock de la méthode _get_ui_elements
self.resolver._get_ui_elements = Mock(return_value=self.ui_elements)
# Résoudre
result = self.resolver.resolve_target(target_spec, self.screen_state)
# Vérifications
assert result is not None, "Devrait trouver un élément avec hints multiples"
assert result.element.element_id == "username_input", "Devrait trouver username_input"
assert result.strategy_used == ResolutionStrategy.COMPOSITE.value
def test_fiche3_performance_with_context_hints(self):
"""
Test de performance - résolution avec context_hints ne devrait pas être trop lente
"""
import time
target_spec = MockTargetSpec(
by_role="text_input",
context_hints={"below_text": "Username"}
)
# Mock de la méthode _get_ui_elements
self.resolver._get_ui_elements = Mock(return_value=self.ui_elements)
# Mesurer le temps
start_time = time.time()
result = self.resolver.resolve_target(target_spec, self.screen_state)
end_time = time.time()
# Vérifications
assert result is not None, "Devrait trouver un élément"
assert (end_time - start_time) < 0.1, "Résolution devrait être rapide (< 100ms)"
if __name__ == "__main__":
pytest.main([__file__, "-v"])