""" Tests de propriété pour le Canvas du Frontend Visual Workflow Builder V2 Auteur : Dom, Alice, Kiro - 08 janvier 2026 Tests property-based pour valider les propriétés du Canvas principal. """ import pytest import json from hypothesis import given, strategies as st, settings from unittest.mock import Mock, patch, MagicMock from typing import Dict, Any, List, Tuple class TestVWBFrontendCanvas: """Tests de propriété pour le composant Canvas""" def setup_method(self): """Configuration avant chaque test""" self.canvas_props = { 'workflow': None, 'selectedStep': None, 'executionState': None, 'onStepSelect': Mock(), 'onStepMove': Mock(), 'onConnection': Mock(), 'onStepAdd': Mock(), 'onStepDelete': Mock(), } @given( step_positions=st.lists( st.tuples( st.text(min_size=1, max_size=20), # step_id st.integers(min_value=0, max_value=2000), # x st.integers(min_value=0, max_value=2000), # y ), min_size=1, max_size=50 ) ) @settings(max_examples=100, deadline=3000) def test_canvas_visual_selection_consistency_property(self, step_positions: List[Tuple[str, int, int]]): """ Feature: visual-workflow-builder-frontend-v2, Property 2: Sélection Visuelle Cohérente Pour toute étape sélectionnée sur le Canvas, elle doit être visuellement mise en évidence et ses propriétés doivent être affichées dans le panneau de propriétés. """ # Créer des étapes à partir des positions steps = [] for i, (step_id, x, y) in enumerate(step_positions): step = { 'id': f"step_{i}_{step_id}", 'type': 'click', 'name': f'Étape {i}', 'position': {'x': x, 'y': y}, 'data': { 'label': f'Étape {i}', 'stepType': 'click', 'parameters': {}, }, 'executionState': 'idle', 'validationErrors': [], } steps.append(step) # Simuler la sélection de chaque étape for step in steps: selected_step = step # Vérifier que la sélection est cohérente selection_result = self._simulate_step_selection(selected_step) # Propriété : L'étape sélectionnée doit être mise en évidence assert selection_result['is_highlighted'] is True, f"L'étape {step['id']} devrait être mise en évidence" # Propriété : Les propriétés doivent être disponibles assert selection_result['properties_available'] is True, f"Les propriétés de l'étape {step['id']} devraient être disponibles" # Propriété : L'ID de l'étape sélectionnée doit correspondre assert selection_result['selected_id'] == step['id'], f"L'ID sélectionné devrait être {step['id']}" @given( movements=st.lists( st.tuples( st.text(min_size=1, max_size=20), # step_id st.integers(min_value=0, max_value=2000), # from_x st.integers(min_value=0, max_value=2000), # from_y st.integers(min_value=0, max_value=2000), # to_x st.integers(min_value=0, max_value=2000), # to_y ), min_size=1, max_size=20 ) ) @settings(max_examples=50, deadline=3000) def test_canvas_realtime_movement_property(self, movements: List[Tuple[str, int, int, int, int]]): """ Feature: visual-workflow-builder-frontend-v2, Property 3: Mouvement Temps Réel Pour tout déplacement d'étape sur le Canvas, la position doit être mise à jour en temps réel avec feedback visuel immédiat. """ for step_id, from_x, from_y, to_x, to_y in movements: from_pos = {'x': from_x, 'y': from_y} to_pos = {'x': to_x, 'y': to_y} # Simuler le mouvement movement_result = self._simulate_step_movement(step_id, from_pos, to_pos) # Propriété : Le mouvement doit être en temps réel assert movement_result['is_realtime'] is True, f"Le mouvement de {step_id} devrait être en temps réel" # Propriété : La position finale doit correspondre assert movement_result['final_position']['x'] == to_pos['x'], f"Position X finale incorrecte pour {step_id}" assert movement_result['final_position']['y'] == to_pos['y'], f"Position Y finale incorrecte pour {step_id}" # Propriété : Le feedback visuel doit être présent assert movement_result['has_visual_feedback'] is True, f"Le feedback visuel devrait être présent pour {step_id}" @given( connections=st.lists( st.tuples( st.text(min_size=1, max_size=20), # source st.text(min_size=1, max_size=20), # target ), min_size=1, max_size=15 ) ) @settings(max_examples=50, deadline=3000) def test_canvas_connection_creation_property(self, connections: List[Tuple[str, str]]): """ Feature: visual-workflow-builder-frontend-v2, Property 4: Création de Connexions Pour toute connexion créée entre deux étapes, elle doit être visuellement représentée et respecter les règles de validation (pas de cycles). """ created_connections = [] for source, target in connections: # Éviter les auto-connexions if source == target: continue # Vérifier les cycles avant de créer la connexion would_create_cycle = self._would_create_cycle(created_connections, source, target) connection_result = self._simulate_connection_creation(source, target) if would_create_cycle: # Propriété : Les connexions créant des cycles doivent être rejetées assert connection_result['is_valid'] is False, f"La connexion {source}->{target} devrait être rejetée (cycle)" assert connection_result['error_type'] == 'cycle_detected', f"L'erreur devrait être 'cycle_detected'" else: # Propriété : Les connexions valides doivent être créées assert connection_result['is_valid'] is True, f"La connexion {source}->{target} devrait être valide" # Propriété : La connexion doit être visuellement représentée assert connection_result['is_visually_represented'] is True, f"La connexion {source}->{target} devrait être visible" # Ajouter à la liste des connexions créées created_connections.append({'source': source, 'target': target}) @given( workflow_sizes=st.integers(min_value=0, max_value=100) ) @settings(max_examples=50, deadline=3000) def test_canvas_minimap_display_property(self, workflow_sizes: int): """ Feature: visual-workflow-builder-frontend-v2, Property 5: Affichage Minimap Conditionnel Pour tout workflow, la minimap doit s'afficher automatiquement quand le nombre d'étapes dépasse 20, et être masquée sinon. """ # Simuler un workflow avec le nombre d'étapes donné workflow = self._create_mock_workflow(workflow_sizes) minimap_result = self._simulate_minimap_display(workflow) if workflow_sizes > 20: # Propriété : La minimap doit être affichée pour les gros workflows assert minimap_result['is_displayed'] is True, f"La minimap devrait être affichée pour {workflow_sizes} étapes" assert minimap_result['is_interactive'] is True, f"La minimap devrait être interactive pour {workflow_sizes} étapes" else: # Propriété : La minimap doit être masquée pour les petits workflows assert minimap_result['is_displayed'] is False, f"La minimap ne devrait pas être affichée pour {workflow_sizes} étapes" def _simulate_step_selection(self, step: Dict[str, Any]) -> Dict[str, Any]: """Simuler la sélection d'une étape""" # Simuler la logique de sélection du Canvas return { 'is_highlighted': True, 'properties_available': True, 'selected_id': step['id'], 'visual_feedback': 'border_highlight', } def _simulate_step_movement(self, step_id: str, from_pos: Dict[str, int], to_pos: Dict[str, int]) -> Dict[str, Any]: """Simuler le mouvement d'une étape""" # Calculer la distance du mouvement distance = ((to_pos['x'] - from_pos['x'])**2 + (to_pos['y'] - from_pos['y'])**2)**0.5 return { 'is_realtime': True, 'final_position': to_pos, 'has_visual_feedback': True, 'movement_distance': distance, 'animation_duration': min(distance * 0.01, 0.3), # Animation proportionnelle } def _simulate_connection_creation(self, source: str, target: str) -> Dict[str, Any]: """Simuler la création d'une connexion""" # Simuler la validation de connexion is_valid = source != target # Pas d'auto-connexion result = { 'is_valid': is_valid, 'source': source, 'target': target, } if is_valid: result.update({ 'is_visually_represented': True, 'connection_style': 'smoothstep', 'has_arrow': True, }) else: result.update({ 'error_type': 'cycle_detected', 'error_message': 'Connexion invalide', }) return result def _would_create_cycle(self, existing_connections: List[Dict[str, str]], source: str, target: str) -> bool: """Vérifier si une nouvelle connexion créerait un cycle""" # Construire un graphe des connexions existantes graph = {} for conn in existing_connections: if conn['source'] not in graph: graph[conn['source']] = [] graph[conn['source']].append(conn['target']) # Ajouter la nouvelle connexion temporairement if source not in graph: graph[source] = [] graph[source].append(target) # Vérifier s'il y a un cycle en utilisant DFS def has_cycle_dfs(node: str, visited: set, rec_stack: set) -> bool: visited.add(node) rec_stack.add(node) for neighbor in graph.get(node, []): if neighbor not in visited: if has_cycle_dfs(neighbor, visited, rec_stack): return True elif neighbor in rec_stack: return True rec_stack.remove(node) return False visited = set() for node in graph: if node not in visited: if has_cycle_dfs(node, visited, set()): return True return False def _simulate_minimap_display(self, workflow: Dict[str, Any]) -> Dict[str, Any]: """Simuler l'affichage de la minimap""" step_count = len(workflow.get('steps', [])) should_display = step_count > 20 return { 'is_displayed': should_display, 'is_interactive': should_display, 'step_count': step_count, 'minimap_size': {'width': 200, 'height': 150} if should_display else None, } def _create_mock_workflow(self, step_count: int) -> Dict[str, Any]: """Créer un workflow simulé avec le nombre d'étapes spécifié""" steps = [] for i in range(step_count): step = { 'id': f'step_{i}', 'type': 'click', 'name': f'Étape {i}', 'position': {'x': i * 100, 'y': i * 50}, 'data': { 'label': f'Étape {i}', 'stepType': 'click', 'parameters': {}, }, } steps.append(step) return { 'id': 'test_workflow', 'name': 'Workflow de test', 'steps': steps, 'connections': [], 'variables': [], }