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/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'feed_cubit_test.mocks.dart'; @GenerateNiceMocks([MockSpec()]) void main() { group('FeedCubit', () { FeedCubit? feedCubit; late MockFeedRepository mockFeedRepository; final testBlogPost = FeedBlogPostModel( id: 1, title: 'Test Post', description: 'Test description', image: 'https://example.com/image.jpg', state: 'published', author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false), videoUrl: 'https://example.com/video.mp4', createdAt: DateTime(2024, 1, 15), updatedAt: DateTime(2024, 1, 15), totalLikes: 10, totalViews: 100, totalComments: 5, wasLikedByMe: false, ); final testBlogPost2 = FeedBlogPostModel( id: 2, title: 'Test Post 2', description: 'Test description 2', image: 'https://example.com/image2.jpg', state: 'published', author: const Author(authorSlug: 'author-2', authorName: 'Author Name 2', authorProfileIcon: 'https://example.com/icon2.jpg', authorIsMe: false), videoUrl: 'https://example.com/video2.mp4', createdAt: DateTime(2024, 1, 16), updatedAt: DateTime(2024, 1, 16), totalLikes: 20, totalViews: 200, totalComments: 10, wasLikedByMe: true, ); setUp(() { mockFeedRepository = MockFeedRepository(); }); tearDown(() async { if (feedCubit != null && !feedCubit!.isClosed) { await Future.delayed(const Duration(milliseconds: 100)); feedCubit!.close(); feedCubit = null; } }); test('начальное состояние - loading', () { feedCubit = FeedCubit(feedRepository: mockFeedRepository); expect(feedCubit!.state, const FeedState.loading()); }); test('loadFeedBlogPostList - успешно загружает список постов', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); final expectedSettings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost], meta: const Meta(currentPage: 1, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => expectedSettings); await feedCubit!.loadFeedBlogPostList(); await Future.delayed(const Duration(milliseconds: 100)); expect(feedCubit!.state, isA()); final loadedState = feedCubit!.state as FeedStateLoaded; expect(loadedState.feedBlogPostList.length, 1); expect(loadedState.feedBlogPostList.first.id, 1); expect(loadedState.isLoading, false); expect(feedCubit!.lastPage, 3); verify(mockFeedRepository.getFeedBlogPosts(query: null)).called(1); }); test('loadFeedBlogPostList - загружает список постов с query', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); const query = 'test query'; final expectedSettings = 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: query)).thenAnswer((_) async => expectedSettings); await feedCubit!.loadFeedBlogPostList(query: query); await Future.delayed(const Duration(milliseconds: 100)); expect(feedCubit!.state, isA()); final loadedState = feedCubit!.state as FeedStateLoaded; expect(loadedState.feedBlogPostList.length, 2); verify(mockFeedRepository.getFeedBlogPosts(query: query)).called(1); }); test('loadFeedBlogPostList - обрабатывает ошибку', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); final exception = Exception('Network error'); when(mockFeedRepository.getFeedBlogPosts(query: null)).thenThrow(exception); await feedCubit!.loadFeedBlogPostList(); await Future.delayed(const Duration(milliseconds: 100)); expect(feedCubit!.state, isA()); final errorState = feedCubit!.state as FeedStateError; expect(errorState.errorMessage, contains('Exception: Network error')); }); test('loadNextPage - загружает следующую страницу', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); final firstPageSettings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost], meta: const Meta(currentPage: 1, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); final secondPageSettings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost2], meta: const Meta(currentPage: 2, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => firstPageSettings); await feedCubit!.loadFeedBlogPostList(); await Future.delayed(const Duration(milliseconds: 100)); when(mockFeedRepository.getFeedBlogPosts(page: 2, query: null)).thenAnswer((_) async => secondPageSettings); await feedCubit!.loadNextPage(); await Future.delayed(const Duration(milliseconds: 100)); expect(feedCubit!.state, isA()); final loadedState = feedCubit!.state as FeedStateLoaded; expect(loadedState.feedBlogPostList.length, 2); expect(feedCubit!.currentPage, 2); expect(loadedState.isLoading, false); verify(mockFeedRepository.getFeedBlogPosts(page: 2, query: null)).called(1); }); test('loadNextPage - не загружает если уже на последней странице', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); final settings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost], meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings); await feedCubit!.loadFeedBlogPostList(); await Future.delayed(const Duration(milliseconds: 100)); await feedCubit!.loadNextPage(); await Future.delayed(const Duration(milliseconds: 100)); verifyNever(mockFeedRepository.getFeedBlogPosts(page: 2, query: anyNamed('query'))); }); test('loadNextPage - не загружает если уже загружается', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); final settings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost], meta: const Meta(currentPage: 1, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings); await feedCubit!.loadFeedBlogPostList(); await Future.delayed(const Duration(milliseconds: 100)); feedCubit!.isLoading = true; await feedCubit!.loadNextPage(); await Future.delayed(const Duration(milliseconds: 100)); verifyNever(mockFeedRepository.getFeedBlogPosts(page: 2, query: anyNamed('query'))); }); test('loadNextPage - обрабатывает ошибку при загрузке следующей страницы', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); final firstPageSettings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost], meta: const Meta(currentPage: 1, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => firstPageSettings); await feedCubit!.loadFeedBlogPostList(); await Future.delayed(const Duration(milliseconds: 100)); final exception = Exception('Network error'); when(mockFeedRepository.getFeedBlogPosts(page: 2, query: null)).thenThrow(exception); await feedCubit!.loadNextPage(); await Future.delayed(const Duration(milliseconds: 100)); expect(feedCubit!.state, isA()); final errorState = feedCubit!.state as FeedStateError; expect(errorState.errorMessage, contains('Exception: Network error')); }); test('loadNextPage - сохраняет query при загрузке следующей страницы', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); const query = 'test query'; final firstPageSettings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost], meta: const Meta(currentPage: 1, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); final secondPageSettings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost2], meta: const Meta(currentPage: 2, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); when(mockFeedRepository.getFeedBlogPosts(query: query)).thenAnswer((_) async => firstPageSettings); await feedCubit!.loadFeedBlogPostList(query: query); await Future.delayed(const Duration(milliseconds: 100)); when(mockFeedRepository.getFeedBlogPosts(page: 2, query: query)).thenAnswer((_) async => secondPageSettings); await feedCubit!.loadNextPage(); await Future.delayed(const Duration(milliseconds: 100)); verify(mockFeedRepository.getFeedBlogPosts(page: 2, query: query)).called(1); }); test('updateFeedBlogPostList - обновляет лайк поста', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); final settings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost], meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings); await feedCubit!.loadFeedBlogPostList(); await Future.delayed(const Duration(milliseconds: 100)); feedCubit!.updateFeedBlogPostList(testBlogPost, true); await Future.delayed(const Duration(milliseconds: 100)); expect(feedCubit!.state, isA()); final loadedState = feedCubit!.state as FeedStateLoaded; expect(loadedState.feedBlogPostList.first.wasLikedByMe, true); expect(loadedState.feedBlogPostList.first.totalLikes, 11); }); test('updateFeedBlogPostList - убирает лайк поста', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); final settings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost2], meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings); await feedCubit!.loadFeedBlogPostList(); await Future.delayed(const Duration(milliseconds: 100)); feedCubit!.updateFeedBlogPostList(testBlogPost2, false); await Future.delayed(const Duration(milliseconds: 100)); expect(feedCubit!.state, isA()); final loadedState = feedCubit!.state as FeedStateLoaded; expect(loadedState.feedBlogPostList.first.wasLikedByMe, false); expect(loadedState.feedBlogPostList.first.totalLikes, 19); }); test('updateFeedBlogPostList - не обновляет если пост не найден', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); final settings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost], meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings); await feedCubit!.loadFeedBlogPostList(); await Future.delayed(const Duration(milliseconds: 100)); final nonExistentPost = testBlogPost.copyWith(id: 999); final initialState = feedCubit!.state; feedCubit!.updateFeedBlogPostList(nonExistentPost, true); await Future.delayed(const Duration(milliseconds: 100)); expect(feedCubit!.state, initialState); }); test('updateFeedBlogPostList - не обновляет если state не loaded', () { feedCubit = FeedCubit(feedRepository: mockFeedRepository); feedCubit!.updateFeedBlogPostList(testBlogPost, true); expect(feedCubit!.state, const FeedState.loading()); }); test('updateFeedBlogPostList - не обновляет если post null', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); final settings = FeedBlogPostSettingsModel( feedBlogPostList: [testBlogPost], meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null), ); when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings); await feedCubit!.loadFeedBlogPostList(); await Future.delayed(const Duration(milliseconds: 100)); final initialState = feedCubit!.state; feedCubit!.updateFeedBlogPostList(null, true); await Future.delayed(const Duration(milliseconds: 100)); expect(feedCubit!.state, initialState); }); test('likePost - лайкает пост', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); const postId = 1; when(mockFeedRepository.likeBlogPost(id: postId)).thenAnswer((_) async => Future.value()); await feedCubit!.likePost(postId); verify(mockFeedRepository.likeBlogPost(id: postId)).called(1); }); test('likePost - обрабатывает ошибку без выбрасывания', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); const postId = 1; final exception = Exception('Network error'); when(mockFeedRepository.likeBlogPost(id: postId)).thenThrow(exception); await feedCubit!.likePost(postId); verify(mockFeedRepository.likeBlogPost(id: postId)).called(1); }); test('unlikedPost - убирает лайк с поста', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); const postId = 1; when(mockFeedRepository.unlikedBlogPost(id: postId)).thenAnswer((_) async => Future.value()); await feedCubit!.unlikedPost(postId); verify(mockFeedRepository.unlikedBlogPost(id: postId)).called(1); }); test('viewPost - отправляет просмотр поста', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); const postId = 1; when(mockFeedRepository.viewPost(postId)).thenAnswer((_) async => Future.value()); await feedCubit!.viewPost(postId); verify(mockFeedRepository.viewPost(postId)).called(1); }); test('viewRecipe - отправляет просмотр рецепта', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); const slug = 'recipe-1'; when(mockFeedRepository.viewRecipe(slug)).thenAnswer((_) async => Future.value()); await feedCubit!.viewRecipe(slug); verify(mockFeedRepository.viewRecipe(slug)).called(1); }); test('viewDiet - отправляет просмотр диеты', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); const dietId = 2; when(mockFeedRepository.viewDiet(dietId)).thenAnswer((_) async => Future.value()); await feedCubit!.viewDiet(dietId); verify(mockFeedRepository.viewDiet(dietId)).called(1); }); test('viewPlan - отправляет просмотр плана', () async { feedCubit = FeedCubit(feedRepository: mockFeedRepository); const slug = 'plan-1'; when(mockFeedRepository.viewPlan(slug)).thenAnswer((_) async => Future.value()); await feedCubit!.viewPlan(slug); verify(mockFeedRepository.viewPlan(slug)).called(1); }); }); }