forked from eatvio/eatvio_app
521 lines
19 KiB
Dart
521 lines
19 KiB
Dart
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);
|
||
});
|
||
});
|
||
}
|