Files
rpa_vision_v3/tests/property/test_vwb_frontend_v2_canvas.py
Dom a27b74cf22 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>
2026-01-29 11:23:51 +01:00

309 lines
13 KiB
Python

"""
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': [],
}