Draft design document for Add Custom Exercise feature - #14
Conversation
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
Important Review skippedBot user detected. To trigger a single review, invoke the You can disable this status message by setting the
WalkthroughAdds provider APIs for custom exercises and session CRUD, new screens for adding/editing exercises and sessions, model copyWith helpers, UI updates across library/history/workout screens, new tests and test mocks, and minor config/version bumps. Changes
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
…ry, routines, workout flow, and analytics screens, along with a workout data provider.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@docs/design/add_custom_exercise.md`:
- Around line 1-104: Markdown headings and fenced code blocks are missing
surrounding blank lines causing markdownlint errors MD022/MD031; add a single
blank line above and below each top-level and subheading (e.g., "Design
Document: Personalized Exercise Library Feature", "1. Overview", "2. Feature
Requirements", etc.) and ensure the fenced code block that starts with ```dart
and the closing ``` each have a blank line before and after (the Exercise model
block in the "Technical Architecture" section is the explicit example to fix),
then re-run markdownlint.
In `@workout-logger/lib/screens/add_custom_exercise_screen.dart`:
- Around line 69-77: The catch block currently shows the raw exception ($e) in
the SnackBar; change it to display a generic, user-friendly message (e.g.,
"Failed to save exercise. Please try again.") while logging the actual error for
debugging using an internal logger or debugPrint; update the catch (e) handler
where ScaffoldMessenger.of(context).showSnackBar and SnackBar are used (in
add_custom_exercise_screen.dart) to keep the mounted check and AppTheme.error
for the UI message but call logger.error(e) or debugPrint(e.toString()) before
or after showing the SnackBar so internal details are not exposed to users.
- Around line 226-238: The code repeatedly calls
MuscleGroups.names.keys.elementAt(index) inside the GridView.builder causing
O(n²) behavior; fix it by materializing the keys once (e.g., create a local
final List<String> muscleKeys = MuscleGroups.names.keys.toList() or a
class-level const if immutable) and then use muscleKeys[index] to derive
muscleId and look up muscleName via MuscleGroups.names[muscleId]; update
references in the itemBuilder (where muscleId and muscleName are set) to use the
cached list.
In `@workout-logger/lib/screens/exercise_library_screen.dart`:
- Around line 611-655: In _confirmDelete, capture the ScaffoldMessengerState
before calling Navigator.of(context).pop() to avoid using a deactivated context:
after receiving success from provider.deleteCustomExercise(exercise.id) (and
after confirming context.mounted), assign ScaffoldMessenger.of(context) to a
local variable, then call Navigator.of(context).pop() to close the bottom sheet
and use the previously captured messenger to show the SnackBar; keep the
existing SnackBar contents and styling but invoke messenger.showSnackBar(...)
instead of ScaffoldMessenger.of(context).showSnackBar(...).
In `@workout-logger/lib/services/workout_provider.dart`:
- Around line 104-137: The addCustomExercise method accepts raw inputs that can
be blank or have inconsistent casing; update addCustomExercise to defensively
validate and normalize inputs: trim and collapse whitespace for name and throw
or return early if name is empty, validate primaryMuscleGroupId is non-empty,
normalize category to a canonical form (e.g., toLowerCase() and map to allowed
categories such as 'compound'/'isolation' or reject unknown values), then
construct the Exercise using the normalized name and category before calling
_storage.saveCustomExercise, updating _allExercises and notifyListeners;
reference the addCustomExercise function, _uuid, Exercise constructor,
_storage.saveCustomExercise, _allExercises, and notifyListeners when applying
the changes.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review, do we have any action items? |
🧩 Analysis chain
🏁 Script executed: Length of output: 529 🏁 Script executed: Length of output: 2148
🎯 Action ItemsCritical
Minor
Future Enhancements (Documented)These are already captured in the design doc for v2:
✅ What's Working Well
Recommendation: Prioritize implementing the tests before merging to ensure the feature is properly validated and regression-proof. ✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@workout-logger/lib/models/models.dart`:
- Around line 149-163: The copyWith implementations (eg. WorkoutSet.copyWith)
use "param ?? this.field" which prevents explicitly setting fields to null;
replace nullable parameters with sentinel-typed optional parameters (e.g., use
Object? weight = _sentinel, Object? reps = _sentinel, etc., and define a private
const _sentinel = Object()) and then assign each field with a conditional like
"weight == _sentinel ? this.weight : weight as double?" (similarly for reps,
isDropset, drops, timeTaken, timestamp). Apply the same sentinel pattern to the
other copyWith methods noted (the copyWith at ~203-211 and ~256-270) so callers
can pass null to clear a value while leaving omitted fields unchanged.
In `@workout-logger/lib/screens/edit_workout_session_screen.dart`:
- Around line 41-56: The mapping that builds _EditableExerciseLog currently
strips set metadata because _EditableSet only contains weight/reps/isDropset;
update the _EditableSet class to include drops, timeTaken, and timestamp fields
(with proper nullable types/defaults), update its constructor and any usages
(e.g., where _EditableSet instances are created in the widget state mapping and
at the other creation sites noted) to populate these new fields from the
original set objects, and ensure the save/serialize logic for
_EditableExerciseLog/_EditableSet preserves and writes these additional fields
back so dropset and timing data are not lost.
- Around line 240-246: Replace the SnackBar that exposes raw exception text in
the catch block of EditWorkoutSessionScreen by showing a generic user-facing
message (e.g., "Failed to save workout") via
ScaffoldMessenger.of(context).showSnackBar(...) and log the actual exception for
debugging using the app logger or debugPrint inside the same catch branch;
target the catch surrounding the save flow (the block that currently builds
SnackBar with Text('Failed to save: $e') and AppTheme.error) and ensure the
existing mounted check remains.
In `@workout-logger/lib/screens/history_screen.dart`:
- Around line 392-397: Don't show raw exception text in the SnackBar; instead
display a generic user-facing message (e.g., "Failed to delete item") when
calling ScaffoldMessenger.of(context).showSnackBar(...) and log the actual
exception for diagnostics. Update the block around context.mounted /
ScaffoldMessenger.of(context).showSnackBar / SnackBar / Text('Failed to delete:
$e') to replace the Text content with a generic message and send the exception
`e` to your logger (e.g., debugPrint, print, or your logging utility) along with
contextual info so devs can investigate.
- Around line 341-348: In _editSession, avoid using the incoming BuildContext
after popping the bottom sheet: capture NavigatorState (e.g., final navigator =
Navigator.of(context)) and ScaffoldMessengerState (e.g., final messenger =
ScaffoldMessenger.of(context)) before calling navigator.pop(), then use
navigator.push<bool>(...) to open EditWorkoutSessionScreen(session: session) and
messenger.showSnackBar(...) afterward; also wrap navigation in try/catch and
when logging or showing error text use a generic, user-friendly message (no raw
exception) while optionally logging the exception to debug logs instead of
exposing it to users.
In `@workout-logger/lib/services/workout_provider.dart`:
- Around line 360-403: deleteWorkoutSession and updateWorkoutSession currently
only call _updateGrowthModel for exercises present in the updated/new session,
leaving removed exercises' derived state (growth models and targets) stale;
change both methods to (1) compute the set of affectedExerciseIds by comparing
previous session exerciseIds from _sessions (before mutation) with
updatedSession.exercises (for update) or using session.exercises (for delete),
(2) for each affected exercise id call _updateGrowthModel and then recompute and
persist its target values based on the latest remaining sessions (or clear the
growth model/targets via _updateGrowthModel when insufficient data), and (3)
ensure storage (_storage.saveWorkoutSession / _storage.deleteWorkoutSession) and
_sessions are updated before recalculation and finally call notifyListeners; use
the existing symbols deleteWorkoutSession, updateWorkoutSession,
_updateGrowthModel, _sessions, and _storage to locate and implement these
changes.
…ise creation, workout history, session editing, and supporting models, services, and tests.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@workout-logger/lib/screens/edit_workout_session_screen.dart`:
- Around line 719-747: The didUpdateWidget handler in _EditableSetRowState is
force-updating _weightController.text and _repsController.text, which can
clobber user input during typing; change this to only sync controller text when
the incoming widget values are genuinely external updates (e.g., when the
controllers' current text differs and the field is not focused) or set a simple
“isLocalEdit”/focus check so updates originating from onChanged do not trigger
controller.text assignments; adjust didUpdateWidget to use FocusScope/FocusNode
or a boolean flag to skip assignments when the user is actively editing, and
ensure onChanged still updates parent state via the widget callbacks without
causing cursor jumps.
In `@workout-logger/test/add_custom_exercise_screen_test.dart`:
- Around line 189-218: Add an expectation to verify the screen popped after
successful save by asserting the AddCustomExerciseScreen is no longer in the
widget tree: after await tester.pumpAndSettle() add an assertion using
find.byType(AddCustomExerciseScreen) with findsNothing (alongside the existing
checks of mockStorage.saveCustomExerciseCalled and
mockStorage.lastSavedExercise). If navigation is intentionally tested elsewhere,
skip adding this assertion.
- Around line 11-98: Extract the duplicated MockStorageService into a shared
test utility (e.g., test/test_utils/mock_storage_service.dart) and replace the
local class in add_custom_exercise_screen_test.dart and
workout_provider_test.dart with an import of that shared class; ensure the
shared MockStorageService preserves the same fields and observable flags
(customExercises/_customExercises, saveCustomExerciseCalled, lastSavedExercise)
and implements all StorageService methods used by tests (saveCustomExercise,
getCustomExercises, deleteCustomExercise, etc.), update imports in both test
files to use the shared utility, and run tests to confirm behavior remains
identical.
In `@workout-logger/test/exercise_library_screen_test.dart`:
- Around line 280-367: Add a test named like testWidgets('should delete exercise
when Delete is confirmed', ...) that mounts ExerciseLibraryScreen with the
existing provider, opens the details for 'Exercise To Delete' by tapping the
text, taps the delete icon (find.byIcon(Icons.delete_outline)), then taps the
confirmation button (e.g. find.widgetWithText(TextButton, 'Delete')) and
pumpAndSettle; finally assert that provider.allExercises.any((e) => e.name ==
'Exercise To Delete') is false to verify the exercise was removed. Use the same
setup (provider and mockStorage) and patterns as the other tests so it
integrates with the existing group.
In `@workout-logger/test/workout_provider_test.dart`:
- Around line 3-4: The test currently uses Mockito but MockStorageService only
contains manual implementations and never uses Mockito features; change
MockStorageService to a pure fake by removing the import
'package:mockito/mockito.dart' and stop extending Mock (declare class
MockStorageService implements StorageService or extends the real
interface/class) and keep the concrete method implementations, and also remove
the mockito dev_dependency from tests (or switch to using when()/verify on
MockStorageService if you prefer to keep Mockito).
- Around line 86-93: The test uses provider.allExercises.firstWhere which throws
if no match is found, so the expect(addedExercise, isNotNull) is redundant;
either remove that assertion (keep expect(addedExercise.name, equals(name))) or
change the lookup to use firstWhere(..., orElse: () => null) and then assert
addedExercise isNotNull before checking its name; update the test in
workout_provider_test.dart by modifying the firstWhere usage or removing the
null check accordingly (refer to provider.allExercises, firstWhere, and
addedExercise).
♻️ Duplicate comments (2)
workout-logger/lib/services/workout_provider.dart (1)
365-431: Targets still not recalculated on session delete/update.The previous review flagged that
updateWorkoutSessionanddeleteWorkoutSessiondon't recalculate targets for affected exercises. While growth models are now correctly retrained for both previous and updated exercises (good fix!), target values (currentValue,isCompleted,estimatedCompletionDate) remain stale when sessions are edited or deleted.For example, if a user deletes a session that contained their best lift for a target, the target's
currentValuewill still reflect the deleted data.Consider calling a target recalculation method (similar to
_updateTargetsFromSession) for all affected exercises after session mutations.workout-logger/lib/screens/history_screen.dart (1)
376-406: Consider capturing navigator/messenger before the async delete operation.While
context.mountedchecks are present,Navigator.of(context)andScaffoldMessenger.of(context)are still called afterawait provider.deleteWorkoutSession(session.id). If the widget tree changes during the async operation, these lookups could fail.♻️ Suggested improvement
if (confirmed == true && context.mounted) { try { + final navigator = Navigator.of(context); + final messenger = ScaffoldMessenger.of(context); await provider.deleteWorkoutSession(session.id); if (context.mounted) { - Navigator.of(context).pop(); // Close the bottom sheet - ScaffoldMessenger.of(context).showSnackBar( + navigator.pop(); // Close the bottom sheet + messenger.showSnackBar( SnackBar( content: Row(
…ider, and comprehensive tests for exercise library and custom exercise management.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
workout-logger/lib/services/workout_provider.dart (1)
79-87: Remove stale growth models when retraining produces insufficient data.When a session is deleted or edited and data points drop below the training threshold of 2, the existing trained model persists in
_growthModelsand gets used for recommendations and target completion predictions. These stale models are trained on data that no longer exists in_sessions, causing inaccurate guidance. Clear the model when you can't retrain it.Fix
if (dataPoints.length >= 2) { _growthModels[exerciseId] = MLService.trainGrowthModel(dataPoints); + } else { + _growthModels.remove(exerciseId); }
🤖 Fix all issues with AI agents
In `@workout-logger/lib/screens/edit_workout_session_screen.dart`:
- Around line 570-586: The _EditableSet constructor currently accepts a nullable
DateTime? timestamp and coerces it with timestamp = timestamp ?? DateTime.now();
remove this dead-code by changing the constructor signature to require a
non-null DateTime timestamp (DateTime timestamp) and assign it directly to the
timestamp field (no null-coalescing). Update the constructor parameter list in
class _EditableSet and remove the initializer expression that performs the
fallback; no callsite changes should be needed since all instantiations already
pass explicit DateTime values.
In `@workout-logger/lib/services/workout_provider.dart`:
- Around line 168-185: The deleteCustomExercise method currently removes an
exercise without verifying references; update deleteCustomExercise to first
locate the exercise via getExercise(exerciseId) and then scan all relevant data
stores (sessions, routines, targets, active logs / any collections that
reference exercises) to see if any entry references this exerciseId; if any
reference exists, do not call _storage.deleteCustomExercise or mutate
_allExercises, instead return false (or surface a clear error) and avoid
notifyListeners; only proceed to call _storage.deleteCustomExercise, update
_allExercises (currently mutated via
List.from(_allExercises)..removeWhere(...)), and notifyListeners() when no
references are found. Include checks against whatever in-memory lists or
repository methods are used to load sessions/routines/targets/logs so you catch
both persisted and active references before deletion.
In `@workout-logger/test/add_custom_exercise_screen_test.dart`:
- Around line 100-131: The test only asserts the saved exercise name; extend
assertions to verify other saved properties on mockStorage.lastSavedExercise
(e.g., muscleGroup, equipment, variations, or any form fields collected by
AddCustomExerciseScreen) after the save action, and keep existing checks
(mockStorage.saveCustomExerciseCalled and navigation). Locate the test case for
AddCustomExerciseScreen that fills TextFormField and taps 'Shoulders'/'Save' and
add expectations asserting those fields match the inputs/submissions produced by
the form mapping.
| testWidgets('should call provider method on valid form submission', ( | ||
| tester, | ||
| ) async { | ||
| // Arrange | ||
| await tester.pumpWidget( | ||
| createTestWidget( | ||
| child: const AddCustomExerciseScreen(), | ||
| provider: provider, | ||
| ), | ||
| ); | ||
|
|
||
| // Act - Fill in valid form | ||
| await tester.enterText(find.byType(TextFormField), 'Cable Lateral Raise'); | ||
| await tester.pump(); | ||
|
|
||
| // Select a muscle group | ||
| await tester.tap(find.text('Shoulders')); | ||
| await tester.pump(); | ||
|
|
||
| // Submit | ||
| await tester.tap(find.text('Save')); | ||
| await tester.pumpAndSettle(); | ||
|
|
||
| // Assert - Storage should have been called | ||
| expect(mockStorage.saveCustomExerciseCalled, isTrue); | ||
| expect( | ||
| mockStorage.lastSavedExercise?.name, | ||
| equals('Cable Lateral Raise'), | ||
| ); | ||
| // Verify screen popped (AddCustomExerciseScreen no longer in tree) | ||
| expect(find.byType(AddCustomExerciseScreen), findsNothing); | ||
| }); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider verifying additional saved exercise properties.
The test correctly verifies storage interaction and navigation. However, it only asserts on the exercise name. Verifying additional properties would strengthen test coverage and catch potential regressions in form data mapping.
♻️ Suggested enhancement
// Assert - Storage should have been called
expect(mockStorage.saveCustomExerciseCalled, isTrue);
expect(
mockStorage.lastSavedExercise?.name,
equals('Cable Lateral Raise'),
);
+ expect(
+ mockStorage.lastSavedExercise?.primaryMuscle,
+ equals('Shoulders'),
+ );
+ expect(
+ mockStorage.lastSavedExercise?.category,
+ equals('Compound'), // Default category
+ );
// Verify screen popped (AddCustomExerciseScreen no longer in tree)
expect(find.byType(AddCustomExerciseScreen), findsNothing);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| testWidgets('should call provider method on valid form submission', ( | |
| tester, | |
| ) async { | |
| // Arrange | |
| await tester.pumpWidget( | |
| createTestWidget( | |
| child: const AddCustomExerciseScreen(), | |
| provider: provider, | |
| ), | |
| ); | |
| // Act - Fill in valid form | |
| await tester.enterText(find.byType(TextFormField), 'Cable Lateral Raise'); | |
| await tester.pump(); | |
| // Select a muscle group | |
| await tester.tap(find.text('Shoulders')); | |
| await tester.pump(); | |
| // Submit | |
| await tester.tap(find.text('Save')); | |
| await tester.pumpAndSettle(); | |
| // Assert - Storage should have been called | |
| expect(mockStorage.saveCustomExerciseCalled, isTrue); | |
| expect( | |
| mockStorage.lastSavedExercise?.name, | |
| equals('Cable Lateral Raise'), | |
| ); | |
| // Verify screen popped (AddCustomExerciseScreen no longer in tree) | |
| expect(find.byType(AddCustomExerciseScreen), findsNothing); | |
| }); | |
| testWidgets('should call provider method on valid form submission', ( | |
| tester, | |
| ) async { | |
| // Arrange | |
| await tester.pumpWidget( | |
| createTestWidget( | |
| child: const AddCustomExerciseScreen(), | |
| provider: provider, | |
| ), | |
| ); | |
| // Act - Fill in valid form | |
| await tester.enterText(find.byType(TextFormField), 'Cable Lateral Raise'); | |
| await tester.pump(); | |
| // Select a muscle group | |
| await tester.tap(find.text('Shoulders')); | |
| await tester.pump(); | |
| // Submit | |
| await tester.tap(find.text('Save')); | |
| await tester.pumpAndSettle(); | |
| // Assert - Storage should have been called | |
| expect(mockStorage.saveCustomExerciseCalled, isTrue); | |
| expect( | |
| mockStorage.lastSavedExercise?.name, | |
| equals('Cable Lateral Raise'), | |
| ); | |
| expect( | |
| mockStorage.lastSavedExercise?.primaryMuscle, | |
| equals('Shoulders'), | |
| ); | |
| expect( | |
| mockStorage.lastSavedExercise?.category, | |
| equals('Compound'), // Default category | |
| ); | |
| // Verify screen popped (AddCustomExerciseScreen no longer in tree) | |
| expect(find.byType(AddCustomExerciseScreen), findsNothing); | |
| }); |
🤖 Prompt for AI Agents
In `@workout-logger/test/add_custom_exercise_screen_test.dart` around lines 100 -
131, The test only asserts the saved exercise name; extend assertions to verify
other saved properties on mockStorage.lastSavedExercise (e.g., muscleGroup,
equipment, variations, or any form fields collected by AddCustomExerciseScreen)
after the save action, and keep existing checks
(mockStorage.saveCustomExerciseCalled and navigation). Locate the test case for
AddCustomExerciseScreen that fills TextFormField and taps 'Shoulders'/'Save' and
add expectations asserting those fields match the inputs/submissions produced by
the form mapping.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@workout-logger/lib/services/workout_provider.dart`:
- Around line 404-410: The current deleteWorkoutSession uses
_sessions.firstWhere(..., orElse: () => throw Exception(...)) which will throw
if the sessionId is stale; change the lookup to a non-throwing pattern (e.g.,
use _sessions.indexWhere((s) => s.id == sessionId) and return early if index ==
-1, or use a nullable lookup and if session == null return) so the method exits
gracefully when the session isn't found before proceeding to collect exercise
IDs and retrain models; update references to the retrieved session variable
accordingly (deleteWorkoutSession, _sessions, session).
♻️ Duplicate comments (1)
workout-logger/lib/services/workout_provider.dart (1)
171-223: Active workout references still not checked before deletion.The method checks
_sessions,_routines, and_targetsfor references, but does not check_currentExerciseLogsor_activeRoutine. If a user deletes a custom exercise while mid-workout using it, finishing that workout will persist a session referencing a deleted exercise—resulting in "Unknown Exercise" displays and broken analytics.🛠️ Suggested guard addition after line 209
// Check for references in Targets for (var target in _targets) { if (target.exerciseId == exerciseId) { debugPrint( 'Cannot delete custom exercise: Used in target ${target.id}', ); return false; } } + // Check for references in active workout + if (_currentExerciseLogs.any((l) => l.exerciseId == exerciseId)) { + debugPrint('Cannot delete custom exercise: Used in active workout'); + return false; + } + if (_activeRoutine?.exerciseIds.contains(exerciseId) ?? false) { + debugPrint('Cannot delete custom exercise: Used in active routine'); + return false; + } // Remove from storage await _storage.deleteCustomExercise(exerciseId);
…and tests for exercise library and custom exercise management.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@workout-logger/lib/screens/edit_workout_session_screen.dart`:
- Around line 544-548: The Icon widget instantiation (Icon(Icons.fitness_center,
size: 48, color: AppTheme.textMuted)) should be made const since all its
arguments are compile-time constants; change the expression to const Icon(...)
in the build tree (wherever Icon(Icons.fitness_center, size: 48, color:
AppTheme.textMuted) appears, e.g., inside EditWorkoutSessionScreen's widget
tree) to improve performance and eliminate unnecessary rebuilds.
In `@workout-logger/lib/services/workout_provider.dart`:
- Around line 630-636: The comment in _updateTargetsFromSession contains a typo:
change "optimzed" to "optimized" in the comment text inside the method to
correct spelling and keep docs clear; locate the comment block beginning "//
This is optimzed..." and update that word to "optimized" (no code logic changes
needed).
In `@workout-logger/test/add_custom_exercise_screen_test.dart`:
- Around line 10-18: Extract the duplicated Widget createTestWidget helper into
a shared test utility file and import it into both
add_custom_exercise_screen_test.dart and exercise_library_screen_test.dart; move
the function signature and implementation (the
ChangeNotifierProvider<WorkoutProvider>.value wrapping MaterialApp) into the
shared helper (e.g., test_helpers.dart) and replace the local copies with
imports and calls to createTestWidget to eliminate duplication while keeping the
same API.
In `@workout-logger/test/exercise_library_screen_test.dart`:
- Around line 25-29: Add a tearDown that disposes the WorkoutProvider created in
setUp: call provider.dispose() (or provider?.dispose()) to clean up the
ChangeNotifier after each test; reference the existing setUp block that creates
provider = WorkoutProvider(mockStorage) and ensure tearDown runs after each test
to avoid lingering listeners or resources.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Added a design document
docs/design/add_custom_exercise.mdthat outlines the plan for implementing the "Add Personalized Exercise" feature. The document includes user stories, technical architecture, implementation steps, and testing strategy, following Flutter best practices.PR created automatically by Jules for task 9646738705101450413 started by @Devasy23
Summary by CodeRabbit
New Features
New Features
Improvements
Tests
Documentation
Chore
✏️ Tip: You can customize this high-level summary in your review settings.