import { Glob } from 'bun'; import type { ParsedFeature, ParsedScenario, ParsedStep } from '../src/types/gherkin'; // Map French screen names to screen IDs (same as navigation.steps.ts) const screenNameMap: Record = { 'accueil': 'home', 'liste des événements': 'events', 'découvrir': 'events', 'détail événement': 'event-detail', 'détail de l\'événement': 'event-detail', 'créer un événement': 'create-event', 'création d\'événement': 'create-event', 'inviter des amis': 'invite', 'invitation': 'invite', 'mon profil': 'profile', 'profil': 'profile', 'profil utilisateur': 'user-profile', 'profil d\'un utilisateur': 'user-profile', 'connexion': 'login', 'paramètres': 'settings', 'réglages': 'settings', 'points de rencontre': 'meeting-points', 'partage de profil': 'share-profile', 'mon réseau': 'friends-list', 'liste des participants': 'participants-list', }; // Valid screen IDs (for direct matches) const validScreenIds = new Set([ 'home', 'events', 'event-detail', 'create-event', 'invite', 'profile', 'user-profile', 'login', 'settings', 'meeting-points', 'share-profile', 'friends-list', 'participants-list', ]); function resolveScreenId(name: string): string | null { const normalized = name.toLowerCase().trim(); // First check if it's already a valid screen ID if (validScreenIds.has(normalized)) { return normalized; } // Then check the French name map return screenNameMap[normalized] || null; } function extractScreenIdsFromSteps(steps: ParsedStep[]): Set { const screenIds = new Set(); for (const step of steps) { // Match patterns like: je vois l'écran "event-detail", je suis sur la page "accueil", etc. const patterns = [ /je vois l'écran "([^"]+)"/gi, /je suis sur la page "([^"]+)"/gi, /je suis redirigé vers "([^"]+)"/gi, /je navigue vers "([^"]+)"/gi, /l'écran "([^"]+)" est affiché/gi, ]; for (const pattern of patterns) { let match; while ((match = pattern.exec(step.text)) !== null) { const captured = match[1]; if (captured) { const screenId = resolveScreenId(captured); if (screenId) { screenIds.add(screenId); } } } } } return screenIds; } async function parseFeatures(): Promise { const glob = new Glob('features/**/*.feature'); const features: ParsedFeature[] = []; for await (const filePath of glob.scan('.')) { const content = await Bun.file(filePath).text(); const parsed = parseGherkinContent(content, filePath); if (parsed) { features.push(parsed); } } features.sort((a, b) => { if (a.priority !== b.priority) return a.priority - b.priority; return a.category.localeCompare(b.category); }); const output = `// Auto-generated by scripts/parse-features.ts // Do not edit manually import type { ParsedFeature } from '../types/gherkin'; export const parsedFeatures: ParsedFeature[] = ${JSON.stringify(features, null, 2)}; export function getFeatureById(id: string): ParsedFeature | undefined { return parsedFeatures.find(f => f.id === id); } export function getFeaturesByCategory(category: string): ParsedFeature[] { return parsedFeatures.filter(f => f.category === category); } export function getFeaturesByPriority(priority: number): ParsedFeature[] { return parsedFeatures.filter(f => f.priority === priority); } export function getAllCategories(): string[] { return [...new Set(parsedFeatures.map(f => f.category))]; } export function getAllPriorities(): number[] { return [...new Set(parsedFeatures.map(f => f.priority))].sort((a, b) => a - b); } `; await Bun.write('src/data/features.ts', output); console.log(`Parsed ${features.length} feature files`); return features; } function parseGherkinContent(content: string, filePath: string): ParsedFeature | null { const lines = content.split('\n'); // Extract tags from the beginning const tagLines: string[] = []; let contentStartIndex = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]?.trim() ?? ''; if (line.startsWith('#')) { contentStartIndex = i + 1; continue; } if (line.startsWith('@')) { tagLines.push(line); contentStartIndex = i + 1; } else if (line !== '') { break; } } const tagMatches = tagLines.join(' ').match(/@[\w-]+/g) || []; // Extract category and priority from tags const categoryTag = tagMatches.find(t => ['@WORKSHOP', '@EVENT', '@USER', '@MEETING', '@NOTIF'].includes(t) ); const category = categoryTag ? categoryTag.slice(1) : 'UNKNOWN'; const priorityTag = tagMatches.find(t => t.startsWith('@priority-')); const priority = priorityTag ? parseInt(priorityTag.replace('@priority-', '')) : 3; // Extract feature name const featureMatch = content.match(/Fonctionnalité:\s*(.+?)(?:\n|$)/); const name = featureMatch?.[1]?.trim() || 'Unknown Feature'; // Extract US ID from name const idMatch = name.match(/US-(\d+)/i); const id = idMatch ? `us-${idMatch[1]}` : filePath.replace(/.*\//, '').replace('.feature', ''); // Extract description (lines after Feature until Contexte or Scénario) const descLines: string[] = []; let inDescription = false; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('Fonctionnalité:')) { inDescription = true; continue; } if (inDescription) { if (trimmed.startsWith('Contexte:') || trimmed.startsWith('Scénario:') || trimmed.startsWith('Plan du Scénario:')) { break; } if (trimmed && !trimmed.startsWith('@') && !trimmed.startsWith('#')) { descLines.push(trimmed); } } } const description = descLines.join(' ').trim(); // Parse scenarios const scenarios: ParsedScenario[] = []; let currentScenario: ParsedScenario | null = null; let inBackground = false; const background: ParsedStep[] = []; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('Contexte:')) { inBackground = true; continue; } if (trimmed.startsWith('Scénario:') || trimmed.startsWith('Plan du Scénario:')) { inBackground = false; if (currentScenario) { scenarios.push(currentScenario); } const scenarioName = trimmed.replace(/^(Scénario:|Plan du Scénario:)\s*/, '').trim(); currentScenario = { name: scenarioName, tags: [], steps: [], }; continue; } // Parse steps const stepKeywords = ['Étant donné que ', "Étant donné qu'", 'Étant donné', 'Etant donné que ', "Etant donné qu'", 'Etant donné', 'Quand', 'Lorsque', 'Alors', 'Et', 'Mais']; for (const keyword of stepKeywords) { if (trimmed.startsWith(keyword)) { const step: ParsedStep = { keyword, text: trimmed.slice(keyword.length).trim(), }; if (inBackground) { background.push(step); } else if (currentScenario) { currentScenario.steps.push(step); } break; } } } // Don't forget the last scenario if (currentScenario) { scenarios.push(currentScenario); } // Extract screen IDs from all steps (background + all scenarios) const allSteps = [...background]; for (const scenario of scenarios) { allSteps.push(...scenario.steps); } const screenIds = Array.from(extractScreenIdsFromSteps(allSteps)).sort(); return { id, name, description, tags: tagMatches, category, priority, background: background.length > 0 ? background : undefined, scenarios, filePath, rawContent: content, screenIds, }; } // Run parser parseFeatures().then(features => { console.log('Features by category:'); const byCategory = features.reduce((acc, f) => { acc[f.category] = (acc[f.category] || 0) + 1; return acc; }, {} as Record); console.log(byCategory); console.log('\nFeatures by priority:'); const byPriority = features.reduce((acc, f) => { acc[`P${f.priority}`] = (acc[`P${f.priority}`] || 0) + 1; return acc; }, {} as Record); console.log(byPriority); }).catch(console.error);