v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- 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>
This commit is contained in:
309
tests/property/test_vwb_frontend_v2_canvas.py
Normal file
309
tests/property/test_vwb_frontend_v2_canvas.py
Normal file
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
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': [],
|
||||
}
|
||||
Reference in New Issue
Block a user