diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 617af8b4..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/lib/core/widgets/info_row.dart b/lib/core/widgets/info_row.dart index 6138195a..fbf0b8bf 100644 --- a/lib/core/widgets/info_row.dart +++ b/lib/core/widgets/info_row.dart @@ -58,7 +58,7 @@ class InfoRow extends StatelessWidget { children: [ SvgPicture.asset('assets/icons/calories.svg', width: 13.3, height: 13.3), const SizedBox(width: 4), - Text('${recipe?.calories.toStringAsFixed(0) ?? '0'} ккал', style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), + Text('${(recipe?.calories ?? 0).toStringAsFixed(0)} ккал', style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)), ], ), const SizedBox(width: 12), diff --git a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart index 41549f55..4b7315ab 100644 --- a/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart +++ b/lib/features/create_diet/presentation/cubit/create_diet_cubit.dart @@ -199,11 +199,17 @@ class CreateDietCubit extends Cubit { } } + + static const int _maxImageBytesForBase64 = 5 * 1024 * 1024; + Future getDietForPreview() async { if (!_isFormValidForDraft()) return null; final authorProfile = await profileRepository.getMyProfile(); final imageBytes = (await state.dietData.image?.readAsBytes()) ?? []; - String image = base64.encode(imageBytes); + String image = ''; + if (imageBytes.isNotEmpty && imageBytes.length <= _maxImageBytesForBase64) { + image = base64.encode(imageBytes); + } if (image.isEmpty && state.dietData.imageUrl != null) { image = state.dietData.imageUrl ?? ''; } diff --git a/lib/features/create_diet/presentation/diet_preview_screen.dart b/lib/features/create_diet/presentation/diet_preview_screen.dart index c7482cd4..fd5a2f3b 100644 --- a/lib/features/create_diet/presentation/diet_preview_screen.dart +++ b/lib/features/create_diet/presentation/diet_preview_screen.dart @@ -44,6 +44,12 @@ class _DietPreviewScreenState extends State { } } + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final s = S.of(context); diff --git a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart index 019cc487..0799fc77 100644 --- a/lib/features/my_diets/presentation/widgets/my_diet_widget.dart +++ b/lib/features/my_diets/presentation/widgets/my_diet_widget.dart @@ -61,8 +61,10 @@ class _MyDietWidgetState extends State { child: SuccessPublishedDietOrPlanBottomSheet(title: widget.diet.name), ); await context.read().loadAllDiets(); + break; case 422: showErrorToast('Пожалуйста, заполните все обязательные поля для публикации'); + break; default: showErrorToast('Произошла ошибка. Попробуйте ещё раз через некоторое время'); } diff --git a/test/features/create_diet/presentation/cubit/create_diet_cubit_test.dart b/test/features/create_diet/presentation/cubit/create_diet_cubit_test.dart index 5feac64f..bd8a2d34 100644 --- a/test/features/create_diet/presentation/cubit/create_diet_cubit_test.dart +++ b/test/features/create_diet/presentation/cubit/create_diet_cubit_test.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:eatvio_app/core/app_config.dart'; import 'package:eatvio_app/core/app_initializer.dart'; import 'package:eatvio_app/core/locator_service.dart'; @@ -13,7 +15,6 @@ import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; - import 'create_diet_cubit_test.mocks.dart'; @GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec()]) @@ -316,6 +317,88 @@ void main() { expect(result, 'error'); }); + test('updateDiet returns error and resets isDietBeingEdited when repository throws', () async { + cubit.setName('Test Diet'); + cubit.setDetails('Test Description'); + cubit.setDuration(0); + cubit.setDietState(DietState.active); + const portion = DailySchedule( + name: 'Apple', + portion: Portion.grams, + quantity: 100, + dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0), + productNutritionalValueId: 1, + ); + cubit.addPortion(dayIndex: 0, mealIndex: 0, portion: portion); + // No image so we reach repository.updateDiet without compression + + when(dietRepository.updateDiet( + dietId: 1, + state: anyNamed('state'), + name: anyNamed('name'), + duration: anyNamed('duration'), + meatType: anyNamed('meatType'), + carbType: anyNamed('carbType'), + vegType: anyNamed('vegType'), + lightType: anyNamed('lightType'), + details: anyNamed('details'), + image: anyNamed('image'), + dailySchedule: anyNamed('dailySchedule'), + )).thenThrow(Exception('network error')); + + final result = await cubit.updateDiet(1); + + expect(result, 'error'); + expect(cubit.state.isDietBeingEdited, false); + }); + + test('createDiet returns error when repository createDiet throws', () async { + cubit.setName('Test Diet'); + cubit.setDetails('Test Description'); + cubit.setDuration(0); + cubit.setDietState(DietState.active); + const portion = DailySchedule( + name: 'Apple', + portion: Portion.grams, + quantity: 100, + dailyNutrition: DailyNutrition(energy: 52.0, proteins: 0.3, fats: 0.2, carbohydrates: 14.0), + productNutritionalValueId: 1, + ); + for (var day = 0; day < 7; day++) { + cubit.addPortion(dayIndex: day, mealIndex: 0, portion: portion); + } + final tempDir = Directory.systemTemp; + final tempFile = File('${tempDir.path}/create_diet_test_image_${DateTime.now().millisecondsSinceEpoch}.png'); + final pngBytes = [ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, + 0xDE, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, + 0x44, 0xAE, 0x42, 0x60, 0x82, + ]; + await tempFile.writeAsBytes(pngBytes); + cubit.setImage(XFile(tempFile.path)); + when(dietRepository.createDiet( + name: anyNamed('name'), + duration: anyNamed('duration'), + state: anyNamed('state'), + meatType: anyNamed('meatType'), + carbType: anyNamed('carbType'), + vegType: anyNamed('vegType'), + lightType: anyNamed('lightType'), + details: anyNamed('details'), + image: anyNamed('image'), + dailySchedule: anyNamed('dailySchedule'), + )).thenThrow(Exception('server error')); + + final result = await cubit.createDiet(); + + expect(result, 'error'); + expect(cubit.state.isDietPublishing, false); + if (await tempFile.exists()) await tempFile.delete(); + }); + test('createDiet validates name length', () async { cubit.setName('Te'); // Less than 3 characters cubit.setDetails('Test Description'); diff --git a/test/features/create_diet/presentation/diet_preview_screen_smoke_test.dart b/test/features/create_diet/presentation/diet_preview_screen_smoke_test.dart index 391143c6..b9dbe004 100644 --- a/test/features/create_diet/presentation/diet_preview_screen_smoke_test.dart +++ b/test/features/create_diet/presentation/diet_preview_screen_smoke_test.dart @@ -139,5 +139,34 @@ void main() { expect(find.text('10'), findsOneWidget); // totalLikes }); }); + + testWidgets('renders diet preview with isImageInBase64 false (URL image)', (tester) async { + await mockNetworkImagesFor(() async { + final testDiet = Diet( + id: 1, + slug: 'test-diet', + name: 'Test Diet URL Image', + image: 'https://example.com/diet-cover.jpg', + duration: 7, + state: DietState.active, + type: const DietType(veg: true, light: false, carb: false, meat: false), + details: 'Test diet details', + dailySchedule: {}, + author: const Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'https://example.com/photo.jpg', authorIsMe: false), + totalLikes: 10, + totalViews: 100, + totalComments: 5, + wasLikedByMe: false, + isFavorite: false, + dailyNutrition: [], + ); + + await tester.pumpWidget(wrap(DietPreviewScreen(diet: testDiet, isImageInBase64: false))); + await tester.pumpAndSettle(); + + expect(find.byType(DietPreviewScreen), findsOneWidget); + expect(find.text('Test Diet URL Image'), findsOneWidget); + }); + }); }); } diff --git a/test/features/my_diets/presentation/cubit/my_diets_cubit_test.dart b/test/features/my_diets/presentation/cubit/my_diets_cubit_test.dart index bb990eec..ef4abc80 100644 --- a/test/features/my_diets/presentation/cubit/my_diets_cubit_test.dart +++ b/test/features/my_diets/presentation/cubit/my_diets_cubit_test.dart @@ -269,11 +269,11 @@ void main() { when(mockCreateDietRepository.updateDiet( dietId: 1, state: DietState.active, - )).thenAnswer((_) async => 200); + )).thenAnswer((_) async => 201); final result = await myDietsCubit!.publishDiet(1); - expect(result, 200); + expect(result, 201); verify(mockCreateDietRepository.updateDiet(dietId: 1, state: DietState.active)).called(1); });