""" Tests de Propriétés pour VisualPropertiesPanel - RPA Vision V3 Ce module contient les tests basés sur les propriétés pour valider que le panneau de propriétés est entièrement visuel et ne contient aucun sélecteur technique. Propriétés testées: - Propriété 1: Élimination Complète des Sélecteurs Techniques - Propriété 9: Métadonnées en Langage Naturel - Validation des exigences 1.1, 1.4, 4.1, 4.2, 4.3, 4.4 Feature: visual-rpa-properties-enhancement """ import pytest import json from hypothesis import given, strategies as st, settings, assume from hypothesis.stateful import RuleBasedStateMachine, rule, initialize, invariant from typing import Dict, Any, List from datetime import datetime # Stratégies Hypothesis pour la génération de données de test @st.composite def valid_visual_node_configs(draw): """Génère des configurations de nodes visuels valides""" node_types = ['click', 'type', 'validate', 'wait', 'navigate'] node_type = draw(st.sampled_from(node_types)) # Paramètres de base selon le type base_params = { 'click': { 'visual_target': generate_mock_visual_target(), 'click_type': draw(st.sampled_from(['left', 'right', 'double'])), 'wait_after': draw(st.integers(min_value=0, max_value=10000)) }, 'type': { 'visual_target': generate_mock_visual_target(), 'text_content': draw(st.text(min_size=1, max_size=100)), 'clear_first': draw(st.booleans()), 'typing_speed': draw(st.sampled_from(['slow', 'normal', 'fast'])) }, 'validate': { 'visual_target': generate_mock_visual_target(), 'validation_type': draw(st.sampled_from(['exists', 'visible', 'text_contains', 'text_equals'])), 'expected_text': draw(st.one_of(st.none(), st.text(min_size=1, max_size=50))) }, 'wait': { 'duration': draw(st.integers(min_value=100, max_value=60000)), 'wait_type': draw(st.sampled_from(['fixed', 'element_visible', 'element_clickable'])), 'visual_target': draw(st.one_of(st.none(), st.just(generate_mock_visual_target()))) }, 'navigate': { 'url': draw(st.text(min_size=10, max_size=100).filter(lambda x: 'http' in x or 'www' in x)), 'wait_for_load': draw(st.booleans()) } } return { 'node_type': node_type, 'parameters': base_params[node_type], 'visual_metadata': { 'nodeType': node_type, 'nodeLabel': f'{node_type.title()} Action', 'lastModified': datetime.now().isoformat() } } def generate_mock_visual_target(): """Génère une cible visuelle simulée""" return { 'embedding': [0.1] * 256, # Embedding simulé 'screenshot': 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', # Image 1x1 en base64 'bounding_box': { 'x': 100, 'y': 100, 'width': 80, 'height': 30 }, 'confidence': 0.95, 'contextual_info': { 'surrounding_elements': [], 'screen_size': {'width': 1920, 'height': 1080}, 'capture_timestamp': datetime.now().isoformat() }, 'signature': f'visual_{int(datetime.now().timestamp())}', 'metadata': { 'element_type': 'Bouton', 'visual_description': 'Bouton avec le texte "Cliquer ici"', 'relative_position': 'au centre de l\'écran', 'text_content': 'Cliquer ici', 'size_description': 'moyenne', 'contextual_elements_count': 0, 'accessibility_info': { 'has_text': True, 'tag_name': 'button', 'attributes_count': 2, 'is_interactive': True } }, 'created_at': datetime.now().isoformat(), 'last_validated': None, 'validation_count': 0 } @st.composite def invalid_technical_selectors(draw): """Génère des sélecteurs techniques qui ne doivent PAS être présents""" selector_types = ['css', 'xpath', 'jquery', 'dom'] selector_type = draw(st.sampled_from(selector_types)) selectors = { 'css': draw(st.sampled_from([ '#button-id', '.btn-primary', 'button[type="submit"]', 'div > span.text', 'input[name="username"]' ])), 'xpath': draw(st.sampled_from([ '//button[@id="submit"]', '//input[@type="text"]', '//*[@class="btn"]', '//div[contains(text(), "Login")]', '//a[starts-with(@href, "http")]' ])), 'jquery': draw(st.sampled_from([ '$("button")', '$(".btn-primary")', '$("[data-testid=submit]")', '$("input:visible")' ])), 'dom': draw(st.sampled_from([ 'document.getElementById("submit")', 'document.querySelector(".btn")', 'document.getElementsByClassName("button")', 'document.getElementsByTagName("input")' ])) } return { 'type': selector_type, 'selector': selectors[selector_type] } class TestVisualPropertiesPanelProperties: """Tests de propriétés pour le panneau de propriétés visuelles""" @given(config=valid_visual_node_configs()) @settings(max_examples=50, deadline=3000) def test_property_1_complete_elimination_of_technical_selectors(self, config): """ **Feature: visual-rpa-properties-enhancement, Property 1: Élimination Complète des Sélecteurs Techniques** Pour tout panneau de propriétés affiché, aucun champ CSS ou XPath ne doit être visible à l'utilisateur. **Valide: Exigences 1.1, 1.4** """ # **PROPRIÉTÉ 1: Vérifier l'absence complète de sélecteurs techniques** # 1. Aucun paramètre ne doit contenir de sélecteur CSS for param_name, param_value in config['parameters'].items(): if isinstance(param_value, str): # Vérifier qu'il n'y a pas de sélecteurs CSS assert not self._contains_css_selector(param_value), \ f"Le paramètre '{param_name}' contient un sélecteur CSS: {param_value}" # Vérifier qu'il n'y a pas de sélecteurs XPath assert not self._contains_xpath_selector(param_value), \ f"Le paramètre '{param_name}' contient un sélecteur XPath: {param_value}" # 2. Les métadonnées ne doivent contenir aucune référence technique metadata = config.get('visual_metadata', {}) metadata_str = json.dumps(metadata, default=str) forbidden_terms = [ 'css_selector', 'xpath_selector', 'querySelector', 'getElementById', 'getElementsByClassName', 'getElementsByTagName', 'jquery', '$(', 'document.', 'window.', 'DOM', 'css:', 'xpath:' ] for term in forbidden_terms: assert term.lower() not in metadata_str.lower(), \ f"Les métadonnées contiennent un terme technique interdit: {term}" # 3. Seules les cibles visuelles doivent être utilisées visual_targets = [v for v in config['parameters'].values() if isinstance(v, dict) and 'signature' in v and 'embedding' in v] # Au moins une cible visuelle doit être présente pour les actions qui en nécessitent action_types_requiring_target = ['click', 'type', 'validate'] if config['node_type'] in action_types_requiring_target: assert len(visual_targets) > 0, \ f"L'action '{config['node_type']}' doit avoir au moins une cible visuelle" # 4. Toutes les cibles visuelles doivent avoir les propriétés requises for target in visual_targets: assert 'signature' in target and target['signature'].startswith('visual_'), \ "Toutes les cibles doivent avoir une signature visuelle" assert 'embedding' in target, \ "Toutes les cibles doivent avoir un embedding" assert 'screenshot' in target, \ "Toutes les cibles doivent avoir une capture d'écran" assert 'metadata' in target, \ "Toutes les cibles doivent avoir des métadonnées" @given(config=valid_visual_node_configs()) @settings(max_examples=30, deadline=2000) def test_property_9_natural_language_metadata(self, config): """ **Feature: visual-rpa-properties-enhancement, Property 9: Métadonnées en Langage Naturel** Pour tout élément sélectionné, ses métadonnées (type, position, caractéristiques) doivent être affichées en langage naturel compréhensible. **Valide: Exigences 4.1, 4.2, 4.3, 4.4** """ # Trouver les cibles visuelles dans la configuration visual_targets = [v for v in config['parameters'].values() if isinstance(v, dict) and 'metadata' in v] for target in visual_targets: metadata = target['metadata'] # **PROPRIÉTÉ 9: Vérifier que les métadonnées sont en langage naturel** # 1. Le type d'élément doit être en français compréhensible element_type = metadata.get('element_type', '') french_element_types = [ 'Bouton', 'Champ de saisie', 'Lien', 'Image', 'Texte', 'Liste déroulante', 'Case à cocher', 'Bouton radio' ] assert element_type in french_element_types, \ f"Le type d'élément '{element_type}' doit être en français compréhensible" # 2. La description visuelle doit être en langage naturel visual_description = metadata.get('visual_description', '') assert len(visual_description) > 0, \ "Une description visuelle doit être présente" assert not self._contains_technical_terms(visual_description), \ f"La description visuelle ne doit pas contenir de termes techniques: {visual_description}" # 3. La position relative doit être descriptive relative_position = metadata.get('relative_position', '') position_terms = ['haut', 'bas', 'gauche', 'droite', 'centre', 'milieu'] assert any(term in relative_position.lower() for term in position_terms), \ f"La position relative doit être descriptive: {relative_position}" # 4. La description de taille doit être compréhensible size_description = metadata.get('size_description', '') size_terms = ['très petite', 'petite', 'moyenne', 'grande', 'très grande'] assert size_description in size_terms, \ f"La description de taille doit être compréhensible: {size_description}" # 5. Les informations d'accessibilité doivent être présentes accessibility_info = metadata.get('accessibility_info', {}) assert isinstance(accessibility_info, dict), \ "Les informations d'accessibilité doivent être présentes" assert 'has_text' in accessibility_info, \ "L'information 'has_text' doit être présente" assert 'is_interactive' in accessibility_info, \ "L'information 'is_interactive' doit être présente" @given( config=valid_visual_node_configs(), technical_selector=invalid_technical_selectors() ) @settings(max_examples=20, deadline=2000) def test_rejection_of_technical_selectors(self, config, technical_selector): """ Teste que les sélecteurs techniques sont rejetés par le système. Cette propriété assure que le système refuse activement tout sélecteur technique et ne permet que les méthodes visuelles. """ # Simuler l'injection d'un sélecteur technique contaminated_config = config.copy() # Tenter d'injecter le sélecteur technique selector_key = f"{technical_selector['type']}_selector" contaminated_config['parameters'][selector_key] = technical_selector['selector'] # Le système doit rejeter cette configuration validation_result = self._validate_visual_config(contaminated_config) assert not validation_result['is_valid'], \ f"Le système doit rejeter les sélecteurs {technical_selector['type']}" assert any('technique' in error.lower() or 'css' in error.lower() or 'xpath' in error.lower() for error in validation_result['errors']), \ "Les erreurs doivent mentionner le rejet des sélecteurs techniques" def _contains_css_selector(self, text: str) -> bool: """Vérifie si le texte contient un sélecteur CSS""" css_patterns = [ '#', # ID selector '.', # Class selector (mais pas dans les URLs) '[', # Attribute selector ':', # Pseudo-selector '>', # Child combinator '+', # Adjacent sibling '~' # General sibling ] # Exclure les URLs et autres cas légitimes if 'http' in text.lower() or 'www' in text.lower(): return False return any(pattern in text for pattern in css_patterns) def _contains_xpath_selector(self, text: str) -> bool: """Vérifie si le texte contient un sélecteur XPath""" xpath_patterns = [ '//', # Descendant axis '/@', # Attribute axis '[contains(', # XPath function '[starts-with(', # XPath function '[text()=', # Text node 'following-sibling:', # XPath axis 'preceding-sibling:', # XPath axis 'ancestor:', # XPath axis 'descendant:' # XPath axis ] return any(pattern in text for pattern in xpath_patterns) def _contains_technical_terms(self, text: str) -> bool: """Vérifie si le texte contient des termes techniques interdits""" technical_terms = [ 'css', 'xpath', 'dom', 'html', 'javascript', 'jquery', 'selector', 'element', 'node', 'attribute', 'property', 'class', 'id', 'tag', 'div', 'span', 'input' ] text_lower = text.lower() return any(term in text_lower for term in technical_terms) def _validate_visual_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """Simule la validation d'une configuration visuelle""" errors = [] warnings = [] # Vérifier la présence de sélecteurs techniques interdits for param_name, param_value in config.get('parameters', {}).items(): if isinstance(param_value, str): if self._contains_css_selector(param_value): errors.append(f"Sélecteur CSS interdit détecté dans {param_name}") if self._contains_xpath_selector(param_value): errors.append(f"Sélecteur XPath interdit détecté dans {param_name}") # Vérifier les clés de paramètres if any(tech in param_name.lower() for tech in ['css', 'xpath', 'selector', 'dom']): errors.append(f"Paramètre technique interdit: {param_name}") return { 'is_valid': len(errors) == 0, 'errors': errors, 'warnings': warnings } class VisualPropertiesPanelStateMachine(RuleBasedStateMachine): """ Machine à états pour tester les propriétés stateful du panneau de propriétés visuelles. Cette classe teste que le panneau maintient ses propriétés visuelles lors de séquences d'opérations complexes. """ def __init__(self): super().__init__() self.node_configs = {} self.operation_count = 0 @initialize() def setup(self): """Initialise l'état de la machine""" self.node_configs.clear() self.operation_count = 0 @rule(config=valid_visual_node_configs()) def add_node_config(self, config): """Règle: Ajouter une configuration de node""" node_id = f"node_{len(self.node_configs)}" self.node_configs[node_id] = config self.operation_count += 1 @rule(node_id=st.sampled_from([])) def update_node_config(self, node_id): """Règle: Mettre à jour une configuration existante""" if node_id in self.node_configs: # Simuler une mise à jour config = self.node_configs[node_id] config['visual_metadata']['lastModified'] = datetime.now().isoformat() self.operation_count += 1 @rule() def validate_all_configs(self): """Règle: Valider toutes les configurations""" for node_id, config in self.node_configs.items(): validation = self._validate_config(config) assert validation['is_valid'], \ f"La configuration du node {node_id} doit rester valide" @invariant() def no_technical_selectors_invariant(self): """Invariant: Aucun sélecteur technique ne doit jamais être présent""" for node_id, config in self.node_configs.items(): for param_name, param_value in config.get('parameters', {}).items(): if isinstance(param_value, str): assert not self._contains_css_selector(param_value), \ f"Sélecteur CSS détecté dans {node_id}.{param_name}" assert not self._contains_xpath_selector(param_value), \ f"Sélecteur XPath détecté dans {node_id}.{param_name}" @invariant() def visual_targets_integrity(self): """Invariant: Toutes les cibles visuelles doivent être intègres""" for node_id, config in self.node_configs.items(): visual_targets = [v for v in config.get('parameters', {}).values() if isinstance(v, dict) and 'signature' in v] for target in visual_targets: assert target['signature'].startswith('visual_'), \ f"Signature visuelle invalide dans {node_id}" assert 'embedding' in target, \ f"Embedding manquant dans {node_id}" assert 'metadata' in target, \ f"Métadonnées manquantes dans {node_id}" @invariant() def natural_language_metadata_invariant(self): """Invariant: Les métadonnées doivent toujours être en langage naturel""" for node_id, config in self.node_configs.items(): visual_targets = [v for v in config.get('parameters', {}).values() if isinstance(v, dict) and 'metadata' in v] for target in visual_targets: metadata = target['metadata'] # Vérifier que les descriptions sont en français assert 'element_type' in metadata, \ f"Type d'élément manquant dans {node_id}" assert 'visual_description' in metadata, \ f"Description visuelle manquante dans {node_id}" # Vérifier l'absence de termes techniques description = metadata.get('visual_description', '') assert not self._contains_technical_terms(description), \ f"Termes techniques dans la description de {node_id}" def _validate_config(self, config): """Valide une configuration""" return {'is_valid': True, 'errors': [], 'warnings': []} def _contains_css_selector(self, text): """Vérifie la présence de sélecteurs CSS""" return any(char in text for char in ['#', '.', '[', '>', '+'] if 'http' not in text.lower()) def _contains_xpath_selector(self, text): """Vérifie la présence de sélecteurs XPath""" return '//' in text or '/@' in text or '[contains(' in text def _contains_technical_terms(self, text): """Vérifie la présence de termes techniques""" technical_terms = ['css', 'xpath', 'dom', 'html', 'selector'] return any(term in text.lower() for term in technical_terms) # Test de la machine à états TestVisualPropertiesPanelStateful = VisualPropertiesPanelStateMachine.TestCase if __name__ == '__main__': pytest.main([__file__, '-v', '--tb=short'])