""" 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" # Le TargetResolver accède à screen_state.window.screen_resolution mock_window = Mock() mock_window.screen_resolution = [1920, 1080] self.screen_state.window = mock_window 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 "hints" in details["criteria_used"], "hints devrait être dans criteria_used" assert "below_text" in details["criteria_used"]["hints"], "below_text devrait être dans hints" 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"])