627 lines
No EOL
19 KiB
TypeScript
627 lines
No EOL
19 KiB
TypeScript
import { DataRelationManager } from '../../core/utils/data-relations'
|
|
import { UserGenerator } from '../../core/generators/users'
|
|
import { AudioGenerator } from '../../core/generators/audio'
|
|
import { ConversationGenerator } from '../../core/generators/conversations'
|
|
import { vezaFaker } from '../../core/utils/faker-config'
|
|
import type { User, Audio, Playlist, Conversation } from '../../core/schemas/database'
|
|
|
|
/**
|
|
* New User Onboarding Scenario
|
|
*
|
|
* Simulates a complete user journey from registration to first meaningful interactions
|
|
*/
|
|
export interface OnboardingScenarioContext {
|
|
user: User
|
|
initialData: {
|
|
recommendedTracks: Audio[]
|
|
welcomePlaylist: Playlist
|
|
welcomeConversation: Conversation
|
|
onboardingSteps: OnboardingStep[]
|
|
}
|
|
expectedBehavior: {
|
|
shouldSeeWelcomeMessage: boolean
|
|
shouldHaveEmptyLibrary: boolean
|
|
shouldReceiveRecommendations: boolean
|
|
shouldJoinWelcomeChannel: boolean
|
|
shouldCompleteOnboarding: boolean
|
|
}
|
|
testData: {
|
|
loginCredentials: { email: string; password: string }
|
|
profileData: Partial<User>
|
|
firstActions: UserAction[]
|
|
}
|
|
}
|
|
|
|
export interface OnboardingStep {
|
|
id: string
|
|
title: string
|
|
description: string
|
|
type: 'welcome' | 'profile' | 'preferences' | 'discovery' | 'social'
|
|
isRequired: boolean
|
|
isCompleted: boolean
|
|
data?: any
|
|
}
|
|
|
|
export interface UserAction {
|
|
type: 'play_track' | 'like_track' | 'create_playlist' | 'follow_user' | 'send_message'
|
|
timestamp: Date
|
|
data: any
|
|
expectedResult: string
|
|
}
|
|
|
|
/**
|
|
* New User Onboarding Scenario Generator
|
|
*/
|
|
export class NewUserOnboardingScenario {
|
|
/**
|
|
* Generate complete onboarding scenario
|
|
*/
|
|
static async setup(): Promise<OnboardingScenarioContext> {
|
|
console.log('🎯 Setting up new user onboarding scenario...')
|
|
|
|
// Generate new user
|
|
const user = UserGenerator.generate({
|
|
role: 'user',
|
|
status: 'active',
|
|
emailVerified: false, // Will be verified during onboarding
|
|
withAvatar: false, // Will be set during profile completion
|
|
withBio: false,
|
|
withStats: false, // New user has no activity
|
|
})
|
|
|
|
// Override stats for new user
|
|
user.stats = {
|
|
tracksUploaded: 0,
|
|
playlistsCreated: 0,
|
|
followersCount: 0,
|
|
followingCount: 0,
|
|
totalPlays: 0,
|
|
}
|
|
|
|
DataRelationManager.registerUser(user)
|
|
|
|
// Generate initial recommendations based on popular content
|
|
const allTracks = Array.from(DataRelationManager.getAll().tracks.values())
|
|
const recommendedTracks = this.generateRecommendations(allTracks, user)
|
|
|
|
// Create welcome playlist
|
|
const welcomePlaylist = AudioGenerator.generatePlaylist({
|
|
createdById: 'system', // System-generated playlist
|
|
trackCount: 10,
|
|
visibility: 'public',
|
|
theme: 'random'
|
|
})
|
|
welcomePlaylist.title = 'Bienvenue sur Veza !'
|
|
welcomePlaylist.description = 'Une sélection de morceaux populaires pour découvrir la plateforme'
|
|
welcomePlaylist.trackIds = recommendedTracks.slice(0, 10).map(t => t.id)
|
|
|
|
DataRelationManager.registerPlaylist(welcomePlaylist)
|
|
|
|
// Create welcome conversation (general channel)
|
|
const welcomeConversation = ConversationGenerator.generateConversation({
|
|
type: 'channel',
|
|
participantIds: [user.id, 'system'], // User + system/moderators
|
|
messageCount: 5, // Few welcome messages
|
|
timeSpan: 'recent'
|
|
})
|
|
welcomeConversation.name = 'Bienvenue'
|
|
welcomeConversation.description = 'Canal d\'accueil pour les nouveaux utilisateurs'
|
|
welcomeConversation.isPrivate = false
|
|
|
|
DataRelationManager.registerConversation(welcomeConversation)
|
|
|
|
// Generate onboarding steps
|
|
const onboardingSteps = this.generateOnboardingSteps(user)
|
|
|
|
// Generate first user actions
|
|
const firstActions = this.generateFirstUserActions(user, recommendedTracks)
|
|
|
|
const context: OnboardingScenarioContext = {
|
|
user,
|
|
initialData: {
|
|
recommendedTracks,
|
|
welcomePlaylist,
|
|
welcomeConversation,
|
|
onboardingSteps
|
|
},
|
|
expectedBehavior: {
|
|
shouldSeeWelcomeMessage: true,
|
|
shouldHaveEmptyLibrary: true,
|
|
shouldReceiveRecommendations: true,
|
|
shouldJoinWelcomeChannel: true,
|
|
shouldCompleteOnboarding: true
|
|
},
|
|
testData: {
|
|
loginCredentials: {
|
|
email: user.email,
|
|
password: 'TestPassword123!'
|
|
},
|
|
profileData: {
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
bio: 'Nouveau sur Veza, passionné de musique !',
|
|
location: user.location,
|
|
preferences: {
|
|
...user.preferences,
|
|
notifications: {
|
|
email: true,
|
|
push: true,
|
|
desktop: false
|
|
}
|
|
}
|
|
},
|
|
firstActions
|
|
}
|
|
}
|
|
|
|
console.log('✅ New user onboarding scenario setup complete')
|
|
return context
|
|
}
|
|
|
|
/**
|
|
* Generate multiple onboarding scenarios for different user types
|
|
*/
|
|
static async setupBatch(count: number = 5): Promise<OnboardingScenarioContext[]> {
|
|
const scenarios: OnboardingScenarioContext[] = []
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const scenario = await this.setup()
|
|
scenarios.push(scenario)
|
|
}
|
|
|
|
return scenarios
|
|
}
|
|
|
|
/**
|
|
* Simulate user completing onboarding steps
|
|
*/
|
|
static simulateOnboardingCompletion(context: OnboardingScenarioContext): {
|
|
completedSteps: OnboardingStep[]
|
|
updatedUser: User
|
|
generatedContent: {
|
|
firstPlaylist: Playlist
|
|
profileUpdates: Partial<User>
|
|
firstInteractions: UserAction[]
|
|
}
|
|
} {
|
|
console.log(`🎮 Simulating onboarding completion for user: ${context.user.username}`)
|
|
|
|
// Mark steps as completed
|
|
const completedSteps = context.initialData.onboardingSteps.map(step => ({
|
|
...step,
|
|
isCompleted: true,
|
|
data: this.generateStepCompletionData(step, context.user)
|
|
}))
|
|
|
|
// Update user profile
|
|
const updatedUser: User = {
|
|
...context.user,
|
|
...context.testData.profileData,
|
|
emailVerified: true,
|
|
avatar: vezaFaker.image.avatar(),
|
|
isVerified: false, // New users aren't verified yet
|
|
preferences: {
|
|
...context.user.preferences,
|
|
...context.testData.profileData.preferences
|
|
},
|
|
updatedAt: new Date()
|
|
}
|
|
|
|
// Create user's first playlist
|
|
const firstPlaylist = AudioGenerator.generatePlaylist({
|
|
createdById: context.user.id,
|
|
trackCount: 5,
|
|
visibility: 'private',
|
|
theme: 'random'
|
|
})
|
|
firstPlaylist.title = 'Ma première playlist'
|
|
firstPlaylist.description = 'Mes premiers coups de cœur sur Veza'
|
|
firstPlaylist.trackIds = context.initialData.recommendedTracks.slice(0, 5).map(t => t.id)
|
|
|
|
DataRelationManager.registerPlaylist(firstPlaylist)
|
|
|
|
// Update user stats
|
|
updatedUser.stats.playlistsCreated = 1
|
|
|
|
// Generate first interactions
|
|
const firstInteractions = this.generateFirstInteractions(context)
|
|
|
|
console.log('✅ Onboarding completion simulation complete')
|
|
|
|
return {
|
|
completedSteps,
|
|
updatedUser,
|
|
generatedContent: {
|
|
firstPlaylist,
|
|
profileUpdates: context.testData.profileData,
|
|
firstInteractions
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate onboarding scenario expectations
|
|
*/
|
|
static validateScenario(context: OnboardingScenarioContext): {
|
|
isValid: boolean
|
|
passedChecks: string[]
|
|
failedChecks: string[]
|
|
warnings: string[]
|
|
} {
|
|
const passedChecks: string[] = []
|
|
const failedChecks: string[] = []
|
|
const warnings: string[] = []
|
|
|
|
// Check user data
|
|
if (context.user.stats.tracksUploaded === 0) {
|
|
passedChecks.push('New user has no uploaded tracks')
|
|
} else {
|
|
failedChecks.push('New user should not have uploaded tracks')
|
|
}
|
|
|
|
if (context.user.stats.playlistsCreated === 0) {
|
|
passedChecks.push('New user has no created playlists')
|
|
} else {
|
|
failedChecks.push('New user should not have created playlists initially')
|
|
}
|
|
|
|
if (!context.user.emailVerified) {
|
|
passedChecks.push('New user email is not verified initially')
|
|
} else {
|
|
warnings.push('New user email should not be verified initially')
|
|
}
|
|
|
|
// Check recommendations
|
|
if (context.initialData.recommendedTracks.length > 0) {
|
|
passedChecks.push('Recommendations are provided')
|
|
} else {
|
|
failedChecks.push('No recommendations provided for new user')
|
|
}
|
|
|
|
// Check welcome content
|
|
if (context.initialData.welcomePlaylist) {
|
|
passedChecks.push('Welcome playlist exists')
|
|
} else {
|
|
failedChecks.push('Welcome playlist not created')
|
|
}
|
|
|
|
if (context.initialData.welcomeConversation) {
|
|
passedChecks.push('Welcome conversation exists')
|
|
} else {
|
|
failedChecks.push('Welcome conversation not created')
|
|
}
|
|
|
|
// Check onboarding steps
|
|
const requiredSteps = context.initialData.onboardingSteps.filter(s => s.isRequired)
|
|
if (requiredSteps.length >= 3) {
|
|
passedChecks.push('Sufficient required onboarding steps')
|
|
} else {
|
|
failedChecks.push('Not enough required onboarding steps')
|
|
}
|
|
|
|
// Check first actions
|
|
if (context.testData.firstActions.length > 0) {
|
|
passedChecks.push('First user actions defined')
|
|
} else {
|
|
warnings.push('No first user actions defined')
|
|
}
|
|
|
|
return {
|
|
isValid: failedChecks.length === 0,
|
|
passedChecks,
|
|
failedChecks,
|
|
warnings
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Private helper methods
|
|
*/
|
|
private static generateRecommendations(allTracks: Audio[], user: User): Audio[] {
|
|
// For new users, recommend popular tracks across different genres
|
|
const popularTracks = allTracks
|
|
.sort((a, b) => b.stats.plays - a.stats.plays)
|
|
.slice(0, 50)
|
|
|
|
// Select diverse genres
|
|
const genreGroups = this.groupTracksByGenre(popularTracks)
|
|
const recommendations: Audio[] = []
|
|
|
|
// Take 2-3 tracks from each popular genre
|
|
Object.entries(genreGroups).forEach(([genre, tracks]) => {
|
|
const genreTracks = vezaFaker.helpers.arrayElements(tracks, Math.min(3, tracks.length))
|
|
recommendations.push(...genreTracks)
|
|
})
|
|
|
|
// Shuffle and limit to 20 recommendations
|
|
return vezaFaker.helpers.shuffle(recommendations).slice(0, 20)
|
|
}
|
|
|
|
private static groupTracksByGenre(tracks: Audio[]): Record<string, Audio[]> {
|
|
return tracks.reduce((groups, track) => {
|
|
if (!groups[track.genre]) {
|
|
groups[track.genre] = []
|
|
}
|
|
groups[track.genre]!.push(track)
|
|
return groups
|
|
}, {} as Record<string, Audio[]>)
|
|
}
|
|
|
|
private static generateOnboardingSteps(user: User): OnboardingStep[] {
|
|
return [
|
|
{
|
|
id: 'welcome',
|
|
title: 'Bienvenue sur Veza !',
|
|
description: 'Découvrez votre nouvelle plateforme musicale',
|
|
type: 'welcome',
|
|
isRequired: true,
|
|
isCompleted: false,
|
|
data: {
|
|
welcomeMessage: `Salut ${user.firstName} ! Bienvenue dans la communauté Veza.`,
|
|
features: ['streaming', 'playlists', 'chat', 'discovery']
|
|
}
|
|
},
|
|
{
|
|
id: 'email_verification',
|
|
title: 'Vérifiez votre email',
|
|
description: 'Confirmez votre adresse email pour sécuriser votre compte',
|
|
type: 'profile',
|
|
isRequired: true,
|
|
isCompleted: false,
|
|
data: {
|
|
email: user.email,
|
|
verificationCode: vezaFaker.string.numeric(6)
|
|
}
|
|
},
|
|
{
|
|
id: 'profile_setup',
|
|
title: 'Complétez votre profil',
|
|
description: 'Ajoutez une photo et une bio pour vous présenter',
|
|
type: 'profile',
|
|
isRequired: false,
|
|
isCompleted: false,
|
|
data: {
|
|
suggestedBio: 'Passionné de musique, nouveau sur Veza !',
|
|
avatarOptions: [
|
|
vezaFaker.image.avatar(),
|
|
vezaFaker.image.avatar(),
|
|
vezaFaker.image.avatar()
|
|
]
|
|
}
|
|
},
|
|
{
|
|
id: 'music_preferences',
|
|
title: 'Choisissez vos genres préférés',
|
|
description: 'Aidez-nous à vous recommander de la musique',
|
|
type: 'preferences',
|
|
isRequired: false,
|
|
isCompleted: false,
|
|
data: {
|
|
availableGenres: ['rock', 'pop', 'jazz', 'electronic', 'hip-hop', 'classical'],
|
|
maxSelections: 5
|
|
}
|
|
},
|
|
{
|
|
id: 'first_playlist',
|
|
title: 'Créez votre première playlist',
|
|
description: 'Organisez vos morceaux préférés',
|
|
type: 'discovery',
|
|
isRequired: false,
|
|
isCompleted: false,
|
|
data: {
|
|
suggestedName: 'Mes découvertes',
|
|
suggestedTracks: 5
|
|
}
|
|
},
|
|
{
|
|
id: 'join_community',
|
|
title: 'Rejoignez la communauté',
|
|
description: 'Participez aux discussions et découvrez de nouveaux artistes',
|
|
type: 'social',
|
|
isRequired: false,
|
|
isCompleted: false,
|
|
data: {
|
|
recommendedChannels: ['Bienvenue', 'Découvertes', 'Feedback'],
|
|
welcomeMessage: 'Salut tout le monde ! Je suis nouveau sur Veza 👋'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
private static generateFirstUserActions(user: User, tracks: Audio[]): UserAction[] {
|
|
const actions: UserAction[] = []
|
|
const now = new Date()
|
|
|
|
// First action: play a recommended track
|
|
const firstTrack = tracks[0]
|
|
if (!firstTrack) return actions
|
|
actions.push({
|
|
type: 'play_track',
|
|
timestamp: new Date(now.getTime() + 5 * 60 * 1000), // 5 minutes after registration
|
|
data: {
|
|
trackId: firstTrack.id,
|
|
duration: Math.min(firstTrack.metadata.duration, 30), // Listen for 30 seconds
|
|
completion: 0.1
|
|
},
|
|
expectedResult: 'Track plays successfully, user engagement recorded'
|
|
})
|
|
|
|
// Second action: like the track
|
|
actions.push({
|
|
type: 'like_track',
|
|
timestamp: new Date(now.getTime() + 6 * 60 * 1000),
|
|
data: {
|
|
trackId: firstTrack.id
|
|
},
|
|
expectedResult: 'Track is added to user likes, recommendation algorithm updated'
|
|
})
|
|
|
|
// Third action: create first playlist
|
|
actions.push({
|
|
type: 'create_playlist',
|
|
timestamp: new Date(now.getTime() + 10 * 60 * 1000),
|
|
data: {
|
|
name: 'Ma première playlist',
|
|
description: 'Mes premiers coups de cœur',
|
|
trackIds: tracks.slice(0, 3).map(t => t.id),
|
|
visibility: 'private'
|
|
},
|
|
expectedResult: 'Playlist created, user stats updated'
|
|
})
|
|
|
|
// Fourth action: send welcome message
|
|
actions.push({
|
|
type: 'send_message',
|
|
timestamp: new Date(now.getTime() + 15 * 60 * 1000),
|
|
data: {
|
|
conversationId: 'welcome-channel',
|
|
content: 'Salut tout le monde ! Je découvre Veza, c\'est génial ! 🎵',
|
|
type: 'text'
|
|
},
|
|
expectedResult: 'Message sent to welcome channel, community engagement started'
|
|
})
|
|
|
|
return actions
|
|
}
|
|
|
|
private static generateStepCompletionData(step: OnboardingStep, user: User): any {
|
|
switch (step.type) {
|
|
case 'welcome':
|
|
return {
|
|
completedAt: new Date(),
|
|
timeSpent: vezaFaker.number.int({ min: 30, max: 120 }), // seconds
|
|
interacted: true
|
|
}
|
|
|
|
case 'profile':
|
|
if (step.id === 'email_verification') {
|
|
return {
|
|
verifiedAt: new Date(),
|
|
verificationCode: step.data?.verificationCode,
|
|
attempts: 1
|
|
}
|
|
}
|
|
return {
|
|
avatarSelected: vezaFaker.image.avatar(),
|
|
bioAdded: true,
|
|
completedAt: new Date()
|
|
}
|
|
|
|
case 'preferences':
|
|
return {
|
|
selectedGenres: vezaFaker.helpers.arrayElements(
|
|
step.data?.availableGenres || [],
|
|
vezaFaker.number.int({ min: 2, max: 5 })
|
|
),
|
|
completedAt: new Date()
|
|
}
|
|
|
|
case 'discovery':
|
|
return {
|
|
playlistCreated: true,
|
|
tracksAdded: vezaFaker.number.int({ min: 3, max: 8 }),
|
|
completedAt: new Date()
|
|
}
|
|
|
|
case 'social':
|
|
return {
|
|
channelsJoined: vezaFaker.helpers.arrayElements(
|
|
step.data?.recommendedChannels || [],
|
|
vezaFaker.number.int({ min: 1, max: 3 })
|
|
),
|
|
messageSent: true,
|
|
completedAt: new Date()
|
|
}
|
|
|
|
default:
|
|
return {
|
|
completedAt: new Date(),
|
|
success: true
|
|
}
|
|
}
|
|
}
|
|
|
|
private static generateFirstInteractions(context: OnboardingScenarioContext): UserAction[] {
|
|
const interactions: UserAction[] = []
|
|
const now = new Date()
|
|
|
|
// Simulate realistic user behavior over first hour
|
|
const timeOffsets = [300, 600, 900, 1200, 1800, 2400, 3000, 3600] // seconds
|
|
|
|
timeOffsets.forEach((offset, index) => {
|
|
const actionTypes = ['play_track', 'like_track', 'create_playlist', 'follow_user', 'send_message']
|
|
const actionType = vezaFaker.helpers.arrayElement(actionTypes) as any
|
|
|
|
let data: any = {}
|
|
let expectedResult = ''
|
|
|
|
switch (actionType) {
|
|
case 'play_track':
|
|
const track = vezaFaker.helpers.arrayElement(context.initialData.recommendedTracks)
|
|
data = {
|
|
trackId: track.id,
|
|
duration: vezaFaker.number.int({ min: 15, max: track.metadata.duration }),
|
|
completion: vezaFaker.number.float({ min: 0.1, max: 1.0 })
|
|
}
|
|
expectedResult = 'Track playback initiated, listening stats updated'
|
|
break
|
|
|
|
case 'like_track':
|
|
data = {
|
|
trackId: vezaFaker.helpers.arrayElement(context.initialData.recommendedTracks).id
|
|
}
|
|
expectedResult = 'Track liked, user preferences updated'
|
|
break
|
|
|
|
case 'send_message':
|
|
data = {
|
|
conversationId: context.initialData.welcomeConversation.id,
|
|
content: vezaFaker.chat.messageContent(),
|
|
type: 'text'
|
|
}
|
|
expectedResult = 'Message sent, conversation activity updated'
|
|
break
|
|
|
|
default:
|
|
data = { action: actionType }
|
|
expectedResult = `${actionType} completed successfully`
|
|
}
|
|
|
|
interactions.push({
|
|
type: actionType,
|
|
timestamp: new Date(now.getTime() + offset * 1000),
|
|
data,
|
|
expectedResult
|
|
})
|
|
})
|
|
|
|
return interactions
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export scenario configuration
|
|
*/
|
|
export const NEW_USER_ONBOARDING_SCENARIO = {
|
|
name: 'New User Onboarding',
|
|
description: 'Complete user journey from registration to first meaningful platform interactions',
|
|
duration: '1 hour',
|
|
complexity: 'medium',
|
|
userTypes: ['new_user'],
|
|
expectedOutcomes: [
|
|
'User successfully completes registration',
|
|
'Email verification completed',
|
|
'Profile setup completed',
|
|
'First playlist created',
|
|
'Community engagement initiated',
|
|
'Recommendation system activated'
|
|
],
|
|
testCoverage: [
|
|
'authentication_flow',
|
|
'profile_management',
|
|
'content_discovery',
|
|
'playlist_creation',
|
|
'social_features',
|
|
'notification_system'
|
|
]
|
|
} |