Files
eatvio_app/test/features/blog_post_create/integration_widget_test.dart

657 lines
22 KiB
Dart

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.isSuccess) {
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?,
);
},
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.isSuccess) {
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?,
existingMediaIds: List<int>.from(
updateFixture['existing_media_ids'] as List,
),
);
},
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.errorMessage != null) {
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',
);
},
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?,
);
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.isLoading) {
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?,
);
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.isSuccess) {
feedProbe.loadFeedBlogPostList();
}
},
child: BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
builder: (context, state) {
if (state.isSuccess) {
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?,
);
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',
);
},
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.isSuccess) {
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?,
);
},
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);
});
});
}