#!/usr/bin/env python3 """ Tests de propriétés pour VWBActionProperties - Détection et gestion des actions VWB Auteur : Dom, Alice, Kiro - 12 janvier 2026 Ce module teste les propriétés universelles du composant VWBActionProperties, en particulier la détection correcte et la gestion des états de chargement. Feature: interface-proprietes-etapes-complete Property 4: Détection correcte des actions VWB Property 5: Gestion des états de chargement VWB Validates: Requirements 2.1, 2.2, 2.4 """ import pytest import json import subprocess import tempfile import os from pathlib import Path from typing import Dict, List, Any, Optional from hypothesis import given, strategies as st, settings, assume, note from hypothesis.stateful import RuleBasedStateMachine, Bundle, rule, initialize, invariant # Configuration des tests de propriétés PROPERTY_TEST_SETTINGS = settings( max_examples=100, deadline=30000, # 30 secondes par test suppress_health_check=[], ) # Stratégies de génération de données @st.composite def vwb_action_strategy(draw): """Génère des actions VWB valides""" action_types = [ 'click_anchor', 'type_text', 'type_secret', 'wait_for_anchor', 'extract_text', 'screenshot_evidence', 'scroll_to_anchor', 'focus_anchor', 'hotkey', 'navigate_to_url' ] action_type = draw(st.sampled_from(action_types)) return { 'id': action_type, 'name': draw(st.text(min_size=5, max_size=50)), 'description': draw(st.text(min_size=10, max_size=200)), 'category': draw(st.sampled_from(['interaction', 'navigation', 'extraction', 'validation'])), 'parameters': draw(vwb_parameters_strategy()), 'examples': draw(st.lists(vwb_example_strategy(), max_size=3)), 'version': draw(st.text(min_size=3, max_size=10)), 'tags': draw(st.lists(st.text(min_size=3, max_size=20), max_size=5)) } @st.composite def vwb_parameters_strategy(draw): """Génère des paramètres d'action VWB""" param_count = draw(st.integers(min_value=1, max_value=6)) parameters = {} for i in range(param_count): param_name = draw(st.sampled_from([ 'target_anchor', 'text_content', 'confidence_threshold', 'timeout_seconds', 'retry_count', 'scroll_direction' ])) param_type = draw(st.sampled_from(['string', 'number', 'boolean', 'VWBVisualAnchor'])) parameters[param_name] = { 'type': param_type, 'required': draw(st.booleans()), 'description': draw(st.text(min_size=10, max_size=100)), 'default': draw(get_default_value_strategy(param_type)) } if param_type == 'number': parameters[param_name]['min'] = draw(st.one_of(st.none(), st.integers(min_value=0, max_value=100))) parameters[param_name]['max'] = draw(st.one_of(st.none(), st.integers(min_value=100, max_value=1000))) return parameters @st.composite def vwb_example_strategy(draw): """Génère des exemples d'utilisation VWB""" return { 'name': draw(st.text(min_size=5, max_size=30)), 'description': draw(st.text(min_size=10, max_size=100)), 'parameters': draw(st.dictionaries( st.text(min_size=3, max_size=20), st.one_of(st.text(), st.integers(), st.booleans()), max_size=5 )), 'expectedResult': draw(st.one_of(st.none(), st.text(min_size=10, max_size=100))) } def get_default_value_strategy(param_type: str): """Retourne une stratégie pour les valeurs par défaut selon le type""" if param_type == 'string': return st.one_of(st.none(), st.text(max_size=50)) elif param_type == 'number': return st.one_of(st.none(), st.integers(min_value=0, max_value=100)) elif param_type == 'boolean': return st.one_of(st.none(), st.booleans()) elif param_type == 'VWBVisualAnchor': return st.none() # Les ancres visuelles n'ont pas de valeur par défaut else: return st.none() @st.composite def loading_state_strategy(draw): """Génère des états de chargement""" return { 'isLoading': draw(st.booleans()), 'hasError': draw(st.booleans()), 'errorMessage': draw(st.one_of(st.none(), st.text(min_size=10, max_size=100))), 'stepType': draw(st.one_of(st.none(), st.sampled_from([ 'click_anchor', 'type_text', 'wait_for_anchor', 'extract_text' ]))) } @st.composite def variable_strategy(draw): """Génère des variables""" return { 'id': draw(st.text(min_size=1, max_size=20)), 'name': draw(st.text(min_size=1, max_size=30, alphabet=st.characters(whitelist_categories=('Lu', 'Ll', 'Nd')))), 'value': draw(st.one_of(st.text(), st.integers(), st.booleans())), 'type': draw(st.sampled_from(['string', 'number', 'boolean'])) } class VWBActionPropertiesTestHelper: """Helper pour tester le composant VWBActionProperties via Node.js""" def __init__(self): self.project_root = Path(__file__).parent.parent.parent self.frontend_path = self.project_root / "visual_workflow_builder" / "frontend" def create_test_script(self, action: Optional[Dict], loading_state: Dict, parameters: Dict, variables: List[Dict]) -> str: """Crée un script de test Node.js pour VWBActionProperties""" test_script = f""" const React = require('react'); // Configuration du test const vwbAction = {json.dumps(action)}; const loadingState = {json.dumps(loading_state)}; const parameters = {json.dumps(parameters)}; const variables = {json.dumps(variables)}; // Simulation du composant VWBActionProperties class VWBActionPropertiesSimulator {{ constructor(action, isLoading, error, stepType, parameters, variables) {{ this.action = action; this.isLoading = isLoading; this.error = error; this.stepType = stepType; this.parameters = parameters; this.variables = variables; this.validationResults = []; this.parameterChanges = []; }} // Simulation de la détection d'action VWB detectVWBAction() {{ const detection = {{ isVWBAction: false, detectionMethods: {{}}, confidence: 0 }}; // Méthodes de détection if (this.action) {{ detection.detectionMethods.hasAction = true; detection.isVWBAction = true; detection.confidence += 0.4; }} if (this.stepType) {{ const vwbPatterns = ['_anchor', '_text', '_secret', 'click_', 'type_', 'wait_', 'extract_']; detection.detectionMethods.hasVWBPattern = vwbPatterns.some(pattern => this.stepType.includes(pattern) ); if (detection.detectionMethods.hasVWBPattern) {{ detection.isVWBAction = true; detection.confidence += 0.3; }} }} if (this.parameters && Object.keys(this.parameters).length > 0) {{ const vwbParamNames = ['target_anchor', 'confidence_threshold', 'visual_anchor']; detection.detectionMethods.hasVWBParams = Object.keys(this.parameters).some(param => vwbParamNames.some(vwbParam => param.includes(vwbParam)) ); if (detection.detectionMethods.hasVWBParams) {{ detection.isVWBAction = true; detection.confidence += 0.2; }} }} detection.detectionMethods.isLoadingState = this.isLoading; detection.detectionMethods.hasError = Boolean(this.error); return detection; }} // Simulation de la gestion des états de chargement handleLoadingStates() {{ const stateHandling = {{ currentState: 'unknown', canRender: false, showsAppropriateUI: false, providesUserFeedback: false, hasRecoveryOptions: false }}; if (this.isLoading) {{ stateHandling.currentState = 'loading'; stateHandling.canRender = true; stateHandling.showsAppropriateUI = true; stateHandling.providesUserFeedback = true; }} else if (this.error) {{ stateHandling.currentState = 'error'; stateHandling.canRender = true; stateHandling.showsAppropriateUI = true; stateHandling.providesUserFeedback = true; stateHandling.hasRecoveryOptions = true; // Bouton retry, suggestions alternatives }} else if (!this.action) {{ stateHandling.currentState = 'not_found'; stateHandling.canRender = true; stateHandling.showsAppropriateUI = true; stateHandling.providesUserFeedback = true; stateHandling.hasRecoveryOptions = true; // Actions alternatives, config manuelle }} else {{ stateHandling.currentState = 'loaded'; stateHandling.canRender = true; stateHandling.showsAppropriateUI = true; stateHandling.providesUserFeedback = true; }} return stateHandling; }} // Simulation de la validation des paramètres validateParameters() {{ const validation = {{ is_valid: true, errors: [], warnings: [], suggestions: [] }}; if (!this.action) {{ // Pas de validation possible sans action return validation; }} // Validation des paramètres requis Object.entries(this.action.parameters || {{}}).forEach(([paramName, paramConfig]) => {{ const value = this.parameters[paramName]; if (paramConfig.required && (value === undefined || value === null || value === '')) {{ validation.is_valid = false; validation.errors.push({{ parameter: paramName, message: `Le paramètre "${{paramName}}" est requis`, code: 'REQUIRED_PARAMETER', severity: 'error' }}); }} // Validation par type if (value !== undefined && value !== null && value !== '') {{ switch (paramConfig.type) {{ case 'number': const numValue = Number(value); if (isNaN(numValue)) {{ validation.is_valid = false; validation.errors.push({{ parameter: paramName, message: `"${{paramName}}" doit être un nombre`, code: 'INVALID_TYPE', severity: 'error' }}); }} else {{ if (paramConfig.min !== undefined && numValue < paramConfig.min) {{ validation.is_valid = false; validation.errors.push({{ parameter: paramName, message: `"${{paramName}}" doit être >= ${{paramConfig.min}}`, code: 'MIN_VALUE', severity: 'error' }}); }} if (paramConfig.max !== undefined && numValue > paramConfig.max) {{ validation.is_valid = false; validation.errors.push({{ parameter: paramName, message: `"${{paramName}}" doit être <= ${{paramConfig.max}}`, code: 'MAX_VALUE', severity: 'error' }}); }} }} break; case 'VWBVisualAnchor': if (typeof value !== 'object' || !value.anchor_id) {{ validation.warnings.push({{ parameter: paramName, message: `"${{paramName}}" nécessite une sélection visuelle valide`, impact: 'medium' }}); }} break; }} }} }}); return validation; }} // Simulation du rendu des alternatives getAlternativeActions() {{ const alternatives = []; if (!this.action || this.error) {{ // Suggérer des alternatives basées sur le type d'étape const stepTypeAlternatives = {{ 'click_anchor': [ {{ name: 'click', description: 'Clic standard sur élément' }}, {{ name: 'type', description: 'Saisie de texte' }} ], 'type_text': [ {{ name: 'type', description: 'Saisie de texte standard' }}, {{ name: 'click', description: 'Clic pour focus puis saisie' }} ], 'wait_for_anchor': [ {{ name: 'wait', description: 'Attente temporelle' }}, {{ name: 'condition', description: 'Attente conditionnelle' }} ] }}; const typeAlternatives = stepTypeAlternatives[this.stepType] || [ {{ name: 'click', description: 'Clic standard' }}, {{ name: 'type', description: 'Saisie standard' }} ]; alternatives.push(...typeAlternatives); }} return alternatives; }} }} // Test des propriétés VWBActionProperties function testVWBActionProperties() {{ const results = {{}}; try {{ const simulator = new VWBActionPropertiesSimulator( vwbAction, loadingState.isLoading, loadingState.hasError ? new Error(loadingState.errorMessage || 'Test error') : null, loadingState.stepType, parameters, variables ); // 1. Test de détection d'action VWB (Property 4) const detection = simulator.detectVWBAction(); results.vwbDetection = {{ isVWBAction: detection.isVWBAction, detectionMethods: detection.detectionMethods, confidence: detection.confidence, methodCount: Object.values(detection.detectionMethods).filter(Boolean).length }}; // 2. Test de gestion des états de chargement (Property 5) const stateHandling = simulator.handleLoadingStates(); results.loadingStateHandling = {{ currentState: stateHandling.currentState, canRender: stateHandling.canRender, showsAppropriateUI: stateHandling.showsAppropriateUI, providesUserFeedback: stateHandling.providesUserFeedback, hasRecoveryOptions: stateHandling.hasRecoveryOptions }}; // 3. Test de validation des paramètres const validation = simulator.validateParameters(); results.parameterValidation = {{ is_valid: validation.is_valid, errorCount: validation.errors.length, warningCount: validation.warnings.length, suggestionCount: validation.suggestions.length, validationPossible: Boolean(vwbAction) }}; // 4. Test des alternatives const alternatives = simulator.getAlternativeActions(); results.alternatives = {{ count: alternatives.length, hasAlternatives: alternatives.length > 0, alternatives: alternatives }}; // 5. Test de cohérence globale results.consistency = {{ stateMatchesData: ( (loadingState.isLoading && stateHandling.currentState === 'loading') || (loadingState.hasError && stateHandling.currentState === 'error') || (!vwbAction && !loadingState.isLoading && !loadingState.hasError && stateHandling.currentState === 'not_found') || (vwbAction && !loadingState.isLoading && !loadingState.hasError && stateHandling.currentState === 'loaded') ), detectionMatchesAction: ( (vwbAction && detection.isVWBAction) || (!vwbAction && loadingState.stepType && detection.isVWBAction) || (!vwbAction && !loadingState.stepType) ), validationMatchesState: ( (!vwbAction && !validation.is_valid) || (vwbAction && validation !== null) ) }}; results.success = true; }} catch (error) {{ results.success = false; results.error = error.message; }} return results; }} // Exécuter le test const testResults = testVWBActionProperties(); console.log(JSON.stringify(testResults, null, 2)); """ return test_script def run_test_script(self, script_content: str) -> Dict[str, Any]: """Exécute un script de test Node.js et retourne les résultats""" with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False) as f: f.write(script_content) script_path = f.name try: # Exécuter le script dans le contexte du frontend result = subprocess.run( ['node', script_path], cwd=self.frontend_path, capture_output=True, text=True, timeout=30 ) if result.returncode == 0: try: return json.loads(result.stdout) except json.JSONDecodeError: return { 'success': False, 'error': f'Invalid JSON output: {result.stdout}', 'stderr': result.stderr } else: return { 'success': False, 'error': f'Script failed with code {result.returncode}', 'stdout': result.stdout, 'stderr': result.stderr } except subprocess.TimeoutExpired: return { 'success': False, 'error': 'Test script timeout' } except Exception as e: return { 'success': False, 'error': f'Execution error: {str(e)}' } finally: # Nettoyer le fichier temporaire try: os.unlink(script_path) except: pass class TestVWBActionPropertiesProperties: """Tests de propriétés pour VWBActionProperties""" def setup_method(self): """Configuration avant chaque test""" self.helper = VWBActionPropertiesTestHelper() @given( action=st.one_of(st.none(), vwb_action_strategy()), loading_state=loading_state_strategy(), parameters=st.dictionaries(st.text(min_size=3, max_size=20), st.one_of(st.text(), st.integers(), st.booleans()), max_size=5), variables=st.lists(variable_strategy(), max_size=3) ) @PROPERTY_TEST_SETTINGS def test_property_4_vwb_action_detection(self, action, loading_state, parameters, variables): """ Property 4: Détection correcte des actions VWB Pour toute étape identifiée comme action VWB par le StepTypeResolver, le PropertiesPanel doit utiliser le composant VWBActionProperties. """ note(f"Testing VWB detection - Action: {action is not None}, Loading: {loading_state}") note(f"Parameters: {len(parameters)}, Variables: {len(variables)}") # Créer et exécuter le test script = self.helper.create_test_script(action, loading_state, parameters, variables) results = self.helper.run_test_script(script) # Vérifications des propriétés assert results.get('success', False), f"Test failed: {results.get('error', 'Unknown error')}" # Property 4.1: Détection basée sur l'action detection = results.get('vwbDetection', {}) if action is not None: assert detection.get('isVWBAction', False), "Action VWB non détectée malgré la présence d'une action" assert detection.get('confidence', 0) > 0, "Confiance de détection nulle avec action présente" # Property 4.2: Détection basée sur le type d'étape step_type = loading_state.get('stepType') if step_type and any(pattern in step_type for pattern in ['_anchor', '_text', 'click_', 'type_']): assert detection.get('isVWBAction', False), f"Type VWB non détecté: {step_type}" # Property 4.3: Méthodes de détection multiples detection_methods = detection.get('detectionMethods', {}) method_count = detection.get('methodCount', 0) assert isinstance(detection_methods, dict), "Méthodes de détection invalides" assert method_count >= 0, "Nombre de méthodes de détection invalide" # Property 4.4: Cohérence de la détection consistency = results.get('consistency', {}) assert consistency.get('detectionMatchesAction', False), "Détection incohérente avec l'action" @given( action=st.one_of(st.none(), vwb_action_strategy()), loading_state=loading_state_strategy(), parameters=st.dictionaries(st.text(min_size=3, max_size=20), st.one_of(st.text(), st.integers()), max_size=3), variables=st.lists(variable_strategy(), max_size=2) ) @PROPERTY_TEST_SETTINGS def test_property_5_loading_state_management(self, action, loading_state, parameters, variables): """ Property 5: Gestion des états de chargement VWB Pour toute action VWB en cours de chargement, le système doit afficher un indicateur de chargement approprié. """ note(f"Testing loading states - Loading: {loading_state.get('isLoading')}, Error: {loading_state.get('hasError')}") script = self.helper.create_test_script(action, loading_state, parameters, variables) results = self.helper.run_test_script(script) assert results.get('success', False), f"Test failed: {results.get('error')}" # Property 5.1: Gestion de l'état de chargement state_handling = results.get('loadingStateHandling', {}) assert state_handling.get('canRender', False), "Composant ne peut pas se rendre" assert state_handling.get('showsAppropriateUI', False), "Interface utilisateur inappropriée" assert state_handling.get('providesUserFeedback', False), "Pas de feedback utilisateur" # Property 5.2: États spécifiques current_state = state_handling.get('currentState', 'unknown') if loading_state.get('isLoading', False): assert current_state == 'loading', f"État incorrect pendant le chargement: {current_state}" elif loading_state.get('hasError', False): assert current_state == 'error', f"État incorrect en cas d'erreur: {current_state}" assert state_handling.get('hasRecoveryOptions', False), "Options de récupération manquantes" elif action is None: assert current_state == 'not_found', f"État incorrect pour action manquante: {current_state}" assert state_handling.get('hasRecoveryOptions', False), "Options de récupération manquantes" else: assert current_state == 'loaded', f"État incorrect pour action chargée: {current_state}" # Property 5.3: Cohérence globale des états consistency = results.get('consistency', {}) assert consistency.get('stateMatchesData', False), "État incohérent avec les données" @given( action=vwb_action_strategy(), parameters=st.dictionaries(st.text(min_size=3, max_size=20), st.one_of(st.text(), st.integers(), st.booleans()), max_size=4), variables=st.lists(variable_strategy(), max_size=3) ) @PROPERTY_TEST_SETTINGS def test_property_vwb_parameter_validation(self, action, parameters, variables): """ Test de validation des paramètres VWB Pour toute action VWB avec paramètres, le système doit : 1. Valider les paramètres requis 2. Valider les types de paramètres 3. Fournir des messages d'erreur appropriés """ note(f"Testing parameter validation for action: {action['id']}") note(f"Parameters: {list(parameters.keys())}") # État normal (pas de chargement, pas d'erreur) loading_state = { 'isLoading': False, 'hasError': False, 'errorMessage': None, 'stepType': action['id'] } script = self.helper.create_test_script(action, loading_state, parameters, variables) results = self.helper.run_test_script(script) assert results.get('success', False), f"Test failed: {results.get('error')}" # Vérifier la validation des paramètres validation = results.get('parameterValidation', {}) assert validation.get('validationPossible', False), "Validation impossible avec action présente" assert isinstance(validation.get('errorCount', -1), int), "Nombre d'erreurs invalide" assert isinstance(validation.get('warningCount', -1), int), "Nombre d'avertissements invalide" # Vérifier la cohérence de la validation if validation.get('errorCount', 0) > 0: assert not validation.get('is_valid', True), "Validation marquée valide malgré les erreurs" @given( loading_state=loading_state_strategy(), parameters=st.dictionaries(st.text(min_size=3, max_size=15), st.text(max_size=50), max_size=3) ) @PROPERTY_TEST_SETTINGS def test_property_alternative_actions_suggestions(self, loading_state, parameters): """ Test des suggestions d'actions alternatives Quand une action VWB n'est pas disponible, le système doit : 1. Suggérer des actions alternatives appropriées 2. Basées sur le type d'étape détecté 3. Permettre la configuration manuelle """ note(f"Testing alternatives for step type: {loading_state.get('stepType')}") # Forcer l'absence d'action pour tester les alternatives action = None variables = [] script = self.helper.create_test_script(action, loading_state, parameters, variables) results = self.helper.run_test_script(script) assert results.get('success', False), f"Test failed: {results.get('error')}" # Vérifier les alternatives alternatives = results.get('alternatives', {}) assert alternatives.get('count', 0) > 0, "Aucune alternative suggérée" assert alternatives.get('hasAlternatives', False), "Flag d'alternatives incorrect" # Vérifier que les alternatives sont appropriées alternative_list = alternatives.get('alternatives', []) assert len(alternative_list) > 0, "Liste d'alternatives vide" for alt in alternative_list: assert 'name' in alt, "Alternative sans nom" assert 'description' in alt, "Alternative sans description" class VWBActionPropertiesStateMachine(RuleBasedStateMachine): """Machine à états pour tester les propriétés de VWBActionProperties""" actions = Bundle('actions') states = Bundle('states') def __init__(self): super().__init__() self.helper = VWBActionPropertiesTestHelper() self.test_results = [] self.current_actions = [] @initialize() def setup(self): """Initialisation de la machine à états""" pass @rule(target=actions, action=vwb_action_strategy()) def add_action(self, action): """Ajoute une action VWB""" self.current_actions.append(action) return action @rule( action=st.one_of(st.none(), actions), loading_state=loading_state_strategy(), parameters=st.dictionaries(st.text(min_size=3, max_size=15), st.text(max_size=30), max_size=3) ) def test_action_properties(self, action, loading_state, parameters): """Teste les propriétés avec une action""" script = self.helper.create_test_script(action, loading_state, parameters, []) results = self.helper.run_test_script(script) self.test_results.append(results) # Vérifications d'état if results.get('success'): detection = results.get('vwbDetection', {}) state_handling = results.get('loadingStateHandling', {}) assert state_handling.get('canRender', False), "Rendu impossible" if action: assert detection.get('isVWBAction', False), "Détection VWB échouée" @invariant() def all_tests_successful(self): """Invariant: tous les tests doivent réussir""" for result in self.test_results: if not result.get('success', False): assert False, f"Test failed: {result.get('error', 'Unknown error')}" # Configuration de la machine à états TestVWBActionPropertiesStateMachine = VWBActionPropertiesStateMachine.TestCase def test_vwb_action_properties_comprehensive(): """Test complet des propriétés de VWBActionProperties""" helper = VWBActionPropertiesTestHelper() # Test de base avec action complète basic_action = { 'id': 'click_anchor', 'name': 'Clic sur ancre visuelle', 'description': 'Clique sur un élément identifié visuellement', 'category': 'interaction', 'parameters': { 'target_anchor': { 'type': 'VWBVisualAnchor', 'required': True, 'description': 'Élément cible à cliquer' }, 'confidence_threshold': { 'type': 'number', 'required': False, 'description': 'Seuil de confiance', 'default': 0.8, 'min': 0.5, 'max': 1.0 } }, 'examples': [], 'version': '1.0.0', 'tags': ['interaction', 'click'] } basic_loading_state = { 'isLoading': False, 'hasError': False, 'errorMessage': None, 'stepType': 'click_anchor' } basic_parameters = { 'target_anchor': None, 'confidence_threshold': 0.8 } script = helper.create_test_script(basic_action, basic_loading_state, basic_parameters, []) results = helper.run_test_script(script) assert results.get('success', False), f"Basic test failed: {results.get('error')}" assert results.get('vwbDetection', {}).get('isVWBAction', False), "VWB detection failed" if __name__ == '__main__': # Exécution directe pour tests rapides test_vwb_action_properties_comprehensive() print("✅ Tests de propriétés VWBActionProperties - Tous les tests passent")