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/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 '../../fixtures/doubles/blog_post_doubles.dart'; import '../../fixtures/fixture_reader.dart'; void main() { group('Blog Post Creation Advanced Widget Tests', () { late FixtureBlogPostRepository blogPostRepository; late BlogPostCreateCubit blogPostCreateCubit; late Map fullCreateFixture; late Map updateFixture; late Map deleteFixture; setUp(() { blogPostRepository = FixtureBlogPostRepository(); blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: blogPostRepository); fullCreateFixture = FixtureReader.readAsMap('blog_post_create/create_request_full.json'); updateFixture = FixtureReader.readAsMap('blog_post_create/update_request.json'); deleteFixture = FixtureReader.readAsMap('blog_post_create/delete_request.json'); }); 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 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: fullCreateFixture['title'] as String?, description: fullCreateFixture['description_md'] as String?, videoUrl: fullCreateFixture['video_url'] as String?, image: null, ); }, child: const Text('Опубликовать'), ), ], ); // Act await tester.pumpWidget(createTestWidget(formWidget)); await tester.pumpAndSettle(); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), fullCreateFixture['title'] as String); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), fullCreateFixture['description_md'] as String); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), fullCreateFixture['video_url'] as String); await tester.pump(); await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton)); await tester.pump(); // Assert expect( blogPostRepository.createPayloads.single.toJson(), fullCreateFixture, ); }); testWidgets('should handle blog post creation with image', (WidgetTester tester) async { // Arrange final expectedPayload = Map.from(fullCreateFixture); expectedPayload['video_url'] = null; 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: fullCreateFixture['title'] as String?, description: fullCreateFixture['description_md'] as String?, videoUrl: null, image: null, // В реальном тесте здесь был бы XFile ); }, child: const Text('Опубликовать'), ), ], ); // Act await tester.pumpWidget(createTestWidget(formWidget)); await tester.pumpAndSettle(); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), fullCreateFixture['title'] as String); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), fullCreateFixture['description_md'] as String); await tester.pump(); await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton)); await tester.pump(); // Assert expect(blogPostRepository.createPayloads.single.toJson(), expectedPayload); }); testWidgets('should handle blog post update', (WidgetTester tester) async { // Arrange 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: updateFixture['id'] as int, title: updateFixture['title'] as String?, description: updateFixture['description_md'] as String?, videoUrl: updateFixture['video_url'] as String?, image: null, oldImageUrl: updateFixture['old_image_url'] as String?, ); }, child: const Text('Обновить'), ), ], ); // Act await tester.pumpWidget(createTestWidget(formWidget)); await tester.pumpAndSettle(); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), updateFixture['title'] as String); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), updateFixture['description_md'] as String); await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), updateFixture['video_url'] as String); await tester.pump(); await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton)); await tester.pump(); // Assert expect(blogPostRepository.updatePayloads.single.toJson(), updateFixture); }); testWidgets('should handle blog post deletion', (WidgetTester tester) async { // Arrange 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 expect(blogPostRepository.deleteCalls.single, deleteFixture['id']); }); testWidgets('should handle loading state during blog post creation', (WidgetTester tester) async { // Arrange blogPostRepository.createDelay = 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: fullCreateFixture['title'] as String?, videoUrl: null, description: fullCreateFixture['description_md'] as String?, 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 blogPostRepository.createError = 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: fullCreateFixture['title'] as String?, videoUrl: null, description: fullCreateFixture['description_md'] as String?, 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 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: fullCreateFixture['title'] as String?, videoUrl: null, description: fullCreateFixture['description_md'] as String?, 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 expect( blogPostRepository.createPayloads.single.toJson(), {'title': '', 'description_md': '', 'video_url': '', 'image_name': null}, ); }); testWidgets('should handle blog post creation with long content', (WidgetTester tester) async { // Arrange const longContent = 'Это очень длинное описание блог поста, которое содержит много текста и должно быть правильно обработано системой. ' 'Оно может содержать несколько абзацев и различные форматирования. ' 'Пользователи должны иметь возможность создавать подробные и информативные посты.'; 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 expect( blogPostRepository.createPayloads.single.toJson(), { 'title': 'Test Title', 'description_md': longContent, 'video_url': null, 'image_name': null, }, ); }); }); }