import 'package:eatvio_app/app_keys.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart'; import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_state.dart'; import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart'; import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'advanced_widget_test.mocks.dart'; @GenerateMocks([BlogPostRepository]) void main() { group('Blog Post Creation Advanced Widget Tests', () { late MockBlogPostRepository mockBlogPostRepository; late BlogPostCreateCubit blogPostCreateCubit; setUp(() { mockBlogPostRepository = MockBlogPostRepository(); blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository); }); tearDown(() { blogPostCreateCubit.close(); }); Widget createTestWidget(Widget child) { return MaterialApp( theme: ThemeHelper().themeData(), home: BlocProvider( create: (context) => blogPostCreateCubit, child: Scaffold(body: SingleChildScrollView(child: child)), ), ); } testWidgets('should handle blog post creation with all fields', (WidgetTester tester) async { // Arrange when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {}); Widget formWidget = Column( children: [ TextFormField( key: AppKeys.blogPostCreateScreenTitleTextField, decoration: const InputDecoration(labelText: 'Заголовок'), ), const SizedBox(height: 16), TextFormField( key: AppKeys.blogPostCreateScreenContentTextField, decoration: const InputDecoration(labelText: 'Описание'), maxLines: 6, minLines: 4, ), const SizedBox(height: 16), TextFormField( key: AppKeys.blogPostCreateScreenVideoLinkTextField, decoration: const InputDecoration(labelText: 'Ссылка на видео'), ), const SizedBox(height: 16), ElevatedButton( key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () async { await blogPostCreateCubit.createBlogPost( title: 'Test Title', description: 'Test Description', videoUrl: 'https://youtube.com/watch?v=test', image: null, ); }, child: const Text('Опубликовать'), ), ], ); // Act await tester.pumpWidget(createTestWidget(formWidget)); await tester.pumpAndSettle(); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Test Title'); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Test Description'); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=test'); await tester.pump(); await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton)); await tester.pump(); // Assert verify(mockBlogPostRepository.createBlogPost('Test Title', 'https://youtube.com/watch?v=test', 'Test Description', null)).called(1); }); testWidgets('should handle blog post creation with image', (WidgetTester tester) async { // Arrange when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {}); Widget formWidget = Column( children: [ TextFormField( key: AppKeys.blogPostCreateScreenTitleTextField, decoration: const InputDecoration(labelText: 'Заголовок'), ), const SizedBox(height: 16), TextFormField( key: AppKeys.blogPostCreateScreenContentTextField, decoration: const InputDecoration(labelText: 'Описание'), maxLines: 6, minLines: 4, ), const SizedBox(height: 16), GestureDetector( key: AppKeys.imageWidget, onTap: () {}, child: Container( height: 200, width: double.infinity, decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8), ), child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey), SizedBox(height: 8), Text('Добавить изображение'), ], ), ), ), ), const SizedBox(height: 16), ElevatedButton( key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () async { await blogPostCreateCubit.createBlogPost( title: 'Test Title', description: 'Test Description', videoUrl: null, image: null, // В реальном тесте здесь был бы XFile ); }, child: const Text('Опубликовать'), ), ], ); // Act await tester.pumpWidget(createTestWidget(formWidget)); await tester.pumpAndSettle(); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Test Title'); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Test Description'); await tester.pump(); await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton)); await tester.pump(); // Assert verify(mockBlogPostRepository.createBlogPost('Test Title', null, 'Test Description', null)).called(1); }); testWidgets('should handle blog post update', (WidgetTester tester) async { // Arrange when(mockBlogPostRepository.updateBlogPost(any, any, any, any, any, any)).thenAnswer((_) async {}); final testBlogPost = FeedBlogPostModel( id: 1, state: 'published', image: 'original_image.jpg', description: 'Original Description', author: const Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'avatar.jpg', authorIsMe: true), videoUrl: 'https://youtube.com/watch?v=original', createdAt: DateTime.now(), updatedAt: DateTime.now(), title: 'Original Title', totalLikes: 0, totalViews: 0, totalComments: 0, wasLikedByMe: false, ); Widget formWidget = Column( children: [ TextFormField( key: AppKeys.blogPostCreateScreenTitleTextField, decoration: const InputDecoration(labelText: 'Заголовок'), controller: TextEditingController(text: testBlogPost.title), ), const SizedBox(height: 16), TextFormField( key: AppKeys.blogPostCreateScreenContentTextField, decoration: const InputDecoration(labelText: 'Описание'), maxLines: 6, minLines: 4, controller: TextEditingController(text: testBlogPost.description), ), const SizedBox(height: 16), TextFormField( key: AppKeys.blogPostCreateScreenVideoLinkTextField, decoration: const InputDecoration(labelText: 'Ссылка на видео'), controller: TextEditingController(text: testBlogPost.videoUrl), ), const SizedBox(height: 16), ElevatedButton( key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () async { await blogPostCreateCubit.updateBlogPost( id: testBlogPost.id, title: 'Updated Title', description: 'Updated Description', videoUrl: 'https://youtube.com/watch?v=updated', image: null, oldImageUrl: testBlogPost.image, ); }, child: const Text('Обновить'), ), ], ); // Act await tester.pumpWidget(createTestWidget(formWidget)); await tester.pumpAndSettle(); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Updated Title'); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Updated Description'); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=updated'); await tester.pump(); await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton)); await tester.pump(); // Assert verify( mockBlogPostRepository.updateBlogPost(1, 'Updated Title', 'https://youtube.com/watch?v=updated', 'Updated Description', null, 'original_image.jpg'), ).called(1); }); testWidgets('should handle blog post deletion', (WidgetTester tester) async { // Arrange when(mockBlogPostRepository.deleteBlogPost(any)).thenAnswer((_) async {}); Widget deleteWidget = ElevatedButton( key: const Key('deleteBlogPostButton'), onPressed: () async { await blogPostCreateCubit.deleteBlogPost(1); }, child: const Text('Удалить пост'), ); // Act await tester.pumpWidget(createTestWidget(deleteWidget)); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('deleteBlogPostButton'))); await tester.pump(); // Assert verify(mockBlogPostRepository.deleteBlogPost(1)).called(1); }); testWidgets('should handle loading state during blog post creation', (WidgetTester tester) async { // Arrange when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async { await Future.delayed(const Duration(milliseconds: 50)); }); Widget loadingWidget = BlocBuilder( builder: (context, state) { if (state is BlogPostCreateStateLoading) { return const Center(child: CircularProgressIndicator()); } return const Text('Not Loading'); }, ); // Act await tester.pumpWidget(createTestWidget(loadingWidget)); await tester.pumpAndSettle(); // Trigger loading state blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null); await tester.pump(); await tester.pump(); // Need to pump twice to get the loading state // Assert expect(find.byType(CircularProgressIndicator), findsOneWidget); // Wait for the async operation to complete await tester.pump(const Duration(milliseconds: 100)); await tester.pumpAndSettle(); }); testWidgets('should handle error state during blog post creation', (WidgetTester tester) async { // Arrange when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenThrow(Exception('Network error')); Widget errorWidget = BlocBuilder( builder: (context, state) { if (state is BlogPostCreateStateError) { return Center(child: Text('Error: ${state.errorMessage}')); } return const Text('No Error'); }, ); // Act await tester.pumpWidget(createTestWidget(errorWidget)); await tester.pumpAndSettle(); // Trigger error state blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null); await tester.pump(); await tester.pump(); // Need to pump twice to get the error state // Assert expect(find.textContaining('Error:'), findsOneWidget); }); testWidgets('should handle success state after blog post creation', (WidgetTester tester) async { // Arrange when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {}); Widget successWidget = BlocBuilder( builder: (context, state) { if (state is BlogPostCreateStateLoaded) { return const Center(child: Text('Blog post created successfully!')); } return const Text('Not Loaded'); }, ); // Act await tester.pumpWidget(createTestWidget(successWidget)); await tester.pumpAndSettle(); // Trigger success state blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: 'Test Description', image: null); await tester.pump(); await tester.pump(); // Need to pump twice to get the success state // Assert expect(find.text('Blog post created successfully!'), findsOneWidget); }); testWidgets('should validate form fields before submission', (WidgetTester tester) async { // Arrange Widget validationWidget = Column( children: [ TextFormField( key: AppKeys.blogPostCreateScreenTitleTextField, decoration: const InputDecoration(labelText: 'Заголовок', errorText: 'Заголовок не может быть пустым'), ), const SizedBox(height: 16), TextFormField( key: AppKeys.blogPostCreateScreenContentTextField, decoration: const InputDecoration(labelText: 'Описание', errorText: 'Описание не может быть пустым'), maxLines: 6, minLines: 4, ), const SizedBox(height: 16), ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')), ], ); // Act await tester.pumpWidget(createTestWidget(validationWidget)); await tester.pumpAndSettle(); // Assert expect(find.text('Заголовок не может быть пустым'), findsOneWidget); expect(find.text('Описание не может быть пустым'), findsOneWidget); }); testWidgets('should handle image picker interaction', (WidgetTester tester) async { // Arrange bool imagePickerCalled = false; Widget imagePickerWidget = GestureDetector( key: AppKeys.imageWidget, onTap: () { imagePickerCalled = true; }, child: Container( height: 200, width: double.infinity, decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8), ), child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey), SizedBox(height: 8), Text('Добавить изображение'), ], ), ), ), ); // Act await tester.pumpWidget(createTestWidget(imagePickerWidget)); await tester.pumpAndSettle(); await tester.tap(find.byKey(AppKeys.imageWidget)); await tester.pump(); // Assert expect(imagePickerCalled, isTrue); }); testWidgets('should handle video URL validation', (WidgetTester tester) async { // Arrange Widget videoValidationWidget = Column( children: [ TextFormField( key: AppKeys.blogPostCreateScreenVideoLinkTextField, decoration: const InputDecoration(labelText: 'Ссылка на видео', errorText: 'Введите корректную ссылку на видео'), ), const SizedBox(height: 16), ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')), ], ); // Act await tester.pumpWidget(createTestWidget(videoValidationWidget)); await tester.pumpAndSettle(); // Assert expect(find.text('Введите корректную ссылку на видео'), findsOneWidget); }); testWidgets('should handle form submission with empty fields', (WidgetTester tester) async { // Arrange Widget emptyFormWidget = Column( children: [ TextFormField( key: AppKeys.blogPostCreateScreenTitleTextField, decoration: const InputDecoration(labelText: 'Заголовок'), ), const SizedBox(height: 16), TextFormField( key: AppKeys.blogPostCreateScreenContentTextField, decoration: const InputDecoration(labelText: 'Описание'), maxLines: 6, minLines: 4, ), const SizedBox(height: 16), ElevatedButton( key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () async { await blogPostCreateCubit.createBlogPost(title: '', videoUrl: '', description: '', image: null); }, child: const Text('Опубликовать'), ), ], ); // Act await tester.pumpWidget(createTestWidget(emptyFormWidget)); await tester.pumpAndSettle(); await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton)); await tester.pump(); // Assert verify(mockBlogPostRepository.createBlogPost('', '', '', null)).called(1); }); testWidgets('should handle blog post creation with long content', (WidgetTester tester) async { // Arrange const longContent = 'Это очень длинное описание блог поста, которое содержит много текста и должно быть правильно обработано системой. ' 'Оно может содержать несколько абзацев и различные форматирования. ' 'Пользователи должны иметь возможность создавать подробные и информативные посты.'; when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {}); Widget longContentWidget = Column( children: [ TextFormField( key: AppKeys.blogPostCreateScreenTitleTextField, decoration: const InputDecoration(labelText: 'Заголовок'), ), const SizedBox(height: 16), TextFormField( key: AppKeys.blogPostCreateScreenContentTextField, decoration: const InputDecoration(labelText: 'Описание'), maxLines: 6, minLines: 4, ), const SizedBox(height: 16), ElevatedButton( key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () async { await blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: longContent, image: null); }, child: const Text('Опубликовать'), ), ], ); // Act await tester.pumpWidget(createTestWidget(longContentWidget)); await tester.pumpAndSettle(); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Test Title'); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), longContent); await tester.pump(); await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton)); await tester.pump(); // Assert verify(mockBlogPostRepository.createBlogPost('Test Title', null, longContent, null)).called(1); }); }); }