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>
This commit is contained in:
309
tests/unit/test_target_resolver_composite_hints.py
Normal file
309
tests/unit/test_target_resolver_composite_hints.py
Normal file
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
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"])
|
||||
Reference in New Issue
Block a user