first commit

This commit is contained in:
Sylvain Duchesne
2026-01-18 11:53:42 +01:00
commit f04f15d926
112 changed files with 24858 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
import { Before, After, BeforeAll, AfterAll, Status } from '@cucumber/cucumber';
import type { FestipodWorld } from './world';
BeforeAll(async function () {
console.log('Starting Festipod BDD tests...');
});
Before(async function (this: FestipodWorld, scenario) {
this.currentRoute = '#/';
this.currentScreenId = null;
this.formFields.clear();
this.navigationHistory = [];
this.isAuthenticated = false;
this.screenSourceContent = '';
this.currentScreen = null;
// Mark @pending scenarios as pending
const isPending = scenario.pickle.tags.some(tag => tag.name === '@pending');
if (isPending) {
return 'pending';
}
});
After(async function (this: FestipodWorld, scenario) {
if (scenario.result?.status === Status.FAILED) {
this.attach(`Current route: ${this.currentRoute}`, 'text/plain');
this.attach(`Current screen: ${this.currentScreenId}`, 'text/plain');
this.attach(`Navigation history: ${JSON.stringify(this.navigationHistory)}`, 'text/plain');
this.attach(`Form fields: ${JSON.stringify(Array.from(this.formFields.entries()))}`, 'text/plain');
if (this.screenSourceContent) {
// Show first 500 chars of source to help debug
this.attach(`Screen source (first 500 chars): ${this.screenSourceContent.substring(0, 500)}...`, 'text/plain');
}
}
// Clean up
this.cleanup();
});
AfterAll(async function () {
console.log('Festipod BDD tests completed.');
});
+338
View File
@@ -0,0 +1,338 @@
import { World, setWorldConstructor, type IWorldOptions } from '@cucumber/cucumber';
import { getScreen, type Screen } from '../../src/screens/index';
import * as fs from 'fs';
import * as path from 'path';
export interface FestipodWorld extends World {
currentRoute: string;
currentScreenId: string | null;
formFields: Map<string, { required: boolean; value: string }>;
navigationHistory: string[];
isAuthenticated: boolean;
// Screen analysis
currentScreen: Screen | null;
screenSourceContent: string;
navigateTo(route: string): void;
getFormField(name: string): { required: boolean; value: string } | undefined;
getCurrentScreenFields(): string[];
setScreenFields(screenId: string): void;
// Methods for screen content analysis
loadScreenSource(screenId: string): void;
getRenderedText(): string;
hasText(text: string): boolean;
hasField(fieldName: string): boolean;
hasElement(selector: string): boolean;
cleanup(): void;
}
// Map screen IDs to their source file names
const screenFileMap: Record<string, string> = {
'home': 'HomeScreen.tsx',
'login': 'LoginScreen.tsx',
'profile': 'ProfileScreen.tsx',
'user-profile': 'UserProfileScreen.tsx',
'settings': 'SettingsScreen.tsx',
'events': 'EventsScreen.tsx',
'event-detail': 'EventDetailScreen.tsx',
'create-event': 'CreateEventScreen.tsx',
'invite': 'InviteScreen.tsx',
'participants-list': 'ParticipantsListScreen.tsx',
'meeting-points': 'MeetingPointsScreen.tsx',
'friends-list': 'FriendsListScreen.tsx',
'share-profile': 'ShareProfileScreen.tsx',
};
// Screen-specific field detectors - each screen has its own precise detectors
// tailored to its actual implementation. This avoids generic matching.
export const screenFieldDetectors: Record<string, Record<string, (source: string) => boolean>> = {
'event-detail': {
// EventDetailScreen.tsx line 29: <Title>Barbecue d'été</Title>
'Titre': (s) => /<Title[^>]*>[^<]+<\/Title>/.test(s),
// EventDetailScreen.tsx line 33: 📅 Samedi 25 janvier 2025
'Date': (s) => /📅[^<]*(?:janvier|février|mars|avril|mai|juin|juillet|août|septembre|octobre|novembre|décembre)[^<]*\d{4}/i.test(s),
// EventDetailScreen.tsx line 36: 🕓 16h00 - 21h00
'Heure': (s) => /🕓[^<]*\d{1,2}h\d{2}/.test(s),
// EventDetailScreen.tsx line 39: 📍 Parc Central, Pelouse Ouest
'Lieu': (s) => /📍[^<]*[A-ZÀ-Ý][a-zà-ÿ]+/.test(s),
// EventDetailScreen.tsx lines 77-81: À propos section with description
'Description': (s) => {
const match = s.match(/À propos[\s\S]*?<Text[^>]*>([\s\S]*?)<\/Text>/);
return match !== null && match[1].trim().length > 50;
},
// EventDetailScreen.tsx lines 8-13: attendees with { name: 'Marie' } rendered via {a.name}
'Nom': (s) => /name:\s*['"][^'"]+['"]/.test(s) && /\{[^}]*\.name\}/.test(s),
'Nom du participant': (s) => /name:\s*['"][^'"]+['"]/.test(s) && /\{[^}]*\.name\}/.test(s),
// EventDetailScreen.tsx: <Avatar> components for participants
'Photo': (s) => /<Avatar/.test(s),
// NOT IMPLEMENTED: no comment UI in EventDetailScreen
'Commentaire': (s) => /<textarea/i.test(s) || /commentaire/i.test(s),
},
'user-profile': {
// UserProfileScreen.tsx line 24: <Title>Jean Durand</Title>
'Nom': (s) => /<Title[^>]*>[A-ZÀ-Ý][a-zà-ÿ]+\s+[A-ZÀ-Ý][a-zà-ÿ]+<\/Title>/.test(s),
// UserProfileScreen.tsx line 25: @jeandurand
'Pseudo': (s) => /@[a-zA-Z0-9_]+/.test(s),
// UserProfileScreen.tsx line 23: <Avatar initials="JD" size="lg" />
'Photo': (s) => /<Avatar/.test(s),
'Photo de profil': (s) => /<Avatar/.test(s),
},
'profile': {
// ProfileScreen.tsx: similar to user-profile
'Nom': (s) => /<Title[^>]*>[A-ZÀ-Ý][a-zà-ÿ]+\s+[A-ZÀ-Ý][a-zà-ÿ]+<\/Title>/.test(s),
'Pseudo': (s) => /@[a-zA-Z0-9_]+/.test(s),
'Photo': (s) => /<Avatar/.test(s),
'Photo de profil': (s) => /<Avatar/.test(s),
},
};
// Expected content that should be present in each screen
// This maps to what the BDD specs verify - based on actual screen content
export const screenExpectedContent: Record<string, string[]> = {
'create-event': [
'Nom de l\'événement',
'Date',
'Heure de début',
'Lieu',
'Thématique',
'Créer l\'événement',
],
'profile': [
'Mon profil',
'Modifier le profil',
'Partager',
'Événement',
],
'user-profile': [
'Profil',
],
'settings': [
'Paramètres',
'Notifications',
'Confidentialité',
'Localisation',
],
'login': [
'Email',
'Mot de passe',
'Se connecter',
],
'event-detail': [
'Participants',
'À propos',
'Participer',
'Inviter',
],
'events': [
'Découvrir',
'Rechercher',
],
'home': [
'Événements à venir',
'Créer un événement',
],
'invite': [
'Inviter',
'Rechercher',
],
'meeting-points': [
'Point de rencontre',
],
'share-profile': [
'Partager',
'QR',
],
'friends-list': [
'Mon réseau',
],
'participants-list': [
'Participants',
],
};
// Required fields that forms should have (for form verification)
export const screenRequiredFields: Record<string, string[]> = {
'create-event': [
'Nom de l\'événement',
'Date',
'Heure de début',
'Lieu',
'Thématique',
],
'profile': [
'Photo de profil',
'Nom',
'Pseudo',
],
'user-profile': [
'Photo de profil',
'Nom',
'Pseudo',
],
'settings': [
'Notifications',
'Confidentialité',
'Rayon de notification',
],
'login': [
'Email',
'Mot de passe',
],
'event-detail': [
'Titre',
'Date',
'Lieu',
'Description',
'Liste des participants',
],
'events': [
'Liste des événements',
'Filtre par date',
],
'home': [
'Événements à venir',
'Navigation',
],
'invite': [
'Liste des contacts',
'Recherche',
],
'meeting-points': [
'Lieu de rencontre',
'Heure',
],
'share-profile': [
'QR Code',
'Lien de partage',
],
};
class CustomWorld extends World implements FestipodWorld {
currentRoute: string = '#/';
currentScreenId: string | null = null;
formFields: Map<string, { required: boolean; value: string }> = new Map();
navigationHistory: string[] = [];
isAuthenticated: boolean = false;
// Screen analysis
currentScreen: Screen | null = null;
screenSourceContent: string = '';
constructor(options: IWorldOptions) {
super(options);
}
navigateTo(route: string): void {
this.navigationHistory.push(route);
this.currentRoute = route;
if (route.startsWith('#/demo/')) {
this.currentScreenId = route.replace('#/demo/', '');
this.setScreenFields(this.currentScreenId);
// Load the screen source for content verification
this.loadScreenSource(this.currentScreenId);
} else if (route === '#/specs' || route.startsWith('#/specs/')) {
this.currentScreenId = null;
} else if (route === '#/stories' || route.startsWith('#/stories/')) {
this.currentScreenId = null;
} else {
this.currentScreenId = null;
}
}
getFormField(name: string) {
return this.formFields.get(name);
}
getCurrentScreenFields(): string[] {
return Array.from(this.formFields.keys());
}
setScreenFields(screenId: string): void {
this.formFields.clear();
const fields = screenRequiredFields[screenId] || [];
fields.forEach(field => {
this.formFields.set(field, { required: true, value: '' });
});
}
loadScreenSource(screenId: string): void {
// Get the screen component
const screen = getScreen(screenId);
if (!screen) {
this.screenSourceContent = '';
this.currentScreen = null;
return;
}
this.currentScreen = screen;
// Read the source file to analyze its content
const fileName = screenFileMap[screenId];
if (fileName) {
const filePath = path.join(process.cwd(), 'src', 'screens', fileName);
try {
this.screenSourceContent = fs.readFileSync(filePath, 'utf-8');
} catch {
this.screenSourceContent = '';
}
} else {
this.screenSourceContent = '';
}
}
getRenderedText(): string {
// Return the source content which contains all the text that will be rendered
return this.screenSourceContent;
}
hasText(text: string): boolean {
// Check if the text appears in the screen source
// This verifies the component contains the expected text
return this.screenSourceContent.includes(text);
}
hasField(fieldName: string): boolean {
// Use screen-specific field detector if available
if (this.currentScreenId) {
const screenDetectors = screenFieldDetectors[this.currentScreenId];
if (screenDetectors && screenDetectors[fieldName]) {
return screenDetectors[fieldName](this.screenSourceContent);
}
}
// Fall back to literal text search
return this.screenSourceContent.includes(fieldName);
}
hasElement(selector: string): boolean {
// Check for common patterns in JSX
if (!this.screenSourceContent) return false;
// Check for element types like textarea, input, button
if (selector === 'textarea') {
return this.screenSourceContent.includes('<textarea') ||
this.screenSourceContent.includes('textarea');
}
if (selector === 'input') {
return this.screenSourceContent.includes('<Input') ||
this.screenSourceContent.includes('<input');
}
if (selector === 'button') {
return this.screenSourceContent.includes('<Button') ||
this.screenSourceContent.includes('<button');
}
return this.screenSourceContent.includes(selector);
}
cleanup(): void {
this.screenSourceContent = '';
this.currentScreen = null;
}
}
setWorldConstructor(CustomWorld);