""" 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"