- 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>
474 lines
21 KiB
Python
474 lines
21 KiB
Python
"""
|
|
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']) |