remove obsolete tests for deleted data, stores & components

Delete tests for removed modules: WorkoutCard, CollectionCard, Skeleton,
achievements, dataService, programs, trainers, useTranslatedData,
workouts, useSupabaseData, activityStore, programStore,
tabataProgramStore, and workoutProgramStore.
This commit is contained in:
Millian Lamiaux
2026-04-21 21:50:19 +02:00
parent 3fe9d926ad
commit 13262305e5
14 changed files with 0 additions and 2974 deletions

View File

@@ -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<string, string> = {
'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)
})
})
})

View File

@@ -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(<CollectionCard collection={mockCollection} />)
expect(screen.getByText('Upper Body Blast')).toBeTruthy()
})
it('renders workout count', () => {
render(<CollectionCard collection={mockCollection} />)
expect(screen.getByText('3 workouts')).toBeTruthy()
})
it('renders icon emoji', () => {
render(<CollectionCard collection={mockCollection} />)
expect(screen.getByText('💪')).toBeTruthy()
})
it('calls onPress when pressed', () => {
const onPress = vi.fn()
render(<CollectionCard collection={mockCollection} onPress={onPress} />)
fireEvent.press(screen.getByText('Upper Body Blast'))
expect(onPress).toHaveBeenCalledTimes(1)
})
it('renders without onPress (no crash)', () => {
const { toJSON } = render(<CollectionCard collection={mockCollection} />)
expect(toJSON()).toMatchSnapshot()
})
it('renders LinearGradient when no imageUrl', () => {
const { toJSON } = render(<CollectionCard collection={mockCollection} />)
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(
<CollectionCard
collection={mockCollection}
imageUrl="https://example.com/image.jpg"
/>
)
// 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(
<CollectionCard collection={collectionNoGradient} />
)
// 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(<CollectionCard collection={mockCollection} />)
expect(screen.getByTestId('blur-view')).toBeTruthy()
})
it('handles empty workoutIds', () => {
const emptyCollection: Collection = {
...mockCollection,
workoutIds: [],
}
render(<CollectionCard collection={emptyCollection} />)
expect(screen.getByText('0 workouts')).toBeTruthy()
})
it('snapshot with imageUrl (different rendering path)', () => {
const { toJSON } = render(
<CollectionCard
collection={mockCollection}
imageUrl="https://example.com/image.jpg"
/>
)
expect(toJSON()).toMatchSnapshot()
})
})

View File

@@ -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<string, any> {
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(<Skeleton />)
expect(toJSON()).toMatchSnapshot()
})
it('applies default width=100% and height=20', () => {
const { toJSON } = render(<Skeleton />)
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(<Skeleton width={200} height={40} />)
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(<Skeleton width="70%" height={20} />)
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(<Skeleton borderRadius={40} />)
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(<Skeleton style={customStyle} />)
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(<Skeleton />)
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(<Skeleton />)
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(<WorkoutCardSkeleton />)
expect(toJSON()).toMatchSnapshot()
})
it('contains multiple Skeleton elements as children', () => {
const { toJSON } = render(<WorkoutCardSkeleton />)
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(<TrainerCardSkeleton />)
expect(toJSON()).toMatchSnapshot()
})
it('contains circular avatar skeleton (borderRadius=40)', () => {
const { toJSON } = render(<TrainerCardSkeleton />)
// 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(<CollectionCardSkeleton />)
expect(toJSON()).toMatchSnapshot()
})
})
describe('StatsCardSkeleton', () => {
it('renders correct structure (snapshot)', () => {
const { toJSON } = render(<StatsCardSkeleton />)
expect(toJSON()).toMatchSnapshot()
})
it('contains header row with two skeleton elements', () => {
const { toJSON } = render(<StatsCardSkeleton />)
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)
})
})

View File

@@ -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)
})
})
})
})

View File

@@ -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()
})
})
})
})

View File

@@ -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<string>()
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)
})
})
})

View File

@@ -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)
})
})
})

View File

@@ -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<string, string> = {
'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<string, string> = {
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')
})
})
})

View File

@@ -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)
})
})
})
})

View File

@@ -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)
})
})
})
})

View File

@@ -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>): 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)
})
})
})

View File

@@ -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)
})
})
})

View File

@@ -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<TabataProgramId, any> = {} 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()
})
})
})

View File

@@ -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])
})
})
})