Session tracking only ever stored a done boolean - no record of what weight or reps were actually performed, so there was no way to derive real progress data even though the dashboard implied "progress tracking" was a thing. Workout templates only hold a static planned value, not a history of performance. Adds actual_weight/actual_reps to workout_session_items, captured inline when checking off a set in a session (defaults to the workout's planned weight/reps, editable before confirming). The exercise detail page now shows the user's personal best (heaviest completed set, tie broken by reps) computed from their completed session history across all workouts containing that exercise. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
148 lines
5.5 KiB
PHP
148 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\BodyPart;
|
|
use App\Models\Exercise;
|
|
use App\Models\User;
|
|
use App\Models\Workout;
|
|
use App\Models\WorkoutExercise;
|
|
use App\Models\WorkoutSession;
|
|
use App\Models\WorkoutSessionItem;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class ExerciseTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_index_lists_exercises(): void
|
|
{
|
|
Exercise::factory()->count(3)->create();
|
|
|
|
$response = $this->get(route('exercises.index'));
|
|
|
|
$response->assertOk();
|
|
$response->assertViewHas('exercises', function ($exercises) {
|
|
return $exercises->total() === 3;
|
|
});
|
|
}
|
|
|
|
public function test_index_can_filter_by_search_query(): void
|
|
{
|
|
Exercise::factory()->create(['name' => 'Barbell Bench Press']);
|
|
Exercise::factory()->create(['name' => 'Dumbbell Curl']);
|
|
|
|
$response = $this->get(route('exercises.index', ['q' => 'Bench']));
|
|
|
|
$response->assertOk();
|
|
$response->assertViewHas('exercises', function ($exercises) {
|
|
return $exercises->total() === 1
|
|
&& $exercises->first()->name === 'Barbell Bench Press';
|
|
});
|
|
}
|
|
|
|
public function test_index_can_filter_by_body_part(): void
|
|
{
|
|
$chest = BodyPart::create(['name' => 'Chest', 'slug' => 'chest']);
|
|
$back = BodyPart::create(['name' => 'Back', 'slug' => 'back']);
|
|
|
|
$chestExercise = Exercise::factory()->create();
|
|
$chestExercise->bodyParts()->attach($chest->id);
|
|
|
|
$backExercise = Exercise::factory()->create();
|
|
$backExercise->bodyParts()->attach($back->id);
|
|
|
|
$response = $this->get(route('exercises.index', ['bodypart' => 'chest']));
|
|
|
|
$response->assertOk();
|
|
$response->assertViewHas('exercises', function ($exercises) use ($chestExercise) {
|
|
return $exercises->total() === 1
|
|
&& $exercises->first()->id === $chestExercise->id;
|
|
});
|
|
}
|
|
|
|
public function test_show_displays_exercise(): void
|
|
{
|
|
$exercise = Exercise::factory()->create(['name' => 'Barbell Squat']);
|
|
|
|
$response = $this->get(route('exercises.show', $exercise->slug));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('Barbell Squat');
|
|
}
|
|
|
|
public function test_show_returns_404_for_unknown_slug(): void
|
|
{
|
|
$response = $this->get('/exercises/does-not-exist');
|
|
|
|
$response->assertNotFound();
|
|
}
|
|
|
|
public function test_show_displays_personal_best_from_completed_session_items(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$exercise = Exercise::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id]);
|
|
$workoutExercise = WorkoutExercise::factory()->create(['workout_id' => $workout->id, 'exercise_id' => $exercise->id]);
|
|
$session = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]);
|
|
|
|
WorkoutSessionItem::factory()->create([
|
|
'session_id' => $session->id,
|
|
'workout_exercise_id' => $workoutExercise->id,
|
|
'done' => true,
|
|
'actual_weight' => 60,
|
|
'actual_reps' => 5,
|
|
]);
|
|
|
|
$response = $this->actingAs($user)->get(route('exercises.show', $exercise->slug));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('60 kg', false);
|
|
}
|
|
|
|
public function test_show_picks_the_heaviest_completed_set_across_sessions_as_personal_best(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$exercise = Exercise::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id]);
|
|
$workoutExercise = WorkoutExercise::factory()->create(['workout_id' => $workout->id, 'exercise_id' => $exercise->id]);
|
|
$earlierSession = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]);
|
|
$laterSession = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]);
|
|
|
|
WorkoutSessionItem::factory()->create([
|
|
'session_id' => $earlierSession->id, 'workout_exercise_id' => $workoutExercise->id,
|
|
'done' => true, 'actual_weight' => 60, 'actual_reps' => 5,
|
|
]);
|
|
WorkoutSessionItem::factory()->create([
|
|
'session_id' => $laterSession->id, 'workout_exercise_id' => $workoutExercise->id,
|
|
'done' => true, 'actual_weight' => 70, 'actual_reps' => 3,
|
|
]);
|
|
|
|
$response = $this->actingAs($user)->get(route('exercises.show', $exercise->slug));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('70 kg', false);
|
|
$response->assertDontSee('60 kg', false);
|
|
}
|
|
|
|
public function test_show_ignores_incomplete_sets_for_personal_best(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$exercise = Exercise::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id]);
|
|
$workoutExercise = WorkoutExercise::factory()->create(['workout_id' => $workout->id, 'exercise_id' => $exercise->id]);
|
|
$session = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]);
|
|
|
|
WorkoutSessionItem::factory()->create([
|
|
'session_id' => $session->id, 'workout_exercise_id' => $workoutExercise->id,
|
|
'done' => false, 'actual_weight' => 90, 'actual_reps' => 5,
|
|
]);
|
|
|
|
$response = $this->actingAs($user)->get(route('exercises.show', $exercise->slug));
|
|
|
|
$response->assertOk();
|
|
$response->assertDontSee('90 kg', false);
|
|
}
|
|
}
|