veza/fixtures/scenarios/user-journey/new-user-onboarding.ts
2025-12-03 22:56:50 +01:00

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'
]
}