feat(test): repostory, cubit and widget tests for feed

This commit is contained in:
misha
2025-11-25 11:13:37 +03:00
parent b83d91e7f2
commit c6c42b0525
5 changed files with 15123 additions and 11671 deletions

View File

@@ -0,0 +1,520 @@
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 'basic_widget_test.mocks.dart';
@GenerateNiceMocks([MockSpec<FeedRepository>()])
void main() {
group('Feed Basic Widget Tests', () {
late MockFeedRepository mockFeedRepository;
late FeedCubit feedCubit;
final testBlogPost = FeedBlogPostModel(
id: 1,
title: 'Test Post',
description: 'Test description',
image: 'base64image', // Empty to avoid network calls in tests
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: 'base64image', // Empty to avoid network calls in tests
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>(() => 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<FeedCubit>(
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<FeedCubit>(
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<FeedCubit>());
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<FeedCubit>(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<FeedStateLoading>());
expect(find.byType(BlogPosts), findsOneWidget);
});
testWidgets('should display BlogPosts widget with loaded state', (WidgetTester tester) 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));
// 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));
// Assert - Verify state and widget structure
expect(feedCubit.state, isA<FeedStateLoaded>());
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);
});
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();
await tester.pump(const Duration(milliseconds: 50));
// Assert
expect(feedCubit.state, isA<FeedStateError>());
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: 50));
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
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 {
// 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();
await tester.pump(const Duration(milliseconds: 50));
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
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 {
// 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.pump();
await tester.pump(const Duration(milliseconds: 50));
// Act - Verify refresh indicator exists
expect(find.byType(RefreshIndicator), findsOneWidget);
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
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 {
// 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();
await tester.pump(const Duration(milliseconds: 50));
// 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));
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));
// Assert
expect(feedCubit.state, isA<FeedStateLoaded>());
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 = <String>[];
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 {
// 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();
await tester.pump(const Duration(milliseconds: 50));
// Assert
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(RefreshIndicator), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
expect(feedCubit.state, isA<FeedStateLoaded>());
});
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 {
// 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();
await tester.pump(const Duration(milliseconds: 50));
// Assert
expect(find.byType(BlogPosts), findsOneWidget);
expect(find.byType(RefreshIndicator), findsOneWidget);
expect(find.byType(ListView), findsOneWidget);
expect(feedCubit.state, isA<FeedStateLoaded>());
final loadedState = feedCubit.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 10);
});
});
}