diff --git a/src/__tests__/components/WorkoutCard.test.tsx b/src/__tests__/components/WorkoutCard.test.tsx deleted file mode 100644 index fe7da59..0000000 --- a/src/__tests__/components/WorkoutCard.test.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { describe, it, expect } from 'vitest' -import React from 'react' -import { View, Text } from 'react-native' -import { render } from '@testing-library/react-native' - -const CATEGORY_COLORS: Record = { - 'full-body': '#FF6B35', - 'core': '#5AC8FA', - 'upper-body': '#BF5AF2', - 'lower-body': '#30D158', - 'cardio': '#FF9500', -} - -describe('WorkoutCard logic', () => { - describe('category colors', () => { - it('should map full-body to primary brand color', () => { - expect(CATEGORY_COLORS['full-body']).toBe('#FF6B35') - }) - - it('should map core to ice blue', () => { - expect(CATEGORY_COLORS['core']).toBe('#5AC8FA') - }) - - it('should map upper-body to purple', () => { - expect(CATEGORY_COLORS['upper-body']).toBe('#BF5AF2') - }) - - it('should map lower-body to green', () => { - expect(CATEGORY_COLORS['lower-body']).toBe('#30D158') - }) - - it('should map cardio to orange', () => { - expect(CATEGORY_COLORS['cardio']).toBe('#FF9500') - }) - }) - - describe('display formatting', () => { - const formatDuration = (minutes: number): string => `${minutes} MIN` - const formatCalories = (calories: number): string => `${calories} CAL` - const formatLevel = (level: string): string => level.toUpperCase() - - it('should format duration correctly', () => { - expect(formatDuration(4)).toBe('4 MIN') - expect(formatDuration(8)).toBe('8 MIN') - expect(formatDuration(12)).toBe('12 MIN') - expect(formatDuration(20)).toBe('20 MIN') - }) - - it('should format calories correctly', () => { - expect(formatCalories(45)).toBe('45 CAL') - expect(formatCalories(100)).toBe('100 CAL') - }) - - it('should format level correctly', () => { - expect(formatLevel('Beginner')).toBe('BEGINNER') - expect(formatLevel('Intermediate')).toBe('INTERMEDIATE') - expect(formatLevel('Advanced')).toBe('ADVANCED') - }) - }) - - describe('card variants', () => { - type CardVariant = 'horizontal' | 'grid' | 'featured' - - const getCardDimensions = (variant: CardVariant) => { - switch (variant) { - case 'horizontal': - return { width: 200, height: 280 } - case 'grid': - return { flex: 1, aspectRatio: 0.75 } - case 'featured': - return { width: 320, height: 400 } - default: - return { width: 200, height: 280 } - } - } - - it('should return correct dimensions for horizontal variant', () => { - const dims = getCardDimensions('horizontal') - expect(dims.width).toBe(200) - expect(dims.height).toBe(280) - }) - - it('should return correct dimensions for grid variant', () => { - const dims = getCardDimensions('grid') - expect(dims.flex).toBe(1) - expect(dims.aspectRatio).toBe(0.75) - }) - - it('should return correct dimensions for featured variant', () => { - const dims = getCardDimensions('featured') - expect(dims.width).toBe(320) - expect(dims.height).toBe(400) - }) - - it('should default to horizontal for unknown variant', () => { - const dims = getCardDimensions('unknown' as CardVariant) - expect(dims.width).toBe(200) - }) - }) - - describe('workout metadata', () => { - const buildMetadata = (duration: number, calories: number, level: string): string => { - return `${duration} MIN • ${calories} CAL • ${level.toUpperCase()}` - } - - it('should build correct metadata string', () => { - expect(buildMetadata(4, 45, 'Beginner')).toBe('4 MIN • 45 CAL • BEGINNER') - }) - - it('should handle different levels', () => { - expect(buildMetadata(8, 90, 'Intermediate')).toBe('8 MIN • 90 CAL • INTERMEDIATE') - expect(buildMetadata(20, 240, 'Advanced')).toBe('20 MIN • 240 CAL • ADVANCED') - }) - }) - - describe('workout filtering helpers', () => { - const workouts = [ - { id: '1', category: 'full-body', level: 'Beginner', duration: 4 }, - { id: '2', category: 'core', level: 'Intermediate', duration: 8 }, - { id: '3', category: 'upper-body', level: 'Advanced', duration: 12 }, - { id: '4', category: 'full-body', level: 'Intermediate', duration: 4 }, - ] - - const filterByCategory = (list: typeof workouts, cat: string) => - list.filter(w => w.category === cat) - - const filterByLevel = (list: typeof workouts, lvl: string) => - list.filter(w => w.level === lvl) - - const filterByDuration = (list: typeof workouts, dur: number) => - list.filter(w => w.duration === dur) - - it('should filter workouts by category', () => { - expect(filterByCategory(workouts, 'full-body')).toHaveLength(2) - expect(filterByCategory(workouts, 'core')).toHaveLength(1) - }) - - it('should filter workouts by level', () => { - expect(filterByLevel(workouts, 'Beginner')).toHaveLength(1) - expect(filterByLevel(workouts, 'Intermediate')).toHaveLength(2) - }) - - it('should filter workouts by duration', () => { - expect(filterByDuration(workouts, 4)).toHaveLength(2) - expect(filterByDuration(workouts, 8)).toHaveLength(1) - }) - }) -}) diff --git a/src/__tests__/components/rendering/CollectionCard.test.tsx b/src/__tests__/components/rendering/CollectionCard.test.tsx deleted file mode 100644 index fce78b5..0000000 --- a/src/__tests__/components/rendering/CollectionCard.test.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, it, expect, vi } from 'vitest' -import React from 'react' -import { render, screen, fireEvent } from '@testing-library/react-native' -import { CollectionCard } from '@/src/shared/components/CollectionCard' -import type { Collection } from '@/src/shared/types' - -const mockCollection: Collection = { - id: 'test-collection', - title: 'Upper Body Blast', - description: 'An intense upper body workout collection', - icon: '💪', - workoutIds: ['w1', 'w2', 'w3'], - gradient: ['#FF6B35', '#FF3B30'], -} - -/** - * Helper to recursively find a node in the rendered tree by type. - */ -function findByType(tree: any, typeName: string): any { - if (!tree) return null - if (tree.type === typeName) return tree - if (tree.children && Array.isArray(tree.children)) { - for (const child of tree.children) { - if (typeof child === 'object') { - const found = findByType(child, typeName) - if (found) return found - } - } - } - return null -} - -describe('CollectionCard', () => { - it('renders collection title', () => { - render() - expect(screen.getByText('Upper Body Blast')).toBeTruthy() - }) - - it('renders workout count', () => { - render() - expect(screen.getByText('3 workouts')).toBeTruthy() - }) - - it('renders icon emoji', () => { - render() - expect(screen.getByText('💪')).toBeTruthy() - }) - - it('calls onPress when pressed', () => { - const onPress = vi.fn() - render() - fireEvent.press(screen.getByText('Upper Body Blast')) - expect(onPress).toHaveBeenCalledTimes(1) - }) - - it('renders without onPress (no crash)', () => { - const { toJSON } = render() - expect(toJSON()).toMatchSnapshot() - }) - - it('renders LinearGradient when no imageUrl', () => { - const { toJSON } = render() - expect(screen.getByTestId('linear-gradient')).toBeTruthy() - // LinearGradient should receive the collection's gradient colors - const gradientNode = findByType(toJSON(), 'LinearGradient') - expect(gradientNode).toBeTruthy() - expect(gradientNode.props.colors).toEqual(['#FF6B35', '#FF3B30']) - }) - - it('renders ImageBackground when imageUrl is provided', () => { - const { toJSON } = render( - - ) - // Should render ImageBackground instead of standalone LinearGradient - const tree = toJSON() - const imageBackground = findByType(tree, 'ImageBackground') - expect(imageBackground).toBeTruthy() - expect(imageBackground.props.source).toEqual({ uri: 'https://example.com/image.jpg' }) - }) - - it('uses default gradient colors when collection has no gradient', () => { - const collectionNoGradient: Collection = { - ...mockCollection, - gradient: undefined, - } - const { toJSON } = render( - - ) - // Should use fallback gradient: [BRAND.PRIMARY, '#FF3B30'] - const gradientNode = findByType(toJSON(), 'LinearGradient') - expect(gradientNode).toBeTruthy() - // BRAND.PRIMARY = '#FF6B35' from constants - expect(gradientNode.props.colors).toEqual(['#FF6B35', '#FF3B30']) - }) - - it('renders blur overlay', () => { - render() - expect(screen.getByTestId('blur-view')).toBeTruthy() - }) - - it('handles empty workoutIds', () => { - const emptyCollection: Collection = { - ...mockCollection, - workoutIds: [], - } - render() - expect(screen.getByText('0 workouts')).toBeTruthy() - }) - - it('snapshot with imageUrl (different rendering path)', () => { - const { toJSON } = render( - - ) - expect(toJSON()).toMatchSnapshot() - }) -}) diff --git a/src/__tests__/components/rendering/Skeleton.test.tsx b/src/__tests__/components/rendering/Skeleton.test.tsx deleted file mode 100644 index 8c864ae..0000000 --- a/src/__tests__/components/rendering/Skeleton.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { describe, it, expect } from 'vitest' -import React from 'react' -import { render } from '@testing-library/react-native' -import { - Skeleton, - WorkoutCardSkeleton, - TrainerCardSkeleton, - CollectionCardSkeleton, - StatsCardSkeleton, -} from '@/src/shared/components/loading/Skeleton' - -/** - * Helper to extract the flattened style from a rendered tree node. - * Style can be a single object or an array of objects. - */ -function flattenStyle(style: any): Record { - if (!style) return {} - if (Array.isArray(style)) { - return Object.assign({}, ...style.filter(Boolean)) - } - return style -} - -describe('Skeleton', () => { - it('renders with default dimensions (snapshot)', () => { - const { toJSON } = render() - expect(toJSON()).toMatchSnapshot() - }) - - it('applies default width=100% and height=20', () => { - const { toJSON } = render() - const tree = toJSON() - const style = flattenStyle(tree?.props?.style) - expect(style.width).toBe('100%') - expect(style.height).toBe(20) - }) - - it('applies custom width and height', () => { - const { toJSON } = render() - const tree = toJSON() - const style = flattenStyle(tree?.props?.style) - expect(style.width).toBe(200) - expect(style.height).toBe(40) - }) - - it('applies percentage width', () => { - const { toJSON } = render() - const tree = toJSON() - const style = flattenStyle(tree?.props?.style) - expect(style.width).toBe('70%') - expect(style.height).toBe(20) - }) - - it('applies custom borderRadius', () => { - const { toJSON } = render() - const tree = toJSON() - const style = flattenStyle(tree?.props?.style) - expect(style.borderRadius).toBe(40) - }) - - it('merges custom style prop', () => { - const customStyle = { marginTop: 10 } - const { toJSON } = render() - const tree = toJSON() - const style = flattenStyle(tree?.props?.style) - expect(style.marginTop).toBe(10) - // Should still have default dimensions - expect(style.width).toBe('100%') - expect(style.height).toBe(20) - }) - - it('renders shimmer overlay as a child element', () => { - const { toJSON } = render() - const tree = toJSON() - // Root View should have at least one child (the shimmer Animated.View) - expect(tree?.children).toBeDefined() - expect(tree!.children!.length).toBeGreaterThanOrEqual(1) - }) - - it('uses theme overlay color for background', () => { - const { toJSON } = render() - const tree = toJSON() - // The Skeleton renders a View with style array including backgroundColor. - // Walk the style array (may be nested) to find backgroundColor. - function findBackgroundColor(node: any): string | undefined { - if (!node?.props?.style) return undefined - const style = flattenStyle(node.props.style) - if (style.backgroundColor) return style.backgroundColor - // Check children - if (node.children && Array.isArray(node.children)) { - for (const child of node.children) { - if (typeof child === 'object') { - const found = findBackgroundColor(child) - if (found) return found - } - } - } - return undefined - } - const bgColor = findBackgroundColor(tree) - expect(bgColor).toBeDefined() - expect(typeof bgColor).toBe('string') - }) -}) - -describe('WorkoutCardSkeleton', () => { - it('renders correct structure (snapshot)', () => { - const { toJSON } = render() - expect(toJSON()).toMatchSnapshot() - }) - - it('contains multiple Skeleton elements as children', () => { - const { toJSON } = render() - const tree = toJSON() - // WorkoutCardSkeleton has: image skeleton + title skeleton + row with 2 skeletons = 4 total - // Count all View nodes (Skeleton renders as View) - function countViews(node: any): number { - if (!node) return 0 - let count = node.type === 'View' ? 1 : 0 - if (node.children && Array.isArray(node.children)) { - for (const child of node.children) { - if (typeof child === 'object') count += countViews(child) - } - } - return count - } - // Should have at least 5 View nodes (card container + skeletons + content wrapper + row) - expect(countViews(tree)).toBeGreaterThanOrEqual(5) - }) -}) - -describe('TrainerCardSkeleton', () => { - it('renders correct structure (snapshot)', () => { - const { toJSON } = render() - expect(toJSON()).toMatchSnapshot() - }) - - it('contains circular avatar skeleton (borderRadius=40)', () => { - const { toJSON } = render() - // First Skeleton inside is the avatar: width=80, height=80, borderRadius=40 - function findCircleSkeleton(node: any): boolean { - if (!node) return false - if (node.type === 'View') { - const style = flattenStyle(node.props?.style) - if (style.width === 80 && style.height === 80 && style.borderRadius === 40) return true - } - if (node.children && Array.isArray(node.children)) { - return node.children.some((child: any) => typeof child === 'object' && findCircleSkeleton(child)) - } - return false - } - expect(findCircleSkeleton(toJSON())).toBe(true) - }) -}) - -describe('CollectionCardSkeleton', () => { - it('renders correct structure (snapshot)', () => { - const { toJSON } = render() - expect(toJSON()).toMatchSnapshot() - }) -}) - -describe('StatsCardSkeleton', () => { - it('renders correct structure (snapshot)', () => { - const { toJSON } = render() - expect(toJSON()).toMatchSnapshot() - }) - - it('contains header row with two skeleton elements', () => { - const { toJSON } = render() - const tree = toJSON() - // StatsCardSkeleton has: card > statsHeader (row) + large skeleton - // statsHeader has 2 children (title skeleton + icon skeleton) - expect(tree?.children).toBeDefined() - expect(tree!.children!.length).toBeGreaterThanOrEqual(2) - }) -}) diff --git a/src/__tests__/data/achievements.test.ts b/src/__tests__/data/achievements.test.ts deleted file mode 100644 index d4bcc18..0000000 --- a/src/__tests__/data/achievements.test.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { ACHIEVEMENTS } from '../../shared/data/achievements' - -describe('achievements data', () => { - describe('ACHIEVEMENTS structure', () => { - it('should have exactly 8 achievements', () => { - expect(ACHIEVEMENTS).toHaveLength(8) - }) - - it('should have all required properties', () => { - ACHIEVEMENTS.forEach(achievement => { - expect(achievement.id).toBeDefined() - expect(achievement.title).toBeDefined() - expect(achievement.description).toBeDefined() - expect(achievement.icon).toBeDefined() - expect(achievement.requirement).toBeDefined() - expect(achievement.type).toBeDefined() - }) - }) - - it('should have unique achievement IDs', () => { - const ids = ACHIEVEMENTS.map(a => a.id) - const uniqueIds = new Set(ids) - expect(uniqueIds.size).toBe(ids.length) - }) - - it('should have unique achievement titles', () => { - const titles = ACHIEVEMENTS.map(a => a.title) - const uniqueTitles = new Set(titles) - expect(uniqueTitles.size).toBe(titles.length) - }) - - it('should have positive requirements', () => { - ACHIEVEMENTS.forEach(achievement => { - expect(achievement.requirement).toBeGreaterThan(0) - }) - }) - }) - - describe('achievement types', () => { - it('should have valid achievement types', () => { - const validTypes = ['workouts', 'streak', 'calories', 'minutes'] - ACHIEVEMENTS.forEach(achievement => { - expect(validTypes).toContain(achievement.type) - }) - }) - - it('should have workouts type achievements', () => { - const workoutAchievements = ACHIEVEMENTS.filter(a => a.type === 'workouts') - expect(workoutAchievements.length).toBeGreaterThan(0) - }) - - it('should have streak type achievements', () => { - const streakAchievements = ACHIEVEMENTS.filter(a => a.type === 'streak') - expect(streakAchievements.length).toBeGreaterThan(0) - }) - - it('should have calories type achievements', () => { - const calorieAchievements = ACHIEVEMENTS.filter(a => a.type === 'calories') - expect(calorieAchievements.length).toBeGreaterThan(0) - }) - - it('should have minutes type achievements', () => { - const minutesAchievements = ACHIEVEMENTS.filter(a => a.type === 'minutes') - expect(minutesAchievements.length).toBeGreaterThan(0) - }) - }) - - describe('specific achievements', () => { - it('should have First Burn achievement', () => { - const achievement = ACHIEVEMENTS.find(a => a.id === 'first-burn') - expect(achievement).toBeDefined() - expect(achievement!.title).toBe('First Burn') - expect(achievement!.requirement).toBe(1) - expect(achievement!.type).toBe('workouts') - }) - - it('should have Week Warrior achievement', () => { - const achievement = ACHIEVEMENTS.find(a => a.id === 'week-warrior') - expect(achievement).toBeDefined() - expect(achievement!.title).toBe('Week Warrior') - expect(achievement!.requirement).toBe(7) - expect(achievement!.type).toBe('streak') - }) - - it('should have Century Club achievement', () => { - const achievement = ACHIEVEMENTS.find(a => a.id === 'century-club') - expect(achievement).toBeDefined() - expect(achievement!.title).toBe('Century Club') - expect(achievement!.requirement).toBe(100) - expect(achievement!.type).toBe('calories') - }) - - it('should have Iron Will achievement', () => { - const achievement = ACHIEVEMENTS.find(a => a.id === 'iron-will') - expect(achievement).toBeDefined() - expect(achievement!.title).toBe('Iron Will') - expect(achievement!.requirement).toBe(10) - expect(achievement!.type).toBe('workouts') - }) - - it('should have Tabata Master achievement', () => { - const achievement = ACHIEVEMENTS.find(a => a.id === 'tabata-master') - expect(achievement).toBeDefined() - expect(achievement!.title).toBe('Tabata Master') - expect(achievement!.requirement).toBe(50) - expect(achievement!.type).toBe('workouts') - }) - - it('should have Marathon Burner achievement', () => { - const achievement = ACHIEVEMENTS.find(a => a.id === 'marathon-burner') - expect(achievement).toBeDefined() - expect(achievement!.title).toBe('Marathon Burner') - expect(achievement!.requirement).toBe(100) - expect(achievement!.type).toBe('minutes') - }) - - it('should have Unstoppable achievement', () => { - const achievement = ACHIEVEMENTS.find(a => a.id === 'unstoppable') - expect(achievement).toBeDefined() - expect(achievement!.title).toBe('Unstoppable') - expect(achievement!.requirement).toBe(30) - expect(achievement!.type).toBe('streak') - }) - - it('should have Calorie Crusher achievement', () => { - const achievement = ACHIEVEMENTS.find(a => a.id === 'calorie-crusher') - expect(achievement).toBeDefined() - expect(achievement!.title).toBe('Calorie Crusher') - expect(achievement!.requirement).toBe(1000) - expect(achievement!.type).toBe('calories') - }) - }) - - describe('achievement progression', () => { - it('should have increasing workout requirements', () => { - const workoutAchievements = ACHIEVEMENTS - .filter(a => a.type === 'workouts') - .sort((a, b) => a.requirement - b.requirement) - - for (let i = 1; i < workoutAchievements.length; i++) { - expect(workoutAchievements[i].requirement).toBeGreaterThan(workoutAchievements[i-1].requirement) - } - }) - - it('should have increasing streak requirements', () => { - const streakAchievements = ACHIEVEMENTS - .filter(a => a.type === 'streak') - .sort((a, b) => a.requirement - b.requirement) - - for (let i = 1; i < streakAchievements.length; i++) { - expect(streakAchievements[i].requirement).toBeGreaterThan(streakAchievements[i-1].requirement) - } - }) - - it('should have increasing calorie requirements', () => { - const calorieAchievements = ACHIEVEMENTS - .filter(a => a.type === 'calories') - .sort((a, b) => a.requirement - b.requirement) - - for (let i = 1; i < calorieAchievements.length; i++) { - expect(calorieAchievements[i].requirement).toBeGreaterThan(calorieAchievements[i-1].requirement) - } - }) - }) - - describe('icon types', () => { - it('should have string icon names', () => { - ACHIEVEMENTS.forEach(achievement => { - expect(typeof achievement.icon).toBe('string') - expect(achievement.icon.length).toBeGreaterThan(0) - }) - }) - - it('should use SF Symbol-like names', () => { - const expectedIcons = ['flame', 'calendar', 'trophy', 'star', 'time', 'rocket'] - ACHIEVEMENTS.forEach(achievement => { - expect(expectedIcons).toContain(achievement.icon) - }) - }) - }) -}) diff --git a/src/__tests__/data/dataService.test.ts b/src/__tests__/data/dataService.test.ts deleted file mode 100644 index edaa697..0000000 --- a/src/__tests__/data/dataService.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { dataService } from '../../shared/data/dataService' -import { WORKOUTS, TRAINERS, PROGRAMS, ACHIEVEMENTS } from '../../shared/data' -import type { Workout, Trainer, Program, Achievement } from '../../shared/types' - -vi.mock('../../shared/supabase', () => ({ - isSupabaseConfigured: vi.fn(() => false), - supabase: { - from: vi.fn(), - auth: { - signInAnonymously: vi.fn(), - }, - storage: { - from: vi.fn(), - }, - }, -})) - -describe('dataService', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('getAllWorkouts', () => { - it('should return local data when Supabase not configured', async () => { - const workouts = await dataService.getAllWorkouts() - - expect(workouts).toEqual(WORKOUTS) - }) - - it('should return workouts with required properties', async () => { - const workouts = await dataService.getAllWorkouts() - - workouts.forEach((workout: Workout) => { - expect(workout.id).toBeDefined() - expect(workout.title).toBeDefined() - expect(workout.trainerId).toBeDefined() - expect(workout.duration).toBeDefined() - expect(workout.calories).toBeDefined() - }) - }) - }) - - describe('getWorkoutById', () => { - it('should return workout by id', async () => { - const workout = await dataService.getWorkoutById('1') - - expect(workout).toBeDefined() - expect(workout?.id).toBe('1') - }) - - it('should return undefined for non-existent workout', async () => { - const workout = await dataService.getWorkoutById('non-existent') - - expect(workout).toBeUndefined() - }) - }) - - describe('getWorkoutsByCategory', () => { - it('should return workouts filtered by category', async () => { - const workouts = await dataService.getWorkoutsByCategory('full-body') - - expect(workouts).toBeDefined() - expect(Array.isArray(workouts)).toBe(true) - - workouts.forEach((workout: Workout) => { - expect(workout.category).toBe('full-body') - }) - }) - - it('should return empty array for non-existent category', async () => { - const workouts = await dataService.getWorkoutsByCategory('non-existent') - - expect(workouts).toEqual([]) - }) - }) - - describe('getWorkoutsByTrainer', () => { - it('should return workouts filtered by trainer', async () => { - const workouts = await dataService.getWorkoutsByTrainer('emma') - - expect(workouts).toBeDefined() - expect(Array.isArray(workouts)).toBe(true) - }) - }) - - describe('getFeaturedWorkouts', () => { - it('should return only featured workouts', async () => { - const workouts = await dataService.getFeaturedWorkouts() - - expect(workouts).toBeDefined() - workouts.forEach((workout: Workout) => { - expect(workout.isFeatured).toBe(true) - }) - }) - }) - - describe('getAllTrainers', () => { - it('should return all trainers', async () => { - const trainers = await dataService.getAllTrainers() - - expect(trainers).toEqual(TRAINERS) - }) - - it('should return trainers with required properties', async () => { - const trainers = await dataService.getAllTrainers() - - trainers.forEach((trainer: Trainer) => { - expect(trainer.id).toBeDefined() - expect(trainer.name).toBeDefined() - expect(trainer.specialty).toBeDefined() - expect(trainer.color).toBeDefined() - }) - }) - }) - - describe('getTrainerById', () => { - it('should return trainer by id', async () => { - const trainer = await dataService.getTrainerById('felia') - - expect(trainer).toBeDefined() - expect(trainer?.id).toBe('felia') - }) - - it('should return undefined for non-existent trainer', async () => { - const trainer = await dataService.getTrainerById('non-existent') - - expect(trainer).toBeUndefined() - }) - }) - - describe('getAllCollections', () => { - it('should return empty array when Supabase not configured', async () => { - const collections = await dataService.getAllCollections() - - expect(collections).toEqual([]) - }) - }) - - describe('getCollectionById', () => { - it('should return undefined when Supabase not configured', async () => { - const collection = await dataService.getCollectionById('morning-energizer') - - expect(collection).toBeUndefined() - }) - - it('should return undefined for non-existent collection', async () => { - const collection = await dataService.getCollectionById('non-existent') - - expect(collection).toBeUndefined() - }) - }) - - describe('getAllPrograms', () => { - it('should return all programs', async () => { - const programs = await dataService.getAllPrograms() - const programValues = Object.values(programs) - - expect(programValues.length).toBe(3) - }) - - it('should return programs with required properties', async () => { - const programs = await dataService.getAllPrograms() - const programValues = Object.values(programs) - - programValues.forEach((program: Program) => { - expect(program.id).toBeDefined() - expect(program.title).toBeDefined() - expect(program.weeks).toBeDefined() - expect(Array.isArray(program.weeks)).toBe(true) - }) - }) - }) - - describe('getAchievements', () => { - it('should return all achievements', async () => { - const achievements = await dataService.getAchievements() - - expect(achievements).toEqual(ACHIEVEMENTS) - }) - - it('should return achievements with required properties', async () => { - const achievements = await dataService.getAchievements() - - achievements.forEach((achievement: Achievement) => { - expect(achievement.id).toBeDefined() - expect(achievement.title).toBeDefined() - expect(achievement.description).toBeDefined() - expect(achievement.requirement).toBeDefined() - expect(achievement.type).toBeDefined() - }) - }) - }) -}) diff --git a/src/__tests__/data/programs.test.ts b/src/__tests__/data/programs.test.ts deleted file mode 100644 index 4260065..0000000 --- a/src/__tests__/data/programs.test.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' -import { PROGRAMS, ASSESSMENT_WORKOUT, ALL_PROGRAM_WORKOUTS, UPPER_BODY_WORKOUTS, LOWER_BODY_WORKOUTS, FULL_BODY_WORKOUTS } from '../../shared/data/programs' -import type { Program, ProgramId } from '../../shared/types/program' - -describe('programs data', () => { - const programIds: ProgramId[] = ['upper-body', 'lower-body', 'full-body'] - - describe('PROGRAMS structure', () => { - it('should have exactly 3 programs', () => { - expect(Object.keys(PROGRAMS)).toHaveLength(3) - }) - - it('should have all required program IDs', () => { - programIds.forEach(id => { - expect(PROGRAMS[id]).toBeDefined() - }) - }) - - it('should have consistent program structure', () => { - programIds.forEach(id => { - const program = PROGRAMS[id] - expect(program.id).toBe(id) - expect(program.title).toBeDefined() - expect(program.description).toBeDefined() - expect(program.durationWeeks).toBe(4) - expect(program.workoutsPerWeek).toBe(5) - expect(program.totalWorkouts).toBe(20) - expect(program.equipment).toBeDefined() - expect(program.focusAreas).toBeDefined() - expect(program.weeks).toHaveLength(4) - }) - }) - }) - - describe('program weeks', () => { - it('should have 4 weeks per program', () => { - programIds.forEach(id => { - expect(PROGRAMS[id].weeks).toHaveLength(4) - }) - }) - - it('should have correct week numbers', () => { - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach((week, index) => { - expect(week.weekNumber).toBe(index + 1) - }) - }) - }) - - it('should have 5 workouts per week', () => { - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - expect(week.workouts).toHaveLength(5) - }) - }) - }) - - it('should have correct week titles', () => { - const expectedTitles = ['Foundation', 'Building', 'Challenge', 'Peak Performance'] - - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach((week, index) => { - expect(week.title).toBe(expectedTitles[index]) - }) - }) - }) - - it('should have week descriptions', () => { - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - expect(week.description).toBeDefined() - expect(week.description.length).toBeGreaterThan(0) - }) - }) - }) - - it('should have week focus', () => { - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - expect(week.focus).toBeDefined() - expect(week.focus.length).toBeGreaterThan(0) - }) - }) - }) - }) - - describe('workout structure', () => { - it('should have 8 exercises per workout', () => { - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - week.workouts.forEach(workout => { - expect(workout.exercises).toHaveLength(8) - }) - }) - }) - }) - - it('should have 4-minute duration for all workouts', () => { - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - week.workouts.forEach(workout => { - expect(workout.duration).toBe(4) - }) - }) - }) - }) - - it('should have exercise names', () => { - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - week.workouts.forEach(workout => { - workout.exercises.forEach(exercise => { - expect(exercise.name).toBeDefined() - expect(exercise.name.length).toBeGreaterThan(0) - }) - }) - }) - }) - }) - - it('should have 20-second exercise duration', () => { - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - week.workouts.forEach(workout => { - workout.exercises.forEach(exercise => { - expect(exercise.duration).toBe(20) - }) - }) - }) - }) - }) - - it('should have workout equipment', () => { - let hasEquipment = false - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - week.workouts.forEach(workout => { - if (workout.equipment && workout.equipment.length > 0) { - hasEquipment = true - } - }) - }) - }) - expect(hasEquipment).toBe(true) - }) - - it('should have workout focus areas', () => { - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - week.workouts.forEach(workout => { - expect(workout.focus).toBeDefined() - expect(workout.focus.length).toBeGreaterThan(0) - }) - }) - }) - }) - - it('should have workout tips', () => { - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - week.workouts.forEach(workout => { - expect(workout.tips).toBeDefined() - expect(workout.tips.length).toBeGreaterThan(0) - }) - }) - }) - }) - }) - - describe('workout IDs', () => { - it('should have unique workout IDs', () => { - const allIds = new Set() - let duplicateFound = false - - programIds.forEach(id => { - PROGRAMS[id].weeks.forEach(week => { - week.workouts.forEach(workout => { - if (allIds.has(workout.id)) { - duplicateFound = true - } - allIds.add(workout.id) - }) - }) - }) - - expect(duplicateFound).toBe(false) - }) - - it('should follow ID naming convention', () => { - const patterns = { - 'upper-body': /^ub-w\d-d\d$/, - 'lower-body': /^lb-w\d-d\d$/, - 'full-body': /^fb-w\d-d\d$/, - } - - programIds.forEach(id => { - const pattern = patterns[id] - PROGRAMS[id].weeks.forEach(week => { - week.workouts.forEach(workout => { - expect(workout.id).toMatch(pattern) - }) - }) - }) - }) - }) - - describe('equipment requirements', () => { - it('should have required equipment for upper body', () => { - expect(PROGRAMS['upper-body'].equipment.required).toContain('Resistance band') - }) - - it('should have required equipment for lower body', () => { - expect(PROGRAMS['lower-body'].equipment.required).toContain('Resistance band') - }) - - it('should have no required equipment for full body', () => { - expect(PROGRAMS['full-body'].equipment.required).toHaveLength(0) - }) - }) - - describe('focus areas', () => { - it('should have upper body focus areas', () => { - const focus = PROGRAMS['upper-body'].focusAreas - expect(focus).toContain('Shoulders') - expect(focus).toContain('Chest') - expect(focus).toContain('Back') - }) - - it('should have lower body focus areas', () => { - const focus = PROGRAMS['lower-body'].focusAreas - expect(focus).toContain('Legs') - expect(focus).toContain('Glutes') - }) - - it('should have full body focus areas', () => { - const focus = PROGRAMS['full-body'].focusAreas - expect(focus).toContain('Total Body') - expect(focus).toContain('Core') - }) - }) - - describe('ALL_PROGRAM_WORKOUTS', () => { - it('should contain all workouts from all programs', () => { - expect(ALL_PROGRAM_WORKOUTS).toHaveLength(60) - }) - - it('should combine upper, lower, and full body workouts', () => { - expect(UPPER_BODY_WORKOUTS).toHaveLength(20) - expect(LOWER_BODY_WORKOUTS).toHaveLength(20) - expect(FULL_BODY_WORKOUTS).toHaveLength(20) - }) - }) - - describe('ASSESSMENT_WORKOUT', () => { - it('should have correct structure', () => { - expect(ASSESSMENT_WORKOUT.id).toBe('initial-assessment') - expect(ASSESSMENT_WORKOUT.title).toBe('Movement Assessment') - expect(ASSESSMENT_WORKOUT.duration).toBe(4) - }) - - it('should have 8 exercises', () => { - expect(ASSESSMENT_WORKOUT.exercises).toHaveLength(8) - }) - - it('should have exercise purposes', () => { - ASSESSMENT_WORKOUT.exercises.forEach(exercise => { - expect(exercise.purpose).toBeDefined() - expect(exercise.purpose.length).toBeGreaterThan(0) - }) - }) - - it('should have tips', () => { - expect(ASSESSMENT_WORKOUT.tips).toBeDefined() - expect(ASSESSMENT_WORKOUT.tips.length).toBeGreaterThan(0) - }) - }) -}) diff --git a/src/__tests__/data/trainers.test.ts b/src/__tests__/data/trainers.test.ts deleted file mode 100644 index 530bcbd..0000000 --- a/src/__tests__/data/trainers.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { TRAINERS } from '../../shared/data/trainers' - -describe('trainers data', () => { - describe('TRAINERS structure', () => { - it('should have exactly 2 trainers', () => { - expect(TRAINERS).toHaveLength(2) - }) - - it('should have all required properties', () => { - TRAINERS.forEach(trainer => { - expect(trainer.id).toBeDefined() - expect(trainer.name).toBeDefined() - expect(trainer.specialty).toBeDefined() - expect(trainer.color).toBeDefined() - expect(trainer.workoutCount).toBeDefined() - }) - }) - - it('should have unique trainer IDs', () => { - const ids = TRAINERS.map(t => t.id) - const uniqueIds = new Set(ids) - expect(uniqueIds.size).toBe(ids.length) - }) - - it('should have unique trainer names', () => { - const names = TRAINERS.map(t => t.name) - const uniqueNames = new Set(names) - expect(uniqueNames.size).toBe(names.length) - }) - - it('should have valid hex colors', () => { - const hexPattern = /^#[0-9A-Fa-f]{6}$/ - TRAINERS.forEach(trainer => { - expect(trainer.color).toMatch(hexPattern) - }) - }) - - it('should have positive workout counts', () => { - TRAINERS.forEach(trainer => { - expect(trainer.workoutCount).toBeGreaterThan(0) - }) - }) - }) - - describe('specific trainers', () => { - it('should have Félia as first trainer', () => { - expect(TRAINERS[0].id).toBe('felia') - expect(TRAINERS[0].name).toBe('Félia') - expect(TRAINERS[0].gender).toBe('female') - expect(TRAINERS[0].specialty).toBe('Core') - }) - - it('should have Félix as second trainer', () => { - expect(TRAINERS[1].id).toBe('felix') - expect(TRAINERS[1].name).toBe('Félix') - expect(TRAINERS[1].gender).toBe('male') - expect(TRAINERS[1].specialty).toBe('Strength') - }) - }) - - describe('gender distribution', () => { - it('should have exactly 1 male and 1 female trainer', () => { - const males = TRAINERS.filter(t => t.gender === 'male') - const females = TRAINERS.filter(t => t.gender === 'female') - expect(males).toHaveLength(1) - expect(females).toHaveLength(1) - }) - }) - - describe('specialty coverage', () => { - it('should cover core workout types', () => { - const specialties = TRAINERS.map(t => t.specialty) - expect(specialties).toContain('Core') - expect(specialties).toContain('Strength') - }) - }) - - describe('workout distribution', () => { - it('should have total workout count of 30', () => { - const total = TRAINERS.reduce((sum, t) => sum + t.workoutCount, 0) - expect(total).toBe(30) - }) - }) -}) diff --git a/src/__tests__/data/useTranslatedData.test.ts b/src/__tests__/data/useTranslatedData.test.ts deleted file mode 100644 index ff27b68..0000000 --- a/src/__tests__/data/useTranslatedData.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { describe, it, expect } from 'vitest' - -function slugify(name: string): string { - return name - .toLowerCase() - .replace(/[()]/g, '') - .replace(/&/g, 'and') - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, '') -} - -describe('useTranslatedData utilities', () => { - describe('slugify', () => { - it('should convert to lowercase', () => { - expect(slugify('Push-Ups')).toBe('push-ups') - expect(slugify('JUMPING JACKS')).toBe('jumping-jacks') - }) - - it('should replace spaces with hyphens', () => { - expect(slugify('mountain climbers')).toBe('mountain-climbers') - expect(slugify('high knees fast')).toBe('high-knees-fast') - }) - - it('should remove parentheses', () => { - expect(slugify('Exercise (Modified)')).toBe('exercise-modified') - expect(slugify('Move (Advanced)')).toBe('move-advanced') - }) - - it('should replace & with "and"', () => { - expect(slugify('Stretch & Cool')).toBe('stretch-and-cool') - expect(slugify('Core & Abs')).toBe('core-and-abs') - }) - - it('should handle multiple special characters', () => { - expect(slugify('Full Body (HIIT) & Cardio')).toBe('full-body-hiit-and-cardio') - }) - - it('should collapse multiple hyphens', () => { - expect(slugify('exercise name')).toBe('exercise-name') - }) - - it('should trim leading and trailing hyphens', () => { - expect(slugify('-exercise-')).toBe('exercise') - expect(slugify('--test--')).toBe('test') - }) - - it('should handle empty string', () => { - expect(slugify('')).toBe('') - }) - - it('should handle already clean strings', () => { - expect(slugify('burpees')).toBe('burpees') - }) - - it('should handle numbers', () => { - expect(slugify('Level 1 Beginner')).toBe('level-1-beginner') - expect(slugify('30 Second Sprint')).toBe('30-second-sprint') - }) - - it('should handle equipment names', () => { - expect(slugify('Dumbbells')).toBe('dumbbells') - expect(slugify('Resistance Band')).toBe('resistance-band') - expect(slugify('Yoga Mat')).toBe('yoga-mat') - }) - - it('should handle complex exercise names', () => { - expect(slugify('Renegade Row (Each Arm)')).toBe('renegade-row-each-arm') - expect(slugify('Plank to Push-Up')).toBe('plank-to-push-up') - }) - }) - - describe('translation key generation', () => { - it('should generate valid i18n keys for workouts', () => { - const workoutId = 'full-body-burn' - const key = `workouts.${workoutId}` - expect(key).toBe('workouts.full-body-burn') - }) - - it('should generate valid i18n keys for exercises', () => { - const exerciseName = 'Mountain Climbers' - const key = `exercises.${slugify(exerciseName)}` - expect(key).toBe('exercises.mountain-climbers') - }) - - it('should generate valid i18n keys for equipment', () => { - const equipmentName = 'Resistance Band' - const key = `equipment.${slugify(equipmentName)}` - expect(key).toBe('equipment.resistance-band') - }) - - it('should generate valid i18n keys for collections', () => { - const collectionId = 'morning-energizer' - const titleKey = `collections.${collectionId}.title` - const descKey = `collections.${collectionId}.description` - expect(titleKey).toBe('collections.morning-energizer.title') - expect(descKey).toBe('collections.morning-energizer.description') - }) - - it('should generate valid i18n keys for programs', () => { - const programId = '4-week-strength' - const titleKey = `programs.${programId}.title` - const descKey = `programs.${programId}.description` - expect(titleKey).toBe('programs.4-week-strength.title') - expect(descKey).toBe('programs.4-week-strength.description') - }) - }) - - describe('defaultValue fallback', () => { - it('should use original value as defaultValue', () => { - const originalTitle = 'High Intensity Interval Training' - const translationOptions = { - defaultValue: originalTitle, - } - expect(translationOptions.defaultValue).toBe(originalTitle) - }) - - it('should preserve workout structure when translating', () => { - const workout = { - id: 'test-workout', - title: 'Test Workout', - exercises: [ - { name: 'Push-Ups', duration: 20 }, - { name: 'Squats', duration: 20 }, - ], - equipment: ['Mat', 'Dumbbells'], - } - - const translatedWorkout = { - ...workout, - title: 'Translated Title', - exercises: workout.exercises.map((ex) => ({ - ...ex, - name: slugify(ex.name), - })), - equipment: workout.equipment.map((item) => slugify(item)), - } - - expect(translatedWorkout.exercises[0].name).toBe('push-ups') - expect(translatedWorkout.equipment[0]).toBe('mat') - }) - }) - - describe('category mapping', () => { - const categoryKeyMap: Record = { - 'full-body': 'categories.fullBody', - 'upper-body': 'categories.upperBody', - 'lower-body': 'categories.lowerBody', - 'core': 'categories.core', - 'cardio': 'categories.cardio', - } - - it('should map full-body category', () => { - expect(categoryKeyMap['full-body']).toBe('categories.fullBody') - }) - - it('should map upper-body category', () => { - expect(categoryKeyMap['upper-body']).toBe('categories.upperBody') - }) - - it('should map lower-body category', () => { - expect(categoryKeyMap['lower-body']).toBe('categories.lowerBody') - }) - - it('should map core category', () => { - expect(categoryKeyMap['core']).toBe('categories.core') - }) - - it('should map cardio category', () => { - expect(categoryKeyMap['cardio']).toBe('categories.cardio') - }) - }) - - describe('music vibe mapping', () => { - const vibeKeyMap: Record = { - electronic: 'musicVibes.electronic', - 'hip-hop': 'musicVibes.hipHop', - pop: 'musicVibes.pop', - rock: 'musicVibes.rock', - chill: 'musicVibes.chill', - } - - it('should map electronic vibe', () => { - expect(vibeKeyMap['electronic']).toBe('musicVibes.electronic') - }) - - it('should map hip-hop vibe', () => { - expect(vibeKeyMap['hip-hop']).toBe('musicVibes.hipHop') - }) - - it('should map pop vibe', () => { - expect(vibeKeyMap['pop']).toBe('musicVibes.pop') - }) - - it('should map rock vibe', () => { - expect(vibeKeyMap['rock']).toBe('musicVibes.rock') - }) - - it('should map chill vibe', () => { - expect(vibeKeyMap['chill']).toBe('musicVibes.chill') - }) - }) -}) diff --git a/src/__tests__/data/workouts.test.ts b/src/__tests__/data/workouts.test.ts deleted file mode 100644 index c83fc18..0000000 --- a/src/__tests__/data/workouts.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { WORKOUTS } from '../../shared/data/workouts' -import type { Workout, WorkoutCategory, WorkoutLevel, WorkoutDuration, MusicVibe } from '../../shared/types' - -describe('workouts data', () => { - describe('data integrity', () => { - it('should have 50 workouts', () => { - expect(WORKOUTS).toHaveLength(50) - }) - - it('should have unique IDs for all workouts', () => { - const ids = WORKOUTS.map((w) => w.id) - const uniqueIds = new Set(ids) - expect(uniqueIds.size).toBe(ids.length) - }) - - it('should have all required fields for each workout', () => { - const requiredFields: (keyof Workout)[] = [ - 'id', 'title', 'trainerId', 'category', 'level', 'duration', - 'calories', 'exercises', 'rounds', 'prepTime', 'workTime', - 'restTime', 'equipment', 'musicVibe', - ] - - WORKOUTS.forEach((workout) => { - requiredFields.forEach((field) => { - expect(workout[field]).toBeDefined() - }) - }) - }) - }) - - describe('category distribution', () => { - const categories: WorkoutCategory[] = ['full-body', 'core', 'upper-body', 'lower-body', 'cardio'] - - categories.forEach((category) => { - it(`should have 10 ${category} workouts`, () => { - const count = WORKOUTS.filter((w) => w.category === category).length - expect(count).toBe(10) - }) - }) - }) - - describe('level distribution', () => { - it('should have Beginner workouts', () => { - const beginners = WORKOUTS.filter((w) => w.level === 'Beginner') - expect(beginners.length).toBeGreaterThan(0) - }) - - it('should have Intermediate workouts', () => { - const intermediates = WORKOUTS.filter((w) => w.level === 'Intermediate') - expect(intermediates.length).toBeGreaterThan(0) - }) - - it('should have Advanced workouts', () => { - const advanced = WORKOUTS.filter((w) => w.level === 'Advanced') - expect(advanced.length).toBeGreaterThan(0) - }) - }) - - describe('duration distribution', () => { - const durations: WorkoutDuration[] = [4, 8, 12, 20] - - durations.forEach((duration) => { - it(`should have ${duration}-minute workouts`, () => { - const count = WORKOUTS.filter((w) => w.duration === duration).length - expect(count).toBeGreaterThan(0) - }) - }) - }) - - describe('workout structure validation', () => { - it('should have valid prep times (5-15 seconds)', () => { - WORKOUTS.forEach((workout) => { - expect(workout.prepTime).toBeGreaterThanOrEqual(5) - expect(workout.prepTime).toBeLessThanOrEqual(15) - }) - }) - - it('should have valid work times (15-30 seconds)', () => { - WORKOUTS.forEach((workout) => { - expect(workout.workTime).toBeGreaterThanOrEqual(15) - expect(workout.workTime).toBeLessThanOrEqual(30) - }) - }) - - it('should have valid rest times (5-15 seconds)', () => { - WORKOUTS.forEach((workout) => { - expect(workout.restTime).toBeGreaterThanOrEqual(5) - expect(workout.restTime).toBeLessThanOrEqual(15) - }) - }) - - it('should have at least 1 exercise per workout', () => { - WORKOUTS.forEach((workout) => { - expect(workout.exercises.length).toBeGreaterThanOrEqual(1) - }) - }) - - it('should have valid exercise durations matching work time', () => { - WORKOUTS.forEach((workout) => { - workout.exercises.forEach((exercise) => { - expect(exercise.duration).toBe(workout.workTime) - }) - }) - }) - - it('should have valid rounds (4-40)', () => { - WORKOUTS.forEach((workout) => { - expect(workout.rounds).toBeGreaterThanOrEqual(4) - expect(workout.rounds).toBeLessThanOrEqual(40) - }) - }) - }) - - describe('calorie estimation', () => { - it('should have positive calorie values', () => { - WORKOUTS.forEach((workout) => { - expect(workout.calories).toBeGreaterThan(0) - }) - }) - - it('should scale calories with duration', () => { - const shortWorkouts = WORKOUTS.filter((w) => w.duration === 4) - const longWorkouts = WORKOUTS.filter((w) => w.duration === 20) - - const avgShortCalories = shortWorkouts.reduce((sum, w) => sum + w.calories, 0) / shortWorkouts.length - const avgLongCalories = longWorkouts.reduce((sum, w) => sum + w.calories, 0) / longWorkouts.length - - expect(avgLongCalories).toBeGreaterThan(avgShortCalories) - }) - }) - - describe('music vibes', () => { - const validVibes: MusicVibe[] = ['electronic', 'hip-hop', 'pop', 'rock', 'chill'] - - it('should only have valid music vibes', () => { - WORKOUTS.forEach((workout) => { - expect(validVibes).toContain(workout.musicVibe) - }) - }) - - validVibes.forEach((vibe) => { - it(`should have workouts with ${vibe} vibe`, () => { - const count = WORKOUTS.filter((w) => w.musicVibe === vibe).length - expect(count).toBeGreaterThan(0) - }) - }) - }) - - describe('equipment field', () => { - it('should have equipment as an array', () => { - WORKOUTS.forEach((workout) => { - expect(Array.isArray(workout.equipment)).toBe(true) - }) - }) - - it('should have at least "No equipment required" for bodyweight workouts', () => { - const noEquipmentWorkouts = WORKOUTS.filter((w) => - w.equipment.some((e) => e.toLowerCase().includes('no equipment')) - ) - expect(noEquipmentWorkouts.length).toBeGreaterThan(0) - }) - }) - - describe('featured workouts', () => { - it('should have some featured workouts', () => { - const featured = WORKOUTS.filter((w) => w.isFeatured) - expect(featured.length).toBeGreaterThan(0) - }) - }) - - describe('trainer assignments', () => { - const validTrainers = ['felia', 'felix'] - - it('should only have valid trainer IDs', () => { - WORKOUTS.forEach((workout) => { - expect(validTrainers).toContain(workout.trainerId) - }) - }) - - validTrainers.forEach((trainer) => { - it(`should have workouts for trainer ${trainer}`, () => { - const count = WORKOUTS.filter((w) => w.trainerId === trainer).length - expect(count).toBeGreaterThan(0) - }) - }) - }) - - describe('duration calculation validation', () => { - it('should have duration matching rounds and intervals', () => { - WORKOUTS.forEach((workout) => { - const totalSeconds = workout.prepTime + (workout.workTime + workout.restTime) * workout.rounds - const totalMinutes = totalSeconds / 60 - - expect(Math.abs(totalMinutes - workout.duration)).toBeLessThan(2) - }) - }) - }) -}) diff --git a/src/__tests__/hooks/useSupabaseData.test.ts b/src/__tests__/hooks/useSupabaseData.test.ts deleted file mode 100644 index c71f5e5..0000000 --- a/src/__tests__/hooks/useSupabaseData.test.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { useQuery } from '@tanstack/react-query' -import { dataService } from '../../shared/data/dataService' -import { - queryKeys, - useWorkouts, - useWorkout, - useWorkoutsByCategory, - useWorkoutsByTrainer, - useFeaturedWorkouts, - usePopularWorkouts, - useTrainers, - useTrainer, - useCollections, - useCollection, - usePrograms, -} from '../../shared/hooks/useSupabaseData' - -// Mock dataService -vi.mock('../../shared/data/dataService', () => ({ - dataService: { - getAllWorkouts: vi.fn().mockResolvedValue([ - { id: 'w1', title: 'Workout 1' }, - { id: 'w2', title: 'Workout 2' }, - { id: 'w3', title: 'Workout 3' }, - ]), - getWorkoutById: vi.fn().mockResolvedValue({ id: 'w1', title: 'Workout 1' }), - getWorkoutsByCategory: vi.fn().mockResolvedValue([{ id: 'w1', title: 'Workout 1' }]), - getWorkoutsByTrainer: vi.fn().mockResolvedValue([{ id: 'w2', title: 'Workout 2' }]), - getFeaturedWorkouts: vi.fn().mockResolvedValue([{ id: 'w1', title: 'Featured' }]), - getAllTrainers: vi.fn().mockResolvedValue([{ id: 't1', name: 'Trainer 1' }]), - getTrainerById: vi.fn().mockResolvedValue({ id: 't1', name: 'Trainer 1' }), - getAllCollections: vi.fn().mockResolvedValue([{ id: 'c1', title: 'Collection 1' }]), - getCollectionById: vi.fn().mockResolvedValue({ id: 'c1', title: 'Collection 1' }), - getAllPrograms: vi.fn().mockResolvedValue([{ id: 'p1', title: 'Program 1' }]), - }, -})) - -// Mock React Query — capture the options passed to useQuery -const mockUseQuery = vi.fn((options: any) => ({ - data: undefined, - isLoading: true, - error: null, - ...options, -})) - -vi.mock('@tanstack/react-query', () => ({ - useQuery: (options: any) => mockUseQuery(options), -})) - -describe('useSupabaseData', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('queryKeys', () => { - it('should have correct static keys', () => { - expect(queryKeys.workouts).toBe('workouts') - expect(queryKeys.trainers).toBe('trainers') - expect(queryKeys.collections).toBe('collections') - expect(queryKeys.programs).toBe('programs') - }) - - it('should generate correct workout key', () => { - expect(queryKeys.workout('abc')).toEqual(['workouts', 'abc']) - }) - - it('should generate correct workoutsByCategory key', () => { - expect(queryKeys.workoutsByCategory('full-body')).toEqual([ - 'workouts', - 'category', - 'full-body', - ]) - }) - - it('should generate correct workoutsByTrainer key', () => { - expect(queryKeys.workoutsByTrainer('trainer-1')).toEqual([ - 'workouts', - 'trainer', - 'trainer-1', - ]) - }) - - it('should have correct featuredWorkouts key', () => { - expect(queryKeys.featuredWorkouts).toEqual(['workouts', 'featured']) - }) - - it('should generate correct trainer key', () => { - expect(queryKeys.trainer('t1')).toEqual(['trainers', 't1']) - }) - - it('should generate correct collection key', () => { - expect(queryKeys.collection('c1')).toEqual(['collections', 'c1']) - }) - }) - - describe('useWorkouts', () => { - it('should use correct queryKey and staleTime', () => { - useWorkouts() - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['workouts'], - staleTime: 300000, - }) - ) - }) - }) - - describe('useWorkout', () => { - it('should be disabled when id is undefined', () => { - useWorkout(undefined) - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - enabled: false, - }) - ) - }) - - it('should be enabled when id is provided', () => { - useWorkout('workout-123') - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['workouts', 'workout-123'], - enabled: true, - }) - ) - }) - - it('should use empty string as fallback key when id is undefined', () => { - useWorkout(undefined) - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['workouts', ''], - }) - ) - }) - }) - - describe('useWorkoutsByCategory', () => { - it('should pass correct queryKey', () => { - useWorkoutsByCategory('full-body') - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['workouts', 'category', 'full-body'], - enabled: true, - }) - ) - }) - - it('should be disabled with empty category', () => { - useWorkoutsByCategory('') - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - enabled: false, - }) - ) - }) - }) - - describe('useWorkoutsByTrainer', () => { - it('should pass correct queryKey', () => { - useWorkoutsByTrainer('trainer-1') - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['workouts', 'trainer', 'trainer-1'], - enabled: true, - }) - ) - }) - - it('should be disabled with empty trainerId', () => { - useWorkoutsByTrainer('') - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - enabled: false, - }) - ) - }) - }) - - describe('useFeaturedWorkouts', () => { - it('should use correct queryKey', () => { - useFeaturedWorkouts() - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['workouts', 'featured'], - }) - ) - }) - }) - - describe('usePopularWorkouts', () => { - it('should default to count 8', () => { - usePopularWorkouts() - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['workouts', 'popular', 8], - }) - ) - }) - - it('should accept custom count', () => { - usePopularWorkouts(5) - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['workouts', 'popular', 5], - }) - ) - }) - - it('queryFn should slice workouts correctly', async () => { - usePopularWorkouts(2) - - const lastCall = mockUseQuery.mock.calls[mockUseQuery.mock.calls.length - 1][0] - const result = await lastCall.queryFn() - - expect(dataService.getAllWorkouts).toHaveBeenCalled() - expect(result).toHaveLength(2) - expect(result[0].id).toBe('w1') - expect(result[1].id).toBe('w2') - }) - }) - - describe('useTrainers', () => { - it('should use correct queryKey', () => { - useTrainers() - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['trainers'], - }) - ) - }) - }) - - describe('useTrainer', () => { - it('should be disabled when id is undefined', () => { - useTrainer(undefined) - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - enabled: false, - }) - ) - }) - - it('should be enabled when id is provided', () => { - useTrainer('t1') - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['trainers', 't1'], - enabled: true, - }) - ) - }) - }) - - describe('useCollections', () => { - it('should use correct queryKey', () => { - useCollections() - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['collections'], - }) - ) - }) - }) - - describe('useCollection', () => { - it('should be disabled when id is undefined', () => { - useCollection(undefined) - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - enabled: false, - }) - ) - }) - - it('should be enabled when id is provided', () => { - useCollection('c1') - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['collections', 'c1'], - enabled: true, - }) - ) - }) - }) - - describe('usePrograms', () => { - it('should use correct queryKey', () => { - usePrograms() - - expect(mockUseQuery).toHaveBeenCalledWith( - expect.objectContaining({ - queryKey: ['programs'], - }) - ) - }) - }) - - describe('staleTime consistency', () => { - it('all hooks should have 5 minute staleTime', () => { - mockUseQuery.mockClear() - - useWorkouts() - useFeaturedWorkouts() - useTrainers() - useCollections() - usePrograms() - - const calls = mockUseQuery.mock.calls - calls.forEach((call: any[]) => { - expect(call[0].staleTime).toBe(1000 * 60 * 5) - }) - }) - }) -}) diff --git a/src/__tests__/stores/activityStore.test.ts b/src/__tests__/stores/activityStore.test.ts deleted file mode 100644 index a0e0a89..0000000 --- a/src/__tests__/stores/activityStore.test.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' -import { useActivityStore, getWeeklyActivity } from '../../shared/stores/activityStore' -import type { WorkoutResult } from '../../shared/types' - -const createWorkoutResult = (daysAgo: number, overrides?: Partial): WorkoutResult => ({ - id: `result-${Date.now()}-${Math.random()}`, - workoutId: 'test-workout', - durationMinutes: 4, - calories: 45, - rounds: 8, - completionRate: 1, - completedAt: Date.now() - daysAgo * 86400000, - ...overrides, -}) - -describe('activityStore', () => { - beforeEach(async () => { - await useActivityStore.persist.clearStorage() - useActivityStore.setState({ history: [], streak: { current: 0, longest: 0 } }) - vi.clearAllMocks() - }) - - describe('initial state', () => { - it('should have empty history and zero streak', () => { - const state = useActivityStore.getState() - - expect(state.history).toEqual([]) - expect(state.streak).toEqual({ current: 0, longest: 0 }) - }) - }) - - describe('addWorkoutResult', () => { - it('should add workout to beginning of history', async () => { - const store = useActivityStore.getState() - const result1 = createWorkoutResult(2) - const result2 = createWorkoutResult(1) - - await store.addWorkoutResult(result1) - await store.addWorkoutResult(result2) - - const history = useActivityStore.getState().history - expect(history).toHaveLength(2) - expect(history[0].completedAt).toBeGreaterThan(history[1].completedAt) - }) - - it('should calculate streak after adding workout', async () => { - const store = useActivityStore.getState() - const todayResult = createWorkoutResult(0) - - await store.addWorkoutResult(todayResult) - - const streak = useActivityStore.getState().streak - expect(streak.current).toBe(1) - expect(streak.longest).toBe(1) - }) - - it('should build consecutive streak', async () => { - const store = useActivityStore.getState() - - await store.addWorkoutResult(createWorkoutResult(2)) - await store.addWorkoutResult(createWorkoutResult(1)) - await store.addWorkoutResult(createWorkoutResult(0)) - - const streak = useActivityStore.getState().streak - expect(streak.current).toBe(3) - expect(streak.longest).toBe(3) - }) - }) - - describe('getWorkoutHistory', () => { - it('should return all workouts', async () => { - const store = useActivityStore.getState() - - await store.addWorkoutResult(createWorkoutResult(2)) - await store.addWorkoutResult(createWorkoutResult(1)) - - const history = store.getWorkoutHistory() - expect(history).toHaveLength(2) - }) - }) -}) - -describe('streak calculation', () => { - beforeEach(async () => { - await useActivityStore.persist.clearStorage() - useActivityStore.setState({ history: [], streak: { current: 0, longest: 0 } }) - }) - - describe('empty history', () => { - it('should return 0 streak for empty history', async () => { - const store = useActivityStore.getState() - - expect(store.history).toEqual([]) - expect(store.streak.current).toBe(0) - expect(store.streak.longest).toBe(0) - }) - }) - - describe('single workout', () => { - it('should return streak 1 for single workout today', async () => { - const store = useActivityStore.getState() - - await store.addWorkoutResult(createWorkoutResult(0)) - - expect(useActivityStore.getState().streak.current).toBe(1) - }) - - it('should return streak 1 for single workout yesterday', async () => { - const store = useActivityStore.getState() - - await store.addWorkoutResult(createWorkoutResult(1)) - - expect(useActivityStore.getState().streak.current).toBe(1) - }) - }) - - describe('consecutive days', () => { - it('should count 3-day streak correctly', async () => { - const store = useActivityStore.getState() - - await store.addWorkoutResult(createWorkoutResult(2)) - await store.addWorkoutResult(createWorkoutResult(1)) - await store.addWorkoutResult(createWorkoutResult(0)) - - const streak = useActivityStore.getState().streak - expect(streak.current).toBe(3) - }) - - it('should count 7-day streak correctly', async () => { - const store = useActivityStore.getState() - - for (let i = 6; i >= 0; i--) { - await store.addWorkoutResult(createWorkoutResult(i)) - } - - const streak = useActivityStore.getState().streak - expect(streak.current).toBe(7) - }) - }) - - describe('streak breaks', () => { - beforeEach(async () => { - await useActivityStore.persist.clearStorage() - useActivityStore.setState({ history: [], streak: { current: 0, longest: 0 } }) - }) - - it('should reset streak when gap exists', async () => { - const store = useActivityStore.getState() - - await store.addWorkoutResult(createWorkoutResult(5)) - await store.addWorkoutResult(createWorkoutResult(4)) - await store.addWorkoutResult(createWorkoutResult(1)) - await store.addWorkoutResult(createWorkoutResult(0)) - - const streak = useActivityStore.getState().streak - expect(streak.current).toBe(2) - expect(streak.longest).toBe(2) - }) - - it('should maintain longest streak even after current streak breaks', async () => { - const store = useActivityStore.getState() - - await store.addWorkoutResult(createWorkoutResult(10)) - await store.addWorkoutResult(createWorkoutResult(9)) - await store.addWorkoutResult(createWorkoutResult(8)) - await store.addWorkoutResult(createWorkoutResult(5)) - await store.addWorkoutResult(createWorkoutResult(4)) - - const streak = useActivityStore.getState().streak - expect(streak.current).toBe(0) - expect(streak.longest).toBe(3) - }) - }) - - describe('multiple workouts same day', () => { - beforeEach(async () => { - await useActivityStore.persist.clearStorage() - useActivityStore.setState({ history: [], streak: { current: 0, longest: 0 } }) - }) - - it('should count same-day workouts as 1 day', async () => { - const store = useActivityStore.getState() - const now = Date.now() - - // Add 3 workouts on the same day (1 hour apart) - await store.addWorkoutResult({ ...createWorkoutResult(0), completedAt: now }) - await store.addWorkoutResult({ ...createWorkoutResult(0), completedAt: now + 3600000 }) - await store.addWorkoutResult({ ...createWorkoutResult(0), completedAt: now + 7200000 }) - - const state = useActivityStore.getState() - // All 3 workout entries are stored in history - expect(state.history).toHaveLength(3) - // But they count as 1 unique day for streak purposes - expect(state.streak.current).toBe(1) - expect(state.streak.longest).toBe(1) - }) - }) - - describe('streak expiration', () => { - beforeEach(async () => { - await useActivityStore.persist.clearStorage() - useActivityStore.setState({ history: [], streak: { current: 0, longest: 0 } }) - }) - - it('should reset streak if last workout was 2+ days ago', async () => { - const store = useActivityStore.getState() - - await store.addWorkoutResult(createWorkoutResult(5)) - await store.addWorkoutResult(createWorkoutResult(4)) - await store.addWorkoutResult(createWorkoutResult(3)) - - const streak = useActivityStore.getState().streak - expect(streak.current).toBe(0) - expect(streak.longest).toBe(3) - }) - }) -}) - -describe('getWeeklyActivity', () => { - it('should return 7 days of activity', () => { - const history: WorkoutResult[] = [] - const activity = getWeeklyActivity(history) - - expect(activity).toHaveLength(7) - activity.forEach((day) => { - expect(day).toHaveProperty('date') - expect(day).toHaveProperty('completed') - expect(day).toHaveProperty('workoutCount') - }) - }) - - it('should mark days with workouts as completed', () => { - const today = Date.now() - const history: WorkoutResult[] = [ - createWorkoutResult(0), - createWorkoutResult(2), - ] - - const activity = getWeeklyActivity(history) - const completedDays = activity.filter((d) => d.completed) - - expect(completedDays.length).toBeGreaterThanOrEqual(1) - }) - - it('should count multiple workouts per day', () => { - const today = Date.now() - const history: WorkoutResult[] = [ - { ...createWorkoutResult(0), completedAt: today }, - { ...createWorkoutResult(0), completedAt: today + 3600000 }, - ] - - const activity = getWeeklyActivity(history) - const todayActivity = activity.find((d) => d.workoutCount > 1) - - expect(todayActivity?.workoutCount).toBe(2) - }) - - it('should return all days with zero workouts for empty history', () => { - const history: WorkoutResult[] = [] - const activity = getWeeklyActivity(history) - - activity.forEach((day) => { - expect(day.completed).toBe(false) - expect(day.workoutCount).toBe(0) - }) - }) -}) diff --git a/src/__tests__/stores/programStore.test.ts b/src/__tests__/stores/programStore.test.ts deleted file mode 100644 index 5239ed7..0000000 --- a/src/__tests__/stores/programStore.test.ts +++ /dev/null @@ -1,398 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' -import { useProgramStore } from '../../shared/stores/programStore' -import type { ProgramId, AssessmentResult } from '../../shared/types/program' - -const resetAllPrograms = () => { - useProgramStore.setState({ - selectedProgramId: null, - programsProgress: { - 'upper-body': { - programId: 'upper-body', - currentWeek: 1, - currentWorkoutIndex: 0, - completedWorkoutIds: [], - isProgramCompleted: false, - startDate: undefined, - lastWorkoutDate: undefined, - }, - 'lower-body': { - programId: 'lower-body', - currentWeek: 1, - currentWorkoutIndex: 0, - completedWorkoutIds: [], - isProgramCompleted: false, - startDate: undefined, - lastWorkoutDate: undefined, - }, - 'full-body': { - programId: 'full-body', - currentWeek: 1, - currentWorkoutIndex: 0, - completedWorkoutIds: [], - isProgramCompleted: false, - startDate: undefined, - lastWorkoutDate: undefined, - }, - }, - assessment: { - isCompleted: false, - result: null, - }, - }) -} - -describe('programStore', () => { - beforeEach(() => { - resetAllPrograms() - }) - - describe('initial state', () => { - it('should have no selected program', () => { - expect(useProgramStore.getState().selectedProgramId).toBeNull() - }) - - it('should have initial progress for all programs', () => { - const progress = useProgramStore.getState().programsProgress - - expect(progress['upper-body']).toBeDefined() - expect(progress['lower-body']).toBeDefined() - expect(progress['full-body']).toBeDefined() - }) - - it('should have incomplete assessment', () => { - const assessment = useProgramStore.getState().assessment - - expect(assessment.isCompleted).toBe(false) - expect(assessment.result).toBeNull() - }) - }) - - describe('selectProgram', () => { - it('should set selected program', () => { - const store = useProgramStore.getState() - - store.selectProgram('upper-body') - - expect(useProgramStore.getState().selectedProgramId).toBe('upper-body') - }) - - it('should set start date when selecting program for first time', () => { - const store = useProgramStore.getState() - - store.selectProgram('upper-body') - - const progress = useProgramStore.getState().programsProgress['upper-body'] - expect(progress.startDate).toBeDefined() - }) - }) - - describe('completeWorkout', () => { - it('should add workout to completed list', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - - store.completeWorkout('upper-body', 'ub-w1-d1') - - const progress = useProgramStore.getState().programsProgress['upper-body'] - expect(progress.completedWorkoutIds).toContain('ub-w1-d1') - }) - - it('should advance workout index', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - - store.completeWorkout('upper-body', 'ub-w1-d1') - - const progress = useProgramStore.getState().programsProgress['upper-body'] - expect(progress.currentWorkoutIndex).toBe(1) - }) - - it('should advance week after completing last workout of week', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - - const progress = useProgramStore.getState().programsProgress['upper-body'] - progress.currentWorkoutIndex = 4 - progress.currentWeek = 1 - - store.completeWorkout('upper-body', 'ub-w1-d5') - - const updated = useProgramStore.getState().programsProgress['upper-body'] - expect(updated.currentWeek).toBe(2) - expect(updated.currentWorkoutIndex).toBe(0) - }) - - it('should set lastWorkoutDate', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - - store.completeWorkout('upper-body', 'ub-w1-d1') - - const progress = useProgramStore.getState().programsProgress['upper-body'] - expect(progress.lastWorkoutDate).toBeDefined() - }) - }) - - describe('completeAssessment', () => { - it('should mark assessment as completed', () => { - const store = useProgramStore.getState() - const result: AssessmentResult = { - completedAt: new Date().toISOString(), - exercisesCompleted: ['exercise-1', 'exercise-2'], - recommendedProgram: 'upper-body', - } - - store.completeAssessment(result) - - const assessment = useProgramStore.getState().assessment - expect(assessment.isCompleted).toBe(true) - expect(assessment.result).toEqual(result) - }) - }) - - describe('skipAssessment', () => { - it('should mark assessment as completed without result', () => { - const store = useProgramStore.getState() - - store.skipAssessment() - - const assessment = useProgramStore.getState().assessment - expect(assessment.isCompleted).toBe(true) - expect(assessment.result).toBeNull() - }) - }) - - describe('resetProgram', () => { - it('should reset program progress', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - store.completeWorkout('upper-body', 'ub-w1-d1') - - store.resetProgram('upper-body') - - const progress = useProgramStore.getState().programsProgress['upper-body'] - expect(progress.completedWorkoutIds).toEqual([]) - expect(progress.currentWeek).toBe(1) - expect(progress.currentWorkoutIndex).toBe(0) - }) - - it('should deselect program if it was the selected one', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - - store.resetProgram('upper-body') - - expect(useProgramStore.getState().selectedProgramId).toBeNull() - }) - }) - - describe('changeProgram', () => { - it('should change to different program', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - - store.changeProgram('lower-body') - - expect(useProgramStore.getState().selectedProgramId).toBe('lower-body') - }) - }) - - describe('getCurrentWorkout', () => { - it('should return null when no program selected', () => { - const store = useProgramStore.getState() - - const workout = store.getCurrentWorkout('upper-body') - expect(workout).toBeDefined() - }) - }) - - describe('getProgramCompletion', () => { - it('should return 0 for new program', () => { - const store = useProgramStore.getState() - - const completion = store.getProgramCompletion('upper-body') - expect(completion).toBe(0) - }) - - it('should calculate completion percentage', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - - for (let i = 1; i <= 10; i++) { - store.completeWorkout('upper-body', `ub-w1-d${i}`) - } - - const completion = useProgramStore.getState().getProgramCompletion('upper-body') - expect(completion).toBe(50) - }) - }) - - describe('getTotalWorkoutsCompleted', () => { - it('should return 0 when no workouts completed', () => { - const store = useProgramStore.getState() - - expect(store.getTotalWorkoutsCompleted()).toBe(0) - }) - - it('should count workouts across all programs', () => { - const store = useProgramStore.getState() - - const ubProgress = useProgramStore.getState().programsProgress['upper-body'] - ubProgress.completedWorkoutIds = ['w1', 'w2'] - - const lbProgress = useProgramStore.getState().programsProgress['lower-body'] - lbProgress.completedWorkoutIds = ['w3'] - - expect(store.getTotalWorkoutsCompleted()).toBe(3) - }) - }) - - describe('getProgramStatus', () => { - it('should return not-started for new program', () => { - const store = useProgramStore.getState() - - expect(store.getProgramStatus('upper-body')).toBe('not-started') - }) - - it('should return in-progress after completing workout', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - store.completeWorkout('upper-body', 'ub-w1-d1') - - expect(useProgramStore.getState().getProgramStatus('upper-body')).toBe('in-progress') - }) - - it('should return completed after all 20 workouts', () => { - const store = useProgramStore.getState() - const progress = useProgramStore.getState().programsProgress['upper-body'] - progress.completedWorkoutIds = Array.from({ length: 20 }, (_, i) => `w${i + 1}`) - progress.isProgramCompleted = true - - expect(store.getProgramStatus('upper-body')).toBe('completed') - }) - }) - - describe('isWeekUnlocked', () => { - it('should always unlock week 1', () => { - const store = useProgramStore.getState() - - expect(store.isWeekUnlocked('upper-body', 1)).toBe(true) - }) - - it('should lock week 2 before completing week 1', () => { - const store = useProgramStore.getState() - - expect(store.isWeekUnlocked('upper-body', 2)).toBe(false) - }) - }) - - describe('getRecommendedNextWorkout', () => { - it('should return null when no programs started', () => { - const store = useProgramStore.getState() - - expect(store.getRecommendedNextWorkout()).toBeNull() - }) - - it('should return current workout from selected program', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - - const recommendation = store.getRecommendedNextWorkout() - - expect(recommendation).not.toBeNull() - expect(recommendation?.programId).toBe('upper-body') - }) - - it('should find in-progress program when no program selected', () => { - const store = useProgramStore.getState() - const progress = useProgramStore.getState().programsProgress['lower-body'] - progress.completedWorkoutIds = ['lb-w1-d1'] - - const recommendation = store.getRecommendedNextWorkout() - - expect(recommendation).not.toBeNull() - expect(recommendation?.programId).toBe('lower-body') - }) - }) - - describe('getNextWorkout', () => { - it('should return next workout in same week', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - - const nextWorkout = store.getNextWorkout('upper-body') - - expect(nextWorkout).not.toBeNull() - expect(nextWorkout?.id).toBe('ub-w1-d2') - }) - - it('should return first workout of next week when at end of week', () => { - const store = useProgramStore.getState() - const progress = useProgramStore.getState().programsProgress['upper-body'] - progress.currentWeek = 1 - progress.currentWorkoutIndex = 4 - - const nextWorkout = store.getNextWorkout('upper-body') - - expect(nextWorkout).not.toBeNull() - expect(nextWorkout?.id).toBe('ub-w2-d1') - }) - - it('should return null when at end of program', () => { - const store = useProgramStore.getState() - const progress = useProgramStore.getState().programsProgress['upper-body'] - progress.currentWeek = 4 - progress.currentWorkoutIndex = 4 - - const nextWorkout = store.getNextWorkout('upper-body') - - expect(nextWorkout).toBeNull() - }) - }) - - describe('isWorkoutUnlocked', () => { - it('should unlock first workout of week 1', () => { - const store = useProgramStore.getState() - - expect(store.isWorkoutUnlocked('upper-body', 'ub-w1-d1')).toBe(true) - }) - - it('should lock later workouts in week 1', () => { - const store = useProgramStore.getState() - - expect(store.isWorkoutUnlocked('upper-body', 'ub-w1-d2')).toBe(false) - expect(store.isWorkoutUnlocked('upper-body', 'ub-w1-d5')).toBe(false) - }) - - it('should unlock workout after completing previous', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - store.completeWorkout('upper-body', 'ub-w1-d1') - - expect(useProgramStore.getState().isWorkoutUnlocked('upper-body', 'ub-w1-d2')).toBe(true) - }) - - it('should lock week 2 workouts before completing week 1', () => { - const store = useProgramStore.getState() - - expect(store.isWorkoutUnlocked('upper-body', 'ub-w2-d1')).toBe(false) - }) - }) - - describe('program completion', () => { - it('should mark program as completed after 20 workouts', () => { - const store = useProgramStore.getState() - store.selectProgram('upper-body') - - for (let week = 1; week <= 4; week++) { - for (let day = 1; day <= 5; day++) { - store.completeWorkout('upper-body', `ub-w${week}-d${day}`) - } - } - - const progress = useProgramStore.getState().programsProgress['upper-body'] - expect(progress.isProgramCompleted).toBe(true) - expect(progress.completedWorkoutIds).toHaveLength(20) - }) - }) -}) diff --git a/src/__tests__/stores/tabataProgramStore.test.ts b/src/__tests__/stores/tabataProgramStore.test.ts deleted file mode 100644 index 1d49399..0000000 --- a/src/__tests__/stores/tabataProgramStore.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' -import { useTabataProgramStore } from '../../shared/stores/tabataProgramStore' -import type { TabataProgramId } from '../../shared/types/program' - -const PROGRAM_IDS: TabataProgramId[] = ['debutant', 'intermediaire', 'avance', 'bureau'] - -const resetStore = () => { - const initial: Record = {} as any - for (const id of PROGRAM_IDS) { - initial[id] = { - programId: id, - currentWeek: 1, - currentSessionIndex: 0, - completedSessionIds: [], - isProgramCompleted: false, - startDate: undefined, - lastSessionDate: undefined, - } - } - useTabataProgramStore.setState({ - selectedProgramId: null, - programsProgress: initial, - }) -} - -describe('tabataProgramStore', () => { - beforeEach(() => { - resetStore() - }) - - describe('initial state', () => { - it('should have no selected program', () => { - expect(useTabataProgramStore.getState().selectedProgramId).toBeNull() - }) - - it('should have initial progress for all 4 programs', () => { - const progress = useTabataProgramStore.getState().programsProgress - for (const id of PROGRAM_IDS) { - expect(progress[id]).toBeDefined() - expect(progress[id].completedSessionIds).toEqual([]) - expect(progress[id].isProgramCompleted).toBe(false) - expect(progress[id].currentWeek).toBe(1) - expect(progress[id].currentSessionIndex).toBe(0) - } - }) - }) - - describe('selectProgram', () => { - it('should set selectedProgramId and startDate on first selection', () => { - useTabataProgramStore.getState().selectProgram('debutant') - const state = useTabataProgramStore.getState() - expect(state.selectedProgramId).toBe('debutant') - expect(state.programsProgress.debutant.startDate).toBeDefined() - }) - - it('should not overwrite startDate on re-selection after progress', () => { - useTabataProgramStore.getState().selectProgram('debutant') - const firstDate = useTabataProgramStore.getState().programsProgress.debutant.startDate - - // Simulate some progress - useTabataProgramStore.setState(s => ({ - programsProgress: { - ...s.programsProgress, - debutant: { - ...s.programsProgress.debutant, - completedSessionIds: ['deb-w1-s1'], - }, - }, - })) - - // Re-select - useTabataProgramStore.getState().selectProgram('debutant') - expect(useTabataProgramStore.getState().selectedProgramId).toBe('debutant') - // startDate should remain unchanged - expect(useTabataProgramStore.getState().programsProgress.debutant.startDate).toBe(firstDate) - }) - }) - - describe('completeSession', () => { - it('should add session ID to completed list', () => { - useTabataProgramStore.getState().completeSession('debutant', 'deb-w1-s1') - const progress = useTabataProgramStore.getState().programsProgress.debutant - expect(progress.completedSessionIds).toContain('deb-w1-s1') - expect(progress.lastSessionDate).toBeDefined() - }) - - it('should not duplicate session IDs', () => { - useTabataProgramStore.getState().completeSession('debutant', 'deb-w1-s1') - useTabataProgramStore.getState().completeSession('debutant', 'deb-w1-s1') - const progress = useTabataProgramStore.getState().programsProgress.debutant - expect(progress.completedSessionIds.filter(id => id === 'deb-w1-s1')).toHaveLength(1) - }) - - it('should advance session index within same week', () => { - useTabataProgramStore.getState().completeSession('debutant', 'deb-w1-s1') - const progress = useTabataProgramStore.getState().programsProgress.debutant - expect(progress.currentSessionIndex).toBe(1) - expect(progress.currentWeek).toBe(1) - }) - - it('should ignore unknown program IDs', () => { - // Should not throw - useTabataProgramStore.getState().completeSession('nonexistent' as TabataProgramId, 'x') - // State unchanged - expect(useTabataProgramStore.getState().programsProgress.debutant.completedSessionIds).toEqual([]) - }) - }) - - describe('resetProgram', () => { - it('should reset progress to initial state', () => { - useTabataProgramStore.getState().selectProgram('debutant') - useTabataProgramStore.getState().completeSession('debutant', 'deb-w1-s1') - useTabataProgramStore.getState().resetProgram('debutant') - - const state = useTabataProgramStore.getState() - expect(state.selectedProgramId).toBeNull() - expect(state.programsProgress.debutant.completedSessionIds).toEqual([]) - expect(state.programsProgress.debutant.startDate).toBeUndefined() - }) - - it('should not affect other programs when resetting one', () => { - useTabataProgramStore.getState().selectProgram('intermediaire') - useTabataProgramStore.getState().completeSession('intermediaire', 'int-w1-s1') - useTabataProgramStore.getState().resetProgram('debutant') - - const state = useTabataProgramStore.getState() - expect(state.selectedProgramId).toBe('intermediaire') - expect(state.programsProgress.intermediaire.completedSessionIds).toContain('int-w1-s1') - }) - }) - - describe('changeProgram', () => { - it('should change selected program without resetting progress', () => { - useTabataProgramStore.getState().selectProgram('debutant') - useTabataProgramStore.getState().completeSession('debutant', 'deb-w1-s1') - useTabataProgramStore.getState().changeProgram('intermediaire') - - const state = useTabataProgramStore.getState() - expect(state.selectedProgramId).toBe('intermediaire') - expect(state.programsProgress.debutant.completedSessionIds).toContain('deb-w1-s1') - }) - }) - - describe('getters', () => { - it('getCurrentSession should return first session initially', () => { - const session = useTabataProgramStore.getState().getCurrentSession('debutant') - expect(session).not.toBeNull() - expect(session?.id).toMatch(/^deb-/) - }) - - it('getCurrentSession should return null for unknown program', () => { - const session = useTabataProgramStore.getState().getCurrentSession('nonexistent' as TabataProgramId) - expect(session).toBeNull() - }) - - it('getProgramCompletion should return 0 initially', () => { - expect(useTabataProgramStore.getState().getProgramCompletion('debutant')).toBe(0) - }) - - it('getTotalSessionsCompleted should sum across all programs', () => { - useTabataProgramStore.getState().completeSession('debutant', 'deb-w1-s1') - useTabataProgramStore.getState().completeSession('intermediaire', 'int-w1-s1') - expect(useTabataProgramStore.getState().getTotalSessionsCompleted()).toBe(2) - }) - - it('getProgramStatus should return not-started initially', () => { - expect(useTabataProgramStore.getState().getProgramStatus('debutant')).toBe('not-started') - }) - - it('getProgramStatus should return in-progress after completing a session', () => { - useTabataProgramStore.getState().completeSession('debutant', 'deb-w1-s1') - expect(useTabataProgramStore.getState().getProgramStatus('debutant')).toBe('in-progress') - }) - - it('isWeekUnlocked should return true for week 1', () => { - expect(useTabataProgramStore.getState().isWeekUnlocked('debutant', 1)).toBe(true) - }) - - it('isWeekUnlocked should return false for week 2 initially', () => { - expect(useTabataProgramStore.getState().isWeekUnlocked('debutant', 2)).toBe(false) - }) - - it('getProgram should return program data', () => { - const program = useTabataProgramStore.getState().getProgram('debutant') - expect(program).toBeDefined() - expect(program?.id).toBe('debutant') - }) - - it('getRecommendedNext should return current session of selected program', () => { - useTabataProgramStore.getState().selectProgram('debutant') - const rec = useTabataProgramStore.getState().getRecommendedNext() - expect(rec).not.toBeNull() - expect(rec?.programId).toBe('debutant') - }) - - it('getRecommendedNext should return null when no programs started', () => { - const rec = useTabataProgramStore.getState().getRecommendedNext() - expect(rec).toBeNull() - }) - }) -}) diff --git a/src/__tests__/stores/workoutProgramStore.test.ts b/src/__tests__/stores/workoutProgramStore.test.ts deleted file mode 100644 index 581df27..0000000 --- a/src/__tests__/stores/workoutProgramStore.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest' -import { useWorkoutProgramStore } from '../../shared/stores/workoutProgramStore' -import type { WorkoutProgram } from '../../shared/types/workoutProgram' - -const resetStore = () => { - useWorkoutProgramStore.setState({ completions: {} }) -} - -const mockPrograms: WorkoutProgram[] = [ - { - id: 'prog-1', - title: 'Upper Body Basics', - description: 'Upper body workout', - bodyZone: 'upper-body', - level: 'Beginner', - isFree: true, - musicVibe: 'electronic', - estimatedDuration: 12, - estimatedCalories: 100, - icon: 'dumbbell', - accentColor: '#FF6B35', - sortOrder: 1, - tabatas: [], - createdAt: '2024-01-01', - updatedAt: '2024-01-01', - }, - { - id: 'prog-2', - title: 'Lower Body Burn', - description: 'Lower body workout', - bodyZone: 'lower-body', - level: 'Intermediate', - isFree: false, - musicVibe: 'hip-hop', - estimatedDuration: 15, - estimatedCalories: 150, - icon: 'figure.walk', - accentColor: '#5AC8FA', - sortOrder: 2, - tabatas: [], - createdAt: '2024-01-01', - updatedAt: '2024-01-01', - }, - { - id: 'prog-3', - title: 'Full Body Advanced', - description: 'Full body workout', - bodyZone: 'full-body', - level: 'Advanced', - isFree: false, - musicVibe: 'rock', - estimatedDuration: 20, - estimatedCalories: 200, - icon: 'bolt', - accentColor: '#30D158', - sortOrder: 3, - tabatas: [], - createdAt: '2024-01-01', - updatedAt: '2024-01-01', - }, -] - -describe('workoutProgramStore', () => { - beforeEach(() => { - resetStore() - }) - - describe('initial state', () => { - it('should have empty completions', () => { - expect(useWorkoutProgramStore.getState().completions).toEqual({}) - }) - }) - - describe('completeProgram', () => { - it('should mark entire program as completed when no tabataPosition', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1') - const state = useWorkoutProgramStore.getState() - expect(state.completions['prog-1']).toBeDefined() - expect(state.completions['prog-1'].tabatasCompleted).toEqual([1, 2, 3]) - expect(state.completions['prog-1'].completedAt).toBeTruthy() - }) - - it('should mark specific tabata as completed', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1', 1) - const state = useWorkoutProgramStore.getState() - expect(state.completions['prog-1'].tabatasCompleted).toEqual([1]) - }) - - it('should accumulate tabata completions', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1', 1) - useWorkoutProgramStore.getState().completeProgram('prog-1', 2) - const state = useWorkoutProgramStore.getState() - expect(state.completions['prog-1'].tabatasCompleted).toEqual([1, 2]) - }) - - it('should not duplicate tabata positions', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1', 1) - useWorkoutProgramStore.getState().completeProgram('prog-1', 1) - const state = useWorkoutProgramStore.getState() - expect(state.completions['prog-1'].tabatasCompleted).toEqual([1]) - }) - - it('should set completedAt when all 3 tabatas done', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1', 1) - useWorkoutProgramStore.getState().completeProgram('prog-1', 2) - useWorkoutProgramStore.getState().completeProgram('prog-1', 3) - const state = useWorkoutProgramStore.getState() - expect(state.completions['prog-1'].completedAt).toBeTruthy() - expect(state.completions['prog-1'].tabatasCompleted).toHaveLength(3) - }) - }) - - describe('resetProgram', () => { - it('should remove program completion', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1') - useWorkoutProgramStore.getState().resetProgram('prog-1') - expect(useWorkoutProgramStore.getState().completions['prog-1']).toBeUndefined() - }) - - it('should not affect other programs', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1') - useWorkoutProgramStore.getState().completeProgram('prog-2') - useWorkoutProgramStore.getState().resetProgram('prog-1') - expect(useWorkoutProgramStore.getState().completions['prog-2']).toBeDefined() - }) - }) - - describe('isProgramCompleted', () => { - it('should return false for uncompleted program', () => { - expect(useWorkoutProgramStore.getState().isProgramCompleted('prog-1')).toBe(false) - }) - - it('should return true when all 3 tabatas completed', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1') - expect(useWorkoutProgramStore.getState().isProgramCompleted('prog-1')).toBe(true) - }) - - it('should return false when only 2 tabatas completed', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1', 1) - useWorkoutProgramStore.getState().completeProgram('prog-1', 2) - expect(useWorkoutProgramStore.getState().isProgramCompleted('prog-1')).toBe(false) - }) - }) - - describe('getCompletedCount', () => { - it('should return 0 initially', () => { - expect(useWorkoutProgramStore.getState().getCompletedCount()).toBe(0) - }) - - it('should count fully completed programs', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1') - useWorkoutProgramStore.getState().completeProgram('prog-2') - useWorkoutProgramStore.getState().completeProgram('prog-3', 1) // partial - expect(useWorkoutProgramStore.getState().getCompletedCount()).toBe(2) - }) - }) - - describe('getRecommendedNext', () => { - it('should recommend first incomplete program sorted by level', () => { - const rec = useWorkoutProgramStore.getState().getRecommendedNext(mockPrograms) - expect(rec).not.toBeNull() - expect(rec?.id).toBe('prog-1') // Beginner first - }) - - it('should skip completed programs', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1') - const rec = useWorkoutProgramStore.getState().getRecommendedNext(mockPrograms) - expect(rec?.id).toBe('prog-2') // Intermediate - }) - - it('should return null when all completed', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1') - useWorkoutProgramStore.getState().completeProgram('prog-2') - useWorkoutProgramStore.getState().completeProgram('prog-3') - expect(useWorkoutProgramStore.getState().getRecommendedNext(mockPrograms)).toBeNull() - }) - }) - - describe('getTabatasCompleted', () => { - it('should return empty array for unknown program', () => { - expect(useWorkoutProgramStore.getState().getTabatasCompleted('unknown')).toEqual([]) - }) - - it('should return completed tabata positions', () => { - useWorkoutProgramStore.getState().completeProgram('prog-1', 2) - expect(useWorkoutProgramStore.getState().getTabatasCompleted('prog-1')).toEqual([2]) - }) - }) -})