Files
eatvio_app/test/features/blog_post_create/integration_widget_test.dart
2025-10-22 18:42:01 +03:00

554 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: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: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 'integration_widget_test.mocks.dart';
@GenerateMocks([BlogPostRepository, FeedCubit])
void main() {
group('Blog Post Creation Integration Widget Tests', () {
late MockBlogPostRepository mockBlogPostRepository;
late MockFeedCubit mockFeedCubit;
late BlogPostCreateCubit blogPostCreateCubit;
setUp(() {
mockBlogPostRepository = MockBlogPostRepository();
mockFeedCubit = MockFeedCubit();
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository);
// Setup mock stubs for FeedCubit
when(mockFeedCubit.stream).thenAnswer((_) => Stream.value(const FeedState.loading()));
when(mockFeedCubit.close()).thenAnswer((_) async {});
});
tearDown(() {
blogPostCreateCubit.close();
});
Widget createTestWidget(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: MultiBlocProvider(
providers: [
BlocProvider<BlogPostCreateCubit>(create: (context) => blogPostCreateCubit),
BlocProvider<FeedCubit>(create: (context) => mockFeedCubit),
],
child: Scaffold(body: SingleChildScrollView(child: child)),
),
);
}
testWidgets('should complete full blog post creation flow', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
when(mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
Widget fullFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
listener: (context, state) {
if (state is BlogPostCreateStateLoaded) {
context.read<FeedCubit>().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: 'Integration Test Title',
videoUrl: 'https://youtube.com/watch?v=integration',
description: 'Integration Test Description',
image: null,
);
},
child: const Text('Опубликовать'),
),
],
),
);
// Act
await tester.pumpWidget(createTestWidget(fullFlowWidget));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Integration Test Title');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Integration Test Description');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=integration');
await tester.pump();
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
await tester.pump();
// Assert
verify(
mockBlogPostRepository.createBlogPost('Integration Test Title', 'https://youtube.com/watch?v=integration', 'Integration Test Description', null),
).called(1);
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
});
testWidgets('should handle blog post update flow', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.updateBlogPost(any, any, any, any, any, any)).thenAnswer((_) async {});
when(mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
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<BlogPostCreateCubit, BlogPostCreateState>(
listener: (context, state) {
if (state is BlogPostCreateStateLoaded) {
context.read<FeedCubit>().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: existingBlogPost.id,
title: 'Updated Title',
videoUrl: 'https://youtube.com/watch?v=updated',
description: 'Updated Description',
image: null,
oldImageUrl: existingBlogPost.image,
);
},
child: const Text('Обновить'),
),
],
),
);
// Act
await tester.pumpWidget(createTestWidget(updateFlowWidget));
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);
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
});
testWidgets('should handle blog post deletion flow', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.deleteBlogPost(any)).thenAnswer((_) async {});
when(mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
Widget deleteFlowWidget = Column(
children: [
const Text('Удалить блог пост?'),
const SizedBox(height: 16),
ElevatedButton(
key: const Key('deleteBlogPostButton'),
onPressed: () async {
await blogPostCreateCubit.deleteBlogPost(1);
mockFeedCubit.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
verify(mockBlogPostRepository.deleteBlogPost(1)).called(1);
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
});
testWidgets('should handle error flow during blog post creation', (WidgetTester tester) async {
// Arrange
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenThrow(Exception('Network error'));
Widget errorFlowWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
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: 'Test Title', videoUrl: null, description: 'Test Description', 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
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {
await Future.delayed(const Duration(milliseconds: 50));
});
Widget loadingFlowWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
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: '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);
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
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
when(mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
Widget successFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
listener: (context, state) {
if (state is BlogPostCreateStateLoaded) {
context.read<FeedCubit>().loadFeedBlogPostList();
}
},
child: BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
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: 'Test Title', videoUrl: null, description: 'Test Description', image: null);
await tester.pump();
// Assert
expect(find.byIcon(Icons.check_circle), findsOneWidget);
expect(find.text('Блог пост успешно создан!'), findsOneWidget);
verify(mockFeedCubit.loadFeedBlogPostList()).called(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
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {});
when(mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
Widget completeFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
listener: (context, state) {
if (state is BlogPostCreateStateLoaded) {
context.read<FeedCubit>().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: 'Complete Test Title',
videoUrl: 'https://youtube.com/watch?v=complete',
description: 'Complete Test Description',
image: null,
);
},
child: const Text('Опубликовать'),
),
],
),
);
// Act
await tester.pumpWidget(createTestWidget(completeFlowWidget));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Complete Test Title');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Complete Test Description');
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=complete');
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
verify(mockBlogPostRepository.createBlogPost('Complete Test Title', 'https://youtube.com/watch?v=complete', 'Complete Test Description', null)).called(1);
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
});
});
}