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:
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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])
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user