diff --git a/pubspec.yaml b/pubspec.yaml index 414e37ab..6bb8b5b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -86,6 +86,7 @@ dev_dependencies: json_serializable: ^6.10.0 retrofit_generator: ^10.0.2 mockito: ^5.5.0 + network_image_mock: ^2.1.1 flutter_test: sdk: flutter diff --git a/test/features/feed/presentation/widgets/basic_widget_test.dart b/test/features/feed/presentation/widgets/basic_widget_test.dart index 8d024a28..16e18b6e 100644 --- a/test/features/feed/presentation/widgets/basic_widget_test.dart +++ b/test/features/feed/presentation/widgets/basic_widget_test.dart @@ -21,6 +21,8 @@ import 'package:go_router/go_router.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:visibility_detector/visibility_detector.dart'; import 'basic_widget_test.mocks.dart'; @@ -29,12 +31,13 @@ void main() { group('Feed Basic Widget Tests', () { late MockFeedRepository mockFeedRepository; late FeedCubit feedCubit; + VisibilityDetectorController.instance.updateInterval = Duration.zero; final testBlogPost = FeedBlogPostModel( id: 1, title: 'Test Post', description: 'Test description', - image: 'base64image', // Empty to avoid network calls in tests + image: '', // Empty - CachedNetworkImage will show errorWidget (SizedBox.shrink) state: 'published', author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: false), videoUrl: '', @@ -50,7 +53,7 @@ void main() { id: 2, title: 'Test Post 2', description: 'Test description 2', - image: 'base64image', // Empty to avoid network calls in tests + image: '', // Empty - CachedNetworkImage will show errorWidget (SizedBox.shrink) state: 'published', author: const Author(authorSlug: 'author-2', authorName: 'Author Name 2', authorProfileIcon: '', authorIsMe: false), videoUrl: '', @@ -251,26 +254,30 @@ void main() { }); testWidgets('should display BlogPosts widget with loaded state', (WidgetTester tester) async { - // Arrange - feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost, testBlogPost2], isLoading: false)); + await mockNetworkImagesFor(() async { + // Arrange + feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost, testBlogPost2], isLoading: false)); - final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); + final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); - // Act - await tester.pumpWidget(createBlogPostsTestWidget(widget)); - // Use pump() multiple times to allow error widgets to render without waiting for network - await tester.pump(); - await tester.pump(const Duration(milliseconds: 50)); - await tester.pump(const Duration(milliseconds: 50)); + // Act + await tester.pumpWidget(createBlogPostsTestWidget(widget)); + // Pump frames to allow widgets to render + // Note: VisibilityDetector creates a 500ms timer, but we don't need to wait for it + // to complete to verify the widget structure + await tester.pump(); - // Assert - Verify state and widget structure - expect(feedCubit.state, isA()); - final loadedState = feedCubit.state as FeedStateLoaded; - expect(loadedState.feedBlogPostList.length, 2); - expect(find.byType(BlogPosts), findsOneWidget); - expect(find.byType(RefreshIndicator), findsOneWidget); - // ListView should be present in loaded state - expect(find.byType(ListView), findsOneWidget); + // Assert - Verify state and widget structure + expect(feedCubit.state, isA()); + final loadedState = feedCubit.state as FeedStateLoaded; + expect(loadedState.feedBlogPostList.length, 2); + expect(find.byType(BlogPosts), findsOneWidget); + expect(find.byType(RefreshIndicator), findsOneWidget); + // ListView should be present in loaded state + expect(find.byType(ListView), findsOneWidget); + }); + // Pump one more time after mockNetworkImagesFor to allow any pending timers to complete + await tester.pump(const Duration(milliseconds: 600)); }); testWidgets('should display BlogPosts widget with error state', (WidgetTester tester) async { @@ -282,7 +289,6 @@ void main() { // Act await tester.pumpWidget(createBlogPostsTestWidget(widget)); await tester.pump(); - await tester.pump(const Duration(milliseconds: 50)); // Assert expect(feedCubit.state, isA()); @@ -300,7 +306,8 @@ void main() { // Act await tester.pumpWidget(createBlogPostsTestWidget(widget)); await tester.pump(); - await tester.pump(const Duration(milliseconds: 50)); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); // Assert expect(feedCubit.state, isA()); @@ -311,22 +318,23 @@ void main() { }); testWidgets('should display BlogPosts with loading indicator when isLoading is true', (WidgetTester tester) async { - // Arrange - feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: true)); + await mockNetworkImagesFor(() async { + // Arrange + feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: true)); - final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); + final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); - // Act - await tester.pumpWidget(createBlogPostsTestWidget(widget)); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 50)); + // Act + await tester.pumpWidget(createBlogPostsTestWidget(widget)); + await tester.pump(); - // Assert - expect(feedCubit.state, isA()); - final loadedState = feedCubit.state as FeedStateLoaded; - expect(loadedState.isLoading, true); - expect(find.byType(BlogPosts), findsOneWidget); - expect(find.byType(ListView), findsOneWidget); + // Assert + expect(feedCubit.state, isA()); + final loadedState = feedCubit.state as FeedStateLoaded; + expect(loadedState.isLoading, true); + expect(find.byType(BlogPosts), findsOneWidget); + expect(find.byType(ListView), findsOneWidget); + }); }); testWidgets('should handle chip selection with multiple chips', (WidgetTester tester) async { @@ -366,28 +374,29 @@ void main() { }); testWidgets('should handle BlogPosts refresh', (WidgetTester tester) async { - // Arrange - feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false)); + await mockNetworkImagesFor(() async { + // Arrange + feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false)); - when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer( - (_) async => FeedBlogPostSettingsModel( - feedBlogPostList: [testBlogPost, testBlogPost2], - meta: const Meta(currentPage: 1, lastPage: 1, total: 2, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), - ), - ); + when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer( + (_) async => FeedBlogPostSettingsModel( + feedBlogPostList: [testBlogPost, testBlogPost2], + meta: const Meta(currentPage: 1, lastPage: 1, total: 2, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ), + ); - final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); + final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); - await tester.pumpWidget(createBlogPostsTestWidget(widget)); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 50)); + await tester.pumpWidget(createBlogPostsTestWidget(widget)); + await tester.pumpAndSettle(); - // Act - Verify refresh indicator exists - expect(find.byType(RefreshIndicator), findsOneWidget); + // Act - Verify refresh indicator exists + expect(find.byType(RefreshIndicator), findsOneWidget); - // Assert - expect(feedCubit.state, isA()); - expect(find.byType(ListView), findsOneWidget); + // Assert + expect(feedCubit.state, isA()); + expect(find.byType(ListView), findsOneWidget); + }); }); testWidgets('should display ChipGroup with correct selected chip', (WidgetTester tester) async { @@ -406,40 +415,42 @@ void main() { }); testWidgets('should handle BlogPosts visibility detection', (WidgetTester tester) async { - // Arrange - feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false)); + await mockNetworkImagesFor(() async { + // Arrange + feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false)); - final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); + final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); - // Act - await tester.pumpWidget(createBlogPostsTestWidget(widget)); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 50)); + // Act + await tester.pumpWidget(createBlogPostsTestWidget(widget)); + await tester.pump(); - // Assert - expect(find.byType(BlogPosts), findsOneWidget); - expect(find.byType(RefreshIndicator), findsOneWidget); - expect(find.byType(ListView), findsOneWidget); - // Visibility detection is handled by VisibilityDetector internally + // Assert + expect(find.byType(BlogPosts), findsOneWidget); + expect(find.byType(RefreshIndicator), findsOneWidget); + expect(find.byType(ListView), findsOneWidget); + // Visibility detection is handled by VisibilityDetector internally + }); }); testWidgets('should display BlogPosts with multiple posts', (WidgetTester tester) async { - // Arrange - feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost, testBlogPost2], isLoading: false)); + await mockNetworkImagesFor(() async { + // Arrange + feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost, testBlogPost2], isLoading: false)); - final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); + final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); - // Act - await tester.pumpWidget(createBlogPostsTestWidget(widget)); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 50)); + // Act + await tester.pumpWidget(createBlogPostsTestWidget(widget)); + await tester.pump(); - // Assert - expect(feedCubit.state, isA()); - final loadedState = feedCubit.state as FeedStateLoaded; - expect(loadedState.feedBlogPostList.length, 2); - expect(find.byType(BlogPosts), findsOneWidget); - expect(find.byType(ListView), findsOneWidget); + // Assert + expect(feedCubit.state, isA()); + final loadedState = feedCubit.state as FeedStateLoaded; + expect(loadedState.feedBlogPostList.length, 2); + expect(find.byType(BlogPosts), findsOneWidget); + expect(find.byType(ListView), findsOneWidget); + }); }); testWidgets('should handle ChipGroup with empty label list', (WidgetTester tester) async { @@ -456,21 +467,22 @@ void main() { }); testWidgets('should display BlogPosts structure correctly', (WidgetTester tester) async { - // Arrange - feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false)); + await mockNetworkImagesFor(() async { + // Arrange + feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false)); - final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); + final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); - // Act - await tester.pumpWidget(createBlogPostsTestWidget(widget)); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 50)); + // Act + await tester.pumpWidget(createBlogPostsTestWidget(widget)); + await tester.pump(); - // Assert - expect(find.byType(BlogPosts), findsOneWidget); - expect(find.byType(RefreshIndicator), findsOneWidget); - expect(find.byType(ListView), findsOneWidget); - expect(feedCubit.state, isA()); + // Assert + expect(find.byType(BlogPosts), findsOneWidget); + expect(find.byType(RefreshIndicator), findsOneWidget); + expect(find.byType(ListView), findsOneWidget); + expect(feedCubit.state, isA()); + }); }); testWidgets('should handle chip selection callback', (WidgetTester tester) async { @@ -497,24 +509,25 @@ void main() { }); testWidgets('should display BlogPosts with scrollable list', (WidgetTester tester) async { - // Arrange - final posts = List.generate(10, (index) => testBlogPost.copyWith(id: index + 1)); - feedCubit.emit(FeedState.loaded(feedBlogPostList: posts, isLoading: false)); + await mockNetworkImagesFor(() async { + // Arrange + final posts = List.generate(10, (index) => testBlogPost.copyWith(id: index + 1)); + feedCubit.emit(FeedState.loaded(feedBlogPostList: posts, isLoading: false)); - final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); + final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); - // Act - await tester.pumpWidget(createBlogPostsTestWidget(widget)); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 50)); + // Act + await tester.pumpWidget(createBlogPostsTestWidget(widget)); + await tester.pump(); - // Assert - expect(find.byType(BlogPosts), findsOneWidget); - expect(find.byType(RefreshIndicator), findsOneWidget); - expect(find.byType(ListView), findsOneWidget); - expect(feedCubit.state, isA()); - final loadedState = feedCubit.state as FeedStateLoaded; - expect(loadedState.feedBlogPostList.length, 10); + // Assert + expect(find.byType(BlogPosts), findsOneWidget); + expect(find.byType(RefreshIndicator), findsOneWidget); + expect(find.byType(ListView), findsOneWidget); + expect(feedCubit.state, isA()); + final loadedState = feedCubit.state as FeedStateLoaded; + expect(loadedState.feedBlogPostList.length, 10); + }); }); }); } 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 new file mode 100644 index 00000000..e5e63672 --- /dev/null +++ b/test/features/my_diets/presentation/cubit/my_diets_cubit_test.dart @@ -0,0 +1,338 @@ +import 'package:eatvio_app/core/models/pagination_model.dart'; +import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'my_diets_cubit_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + group('MyDietsCubit', () { + MyDietsCubit? myDietsCubit; + late MockMyDietsRepository mockMyDietsRepository; + late MockCreateDietRepository mockCreateDietRepository; + + const testDiet = Diet( + id: 1, + slug: 'test-diet', + name: 'Test Diet', + image: 'https://someurl.com/img.jpg', + duration: 30, + state: DietState.active, + type: DietType(light: false, veg: false, carb: false, meat: false), + details: 'Test diet description', + dailySchedule: {}, + author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false), + totalLikes: 1, + totalViews: 1, + totalComments: 1, + wasLikedByMe: false, + isFavorite: false, + dailyNutrition: [], + ); + + final testDietsPagination = DietsPagination( + data: [testDiet], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + setUp(() { + mockMyDietsRepository = MockMyDietsRepository(); + mockCreateDietRepository = MockCreateDietRepository(); + }); + + tearDown(() async { + if (myDietsCubit != null && !myDietsCubit!.isClosed) { + await Future.delayed(const Duration(milliseconds: 100)); + myDietsCubit!.close(); + myDietsCubit = null; + } + }); + + test('начальное состояние - empty state', () { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + expect(myDietsCubit!.state.activeDiets, isEmpty); + expect(myDietsCubit!.state.draftDiets, isEmpty); + expect(myDietsCubit!.state.personalDiets, isEmpty); + expect(myDietsCubit!.state.isLoading, false); + expect(myDietsCubit!.state.selectedStateIndex, 0); + }); + + test('loadAllDiets - загружает все диеты', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockMyDietsRepository.getDiets( + page: 1, + query: null, + state: 'active', + types: null, + duration: null, + sorting: null, + )).thenAnswer((_) async => testDietsPagination); + + when(mockMyDietsRepository.getDiets( + page: 1, + query: null, + state: 'drafted', + types: null, + duration: null, + sorting: null, + )).thenAnswer((_) async => testDietsPagination); + + when(mockMyDietsRepository.getDiets( + page: 1, + query: null, + state: 'personal', + types: null, + duration: null, + sorting: null, + )).thenAnswer((_) async => testDietsPagination); + + await myDietsCubit!.loadAllDiets(); + await Future.delayed(const Duration(milliseconds: 200)); + + expect(myDietsCubit!.state.activeDiets.length, 1); + expect(myDietsCubit!.state.draftDiets.length, 1); + expect(myDietsCubit!.state.personalDiets.length, 1); + expect(myDietsCubit!.state.isLoading, false); + }); + + test('loadCurrentDiets - загружает активные диеты', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockMyDietsRepository.getDiets( + page: 1, + query: null, + state: 'active', + types: null, + duration: null, + sorting: null, + )).thenAnswer((_) async => testDietsPagination); + + await myDietsCubit!.loadCurrentDiets(); + await Future.delayed(const Duration(milliseconds: 200)); + + expect(myDietsCubit!.state.activeDiets.length, 1); + }); + + test('changeSelectedStateIndex - меняет выбранный индекс состояния', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + myDietsCubit!.changeSelectedStateIndex(1); + expect(myDietsCubit!.state.selectedStateIndex, 1); + + // Wait for debounce to complete + await Future.delayed(const Duration(milliseconds: 1200)); + + myDietsCubit!.changeSelectedStateIndex(2); + expect(myDietsCubit!.state.selectedStateIndex, 2); + + // Wait for debounce to complete before tearDown + await Future.delayed(const Duration(milliseconds: 1200)); + }); + + test('changeTypeFilter - меняет фильтр типа для активных диет', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockMyDietsRepository.getDiets( + page: 1, + query: null, + state: 'active', + types: 'light', + duration: null, + sorting: null, + )).thenAnswer((_) async => testDietsPagination); + + myDietsCubit!.changeTypeFilter(1); + await Future.delayed(const Duration(milliseconds: 1200)); + + expect(myDietsCubit!.state.activeFilterState.typeIndex, 1); + }); + + test('changeSortingType - меняет тип сортировки', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockMyDietsRepository.getDiets( + page: 1, + query: null, + state: 'active', + types: null, + duration: null, + sorting: 'chosen_diets_count:desc', + )).thenAnswer((_) async => testDietsPagination); + + myDietsCubit!.changeSortingType(1); + await Future.delayed(const Duration(milliseconds: 1200)); + + expect(myDietsCubit!.state.activeFilterState.sortingIndex, 1); + }); + + test('changeDurationFilter - меняет фильтр длительности', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockMyDietsRepository.getDiets( + page: 1, + query: null, + state: 'active', + types: null, + duration: 7, + sorting: null, + )).thenAnswer((_) async => testDietsPagination); + + myDietsCubit!.changeDurationFilter(1); + await Future.delayed(const Duration(milliseconds: 1200)); + + expect(myDietsCubit!.state.activeFilterState.durationIndex, 1); + }); + + test('deleteDiet - удаляет диету успешно', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockMyDietsRepository.deleteDiet(1)).thenAnswer((_) async => Future.value()); + + final result = await myDietsCubit!.deleteDiet(1); + + expect(result, true); + verify(mockMyDietsRepository.deleteDiet(1)).called(1); + }); + + test('deleteDiet - обрабатывает ошибку при удалении', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockMyDietsRepository.deleteDiet(1)).thenThrow(Exception('Network error')); + + final result = await myDietsCubit!.deleteDiet(1); + + expect(result, false); + expect(myDietsCubit!.state.error, isNotNull); + }); + + test('publishDiet - публикует диету успешно', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockCreateDietRepository.updateDiet( + dietId: 1, + state: DietState.active, + )).thenAnswer((_) async => Future.value()); + + final result = await myDietsCubit!.publishDiet(1); + + expect(result, true); + verify(mockCreateDietRepository.updateDiet( + dietId: 1, + state: DietState.active, + )).called(1); + }); + + test('publishDiet - обрабатывает ошибку при публикации', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockCreateDietRepository.updateDiet( + dietId: 1, + state: DietState.active, + )).thenThrow(Exception('Network error')); + + final result = await myDietsCubit!.publishDiet(1); + + expect(result, false); + }); + + test('unpublishDiet - снимает с публикации диету успешно', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockCreateDietRepository.updateDiet( + dietId: 1, + state: DietState.draft, + )).thenAnswer((_) async => Future.value()); + + final result = await myDietsCubit!.unpublishDiet(1); + + expect(result, true); + verify(mockCreateDietRepository.updateDiet( + dietId: 1, + state: DietState.draft, + )).called(1); + }); + + test('unpublishDiet - обрабатывает ошибку при снятии с публикации', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockCreateDietRepository.updateDiet( + dietId: 1, + state: DietState.draft, + )).thenThrow(Exception('Network error')); + + final result = await myDietsCubit!.unpublishDiet(1); + + expect(result, false); + }); + + test('_loadActiveDiets - обрабатывает ошибку', () async { + myDietsCubit = MyDietsCubit( + myDietsRepository: mockMyDietsRepository, + createDietRepository: mockCreateDietRepository, + ); + + when(mockMyDietsRepository.getDiets( + page: 1, + query: null, + state: 'active', + types: null, + duration: null, + sorting: null, + )).thenThrow(Exception('Network error')); + + await myDietsCubit!.loadCurrentDiets(); + await Future.delayed(const Duration(milliseconds: 200)); + + expect(myDietsCubit!.state.activeDietsError, isNotNull); + }); + }); +} + diff --git a/test/features/my_diets/presentation/widgets/basic_widget_test.dart b/test/features/my_diets/presentation/widgets/basic_widget_test.dart new file mode 100644 index 00000000..9afb6818 --- /dev/null +++ b/test/features/my_diets/presentation/widgets/basic_widget_test.dart @@ -0,0 +1,267 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/animated_option_button.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; +import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_state.dart'; +import 'package:eatvio_app/features/my_diets/presentation/widgets/my_diet_widget.dart'; +import 'package:eatvio_app/features/my_diets/presentation/widgets/my_diets_tab.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +import 'basic_widget_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('MyDiets Basic Widget Tests', () { + late MockMyDietsRepository mockMyDietsRepository; + late MockCreateDietRepository mockCreateDietRepository; + late MyDietsCubit myDietsCubit; + + const testDiet = Diet( + id: 1, + slug: 'test-diet', + name: 'Test Diet', + image: 'test-image.jpg', + duration: 30, + state: DietState.active, + type: DietType(light: false, veg: false, carb: false, meat: false), + details: 'Test diet description', + dailySchedule: {}, + author: Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false), + totalLikes: 1, + totalViews: 1, + totalComments: 1, + wasLikedByMe: false, + isFavorite: false, + dailyNutrition: [], + ); + + setUp(() async { + await sl.reset(); + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + mockMyDietsRepository = MockMyDietsRepository(); + mockCreateDietRepository = MockCreateDietRepository(); + myDietsCubit = MyDietsCubit(myDietsRepository: mockMyDietsRepository, createDietRepository: mockCreateDietRepository); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + tearDown(() async { + await Future.delayed(const Duration(milliseconds: 100)); + if (!myDietsCubit.isClosed) { + myDietsCubit.close(); + } + }); + + Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) { + final widget = BlocProvider( + create: (context) => myDietsCubit, + child: Portal( + child: wrapInScaffold ? Scaffold(body: SingleChildScrollView(child: child)) : SizedBox(width: 800, height: 600, child: child), + ), + ); + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: widget, + ); + } + + testWidgets('should create MyDietsCubit successfully', (WidgetTester tester) async { + // Arrange & Act + final cubit = MyDietsCubit(myDietsRepository: mockMyDietsRepository, createDietRepository: mockCreateDietRepository); + + // Assert + expect(cubit, isA()); + expect(cubit.state.activeDiets, isEmpty); + + cubit.close(); + }); + + testWidgets('should display MyDietsTab with all tabs', (WidgetTester tester) async { + // Arrange + myDietsCubit.emit(MyDietsState(selectedStateIndex: 0)); + const widget = MyDietsTab(); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(MyDietsTab), findsOneWidget); + expect(find.byType(AnimatedOptionButton), findsNWidgets(3)); + }); + + testWidgets('should handle tab selection in MyDietsTab', (WidgetTester tester) async { + // Arrange + myDietsCubit.emit(MyDietsState(selectedStateIndex: 0)); + const widget = MyDietsTab(); + + await tester.pumpWidget(createTestWidget(widget)); + await tester.pumpAndSettle(); + + // Act - Find and tap the second tab + final tabButtons = find.byType(AnimatedOptionButton); + expect(tabButtons, findsNWidgets(3)); + + // Tap the second tab (index 1) + await tester.tap(tabButtons.at(1)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 1200)); + + // Assert + expect(myDietsCubit.state.selectedStateIndex, 1); + }); + + testWidgets('should display MyDietWidget with diet name', (WidgetTester tester) async { + // Arrange + myDietsCubit.emit(MyDietsState(selectedStateIndex: 0)); + const widget = MyDietWidget(diet: testDiet); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.text('Test Diet'), findsOneWidget); + expect(find.byType(MyDietWidget), findsOneWidget); + }); + + testWidgets('should display MyDietWidget with description when details are present', (WidgetTester tester) async { + // Arrange + myDietsCubit.emit(MyDietsState(selectedStateIndex: 0)); + const widget = MyDietWidget(diet: testDiet); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.text('Test Diet'), findsOneWidget); + expect(find.byType(MyDietWidget), findsOneWidget); + }); + + testWidgets('should display MyDietWidget buttons for active state', (WidgetTester tester) async { + // Arrange + myDietsCubit.emit(MyDietsState(selectedStateIndex: 0)); + const widget = MyDietWidget(diet: testDiet); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyDietWidget), findsOneWidget); + expect(find.byType(CustomOutlinedButton), findsWidgets); + }); + + testWidgets('should display MyDietWidget buttons for draft state', (WidgetTester tester) async { + // Arrange + myDietsCubit.emit(MyDietsState(selectedStateIndex: 1)); + const widget = MyDietWidget(diet: testDiet); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyDietWidget), findsOneWidget); + expect(find.byType(CustomOutlinedButton), findsWidgets); + }); + + testWidgets('should display MyDietWidget buttons for personal state', (WidgetTester tester) async { + // Arrange + myDietsCubit.emit(MyDietsState(selectedStateIndex: 2)); + const widget = MyDietWidget(diet: testDiet); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyDietWidget), findsOneWidget); + expect(find.byType(CustomOutlinedButton), findsWidgets); + }); + + testWidgets('should display MyDietsTab with correct selected tab', (WidgetTester tester) async { + // Arrange + myDietsCubit.emit(MyDietsState(selectedStateIndex: 1)); + const widget = MyDietsTab(); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(MyDietsTab), findsOneWidget); + expect(find.byType(AnimatedOptionButton), findsNWidgets(3)); + }); + + testWidgets('should handle MyDietsTab additionalOnTap callback', (WidgetTester tester) async { + // Arrange + bool callbackCalled = false; + myDietsCubit.emit(MyDietsState(selectedStateIndex: 0)); + final widget = MyDietsTab( + additionalOnTap: () { + callbackCalled = true; + }, + ); + + await tester.pumpWidget(createTestWidget(widget)); + await tester.pumpAndSettle(); + + // Act + final tabButtons = find.byType(AnimatedOptionButton); + await tester.tap(tabButtons.at(0)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 1200)); + + // Assert + expect(callbackCalled, true); + }); + + testWidgets('should display MyDietWidget structure correctly', (WidgetTester tester) async { + // Arrange + myDietsCubit.emit(MyDietsState(selectedStateIndex: 0)); + const widget = MyDietWidget(diet: testDiet); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyDietWidget), findsOneWidget); + expect(find.text('Test Diet'), findsOneWidget); + expect(find.byType(CustomOutlinedButton), findsWidgets); + }); + }); +} diff --git a/test/features/my_diets/repositories/my_diets_repository_test.dart b/test/features/my_diets/repositories/my_diets_repository_test.dart new file mode 100644 index 00000000..3454b72f --- /dev/null +++ b/test/features/my_diets/repositories/my_diets_repository_test.dart @@ -0,0 +1,213 @@ +import 'package:eatvio_app/core/models/pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'my_diets_repository_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MyDietsRepository', () { + late MyDietsRepository myDietsRepository; + late MockMyDietsService mockMyDietsService; + + setUp(() { + mockMyDietsService = MockMyDietsService(); + myDietsRepository = MyDietsRepository(myDietsService: mockMyDietsService); + }); + + test('getDiets returns pagination with all parameters', () async { + const page = 1; + const query = 'test diet'; + const state = 'active'; + const types = 'light,veg'; + const duration = 30; + const sorting = 'name'; + final expected = DietsPagination( + data: [ + Diet( + id: 1, + slug: 'test-diet', + name: 'Test Diet', + image: 'https://someurl.com/img.jpg', + duration: duration, + state: DietState.active, + type: const DietType(light: true, veg: false, carb: false, meat: false), + details: 'Test diet description', + dailySchedule: {}, + author: const Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false), + totalLikes: 1, + totalViews: 1, + totalComments: 1, + wasLikedByMe: false, + isFavorite: false, + dailyNutrition: [], + ), + ], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when(mockMyDietsService.getDiets( + page: page, + query: query, + state: state, + types: types, + duration: duration, + sorting: sorting, + )).thenAnswer((_) async => expected); + + final result = await myDietsRepository.getDiets( + page: page, + query: query, + state: state, + types: types, + duration: duration, + sorting: sorting, + ); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.name, 'Test Diet'); + verify(mockMyDietsService.getDiets( + page: page, + query: query, + state: state, + types: types, + duration: duration, + sorting: sorting, + )).called(1); + }); + + test('getDiets returns pagination with default parameters', () async { + final expected = DietsPagination( + data: [], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 1, lastPage: 1, total: 0, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when(mockMyDietsService.getDiets( + page: 1, + query: null, + state: null, + types: null, + duration: null, + sorting: null, + )).thenAnswer((_) async => expected); + + final result = await myDietsRepository.getDiets(); + + expect(result, isA()); + expect(result.data.length, 0); + verify(mockMyDietsService.getDiets( + page: 1, + query: null, + state: null, + types: null, + duration: null, + sorting: null, + )).called(1); + }); + + test('getDiets returns pagination with partial parameters', () async { + const page = 2; + const query = 'wellness'; + const duration = 21; + final expected = DietsPagination( + data: [ + Diet( + id: 2, + slug: 'another-diet', + name: 'Another Diet', + image: 'https://someurl.com/img2.jpg', + duration: duration, + state: DietState.draft, + type: const DietType(light: false, veg: true, carb: false, meat: false), + details: '', + dailySchedule: {}, + author: const Author(authorSlug: 'author-slug', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: true), + totalLikes: 5, + totalViews: 10, + totalComments: 2, + wasLikedByMe: true, + isFavorite: true, + dailyNutrition: [], + ), + ], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 2, lastPage: 2, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when(mockMyDietsService.getDiets( + page: page, + query: query, + state: null, + types: null, + duration: duration, + sorting: null, + )).thenAnswer((_) async => expected); + + final result = await myDietsRepository.getDiets( + page: page, + query: query, + duration: duration, + ); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.id, 2); + verify(mockMyDietsService.getDiets( + page: page, + query: query, + state: null, + types: null, + duration: duration, + sorting: null, + )).called(1); + }); + + test('deleteDiet deletes diet by id', () async { + const dietId = 1; + + when(mockMyDietsService.deleteDiet(dietId: dietId)).thenAnswer((_) async => Future.value()); + + await myDietsRepository.deleteDiet(dietId); + + verify(mockMyDietsService.deleteDiet(dietId: dietId)).called(1); + }); + + test('deleteDiet rethrows exception on error', () async { + const dietId = 1; + + when(mockMyDietsService.deleteDiet(dietId: dietId)).thenThrow(Exception('Network error')); + + expect(() => myDietsRepository.deleteDiet(dietId), throwsException); + verify(mockMyDietsService.deleteDiet(dietId: dietId)).called(1); + }); + + test('getDiets rethrows exception on error', () async { + when(mockMyDietsService.getDiets( + page: 1, + query: null, + state: null, + types: null, + duration: null, + sorting: null, + )).thenThrow(Exception('Network error')); + + expect(() => myDietsRepository.getDiets(), throwsException); + verify(mockMyDietsService.getDiets( + page: 1, + query: null, + state: null, + types: null, + duration: null, + sorting: null, + )).called(1); + }); + }); +} + diff --git a/test/features/my_diets/services/my_diets_service_test.dart b/test/features/my_diets/services/my_diets_service_test.dart new file mode 100644 index 00000000..2f7030b5 --- /dev/null +++ b/test/features/my_diets/services/my_diets_service_test.dart @@ -0,0 +1,220 @@ +import 'package:eatvio_app/core/models/pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'my_diets_service_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MyDietsService', () { + late MockMyDietsService mockMyDietsService; + + setUp(() { + mockMyDietsService = MockMyDietsService(); + }); + + test('getDiets returns pagination with all parameters', () async { + const page = 1; + const query = 'test diet'; + const state = 'active'; + const types = 'light,veg'; + const duration = 30; + const sorting = 'name'; + final expected = DietsPagination( + data: [ + Diet( + id: 1, + slug: 'test-diet', + name: 'Test Diet', + image: 'https://someurl.com/img.jpg', + duration: duration, + state: DietState.active, + type: const DietType(light: true, veg: false, carb: false, meat: false), + details: 'Test diet description', + dailySchedule: {}, + author: const Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false), + totalLikes: 1, + totalViews: 1, + totalComments: 1, + wasLikedByMe: false, + isFavorite: false, + dailyNutrition: [], + ), + ], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when( + mockMyDietsService.getDiets(page: page, query: query, state: state, types: types, duration: duration, sorting: sorting), + ).thenAnswer((_) async => expected); + + final result = await mockMyDietsService.getDiets(page: page, query: query, state: state, types: types, duration: duration, sorting: sorting); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.name, 'Test Diet'); + verify(mockMyDietsService.getDiets(page: page, query: query, state: state, types: types, duration: duration, sorting: sorting)).called(1); + }); + + test('getDiets returns pagination with minimal parameters', () async { + const page = 1; + final expected = DietsPagination( + data: [ + Diet( + id: 2, + slug: 'another-diet', + name: 'Another Diet', + image: 'https://someurl.com/img2.jpg', + duration: 14, + state: DietState.draft, + type: const DietType(light: false, veg: true, carb: false, meat: false), + details: '', + dailySchedule: {}, + author: const Author(authorSlug: 'author-slug', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: true), + totalLikes: 5, + totalViews: 10, + totalComments: 2, + wasLikedByMe: true, + isFavorite: true, + dailyNutrition: [], + ), + ], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when(mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: null)).thenAnswer((_) async => expected); + + final result = await mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: null); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.id, 2); + verify(mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: null)).called(1); + }); + + test('getDiets returns pagination with partial parameters', () async { + const page = 2; + const query = 'wellness'; + const state = 'draft'; + const duration = 21; + final expected = DietsPagination( + data: [], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 2, lastPage: 2, total: 0, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when( + mockMyDietsService.getDiets(page: page, query: query, state: state, types: null, duration: duration, sorting: null), + ).thenAnswer((_) async => expected); + + final result = await mockMyDietsService.getDiets(page: page, query: query, state: state, types: null, duration: duration, sorting: null); + + expect(result, isA()); + expect(result.data.length, 0); + expect(result.meta.currentPage, 2); + verify(mockMyDietsService.getDiets(page: page, query: query, state: state, types: null, duration: duration, sorting: null)).called(1); + }); + + test('getDiets returns pagination with state filter', () async { + const page = 1; + const state = 'active'; + final expected = DietsPagination( + data: [ + Diet( + id: 3, + slug: 'active-diet', + name: 'Active Diet', + image: 'https://someurl.com/img3.jpg', + duration: 7, + state: DietState.active, + type: const DietType(light: true, veg: true, carb: false, meat: false), + details: 'Active diet description', + dailySchedule: {}, + author: const Author(authorSlug: 'author-2', authorName: 'Author 2', authorProfileIcon: '', authorIsMe: false), + totalLikes: 10, + totalViews: 20, + totalComments: 5, + wasLikedByMe: false, + isFavorite: false, + dailyNutrition: [], + ), + ], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when(mockMyDietsService.getDiets(page: page, query: null, state: state, types: null, duration: null, sorting: null)).thenAnswer((_) async => expected); + + final result = await mockMyDietsService.getDiets(page: page, query: null, state: state, types: null, duration: null, sorting: null); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.state, DietState.active); + verify(mockMyDietsService.getDiets(page: page, query: null, state: state, types: null, duration: null, sorting: null)).called(1); + }); + + test('getDiets returns pagination with sorting parameter', () async { + const page = 1; + const sorting = 'duration'; + final expected = DietsPagination( + data: [ + Diet( + id: 4, + slug: 'sorted-diet', + name: 'Sorted Diet', + image: 'https://someurl.com/img4.jpg', + duration: 60, + state: DietState.personal, + type: const DietType(light: false, veg: false, carb: true, meat: true), + details: 'Sorted diet description', + dailySchedule: {}, + author: const Author(authorSlug: 'author-3', authorName: 'Author 3', authorProfileIcon: '', authorIsMe: false), + totalLikes: 15, + totalViews: 30, + totalComments: 8, + wasLikedByMe: true, + isFavorite: true, + dailyNutrition: [], + ), + ], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when(mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: sorting)).thenAnswer((_) async => expected); + + final result = await mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: sorting); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.duration, 60); + verify(mockMyDietsService.getDiets(page: page, query: null, state: null, types: null, duration: null, sorting: sorting)).called(1); + }); + + test('deleteDiet deletes diet by id', () async { + const dietId = 1; + + when(mockMyDietsService.deleteDiet(dietId: dietId)).thenAnswer((_) async => Future.value()); + + await mockMyDietsService.deleteDiet(dietId: dietId); + + verify(mockMyDietsService.deleteDiet(dietId: dietId)).called(1); + }); + + test('deleteDiet deletes diet with different id', () async { + const dietId = 42; + + when(mockMyDietsService.deleteDiet(dietId: dietId)).thenAnswer((_) async => Future.value()); + + await mockMyDietsService.deleteDiet(dietId: dietId); + + verify(mockMyDietsService.deleteDiet(dietId: dietId)).called(1); + }); + }); +} + diff --git a/test/features/my_plans/presentation/cubit/my_plans_cubit_test.dart b/test/features/my_plans/presentation/cubit/my_plans_cubit_test.dart new file mode 100644 index 00000000..215d4d6c --- /dev/null +++ b/test/features/my_plans/presentation/cubit/my_plans_cubit_test.dart @@ -0,0 +1,256 @@ +import 'package:eatvio_app/core/models/pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'my_plans_cubit_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MyPlansCubit', () { + MyPlansCubit? myPlansCubit; + late MockMyPlansRepository mockMyPlansRepository; + + final testPlan = PlanModel( + id: 1, + slug: 'test-plan', + name: 'Test Plan', + image: 'https://someurl.com/img.jpg', + duration: 30, + state: 'active', + type: const PlanType(fit: true, slim: false, behavior: false, wellness: false), + details: 'Test plan description', + packetsCount: 1, + author: const Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false), + totalLikes: 1, + totalViews: 1, + totalComments: 1, + isFavorite: false, + minPriceOfOptions: 100.0, + maxPriceOfOptions: 500.0, + wasLikedByMe: false, + ); + + final testPlansPagination = PlanPaginationModel( + data: [testPlan], + links: const Links(first: '', last: '', prev: null, next: null), + meta: Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null, totalMinPrice: 100, totalMaxPrice: 500), + ); + + setUp(() { + mockMyPlansRepository = MockMyPlansRepository(); + }); + + tearDown(() async { + if (myPlansCubit != null && !myPlansCubit!.isClosed) { + await Future.delayed(const Duration(milliseconds: 100)); + myPlansCubit!.close(); + myPlansCubit = null; + } + }); + + test('начальное состояние - empty state', () { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + expect(myPlansCubit!.state.activePlans, isEmpty); + expect(myPlansCubit!.state.draftPlans, isEmpty); + expect(myPlansCubit!.state.isLoading, false); + expect(myPlansCubit!.state.selectedStateIndex, 0); + }); + + test('loadAllPlans - загружает все планы', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).thenAnswer((_) async => testPlansPagination); + + await myPlansCubit!.loadAllPlans(); + await Future.delayed(const Duration(milliseconds: 200)); + + expect(myPlansCubit!.state.activePlans.length, 1); + expect(myPlansCubit!.state.draftPlans.length, 1); + expect(myPlansCubit!.state.isLoading, false); + }); + + test('loadPlans - загружает активные планы', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).thenAnswer((_) async => testPlansPagination); + + await myPlansCubit!.loadPlans(); + await Future.delayed(const Duration(milliseconds: 200)); + + expect(myPlansCubit!.state.activePlans.length, 1); + }); + + test('changeSelectedStateIndex - меняет выбранный индекс состояния', () { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + myPlansCubit!.changeSelectedStateIndex(1); + expect(myPlansCubit!.state.selectedStateIndex, 1); + + myPlansCubit!.changeSelectedStateIndex(0); + expect(myPlansCubit!.state.selectedStateIndex, 0); + }); + + test('changeTypeFilter - меняет фильтр типа для активных планов', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.getPlans( + page: 1, + query: null, + types: 'fit', + duration: null, + minPrice: null, + maxPrice: null, + )).thenAnswer((_) async => testPlansPagination); + + myPlansCubit!.changeTypeFilter(1); + await Future.delayed(const Duration(milliseconds: 1200)); + + expect(myPlansCubit!.state.activeFilterState.typeIndex, 1); + }); + + test('changeDurationFilter - меняет фильтр длительности', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.getPlans( + page: 1, + query: null, + types: null, + duration: 7, + minPrice: null, + maxPrice: null, + )).thenAnswer((_) async => testPlansPagination); + + myPlansCubit!.changeDurationFilter(1); + await Future.delayed(const Duration(milliseconds: 1200)); + + expect(myPlansCubit!.state.activeFilterState.durationIndex, 1); + }); + + test('changePriceFilter - меняет фильтр цены', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: 100, + maxPrice: 500, + )).thenAnswer((_) async => testPlansPagination); + + myPlansCubit!.changePriceFilter(100, 500); + await Future.delayed(const Duration(milliseconds: 1200)); + + expect(myPlansCubit!.state.activeFilterState.priceFilter.$1, 100); + expect(myPlansCubit!.state.activeFilterState.priceFilter.$2, 500); + }); + + test('deletePlan - удаляет план успешно', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.deletePlan('test-plan-slug')).thenAnswer((_) async => Future.value()); + when(mockMyPlansRepository.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).thenAnswer((_) async => testPlansPagination); + + final result = await myPlansCubit!.deletePlan('test-plan-slug'); + await Future.delayed(const Duration(milliseconds: 200)); + + expect(result, true); + verify(mockMyPlansRepository.deletePlan('test-plan-slug')).called(1); + }); + + test('deletePlan - обрабатывает ошибку при удалении', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.deletePlan('test-plan-slug')).thenThrow(Exception('Network error')); + + final result = await myPlansCubit!.deletePlan('test-plan-slug'); + + expect(result, false); + expect(myPlansCubit!.state.error, isNotNull); + }); + + test('unpublish - снимает с публикации план успешно', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.unpublish('test-plan-slug')).thenAnswer((_) async => Future.value()); + + final result = await myPlansCubit!.unpublish('test-plan-slug'); + + expect(result, true); + verify(mockMyPlansRepository.unpublish('test-plan-slug')).called(1); + }); + + test('unpublish - обрабатывает ошибку при снятии с публикации', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.unpublish('test-plan-slug')).thenThrow(Exception('Network error')); + + final result = await myPlansCubit!.unpublish('test-plan-slug'); + + expect(result, false); + }); + + test('_loadActivePlans - обрабатывает ошибку', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).thenThrow(Exception('Network error')); + + await myPlansCubit!.loadPlans(); + await Future.delayed(const Duration(milliseconds: 200)); + + expect(myPlansCubit!.state.error, isNotNull); + }); + + test('_loadActivePlans - устанавливает ценовой фильтр из мета', () async { + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + when(mockMyPlansRepository.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).thenAnswer((_) async => testPlansPagination); + + await myPlansCubit!.loadPlans(); + await Future.delayed(const Duration(milliseconds: 200)); + + expect(myPlansCubit!.state.minActivePlanPrice, 100); + expect(myPlansCubit!.state.maxActivePlanPrice, 500); + }); + }); +} + diff --git a/test/features/my_plans/presentation/widgets/basic_widget_test.dart b/test/features/my_plans/presentation/widgets/basic_widget_test.dart new file mode 100644 index 00000000..f79c7c58 --- /dev/null +++ b/test/features/my_plans/presentation/widgets/basic_widget_test.dart @@ -0,0 +1,195 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; +import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_state.dart'; +import 'package:eatvio_app/features/my_plans/presentation/widgets/my_plan_widget.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +import 'basic_widget_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MyPlans Basic Widget Tests', () { + late MockMyPlansRepository mockMyPlansRepository; + late MyPlansCubit myPlansCubit; + + final testPlan = PlanModel( + id: 1, + slug: 'test-plan', + name: 'Test Plan', + image: 'test-image.jpg', + duration: 30, + state: 'active', + type: const PlanType(fit: true, slim: false, behavior: false, wellness: false), + details: 'Test plan description', + packetsCount: 1, + author: const Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false), + totalLikes: 1, + totalViews: 1, + totalComments: 1, + isFavorite: false, + minPriceOfOptions: 100.0, + maxPriceOfOptions: 500.0, + wasLikedByMe: false, + packet1Options: null, + ); + + setUp(() async { + await sl.reset(); + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + mockMyPlansRepository = MockMyPlansRepository(); + myPlansCubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + tearDown(() async { + await Future.delayed(const Duration(milliseconds: 100)); + if (!myPlansCubit.isClosed) { + myPlansCubit.close(); + } + }); + + Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) { + final widget = BlocProvider( + create: (context) => myPlansCubit, + child: Portal( + child: wrapInScaffold ? Scaffold(body: SingleChildScrollView(child: child)) : SizedBox(width: 800, height: 600, child: child), + ), + ); + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: widget, + ); + } + + testWidgets('should create MyPlansCubit successfully', (WidgetTester tester) async { + // Arrange & Act + final cubit = MyPlansCubit(myPlansRepository: mockMyPlansRepository); + + // Assert + expect(cubit, isA()); + expect(cubit.state.activePlans, isEmpty); + + cubit.close(); + }); + + testWidgets('should display MyPlanWidget with plan name', (WidgetTester tester) async { + // Arrange + myPlansCubit.emit(MyPlansState(selectedStateIndex: 0)); + final widget = MyPlanWidget(plan: testPlan); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.text('Test Plan'), findsOneWidget); + expect(find.byType(MyPlanWidget), findsOneWidget); + }); + + testWidgets('should display MyPlanWidget with description when details are present', (WidgetTester tester) async { + // Arrange + myPlansCubit.emit(MyPlansState(selectedStateIndex: 0)); + final widget = MyPlanWidget(plan: testPlan); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.text('Test Plan'), findsOneWidget); + expect(find.byType(MyPlanWidget), findsOneWidget); + }); + + testWidgets('should display MyPlanWidget with price', (WidgetTester tester) async { + // Arrange + myPlansCubit.emit(MyPlansState(selectedStateIndex: 0)); + final widget = MyPlanWidget(plan: testPlan); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.text('Test Plan'), findsOneWidget); + expect(find.byType(MyPlanWidget), findsOneWidget); + }); + + testWidgets('should display MyPlanWidget buttons', (WidgetTester tester) async { + // Arrange + myPlansCubit.emit(MyPlansState(selectedStateIndex: 0)); + final widget = MyPlanWidget(plan: testPlan); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyPlanWidget), findsOneWidget); + expect(find.byType(CustomOutlinedButton), findsWidgets); + }); + + testWidgets('should display MyPlanWidget structure correctly', (WidgetTester tester) async { + // Arrange + myPlansCubit.emit(MyPlansState(selectedStateIndex: 0)); + final widget = MyPlanWidget(plan: testPlan); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyPlanWidget), findsOneWidget); + expect(find.text('Test Plan'), findsOneWidget); + expect(find.byType(CustomOutlinedButton), findsWidgets); + }); + + testWidgets('should display MyPlanWidget with empty details', (WidgetTester tester) async { + // Arrange + myPlansCubit.emit(MyPlansState(selectedStateIndex: 0)); + final planWithoutDetails = testPlan.copyWith(details: ''); + final widget = MyPlanWidget(plan: planWithoutDetails); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.text('Test Plan'), findsOneWidget); + expect(find.byType(MyPlanWidget), findsOneWidget); + }); + }); +} + diff --git a/test/features/my_plans/repositories/my_plans_repository_test.dart b/test/features/my_plans/repositories/my_plans_repository_test.dart new file mode 100644 index 00000000..0f22aca8 --- /dev/null +++ b/test/features/my_plans/repositories/my_plans_repository_test.dart @@ -0,0 +1,236 @@ +import 'package:eatvio_app/core/models/pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; +import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'my_plans_repository_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MyPlansRepository', () { + late MyPlansRepository myPlansRepository; + late MockMyPlansService mockMyPlansService; + + setUp(() { + mockMyPlansService = MockMyPlansService(); + myPlansRepository = MyPlansRepository(myPlansService: mockMyPlansService); + }); + + test('getPlans returns pagination with all parameters', () async { + const page = 1; + const query = 'test plan'; + const types = 'fit,slim'; + const duration = 30; + const minPrice = 100; + const maxPrice = 500; + final expected = PlanPaginationModel( + data: [ + PlanModel( + id: 1, + slug: 'test-plan', + name: 'Test Plan', + image: 'https://someurl.com/img.jpg', + duration: duration, + state: 'active', + type: const PlanType(fit: true, slim: false, behavior: false, wellness: false), + details: 'Test plan description', + packetsCount: 1, + author: const Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false), + totalLikes: 1, + totalViews: 1, + totalComments: 1, + isFavorite: false, + minPriceOfOptions: minPrice.toDouble(), + maxPriceOfOptions: maxPrice.toDouble(), + wasLikedByMe: false, + ), + ], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when(mockMyPlansService.getPlans( + page: page, + query: query, + types: types, + duration: duration, + minPrice: minPrice, + maxPrice: maxPrice, + )).thenAnswer((_) async => expected); + + final result = await myPlansRepository.getPlans( + page: page, + query: query, + types: types, + duration: duration, + minPrice: minPrice, + maxPrice: maxPrice, + ); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.name, 'Test Plan'); + verify(mockMyPlansService.getPlans( + page: page, + query: query, + types: types, + duration: duration, + minPrice: minPrice, + maxPrice: maxPrice, + )).called(1); + }); + + test('getPlans returns pagination with default parameters', () async { + final expected = PlanPaginationModel( + data: [ + PlanModel( + id: 2, + slug: 'another-plan', + name: 'Another Plan', + image: 'https://someurl.com/img2.jpg', + duration: 14, + state: 'draft', + type: const PlanType(fit: false, slim: true, behavior: false, wellness: false), + details: '', + packetsCount: 2, + author: const Author(authorSlug: 'author-slug', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: true), + totalLikes: 5, + totalViews: 10, + totalComments: 2, + isFavorite: true, + minPriceOfOptions: 200.0, + maxPriceOfOptions: 600.0, + wasLikedByMe: true, + ), + ], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when(mockMyPlansService.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).thenAnswer((_) async => expected); + + final result = await myPlansRepository.getPlans(); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.id, 2); + verify(mockMyPlansService.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).called(1); + }); + + test('getPlans returns pagination with partial parameters', () async { + const page = 2; + const query = 'wellness'; + const duration = 21; + final expected = PlanPaginationModel( + data: [], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 2, lastPage: 2, total: 0, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when(mockMyPlansService.getPlans( + page: page, + query: query, + types: null, + duration: duration, + minPrice: null, + maxPrice: null, + )).thenAnswer((_) async => expected); + + final result = await myPlansRepository.getPlans( + page: page, + query: query, + duration: duration, + ); + + expect(result, isA()); + expect(result.data.length, 0); + expect(result.meta.currentPage, 2); + verify(mockMyPlansService.getPlans( + page: page, + query: query, + types: null, + duration: duration, + minPrice: null, + maxPrice: null, + )).called(1); + }); + + test('deletePlan deletes plan by slug', () async { + const slug = 'test-plan-slug'; + + when(mockMyPlansService.deletePlan(slug: slug)).thenAnswer((_) async => Future.value()); + + await myPlansRepository.deletePlan(slug); + + verify(mockMyPlansService.deletePlan(slug: slug)).called(1); + }); + + test('unpublish unpublishes plan by slug', () async { + const slug = 'draft-plan-slug'; + + when(mockMyPlansService.unpublish(slug: slug)).thenAnswer((_) async => Future.value()); + + await myPlansRepository.unpublish(slug); + + verify(mockMyPlansService.unpublish(slug: slug)).called(1); + }); + + test('deletePlan rethrows exception on error', () async { + const slug = 'test-plan-slug'; + + when(mockMyPlansService.deletePlan(slug: slug)).thenThrow(Exception('Network error')); + + expect(() => myPlansRepository.deletePlan(slug), throwsException); + verify(mockMyPlansService.deletePlan(slug: slug)).called(1); + }); + + test('unpublish rethrows exception on error', () async { + const slug = 'draft-plan-slug'; + + when(mockMyPlansService.unpublish(slug: slug)).thenThrow(Exception('Network error')); + + expect(() => myPlansRepository.unpublish(slug), throwsException); + verify(mockMyPlansService.unpublish(slug: slug)).called(1); + }); + + test('getPlans rethrows exception on error', () async { + when(mockMyPlansService.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).thenThrow(Exception('Network error')); + + expect(() => myPlansRepository.getPlans(), throwsException); + verify(mockMyPlansService.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).called(1); + }); + }); +} + diff --git a/test/features/my_plans/services/my_plans_service_test.dart b/test/features/my_plans/services/my_plans_service_test.dart new file mode 100644 index 00000000..aeef0da5 --- /dev/null +++ b/test/features/my_plans/services/my_plans_service_test.dart @@ -0,0 +1,165 @@ +import 'package:eatvio_app/core/models/pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'my_plans_service_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MyPlansService', () { + late MockMyPlansService mockMyPlansService; + + setUp(() { + mockMyPlansService = MockMyPlansService(); + }); + + test('getPlans returns pagination with all parameters', () async { + const page = 1; + const query = 'test plan'; + const types = 'fit,slim'; + const duration = 30; + const minPrice = 100; + const maxPrice = 500; + final expected = PlanPaginationModel( + data: [ + PlanModel( + id: 1, + slug: 'test-plan', + name: 'Test Plan', + image: 'https://someurl.com/img.jpg', + duration: duration, + state: 'active', + type: const PlanType(fit: true, slim: false, behavior: false, wellness: false), + details: 'Test plan description', + packetsCount: 1, + author: const Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false), + totalLikes: 1, + totalViews: 1, + totalComments: 1, + isFavorite: false, + minPriceOfOptions: minPrice.toDouble(), + maxPriceOfOptions: maxPrice.toDouble(), + wasLikedByMe: false, + ), + ], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when( + mockMyPlansService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice), + ).thenAnswer((_) async => expected); + + final result = await mockMyPlansService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.name, 'Test Plan'); + verify(mockMyPlansService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice)).called(1); + }); + + test('getPlans returns pagination with minimal parameters', () async { + const page = 1; + const expected = PlanPaginationModel( + data: [ + PlanModel( + id: 2, + slug: 'another-plan', + name: 'Another Plan', + image: 'https://someurl.com/img2.jpg', + duration: 14, + state: 'draft', + type: PlanType(fit: false, slim: true, behavior: false, wellness: false), + details: '', + packetsCount: 2, + author: Author(authorSlug: 'author-slug', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: true), + totalLikes: 5, + totalViews: 10, + totalComments: 2, + isFavorite: true, + minPriceOfOptions: 200.0, + maxPriceOfOptions: 600.0, + wasLikedByMe: true, + ), + ], + links: Links(first: '', last: '', prev: null, next: null), + meta: Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when(mockMyPlansService.getPlans(page: page, query: null, types: null, duration: null, minPrice: null, maxPrice: null)).thenAnswer((_) async => expected); + + final result = await mockMyPlansService.getPlans(page: page, query: null, types: null, duration: null, minPrice: null, maxPrice: null); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.id, 2); + verify(mockMyPlansService.getPlans(page: page, query: null, types: null, duration: null, minPrice: null, maxPrice: null)).called(1); + }); + + test('getPlans returns pagination with partial parameters', () async { + const page = 2; + const query = 'wellness'; + const duration = 21; + const expected = PlanPaginationModel( + data: [], + links: Links(first: '', last: '', prev: null, next: null), + meta: Meta(currentPage: 2, lastPage: 2, total: 0, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), + ); + + when( + mockMyPlansService.getPlans(page: page, query: query, types: null, duration: duration, minPrice: null, maxPrice: null), + ).thenAnswer((_) async => expected); + + final result = await mockMyPlansService.getPlans(page: page, query: query, types: null, duration: duration, minPrice: null, maxPrice: null); + + expect(result, isA()); + expect(result.data.length, 0); + expect(result.meta.currentPage, 2); + verify(mockMyPlansService.getPlans(page: page, query: query, types: null, duration: duration, minPrice: null, maxPrice: null)).called(1); + }); + + test('deletePlan deletes plan by slug', () async { + const slug = 'test-plan-slug'; + + when(mockMyPlansService.deletePlan(slug: slug)).thenAnswer((_) async => Future.value()); + + await mockMyPlansService.deletePlan(slug: slug); + + verify(mockMyPlansService.deletePlan(slug: slug)).called(1); + }); + + test('deletePlan deletes plan with different slug', () async { + const slug = 'another-plan-slug'; + + when(mockMyPlansService.deletePlan(slug: slug)).thenAnswer((_) async => Future.value()); + + await mockMyPlansService.deletePlan(slug: slug); + + verify(mockMyPlansService.deletePlan(slug: slug)).called(1); + }); + + test('unpublish unpublishes plan by slug', () async { + const slug = 'draft-plan-slug'; + + when(mockMyPlansService.unpublish(slug: slug)).thenAnswer((_) async => Future.value()); + + await mockMyPlansService.unpublish(slug: slug); + + verify(mockMyPlansService.unpublish(slug: slug)).called(1); + }); + + test('unpublish unpublishes plan with different slug', () async { + const slug = 'published-plan-slug'; + + when(mockMyPlansService.unpublish(slug: slug)).thenAnswer((_) async => Future.value()); + + await mockMyPlansService.unpublish(slug: slug); + + verify(mockMyPlansService.unpublish(slug: slug)).called(1); + }); + }); +} diff --git a/test/features/recipes/presentation/cubit/my_recipes_cubit_test.dart b/test/features/recipes/presentation/cubit/my_recipes_cubit_test.dart new file mode 100644 index 00000000..1830435a --- /dev/null +++ b/test/features/recipes/presentation/cubit/my_recipes_cubit_test.dart @@ -0,0 +1,244 @@ +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart'; +import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; +import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'my_recipes_cubit_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + group('MyRecipesCubit', () { + MyRecipesCubit? myRecipesCubit; + late MockMyRecipesRepository mockMyRecipesRepository; + late MockAboutRecipeRepository mockAboutRecipeRepository; + + final testActiveRecipe = MyRecipeModel( + id: 1, + image: 'https://someurl.com/img.jpg', + state: 'active', + cookingTime: 30, + activeCookingTime: 20, + description: 'Test recipe description', + steps: [ + const Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1 description'), + ], + videoUrl: 'https://someurl.com/video.mp4', + title: 'Test Recipe', + slug: 'test-recipe', + calories: 250.0, + likesCount: 5, + viewsCount: 10, + ); + + final testArchivedRecipe = MyRecipeModel( + id: 2, + image: 'https://someurl.com/img2.jpg', + state: 'archived', + cookingTime: 45, + activeCookingTime: 30, + description: 'Archived recipe description', + steps: null, + videoUrl: null, + title: 'Archived Recipe', + slug: 'archived-recipe', + calories: 350.0, + likesCount: 8, + viewsCount: 15, + ); + + final testRecipe = Recipe( + id: 1, + image: 'https://someurl.com/img.jpg', + state: 'published', + cookingTime: 30, + activeCookingTime: 20, + ingredients: null, + description: 'Test recipe description', + steps: [ + const Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1 description'), + ], + videoUrl: 'https://someurl.com/video.mp4', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + title: 'Test Recipe', + type: 'recipe', + slug: 'test-recipe', + commentsCount: 5, + wasLikedByMe: false, + isFavorite: false, + calories: 250.0, + servings: 1, + likesCount: 5, + viewsCount: 10, + tags: [], + author: const Author(authorSlug: '', authorName: '', authorProfileIcon: '', authorIsMe: false), + ); + + setUp(() { + mockMyRecipesRepository = MockMyRecipesRepository(); + mockAboutRecipeRepository = MockAboutRecipeRepository(); + }); + + tearDown(() async { + if (myRecipesCubit != null && !myRecipesCubit!.isClosed) { + await Future.delayed(const Duration(milliseconds: 100)); + myRecipesCubit!.close(); + myRecipesCubit = null; + } + }); + + test('начальное состояние - loading', () { + myRecipesCubit = MyRecipesCubit( + myRecipesRepository: mockMyRecipesRepository, + aboutRecipeRepository: mockAboutRecipeRepository, + ); + expect(myRecipesCubit!.state, isA()); + }); + + test('getMyRecipes - загружает активные рецепты', () async { + myRecipesCubit = MyRecipesCubit( + myRecipesRepository: mockMyRecipesRepository, + aboutRecipeRepository: mockAboutRecipeRepository, + ); + + when(mockMyRecipesRepository.getMyRecipes()).thenAnswer( + (_) async => [testActiveRecipe, testArchivedRecipe], + ); + + await myRecipesCubit!.getMyRecipes(); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(myRecipesCubit!.state, isA()); + final loadedState = myRecipesCubit!.state as MyRecipesStateLoaded; + expect(loadedState.recipes.length, 1); + expect(loadedState.recipes.first.state, 'active'); + expect(loadedState.selectedState, 'active'); + }); + + test('getMyRecipes - обрабатывает ошибку', () async { + myRecipesCubit = MyRecipesCubit( + myRecipesRepository: mockMyRecipesRepository, + aboutRecipeRepository: mockAboutRecipeRepository, + ); + + when(mockMyRecipesRepository.getMyRecipes()).thenThrow(Exception('Network error')); + + await myRecipesCubit!.getMyRecipes(); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(myRecipesCubit!.state, isA()); + }); + + test('setActiveState - устанавливает активное состояние', () async { + myRecipesCubit = MyRecipesCubit( + myRecipesRepository: mockMyRecipesRepository, + aboutRecipeRepository: mockAboutRecipeRepository, + ); + + when(mockMyRecipesRepository.getMyRecipes()).thenAnswer( + (_) async => [testActiveRecipe, testArchivedRecipe], + ); + + await myRecipesCubit!.setActiveState(); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(myRecipesCubit!.state, isA()); + final loadedState = myRecipesCubit!.state as MyRecipesStateLoaded; + expect(loadedState.recipes.length, 1); + expect(loadedState.recipes.first.state, 'active'); + expect(loadedState.selectedState, 'active'); + }); + + test('setActiveState - обрабатывает ошибку', () async { + myRecipesCubit = MyRecipesCubit( + myRecipesRepository: mockMyRecipesRepository, + aboutRecipeRepository: mockAboutRecipeRepository, + ); + + when(mockMyRecipesRepository.getMyRecipes()).thenThrow(Exception('Network error')); + + await myRecipesCubit!.setActiveState(); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(myRecipesCubit!.state, isA()); + }); + + test('setArchivedState - устанавливает архивное состояние', () async { + myRecipesCubit = MyRecipesCubit( + myRecipesRepository: mockMyRecipesRepository, + aboutRecipeRepository: mockAboutRecipeRepository, + ); + + when(mockMyRecipesRepository.getMyRecipes()).thenAnswer( + (_) async => [testActiveRecipe, testArchivedRecipe], + ); + + await myRecipesCubit!.setArchivedState(); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(myRecipesCubit!.state, isA()); + final loadedState = myRecipesCubit!.state as MyRecipesStateLoaded; + expect(loadedState.recipes.length, 1); + expect(loadedState.recipes.first.state, 'archived'); + expect(loadedState.selectedState, 'archived'); + }); + + test('setArchivedState - обрабатывает ошибку', () async { + myRecipesCubit = MyRecipesCubit( + myRecipesRepository: mockMyRecipesRepository, + aboutRecipeRepository: mockAboutRecipeRepository, + ); + + when(mockMyRecipesRepository.getMyRecipes()).thenThrow(Exception('Network error')); + + await myRecipesCubit!.setArchivedState(); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(myRecipesCubit!.state, isA()); + }); + + test('getRecipeById - получает рецепт по slug', () async { + myRecipesCubit = MyRecipesCubit( + myRecipesRepository: mockMyRecipesRepository, + aboutRecipeRepository: mockAboutRecipeRepository, + ); + + when(mockAboutRecipeRepository.getRecipe('test-recipe')).thenAnswer((_) async => testRecipe); + + final result = await myRecipesCubit!.getRecipeById('test-recipe'); + + expect(result, isA()); + expect(result.id, 1); + expect(result.slug, 'test-recipe'); + verify(mockAboutRecipeRepository.getRecipe('test-recipe')).called(1); + }); + + test('getMyRecipes - фильтрует только активные рецепты', () async { + myRecipesCubit = MyRecipesCubit( + myRecipesRepository: mockMyRecipesRepository, + aboutRecipeRepository: mockAboutRecipeRepository, + ); + + when(mockMyRecipesRepository.getMyRecipes()).thenAnswer( + (_) async => [testActiveRecipe, testArchivedRecipe, testActiveRecipe.copyWith(id: 3)], + ); + + await myRecipesCubit!.getMyRecipes(); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(myRecipesCubit!.state, isA()); + final loadedState = myRecipesCubit!.state as MyRecipesStateLoaded; + expect(loadedState.recipes.length, 2); + expect(loadedState.recipes.every((r) => r.state == 'active'), true); + }); + }); +} + diff --git a/test/features/recipes/presentation/widgets/basic_widget_test.dart b/test/features/recipes/presentation/widgets/basic_widget_test.dart new file mode 100644 index 00000000..14b0ead8 --- /dev/null +++ b/test/features/recipes/presentation/widgets/basic_widget_test.dart @@ -0,0 +1,278 @@ +import 'package:eatvio_app/core/app_config.dart'; +import 'package:eatvio_app/core/app_initializer.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/widgets/animated_option_button.dart'; +import 'package:eatvio_app/core/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart' as recipes; +import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart'; +import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart'; +import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipe_widget.dart'; +import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipes_tab.dart'; +import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart'; +import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +import 'basic_widget_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + group('MyRecipes Basic Widget Tests', () { + late MockMyRecipesRepository mockMyRecipesRepository; + late MockAboutRecipeRepository mockAboutRecipeRepository; + late MyRecipesCubit myRecipesCubit; + + const testRecipe = MyRecipeModel( + id: 1, + image: 'test-image.jpg', + state: 'active', + cookingTime: 30, + activeCookingTime: 20, + description: 'Test recipe description', + steps: [ + recipes.Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1 description'), + recipes.Step(image: 'https://someurl.com/step2.jpg', description: 'Step 2 description'), + ], + videoUrl: 'https://someurl.com/video.mp4', + title: 'Test Recipe', + slug: 'test-recipe', + calories: 250.0, + likesCount: 5, + viewsCount: 10, + ); + + setUp(() async { + await sl.reset(); + dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); + mockMyRecipesRepository = MockMyRecipesRepository(); + mockAboutRecipeRepository = MockAboutRecipeRepository(); + myRecipesCubit = MyRecipesCubit(myRecipesRepository: mockMyRecipesRepository, aboutRecipeRepository: mockAboutRecipeRepository); + sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); + }); + + tearDown(() async { + await Future.delayed(const Duration(milliseconds: 100)); + if (!myRecipesCubit.isClosed) { + myRecipesCubit.close(); + } + }); + + Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) { + final widget = BlocProvider( + create: (context) => myRecipesCubit, + child: Portal( + child: wrapInScaffold ? Scaffold(body: SingleChildScrollView(child: child)) : SizedBox(width: 800, height: 600, child: child), + ), + ); + return MaterialApp( + theme: ThemeHelper().themeData(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [Locale('ru')], + home: widget, + ); + } + + testWidgets('should create MyRecipesCubit successfully', (WidgetTester tester) async { + // Arrange & Act + final cubit = MyRecipesCubit(myRecipesRepository: mockMyRecipesRepository, aboutRecipeRepository: mockAboutRecipeRepository); + + // Assert + expect(cubit, isA()); + expect(cubit.state, isA()); + + cubit.close(); + }); + + testWidgets('should display MyRecipesTab with all tabs', (WidgetTester tester) async { + // Arrange + const widget = MyRecipesTab(); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(MyRecipesTab), findsOneWidget); + expect(find.byType(AnimatedOptionButton), findsNWidgets(2)); + }); + + testWidgets('should handle tab selection in MyRecipesTab', (WidgetTester tester) async { + // Arrange + bool callbackCalled = false; + final widget = MyRecipesTab( + additionalOnTap: () { + callbackCalled = true; + }, + ); + + await tester.pumpWidget(createTestWidget(widget)); + await tester.pumpAndSettle(); + + // Act + final tabButtons = find.byType(AnimatedOptionButton); + expect(tabButtons, findsNWidgets(2)); + + await tester.tap(tabButtons.at(0)); + await tester.pump(); + + // Assert + expect(callbackCalled, true); + }); + + testWidgets('should display MyRecipeWidget with recipe title', (WidgetTester tester) async { + // Arrange + myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active')); + const widget = MyRecipeWidget(recipe: testRecipe); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.text('Test Recipe'), findsOneWidget); + expect(find.byType(MyRecipeWidget), findsOneWidget); + }); + + testWidgets('should display MyRecipeWidget with description when description is present', (WidgetTester tester) async { + // Arrange + myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active')); + const widget = MyRecipeWidget(recipe: testRecipe); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.text('Test Recipe'), findsOneWidget); + expect(find.byType(MyRecipeWidget), findsOneWidget); + }); + + testWidgets('should display MyRecipeWidget buttons', (WidgetTester tester) async { + // Arrange + myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active')); + const widget = MyRecipeWidget(recipe: testRecipe); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyRecipeWidget), findsOneWidget); + expect(find.byType(CustomOutlinedButton), findsWidgets); + }); + + testWidgets('should display MyRecipeWidget with cooking time', (WidgetTester tester) async { + // Arrange + myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active')); + const widget = MyRecipeWidget(recipe: testRecipe); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyRecipeWidget), findsOneWidget); + expect(find.text('Test Recipe'), findsOneWidget); + }); + + testWidgets('should display MyRecipeWidget with steps count', (WidgetTester tester) async { + // Arrange + myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active')); + const widget = MyRecipeWidget(recipe: testRecipe); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyRecipeWidget), findsOneWidget); + expect(find.text('Test Recipe'), findsOneWidget); + }); + + testWidgets('should display MyRecipeWidget with calories', (WidgetTester tester) async { + // Arrange + myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active')); + const widget = MyRecipeWidget(recipe: testRecipe); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyRecipeWidget), findsOneWidget); + expect(find.text('Test Recipe'), findsOneWidget); + }); + + testWidgets('should display MyRecipeWidget with null steps', (WidgetTester tester) async { + // Arrange + myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active')); + final recipeWithoutSteps = testRecipe.copyWith(steps: null); + final widget = MyRecipeWidget(recipe: recipeWithoutSteps); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyRecipeWidget), findsOneWidget); + expect(find.text('Test Recipe'), findsOneWidget); + }); + + testWidgets('should display MyRecipeWidget structure correctly', (WidgetTester tester) async { + // Arrange + myRecipesCubit.emit(const MyRecipesState.loaded(recipes: [testRecipe], selectedState: 'active')); + const widget = MyRecipeWidget(recipe: testRecipe); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + // Assert + expect(find.byType(MyRecipeWidget), findsOneWidget); + expect(find.text('Test Recipe'), findsOneWidget); + expect(find.byType(CustomOutlinedButton), findsWidgets); + }); + + testWidgets('should display MyRecipesTab structure correctly', (WidgetTester tester) async { + // Arrange + const widget = MyRecipesTab(); + + // Act + await tester.pumpWidget(createTestWidget(widget)); + await tester.pumpAndSettle(); + + // Assert + expect(find.byType(MyRecipesTab), findsOneWidget); + expect(find.byType(AnimatedOptionButton), findsNWidgets(2)); + }); + }); +} diff --git a/test/features/recipes/repositories/my_recipes_repository_test.dart b/test/features/recipes/repositories/my_recipes_repository_test.dart new file mode 100644 index 00000000..5f046d32 --- /dev/null +++ b/test/features/recipes/repositories/my_recipes_repository_test.dart @@ -0,0 +1,127 @@ +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; +import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart'; +import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'my_recipes_repository_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MyRecipesRepository', () { + late MyRecipesRepository myRecipesRepository; + late MockMyRecipesService mockMyRecipesService; + + setUp(() { + mockMyRecipesService = MockMyRecipesService(); + myRecipesRepository = MyRecipesRepository(myRecipesService: mockMyRecipesService); + }); + + test('getMyRecipes returns list of recipes', () async { + final expectedPagination = MyRecipePaginationModel( + data: [ + MyRecipeModel( + id: 1, + image: 'https://someurl.com/img.jpg', + state: 'published', + cookingTime: 30, + activeCookingTime: 20, + description: 'Test recipe description', + steps: [ + const Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1 description'), + ], + videoUrl: 'https://someurl.com/video.mp4', + title: 'Test Recipe', + slug: 'test-recipe', + calories: 250.0, + likesCount: 5, + viewsCount: 10, + ), + ], + ); + + when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expectedPagination); + + final result = await myRecipesRepository.getMyRecipes(); + + expect(result, isA>()); + expect(result.length, 1); + expect(result.first.title, 'Test Recipe'); + expect(result.first.id, 1); + verify(mockMyRecipesService.getMyRecipes()).called(1); + }); + + test('getMyRecipes returns empty list', () async { + final expectedPagination = MyRecipePaginationModel( + data: [], + ); + + when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expectedPagination); + + final result = await myRecipesRepository.getMyRecipes(); + + expect(result, isA>()); + expect(result.length, 0); + verify(mockMyRecipesService.getMyRecipes()).called(1); + }); + + test('getMyRecipes returns multiple recipes', () async { + final expectedPagination = MyRecipePaginationModel( + data: [ + MyRecipeModel( + id: 1, + image: 'https://someurl.com/img1.jpg', + state: 'published', + cookingTime: 30, + activeCookingTime: 20, + description: 'First recipe', + steps: null, + videoUrl: null, + title: 'First Recipe', + slug: 'first-recipe', + calories: 250.0, + likesCount: 5, + viewsCount: 10, + ), + MyRecipeModel( + id: 2, + image: 'https://someurl.com/img2.jpg', + state: 'draft', + cookingTime: 45, + activeCookingTime: 30, + description: 'Second recipe', + steps: [ + const Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1'), + ], + videoUrl: 'https://someurl.com/video.mp4', + title: 'Second Recipe', + slug: 'second-recipe', + calories: 350.0, + likesCount: 8, + viewsCount: 15, + ), + ], + ); + + when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expectedPagination); + + final result = await myRecipesRepository.getMyRecipes(); + + expect(result, isA>()); + expect(result.length, 2); + expect(result.first.title, 'First Recipe'); + expect(result.last.title, 'Second Recipe'); + verify(mockMyRecipesService.getMyRecipes()).called(1); + }); + + test('getMyRecipes rethrows exception on error', () async { + when(mockMyRecipesService.getMyRecipes()).thenThrow(Exception('Network error')); + + expect(() => myRecipesRepository.getMyRecipes(), throwsException); + verify(mockMyRecipesService.getMyRecipes()).called(1); + }); + }); +} + diff --git a/test/features/recipes/services/my_recipes_service_test.dart b/test/features/recipes/services/my_recipes_service_test.dart new file mode 100644 index 00000000..5389cc38 --- /dev/null +++ b/test/features/recipes/services/my_recipes_service_test.dart @@ -0,0 +1,151 @@ +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart'; +import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'my_recipes_service_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('MyRecipesService', () { + late MockMyRecipesService mockMyRecipesService; + + setUp(() { + mockMyRecipesService = MockMyRecipesService(); + }); + + test('getMyRecipes returns pagination with recipes', () async { + final expected = MyRecipePaginationModel( + data: [ + MyRecipeModel( + id: 1, + image: 'https://someurl.com/img.jpg', + state: 'published', + cookingTime: 30, + activeCookingTime: 20, + description: 'Test recipe description', + steps: [ + const Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1 description'), + const Step(image: 'https://someurl.com/step2.jpg', description: 'Step 2 description'), + ], + videoUrl: 'https://someurl.com/video.mp4', + title: 'Test Recipe', + slug: 'test-recipe', + calories: 250.0, + likesCount: 5, + viewsCount: 10, + ), + ], + ); + + when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expected); + + final result = await mockMyRecipesService.getMyRecipes(); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.title, 'Test Recipe'); + expect(result.data.first.id, 1); + verify(mockMyRecipesService.getMyRecipes()).called(1); + }); + + test('getMyRecipes returns pagination with multiple recipes', () async { + final expected = MyRecipePaginationModel( + data: [ + MyRecipeModel( + id: 1, + image: 'https://someurl.com/img1.jpg', + state: 'published', + cookingTime: 30, + activeCookingTime: 20, + description: 'First recipe description', + steps: [ + const Step(image: 'https://someurl.com/step1.jpg', description: 'Step 1'), + ], + videoUrl: 'https://someurl.com/video1.mp4', + title: 'First Recipe', + slug: 'first-recipe', + calories: 250.0, + likesCount: 5, + viewsCount: 10, + ), + MyRecipeModel( + id: 2, + image: 'https://someurl.com/img2.jpg', + state: 'draft', + cookingTime: 45, + activeCookingTime: 30, + description: 'Second recipe description', + steps: null, + videoUrl: null, + title: 'Second Recipe', + slug: 'second-recipe', + calories: 350.0, + likesCount: 8, + viewsCount: 15, + ), + ], + ); + + when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expected); + + final result = await mockMyRecipesService.getMyRecipes(); + + expect(result, isA()); + expect(result.data.length, 2); + expect(result.data.first.title, 'First Recipe'); + expect(result.data.last.title, 'Second Recipe'); + verify(mockMyRecipesService.getMyRecipes()).called(1); + }); + + test('getMyRecipes returns empty pagination', () async { + final expected = MyRecipePaginationModel( + data: [], + ); + + when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expected); + + final result = await mockMyRecipesService.getMyRecipes(); + + expect(result, isA()); + expect(result.data.length, 0); + verify(mockMyRecipesService.getMyRecipes()).called(1); + }); + + test('getMyRecipes returns recipe with null steps and videoUrl', () async { + final expected = MyRecipePaginationModel( + data: [ + MyRecipeModel( + id: 3, + image: 'https://someurl.com/img3.jpg', + state: 'draft', + cookingTime: 15, + activeCookingTime: 10, + description: 'Simple recipe without steps', + steps: null, + videoUrl: null, + title: 'Simple Recipe', + slug: 'simple-recipe', + calories: 150.0, + likesCount: 2, + viewsCount: 5, + ), + ], + ); + + when(mockMyRecipesService.getMyRecipes()).thenAnswer((_) async => expected); + + final result = await mockMyRecipesService.getMyRecipes(); + + expect(result, isA()); + expect(result.data.length, 1); + expect(result.data.first.steps, isNull); + expect(result.data.first.videoUrl, isNull); + expect(result.data.first.state, 'draft'); + verify(mockMyRecipesService.getMyRecipes()).called(1); + }); + }); +} +