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 Integration Widget Tests', () { late FixtureBlogPostRepository blogPostRepository; late BlogPostCreateCubit blogPostCreateCubit; late FeedReloadProbe feedProbe; late Map fullCreateFixture; late Map updateFixture; setUp(() { blogPostRepository = FixtureBlogPostRepository(); blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: blogPostRepository); feedProbe = FeedReloadProbe(); fullCreateFixture = FixtureReader.readAsMap('blog_post_create/create_request_full.json'); updateFixture = FixtureReader.readAsMap('blog_post_create/update_request.json'); }); tearDown(() { blogPostCreateCubit.close(); }); Widget createTestWidget(Widget child) { return MaterialApp( theme: ThemeHelper().themeData(), home: BlocProvider.value( value: blogPostCreateCubit, child: Scaffold(body: SingleChildScrollView(child: child)), ), ); } testWidgets('should complete full blog post creation flow', (WidgetTester tester) async { // Arrange Widget fullFlowWidget = BlocListener( listener: (context, state) { if (state is BlogPostCreateStateLoaded) { feedProbe.loadFeedBlogPostList(); } }, child: 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), 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?, videoUrl: fullCreateFixture['video_url'] as String?, description: fullCreateFixture['description_md'] as String?, image: null, ); }, child: const Text('Опубликовать'), ), ], ), ); // Act await tester.pumpWidget(createTestWidget(fullFlowWidget)); 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); expect(feedProbe.loadRequests, 1); }); testWidgets('should handle blog post update flow', (WidgetTester tester) async { // Arrange final existingBlogPost = 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: 5, totalViews: 10, totalComments: 3, wasLikedByMe: false, ); Widget updateFlowWidget = BlocListener( listener: (context, state) { if (state is BlogPostCreateStateLoaded) { feedProbe.loadFeedBlogPostList(); } }, child: Column( children: [ TextFormField( key: AppKeys.blogPostCreateScreenTitleTextField, decoration: const InputDecoration(labelText: 'Заголовок'), controller: TextEditingController(text: existingBlogPost.title), ), const SizedBox(height: 16), TextFormField( key: AppKeys.blogPostCreateScreenContentTextField, decoration: const InputDecoration(labelText: 'Описание'), maxLines: 6, minLines: 4, controller: TextEditingController(text: existingBlogPost.description), ), const SizedBox(height: 16), TextFormField( key: AppKeys.blogPostCreateScreenVideoLinkTextField, decoration: const InputDecoration(labelText: 'Ссылка на видео'), controller: TextEditingController(text: existingBlogPost.videoUrl), ), const SizedBox(height: 16), ElevatedButton( key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () async { await blogPostCreateCubit.updateBlogPost( id: updateFixture['id'] as int, title: updateFixture['title'] as String?, videoUrl: updateFixture['video_url'] as String?, description: updateFixture['description_md'] as String?, image: null, oldImageUrl: updateFixture['old_image_url'] as String?, ); }, child: const Text('Обновить'), ), ], ), ); // Act await tester.pumpWidget(createTestWidget(updateFlowWidget)); 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); expect(feedProbe.loadRequests, 1); }); testWidgets('should handle blog post deletion flow', (WidgetTester tester) async { // Arrange Widget deleteFlowWidget = Column( children: [ const Text('Удалить блог пост?'), const SizedBox(height: 16), ElevatedButton( key: const Key('deleteBlogPostButton'), onPressed: () async { await blogPostCreateCubit.deleteBlogPost(1); feedProbe.loadFeedBlogPostList(); }, child: const Text('Удалить'), ), ], ); // Act await tester.pumpWidget(createTestWidget(deleteFlowWidget)); await tester.pumpAndSettle(); await tester.tap(find.byKey(const Key('deleteBlogPostButton'))); await tester.pump(); // Assert final deleteFixture = FixtureReader.readAsMap('blog_post_create/delete_request.json'); expect(blogPostRepository.deleteCalls.single, deleteFixture['id']); expect(feedProbe.loadRequests, 1); }); testWidgets('should handle error flow during blog post creation', (WidgetTester tester) async { // Arrange blogPostRepository.createError = Exception('Network error'); Widget errorFlowWidget = BlocBuilder( builder: (context, state) { if (state is BlogPostCreateStateError) { return Column( children: [ Text('Error: ${state.errorMessage}'), const SizedBox(height: 16), ElevatedButton( key: const Key('retryButton'), onPressed: () async { await blogPostCreateCubit.createBlogPost(title: 'Retry Title', videoUrl: null, description: 'Retry Description', image: null); }, child: const Text('Повторить'), ), ], ); } return const Text('No Error'); }, ); // Act await tester.pumpWidget(createTestWidget(errorFlowWidget)); await tester.pumpAndSettle(); // Trigger error await blogPostCreateCubit.createBlogPost( title: fullCreateFixture['title'] as String?, videoUrl: null, description: fullCreateFixture['description_md'] as String?, image: null, ); await tester.pump(); // Assert expect(find.textContaining('Error:'), findsOneWidget); expect(find.byKey(const Key('retryButton')), findsOneWidget); }); testWidgets('should handle loading state during blog post creation', (WidgetTester tester) async { // Arrange blogPostRepository.createDelay = const Duration(milliseconds: 50); Widget loadingFlowWidget = BlocBuilder( builder: (context, state) { if (state is BlogPostCreateStateLoading) { return const Column(children: [CircularProgressIndicator(), SizedBox(height: 16), Text('Создание блог поста...')]); } return const Text('Not Loading'); }, ); // Act await tester.pumpWidget(createTestWidget(loadingFlowWidget)); await tester.pumpAndSettle(); // Trigger loading 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); expect(find.text('Создание блог поста...'), findsOneWidget); // Wait for the async operation to complete await tester.pump(const Duration(milliseconds: 100)); await tester.pumpAndSettle(); }); testWidgets('should handle success state after blog post creation', (WidgetTester tester) async { // Arrange Widget successFlowWidget = BlocListener( listener: (context, state) { if (state is BlogPostCreateStateLoaded) { feedProbe.loadFeedBlogPostList(); } }, child: BlocBuilder( builder: (context, state) { if (state is BlogPostCreateStateLoaded) { return const Column( children: [ Icon(Icons.check_circle, color: Colors.green, size: 48), SizedBox(height: 16), Text('Блог пост успешно создан!'), ], ); } return const Text('Not Loaded'); }, ), ); // Act await tester.pumpWidget(createTestWidget(successFlowWidget)); await tester.pumpAndSettle(); // Trigger success await blogPostCreateCubit.createBlogPost( title: fullCreateFixture['title'] as String?, videoUrl: null, description: fullCreateFixture['description_md'] as String?, image: null, ); await tester.pump(); // Assert expect(find.byIcon(Icons.check_circle), findsOneWidget); expect(find.text('Блог пост успешно создан!'), findsOneWidget); expect(feedProbe.loadRequests, 1); }); testWidgets('should handle form validation flow', (WidgetTester tester) async { // Arrange Widget validationFlowWidget = 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), 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(validationFlowWidget)); await tester.pumpAndSettle(); // Assert expect(find.text('Заголовок обязателен'), findsOneWidget); expect(find.text('Описание обязательно'), findsOneWidget); expect(find.text('Введите корректную ссылку'), findsOneWidget); }); testWidgets('should handle image upload flow', (WidgetTester tester) async { // Arrange bool imageUploadInitiated = false; Widget imageUploadWidget = Column( children: [ const Text('Изображение поста'), const SizedBox(height: 8), GestureDetector( key: AppKeys.imageWidget, onTap: () { imageUploadInitiated = 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('Добавить изображение'), ], ), ), ), ), const SizedBox(height: 16), ElevatedButton( key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () async { await blogPostCreateCubit.createBlogPost( title: 'Test Title', videoUrl: null, description: 'Test Description', image: null, // В реальном тесте здесь был бы XFile ); }, child: const Text('Опубликовать'), ), ], ); // Act await tester.pumpWidget(createTestWidget(imageUploadWidget)); await tester.pumpAndSettle(); await tester.tap(find.byKey(AppKeys.imageWidget)); await tester.pump(); // Assert expect(imageUploadInitiated, isTrue); }); testWidgets('should handle complete blog post creation with all features', (WidgetTester tester) async { // Arrange Widget completeFlowWidget = BlocListener( listener: (context, state) { if (state is BlogPostCreateStateLoaded) { feedProbe.loadFeedBlogPostList(); } }, child: Column( children: [ const Text('Заголовок'), const SizedBox(height: 8), TextFormField( key: AppKeys.blogPostCreateScreenTitleTextField, decoration: const InputDecoration(hintText: 'Введите заголовок поста'), ), const SizedBox(height: 16), const Text('Описание'), const SizedBox(height: 8), TextFormField(key: AppKeys.blogPostCreateScreenContentTextField, decoration: const InputDecoration(), maxLines: 6, minLines: 4), const SizedBox(height: 16), const Text('Изображение поста'), const SizedBox(height: 8), 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), const Text('Ссылка на видео'), const SizedBox(height: 8), TextFormField(key: AppKeys.blogPostCreateScreenVideoLinkTextField, decoration: const InputDecoration()), const SizedBox(height: 32), ElevatedButton( key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () async { await blogPostCreateCubit.createBlogPost( title: fullCreateFixture['title'] as String?, videoUrl: fullCreateFixture['video_url'] as String?, description: fullCreateFixture['description_md'] as String?, image: null, ); }, child: const Text('Опубликовать'), ), ], ), ); // Act await tester.pumpWidget(createTestWidget(completeFlowWidget)); 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(); // Scroll to make sure the button is visible await tester.drag(find.byType(SingleChildScrollView), const Offset(0, -200)); await tester.pump(); await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton)); await tester.pump(); // Assert expect(blogPostRepository.createPayloads.single.toJson(), fullCreateFixture); expect(feedProbe.loadRequests, 1); }); }); }