forked from eatvio/eatvio_app
637 lines
21 KiB
Dart
637 lines
21 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 Advanced Widget Tests', () {
|
|
late FixtureBlogPostRepository blogPostRepository;
|
|
late BlogPostCreateCubit blogPostCreateCubit;
|
|
late Map<String, dynamic> fullCreateFixture;
|
|
late Map<String, dynamic> updateFixture;
|
|
late Map<String, dynamic> 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<BlogPostCreateCubit>(
|
|
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?,
|
|
);
|
|
},
|
|
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<String, dynamic>.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,
|
|
);
|
|
},
|
|
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?,
|
|
existingMediaIds: List<int>.from(
|
|
updateFixture['existing_media_ids'] as List,
|
|
),
|
|
);
|
|
},
|
|
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<BlogPostCreateCubit, BlogPostCreateState>(
|
|
builder: (context, state) {
|
|
if (state.isLoading) {
|
|
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?,
|
|
);
|
|
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<BlogPostCreateCubit, BlogPostCreateState>(
|
|
builder: (context, state) {
|
|
if (state.errorMessage != null) {
|
|
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?,
|
|
);
|
|
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<BlogPostCreateCubit, BlogPostCreateState>(
|
|
builder: (context, state) {
|
|
if (state.isSuccess) {
|
|
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?,
|
|
);
|
|
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: '',
|
|
);
|
|
},
|
|
child: const Text('Опубликовать'),
|
|
),
|
|
],
|
|
);
|
|
|
|
// Act
|
|
await tester.pumpWidget(createTestWidget(emptyFormWidget));
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
|
|
await tester.pump();
|
|
|
|
// Assert — invalid empty payload must not reach the repository
|
|
expect(blogPostRepository.createPayloads, isEmpty);
|
|
expect(blogPostCreateCubit.state.hasValidationErrors, isTrue);
|
|
});
|
|
|
|
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,
|
|
);
|
|
},
|
|
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,
|
|
});
|
|
});
|
|
});
|
|
}
|