Files
eatvio_app/test/features/blog_post_create/integration_widget_test.dart
2025-11-21 23:24:49 +03:00

541 lines
21 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/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<String, dynamic> fullCreateFixture;
late Map<String, dynamic> 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<BlogPostCreateCubit>.value(
value: blogPostCreateCubit,
child: Scaffold(body: SingleChildScrollView(child: child)),
),
);
}
testWidgets('should complete full blog post creation flow', (WidgetTester tester) async {
// Arrange
Widget fullFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
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<BlogPostCreateCubit, BlogPostCreateState>(
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<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: 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<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: 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<BlogPostCreateCubit, BlogPostCreateState>(
listener: (context, state) {
if (state is BlogPostCreateStateLoaded) {
feedProbe.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: 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<BlogPostCreateCubit, BlogPostCreateState>(
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);
});
});
}