create([ 'user_id' => $user->id, 'workout_id' => $workout->id, 'completed_at' => $completedAt, ]); } public function test_user_with_no_sessions_has_no_streak_or_badges(): void { $user = User::factory()->create(); $result = Gamification::forUser($user); $this->assertEquals(0, $result['current_streak']); $this->assertEquals(0, $result['longest_streak']); $this->assertTrue(collect($result['badges'])->every(fn ($b) => $b['earned'] === false)); } public function test_a_session_completed_today_counts_as_a_one_day_streak(): void { $user = User::factory()->create(); $workout = Workout::factory()->create(['user_id' => $user->id]); $this->completeSession($user, $workout, now()->toDateString()); $result = Gamification::forUser($user); $this->assertEquals(1, $result['current_streak']); } public function test_consecutive_days_extend_the_current_streak(): void { $user = User::factory()->create(); $workout = Workout::factory()->create(['user_id' => $user->id]); $this->completeSession($user, $workout, now()->toDateString()); $this->completeSession($user, $workout, now()->subDay()->toDateString()); $this->completeSession($user, $workout, now()->subDays(2)->toDateString()); $result = Gamification::forUser($user); $this->assertEquals(3, $result['current_streak']); } public function test_a_gap_breaks_the_current_streak_but_longest_streak_remembers_it(): void { $user = User::factory()->create(); $workout = Workout::factory()->create(['user_id' => $user->id]); // A 3-day streak, a week ago, with a gap since. $this->completeSession($user, $workout, now()->subDays(10)->toDateString()); $this->completeSession($user, $workout, now()->subDays(9)->toDateString()); $this->completeSession($user, $workout, now()->subDays(8)->toDateString()); // Today, isolated. $this->completeSession($user, $workout, now()->toDateString()); $result = Gamification::forUser($user); $this->assertEquals(1, $result['current_streak']); $this->assertEquals(3, $result['longest_streak']); } public function test_missing_only_today_does_not_break_the_streak_yet(): void { $user = User::factory()->create(); $workout = Workout::factory()->create(['user_id' => $user->id]); $this->completeSession($user, $workout, now()->subDay()->toDateString()); $this->completeSession($user, $workout, now()->subDays(2)->toDateString()); $result = Gamification::forUser($user); $this->assertEquals(2, $result['current_streak']); } public function test_first_steps_badge_earned_after_one_completed_session(): void { $user = User::factory()->create(); $workout = Workout::factory()->create(['user_id' => $user->id]); $this->completeSession($user, $workout, now()->toDateString()); $badges = collect(Gamification::forUser($user)['badges'])->keyBy('key'); $this->assertTrue($badges['first_steps']['earned']); $this->assertFalse($badges['consistent']['earned']); } public function test_consistent_badge_earned_after_ten_completed_sessions(): void { $user = User::factory()->create(); $workout = Workout::factory()->create(['user_id' => $user->id]); for ($i = 0; $i < 10; $i++) { WorkoutSession::factory()->create([ 'user_id' => $user->id, 'workout_id' => $workout->id, 'completed_at' => now()->subDays($i * 3)->toDateString(), ]); } $badges = collect(Gamification::forUser($user)['badges'])->keyBy('key'); $this->assertTrue($badges['consistent']['earned']); } public function test_century_club_badge_earned_from_a_completed_hundred_kilo_set(): void { $user = User::factory()->create(); $workout = Workout::factory()->create(['user_id' => $user->id]); $item = WorkoutExercise::factory()->create(['workout_id' => $workout->id]); $session = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]); WorkoutSessionItem::factory()->create([ 'session_id' => $session->id, 'workout_exercise_id' => $item->id, 'done' => true, 'actual_weight' => 100, ]); $badges = collect(Gamification::forUser($user)['badges'])->keyBy('key'); $this->assertTrue($badges['century_club']['earned']); } public function test_century_club_badge_not_earned_from_an_incomplete_set(): void { $user = User::factory()->create(); $workout = Workout::factory()->create(['user_id' => $user->id]); $item = WorkoutExercise::factory()->create(['workout_id' => $workout->id]); $session = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]); WorkoutSessionItem::factory()->create([ 'session_id' => $session->id, 'workout_exercise_id' => $item->id, 'done' => false, 'actual_weight' => 150, ]); $badges = collect(Gamification::forUser($user)['badges'])->keyBy('key'); $this->assertFalse($badges['century_club']['earned']); } public function test_dashboard_renders_streak_and_badges(): void { $user = User::factory()->create(); $workout = Workout::factory()->create(['user_id' => $user->id]); $this->completeSession($user, $workout, now()->toDateString()); $response = $this->actingAs($user)->get(route('dashboard')); $response->assertOk(); $response->assertSee('First Steps'); $response->assertSee('1', false); } }