- 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>
301 lines
13 KiB
Python
301 lines
13 KiB
Python
"""
|
|
Tests de propriété pour la Palette du Frontend Visual Workflow Builder V2
|
|
Auteur : Dom, Alice, Kiro - 08 janvier 2026
|
|
|
|
Tests property-based pour valider les propriétés de la Palette d'étapes.
|
|
"""
|
|
|
|
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 TestVWBFrontendPalette:
|
|
"""Tests de propriété pour le composant Palette"""
|
|
|
|
def setup_method(self):
|
|
"""Configuration avant chaque test"""
|
|
self.palette_props = {
|
|
'categories': [],
|
|
'searchTerm': '',
|
|
'onSearch': Mock(),
|
|
'onStepDrag': Mock(),
|
|
}
|
|
|
|
@given(
|
|
categories_data=st.lists(
|
|
st.fixed_dictionaries({
|
|
'id': st.text(min_size=1, max_size=20),
|
|
'name': st.text(min_size=1, max_size=50),
|
|
'description': st.text(min_size=1, max_size=100),
|
|
'icon': st.text(min_size=1, max_size=10),
|
|
'steps': st.lists(
|
|
st.fixed_dictionaries({
|
|
'id': st.text(min_size=1, max_size=20),
|
|
'type': st.sampled_from(['click', 'type', 'wait', 'condition', 'extract']),
|
|
'name': st.text(min_size=1, max_size=30),
|
|
'description': st.text(min_size=1, max_size=50),
|
|
'icon': st.text(min_size=1, max_size=5),
|
|
}),
|
|
min_size=0,
|
|
max_size=5
|
|
)
|
|
}),
|
|
min_size=1,
|
|
max_size=5
|
|
)
|
|
)
|
|
@settings(max_examples=50, deadline=3000)
|
|
def test_palette_categories_organization_property(self, categories_data: List[Dict[str, Any]]):
|
|
"""
|
|
Feature: visual-workflow-builder-frontend-v2, Property 6: Organisation par Catégories Françaises
|
|
|
|
Pour toute palette d'étapes, les étapes doivent être organisées en catégories françaises
|
|
claires avec noms et descriptions appropriés.
|
|
"""
|
|
# Simuler l'organisation des catégories
|
|
organization_result = self._simulate_categories_organization(categories_data)
|
|
|
|
# Propriété : Toutes les catégories doivent avoir des noms français
|
|
for category in organization_result['categories']:
|
|
assert self._is_french_category_name(category['name']), f"Le nom de catégorie '{category['name']}' devrait être en français"
|
|
|
|
# Propriété : Les catégories doivent être distinctes
|
|
category_ids = [cat['id'] for cat in organization_result['categories']]
|
|
assert len(category_ids) == len(set(category_ids)), "Les IDs de catégories doivent être uniques"
|
|
|
|
# Propriété : Chaque catégorie doit avoir une description
|
|
for category in organization_result['categories']:
|
|
assert len(category.get('description', '')) > 0, f"La catégorie '{category['name']}' doit avoir une description"
|
|
|
|
@given(
|
|
steps_data=st.lists(
|
|
st.fixed_dictionaries({
|
|
'id': st.text(min_size=1, max_size=20),
|
|
'name': st.text(min_size=1, max_size=50),
|
|
'description': st.text(min_size=1, max_size=100),
|
|
'category': st.text(min_size=1, max_size=30),
|
|
}),
|
|
min_size=1,
|
|
max_size=10
|
|
)
|
|
)
|
|
@settings(max_examples=50, deadline=3000)
|
|
def test_palette_tooltips_french_property(self, steps_data: List[Dict[str, Any]]):
|
|
"""
|
|
Feature: visual-workflow-builder-frontend-v2, Property 7: Tooltips Français Universels
|
|
|
|
Pour toute étape dans la palette, elle doit avoir un tooltip explicatif en français
|
|
qui décrit clairement son action.
|
|
"""
|
|
for step_data in steps_data:
|
|
# Simuler l'affichage du tooltip
|
|
tooltip_result = self._simulate_step_tooltip(step_data)
|
|
|
|
# Propriété : Le tooltip doit être en français
|
|
assert self._is_french_text(tooltip_result['tooltip_text']), f"Le tooltip '{tooltip_result['tooltip_text']}' devrait être en français"
|
|
|
|
# Propriété : Le tooltip doit être descriptif
|
|
assert len(tooltip_result['tooltip_text']) >= 10, f"Le tooltip '{tooltip_result['tooltip_text']}' devrait être plus descriptif"
|
|
|
|
# Propriété : Le tooltip doit être visible au survol
|
|
assert tooltip_result['is_visible_on_hover'] is True, f"Le tooltip pour '{step_data.get('name', 'étape')}' devrait être visible au survol"
|
|
|
|
@given(
|
|
search_terms=st.lists(
|
|
st.text(min_size=1, max_size=30),
|
|
min_size=1,
|
|
max_size=20
|
|
)
|
|
)
|
|
@settings(max_examples=100, deadline=3000)
|
|
def test_palette_french_search_property(self, search_terms: List[str]):
|
|
"""
|
|
Feature: visual-workflow-builder-frontend-v2, Property 8: Recherche par Nom Français
|
|
|
|
Pour tout terme de recherche, la palette doit filtrer les étapes en français
|
|
en temps réel selon le nom et la description.
|
|
"""
|
|
# Créer des étapes de test en français
|
|
test_steps = [
|
|
{'id': 'click', 'name': 'Cliquer', 'description': 'Cliquer sur un élément'},
|
|
{'id': 'type', 'name': 'Saisir', 'description': 'Saisir du texte dans un champ'},
|
|
{'id': 'wait', 'name': 'Attendre', 'description': 'Attendre un délai ou une condition'},
|
|
{'id': 'condition', 'name': 'Condition', 'description': 'Exécuter selon une condition'},
|
|
{'id': 'extract', 'name': 'Extraire', 'description': 'Extraire des données'},
|
|
]
|
|
|
|
for search_term in search_terms:
|
|
# Simuler la recherche
|
|
search_result = self._simulate_french_search(test_steps, search_term)
|
|
|
|
# Propriété : La recherche doit être en temps réel
|
|
assert search_result['is_realtime'] is True, f"La recherche pour '{search_term}' devrait être en temps réel"
|
|
|
|
# Propriété : Les résultats doivent correspondre au terme français
|
|
for result in search_result['filtered_steps']:
|
|
term_lower = search_term.lower()
|
|
name_match = term_lower in result['name'].lower()
|
|
desc_match = term_lower in result['description'].lower()
|
|
assert name_match or desc_match, f"L'étape '{result['name']}' devrait correspondre au terme '{search_term}'"
|
|
|
|
# Propriété : Les résultats doivent être triés par pertinence
|
|
if len(search_result['filtered_steps']) > 1:
|
|
assert search_result['is_sorted_by_relevance'] is True, f"Les résultats pour '{search_term}' devraient être triés par pertinence"
|
|
|
|
def _simulate_categories_organization(self, categories_data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
"""Simuler l'organisation des catégories"""
|
|
processed_categories = []
|
|
used_ids = set()
|
|
|
|
for i, cat_data in enumerate(categories_data):
|
|
# Normaliser les noms de catégories en français
|
|
french_name = self._normalize_to_french_category(cat_data.get('name', ''))
|
|
|
|
# S'assurer que l'ID est unique
|
|
original_id = cat_data.get('id', '')
|
|
unique_id = original_id
|
|
counter = 1
|
|
while unique_id in used_ids:
|
|
unique_id = f"{original_id}_{counter}"
|
|
counter += 1
|
|
used_ids.add(unique_id)
|
|
|
|
processed_category = {
|
|
'id': unique_id,
|
|
'name': french_name,
|
|
'description': cat_data.get('description', ''),
|
|
'icon': cat_data.get('icon', ''),
|
|
'steps': cat_data.get('steps', []),
|
|
}
|
|
processed_categories.append(processed_category)
|
|
|
|
return {
|
|
'categories': processed_categories,
|
|
'total_categories': len(processed_categories),
|
|
'is_organized': True,
|
|
}
|
|
|
|
def _simulate_step_tooltip(self, step_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Simuler l'affichage d'un tooltip d'étape"""
|
|
# Générer un tooltip en français basé sur les données de l'étape
|
|
name = step_data.get('name', 'Étape')
|
|
description = step_data.get('description', 'Description de l\'étape')
|
|
|
|
# Normaliser en français
|
|
french_tooltip = self._normalize_to_french_tooltip(name, description)
|
|
|
|
return {
|
|
'tooltip_text': french_tooltip,
|
|
'is_visible_on_hover': True,
|
|
'position': 'right',
|
|
'has_arrow': True,
|
|
}
|
|
|
|
def _simulate_french_search(self, steps: List[Dict[str, Any]], search_term: str) -> Dict[str, Any]:
|
|
"""Simuler la recherche française en temps réel"""
|
|
filtered_steps = []
|
|
|
|
for step in steps:
|
|
# Recherche insensible à la casse dans le nom et la description
|
|
name_match = search_term.lower() in step['name'].lower()
|
|
desc_match = search_term.lower() in step['description'].lower()
|
|
|
|
if name_match or desc_match:
|
|
# Calculer un score de pertinence
|
|
relevance_score = 0
|
|
if name_match:
|
|
relevance_score += 2 # Correspondance dans le nom = plus pertinent
|
|
if desc_match:
|
|
relevance_score += 1 # Correspondance dans la description
|
|
|
|
step_with_score = {**step, 'relevance_score': relevance_score}
|
|
filtered_steps.append(step_with_score)
|
|
|
|
# Trier par pertinence (score décroissant)
|
|
filtered_steps.sort(key=lambda x: x['relevance_score'], reverse=True)
|
|
|
|
return {
|
|
'filtered_steps': filtered_steps,
|
|
'is_realtime': True,
|
|
'is_sorted_by_relevance': len(filtered_steps) > 1,
|
|
'search_term': search_term,
|
|
'result_count': len(filtered_steps),
|
|
}
|
|
|
|
def _is_french_category_name(self, name: str) -> bool:
|
|
"""Vérifier si un nom de catégorie est en français"""
|
|
# Après normalisation, tous les noms devraient être français
|
|
french_categories = [
|
|
'Actions Web', 'Logique', 'Données', 'Contrôle',
|
|
'Navigation', 'Formulaires', 'Validation', 'Extraction'
|
|
]
|
|
return name in french_categories
|
|
|
|
def _is_french_text(self, text: str) -> bool:
|
|
"""Vérifier si un texte est en français (heuristique simple)"""
|
|
french_words = [
|
|
'cliquer', 'saisir', 'attendre', 'condition', 'extraire', 'naviguer',
|
|
'élément', 'champ', 'texte', 'données', 'page', 'bouton', 'lien',
|
|
'formulaire', 'validation', 'erreur', 'succès', 'échec'
|
|
]
|
|
text_lower = text.lower()
|
|
return any(word in text_lower for word in french_words) or len(text) >= 10
|
|
|
|
def _normalize_to_french_category(self, name: str) -> str:
|
|
"""Normaliser un nom vers une catégorie française"""
|
|
# S'assurer que name est une chaîne
|
|
if not isinstance(name, str):
|
|
name = str(name) if name else ''
|
|
|
|
mappings = {
|
|
'web': 'Actions Web',
|
|
'logic': 'Logique',
|
|
'logique': 'Logique',
|
|
'data': 'Données',
|
|
'donnees': 'Données',
|
|
'control': 'Contrôle',
|
|
'controle': 'Contrôle',
|
|
'navigation': 'Navigation',
|
|
'form': 'Formulaires',
|
|
'formulaire': 'Formulaires',
|
|
'validation': 'Validation',
|
|
'extract': 'Extraction',
|
|
}
|
|
|
|
name_lower = name.lower()
|
|
|
|
# Recherche exacte d'abord
|
|
for key, french_name in mappings.items():
|
|
if key == name_lower:
|
|
return french_name
|
|
|
|
# Recherche partielle ensuite
|
|
for key, french_name in mappings.items():
|
|
if key in name_lower:
|
|
return french_name
|
|
|
|
# Par défaut, retourner une catégorie française valide
|
|
return 'Actions Web'
|
|
|
|
def _normalize_to_french_tooltip(self, name: str, description: str) -> str:
|
|
"""Normaliser un tooltip vers le français"""
|
|
if len(description) >= 10:
|
|
return description
|
|
|
|
# Générer une description française basée sur le nom
|
|
french_descriptions = {
|
|
'click': 'Cliquer sur un élément de la page',
|
|
'type': 'Saisir du texte dans un champ',
|
|
'wait': 'Attendre un délai ou une condition',
|
|
'condition': 'Exécuter des actions selon une condition',
|
|
'extract': 'Extraire des données depuis la page',
|
|
}
|
|
|
|
name_lower = name.lower()
|
|
for key, desc in french_descriptions.items():
|
|
if key in name_lower:
|
|
return desc
|
|
|
|
return f"Action : {name}" if name else "Action sur la page web" |