From e9a2a02b91eb641bb0ac8d1a28dc8971189770e2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 18:01:23 +0000 Subject: [PATCH] feat: Add clone routine option to routines screen - Add 'Clone Routine' option in the long-press options menu. - Show an input dialog with the original routine's name as a hint. - Create a copy of the routine using the entered name (or the original if empty). - Include widget tests in `routines_screen_test.dart` to verify cloning. Co-authored-by: Devasy23 <110348311+Devasy23@users.noreply.github.com> --- .../lib/screens/routines_screen.dart | 44 +++++++ .../test/screens/routines_screen_test.dart | 109 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 workout-logger/test/screens/routines_screen_test.dart diff --git a/workout-logger/lib/screens/routines_screen.dart b/workout-logger/lib/screens/routines_screen.dart index 81380f4..88e8e15 100644 --- a/workout-logger/lib/screens/routines_screen.dart +++ b/workout-logger/lib/screens/routines_screen.dart @@ -259,6 +259,14 @@ class _RoutineCard extends StatelessWidget { ); }, ), + ListTile( + leading: const Icon(Icons.copy), + title: const Text('Clone Routine'), + onTap: () { + Navigator.pop(context); + _showCloneDialog(context); + }, + ), ListTile( leading: const Icon(Icons.delete, color: AppTheme.error), title: const Text( @@ -276,6 +284,42 @@ class _RoutineCard extends StatelessWidget { ); } + void _showCloneDialog(BuildContext context) { + final TextEditingController nameController = TextEditingController(); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Clone Routine'), + content: TextField( + controller: nameController, + autofocus: true, + decoration: InputDecoration( + hintText: routine.name, + labelText: 'New Routine Name', + ), + textCapitalization: TextCapitalization.words, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () { + final newName = nameController.text.trim().isEmpty + ? routine.name + : nameController.text.trim(); + provider.createRoutine(newName, routine.exerciseIds); + Navigator.pop(context); + }, + child: const Text('Clone'), + ), + ], + ), + ); + } + void _confirmDelete(BuildContext context) { showDialog( context: context, diff --git a/workout-logger/test/screens/routines_screen_test.dart b/workout-logger/test/screens/routines_screen_test.dart new file mode 100644 index 0000000..2fd87a5 --- /dev/null +++ b/workout-logger/test/screens/routines_screen_test.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:repforge/models/models.dart'; +import 'package:repforge/screens/routines_screen.dart'; +import 'package:repforge/services/managers/program_manager.dart'; +import 'package:repforge/services/workout_provider.dart'; +import '../test_utils/mock_storage_service.dart'; + +void main() { + group('RoutinesScreen clone routine tests', () { + late MockStorageService mockStorage; + late WorkoutProvider provider; + + setUp(() async { + mockStorage = MockStorageService(); + provider = WorkoutProvider( + mockStorage, + programManager: ProgramManager(mockStorage), + ); + await provider.init(); + + // Setup a pre-existing routine + await provider.createRoutine('Leg Day', ['squat', 'leg_press']); + }); + + testWidgets('shows clone option and clones routine successfully', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: provider, + child: const RoutinesScreen(), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Find the routine card + final routineCard = find.text('Leg Day'); + expect(routineCard, findsOneWidget); + + // Long press to open options + await tester.longPress(routineCard); + await tester.pumpAndSettle(); + + // Find and tap the clone option + final cloneOption = find.text('Clone Routine'); + expect(cloneOption, findsOneWidget); + await tester.tap(cloneOption); + await tester.pumpAndSettle(); + + // Verify clone dialog appears + final dialogTitle = find.text('Clone Routine'); + expect(dialogTitle, findsOneWidget); + + // Provide a new name + final textField = find.byType(TextField); + expect(textField, findsOneWidget); + await tester.enterText(textField, 'Leg Day Copy'); + await tester.pumpAndSettle(); + + // Tap Clone button + final cloneButton = find.widgetWithText(FilledButton, 'Clone'); + expect(cloneButton, findsOneWidget); + await tester.tap(cloneButton); + await tester.pumpAndSettle(); + + // Verify dialog is closed and new routine is in the provider + expect(find.text('Clone Routine'), findsNothing); + expect(provider.routines.length, 2); + expect(provider.routines[1].name, 'Leg Day Copy'); + expect(provider.routines[1].exerciseIds, ['squat', 'leg_press']); + + // Verify new routine is shown in UI + expect(find.text('Leg Day Copy'), findsOneWidget); + }); + + testWidgets('clones with original name if text field is empty', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ChangeNotifierProvider.value( + value: provider, + child: const RoutinesScreen(), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Long press to open options + await tester.longPress(find.text('Leg Day')); + await tester.pumpAndSettle(); + + // Tap clone + await tester.tap(find.text('Clone Routine')); + await tester.pumpAndSettle(); + + // Do NOT enter text, just tap Clone button + await tester.tap(find.widgetWithText(FilledButton, 'Clone')); + await tester.pumpAndSettle(); + + // Verify provider has the cloned routine with the original name + expect(provider.routines.length, 2); + expect(provider.routines[1].name, 'Leg Day'); + expect(provider.routines[1].exerciseIds, ['squat', 'leg_press']); + }); + }); +}