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/models/pagination_model.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/routing/route_constants.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/presentation/widgets/blog_posts.dart'; import 'package:eatvio_app/features/feed/presentation/widgets/chip_group.dart'; import 'package:eatvio_app/features/feed/repositories/feed_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_test/flutter_test.dart'; 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'; @GenerateNiceMocks([MockSpec()]) 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: '', // Empty - CachedNetworkImage will show errorWidget (SizedBox.shrink) state: 'published', author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: '', authorIsMe: false), videoUrl: '', 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: '', // Empty - CachedNetworkImage will show errorWidget (SizedBox.shrink) state: 'published', author: const Author(authorSlug: 'author-2', authorName: 'Author Name 2', authorProfileIcon: '', authorIsMe: false), videoUrl: '', createdAt: DateTime(2024, 1, 16), updatedAt: DateTime(2024, 1, 16), totalLikes: 20, totalViews: 200, totalComments: 10, wasLikedByMe: true, ); setUp(() async { await sl.reset(); dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost'); mockFeedRepository = MockFeedRepository(); feedCubit = FeedCubit(feedRepository: mockFeedRepository); // Register AppConfig with proper baseUrl for image URL construction sl.registerLazySingleton(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost')); }); tearDown(() async { await Future.delayed(const Duration(milliseconds: 100)); if (!feedCubit.isClosed) { feedCubit.close(); } }); Widget createTestWidget(Widget child, {bool wrapInScaffold = true}) { final widget = BlocProvider( create: (context) => feedCubit, 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, ); } Widget createBlogPostsTestWidget(Widget child) { // Create a minimal GoRouter for tests final router = GoRouter( initialLocation: '/', routes: [ GoRoute( path: '/', builder: (context, state) => BlocProvider( create: (context) => feedCubit, child: Scaffold( body: LayoutBuilder( builder: (context, constraints) => SizedBox( width: constraints.maxWidth > 0 ? constraints.maxWidth : 800, height: constraints.maxHeight > 0 ? constraints.maxHeight : 600, child: child, ), ), ), ), ), GoRoute( path: RouteConstants.blogPostScreen, name: 'blogPostScreen', builder: (context, state) => Scaffold( appBar: AppBar(title: const Text('Blog Post')), body: Center(child: Text('Blog Post ${state.extra ?? ''}')), ), ), ], ); return MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: router, localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [Locale('ru')], ); } testWidgets('should create FeedCubit successfully', (WidgetTester tester) async { // Arrange & Act final cubit = FeedCubit(feedRepository: mockFeedRepository); // Assert expect(cubit, isA()); expect(cubit.state, const FeedState.loading()); cubit.close(); }); testWidgets('should display ChipGroup with all chips', (WidgetTester tester) async { // Arrange final chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; int selectedIndex = 0; final widget = ChipGroup( selectedChipIndex: selectedIndex, chipsLabelList: chipsLabelList, setSelectedChipIndex: (index) { selectedIndex = index; }, ); // Act await tester.pumpWidget(createTestWidget(widget)); await tester.pumpAndSettle(); // Assert expect(find.text('Все'), findsOneWidget); expect(find.text('Рецепты'), findsOneWidget); expect(find.text('Диеты'), findsOneWidget); expect(find.text('Планы питания'), findsOneWidget); expect(find.text('Консультанты'), findsOneWidget); }); testWidgets('should handle chip selection in ChipGroup', (WidgetTester tester) async { // Arrange final chipsLabelList = ['Все', 'Рецепты', 'Диеты']; int selectedIndex = 0; final widget = ChipGroup( selectedChipIndex: selectedIndex, chipsLabelList: chipsLabelList, setSelectedChipIndex: (index) { selectedIndex = index; }, ); await tester.pumpWidget(createTestWidget(widget)); await tester.pumpAndSettle(); // Act await tester.tap(find.text('Рецепты')); await tester.pump(); // Assert expect(selectedIndex, 1); }); testWidgets('should display ChipGroup with custom labels', (WidgetTester tester) async { // Arrange final chipsLabelList = ['Tab 1', 'Tab 2', 'Tab 3']; final widget = ChipGroup(selectedChipIndex: 0, chipsLabelList: chipsLabelList, setSelectedChipIndex: (index) {}); // Act await tester.pumpWidget(createTestWidget(widget)); await tester.pumpAndSettle(); // Assert expect(find.text('Tab 1'), findsOneWidget); expect(find.text('Tab 2'), findsOneWidget); expect(find.text('Tab 3'), findsOneWidget); }); testWidgets('should display BlogPosts widget with loading state', (WidgetTester tester) async { // Arrange feedCubit.emit(const FeedState.loading()); final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); // Act final router = GoRouter( initialLocation: '/', routes: [ GoRoute( path: '/', builder: (context, state) => BlocProvider(create: (context) => feedCubit, child: widget), ), ], ); await tester.pumpWidget( MaterialApp.router( theme: ThemeHelper().themeData(), routerConfig: router, localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [Locale('ru')], ), ); await tester.pump(); // Assert expect(feedCubit.state, isA()); expect(find.byType(BlogPosts), findsOneWidget); }); testWidgets('should display BlogPosts widget with loaded state', (WidgetTester tester) async { await mockNetworkImagesFor(() async { // Arrange feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost, testBlogPost2], isLoading: false)); final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); // 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); }); // 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 { // Arrange feedCubit.emit(const FeedState.error(errorMessage: 'Ошибка загрузки постов')); final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); // Act await tester.pumpWidget(createBlogPostsTestWidget(widget)); await tester.pump(); // Assert expect(feedCubit.state, isA()); final errorState = feedCubit.state as FeedStateError; expect(errorState.errorMessage, 'Ошибка загрузки постов'); expect(find.byType(BlogPosts), findsOneWidget); }); testWidgets('should display BlogPosts with empty list', (WidgetTester tester) async { // Arrange feedCubit.emit(const FeedState.loaded(feedBlogPostList: [], isLoading: false)); 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: 100)); await tester.pump(const Duration(milliseconds: 100)); // Assert expect(feedCubit.state, isA()); final loadedState = feedCubit.state as FeedStateLoaded; expect(loadedState.feedBlogPostList.isEmpty, true); expect(find.byType(BlogPosts), findsOneWidget); expect(find.byType(ListView), findsOneWidget); }); testWidgets('should display BlogPosts with loading indicator when isLoading is true', (WidgetTester tester) async { await mockNetworkImagesFor(() async { // Arrange feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: true)); final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); // 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); }); }); testWidgets('should handle chip selection with multiple chips', (WidgetTester tester) async { // Arrange final chipsLabelList = ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты']; int selectedIndex = 0; final widget = ChipGroup( selectedChipIndex: selectedIndex, chipsLabelList: chipsLabelList, setSelectedChipIndex: (index) { selectedIndex = index; }, ); await tester.pumpWidget(createTestWidget(widget)); await tester.pumpAndSettle(); // Act await tester.tap(find.text('Диеты')); await tester.pump(); // Assert expect(selectedIndex, 2); }); testWidgets('should display ChipGroup with single chip', (WidgetTester tester) async { // Arrange final chipsLabelList = ['Все']; final widget = ChipGroup(selectedChipIndex: 0, chipsLabelList: chipsLabelList, setSelectedChipIndex: (index) {}); // Act await tester.pumpWidget(createTestWidget(widget)); await tester.pumpAndSettle(); // Assert expect(find.text('Все'), findsOneWidget); }); testWidgets('should handle BlogPosts refresh', (WidgetTester tester) async { 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), ), ); final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); await tester.pumpWidget(createBlogPostsTestWidget(widget)); await tester.pumpAndSettle(); // Act - Verify refresh indicator exists expect(find.byType(RefreshIndicator), findsOneWidget); // Assert expect(feedCubit.state, isA()); expect(find.byType(ListView), findsOneWidget); }); }); testWidgets('should display ChipGroup with correct selected chip', (WidgetTester tester) async { // Arrange final chipsLabelList = ['Все', 'Рецепты', 'Диеты']; final widget = ChipGroup(selectedChipIndex: 1, chipsLabelList: chipsLabelList, setSelectedChipIndex: (index) {}); // Act await tester.pumpWidget(createTestWidget(widget)); await tester.pumpAndSettle(); // Assert expect(find.text('Рецепты'), findsOneWidget); expect(find.text('Все'), findsOneWidget); expect(find.text('Диеты'), findsOneWidget); }); testWidgets('should handle BlogPosts visibility detection', (WidgetTester tester) async { await mockNetworkImagesFor(() async { // Arrange feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false)); final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); // 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 }); }); testWidgets('should display BlogPosts with multiple posts', (WidgetTester tester) async { await mockNetworkImagesFor(() async { // Arrange feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost, testBlogPost2], isLoading: false)); final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); // 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); }); }); testWidgets('should handle ChipGroup with empty label list', (WidgetTester tester) async { // Arrange final chipsLabelList = []; final widget = ChipGroup(selectedChipIndex: 0, chipsLabelList: chipsLabelList, setSelectedChipIndex: (index) {}); // Act await tester.pumpWidget(createTestWidget(widget)); await tester.pumpAndSettle(); // Assert expect(find.byType(ChipGroup), findsOneWidget); }); testWidgets('should display BlogPosts structure correctly', (WidgetTester tester) async { await mockNetworkImagesFor(() async { // Arrange feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false)); final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {}); // 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()); }); }); testWidgets('should handle chip selection callback', (WidgetTester tester) async { // Arrange final chipsLabelList = ['Все', 'Рецепты']; int callbackIndex = -1; final widget = ChipGroup( selectedChipIndex: 0, chipsLabelList: chipsLabelList, setSelectedChipIndex: (index) { callbackIndex = index; }, ); await tester.pumpWidget(createTestWidget(widget)); await tester.pumpAndSettle(); // Act await tester.tap(find.text('Рецепты')); await tester.pump(); // Assert expect(callbackIndex, 1); }); testWidgets('should display BlogPosts with scrollable list', (WidgetTester tester) async { 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}) {}); // 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); }); }); }); }