forked from eatvio/eatvio_app
feat(post_creation): added multiple image posts creation and validation
This commit is contained in:
@@ -21,10 +21,18 @@ void main() {
|
||||
|
||||
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');
|
||||
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(() {
|
||||
@@ -41,7 +49,9 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('should handle blog post creation with all fields', (WidgetTester tester) async {
|
||||
testWidgets('should handle blog post creation with all fields', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget formWidget = Column(
|
||||
children: [
|
||||
@@ -69,7 +79,6 @@ void main() {
|
||||
title: fullCreateFixture['title'] as String?,
|
||||
description: fullCreateFixture['description_md'] as String?,
|
||||
videoUrl: fullCreateFixture['video_url'] as String?,
|
||||
image: null,
|
||||
);
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
@@ -81,9 +90,18 @@ void main() {
|
||||
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.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));
|
||||
@@ -96,7 +114,9 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should handle blog post creation with image', (WidgetTester tester) async {
|
||||
testWidgets('should handle blog post creation with image', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
final expectedPayload = Map<String, dynamic>.from(fullCreateFixture);
|
||||
expectedPayload['video_url'] = null;
|
||||
@@ -129,7 +149,11 @@ void main() {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey),
|
||||
Icon(
|
||||
Icons.add_photo_alternate,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text('Добавить изображение'),
|
||||
],
|
||||
@@ -145,7 +169,6 @@ void main() {
|
||||
title: fullCreateFixture['title'] as String?,
|
||||
description: fullCreateFixture['description_md'] as String?,
|
||||
videoUrl: null,
|
||||
image: null, // В реальном тесте здесь был бы XFile
|
||||
);
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
@@ -157,15 +180,24 @@ void main() {
|
||||
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.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);
|
||||
expect(
|
||||
blogPostRepository.createPayloads.single.toJson(),
|
||||
expectedPayload,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should handle blog post update', (WidgetTester tester) async {
|
||||
@@ -175,7 +207,12 @@ void main() {
|
||||
state: 'published',
|
||||
image: 'original_image.jpg',
|
||||
description: 'Original Description',
|
||||
author: const Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'avatar.jpg', authorIsMe: true),
|
||||
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(),
|
||||
@@ -216,8 +253,9 @@ void main() {
|
||||
title: updateFixture['title'] as String?,
|
||||
description: updateFixture['description_md'] as String?,
|
||||
videoUrl: updateFixture['video_url'] as String?,
|
||||
image: null,
|
||||
oldImageUrl: updateFixture['old_image_url'] as String?,
|
||||
existingMediaIds: List<int>.from(
|
||||
updateFixture['existing_media_ids'] as List,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Обновить'),
|
||||
@@ -229,9 +267,18 @@ void main() {
|
||||
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.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));
|
||||
@@ -241,7 +288,9 @@ void main() {
|
||||
expect(blogPostRepository.updatePayloads.single.toJson(), updateFixture);
|
||||
});
|
||||
|
||||
testWidgets('should handle blog post deletion', (WidgetTester tester) async {
|
||||
testWidgets('should handle blog post deletion', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget deleteWidget = ElevatedButton(
|
||||
key: const Key('deleteBlogPostButton'),
|
||||
@@ -262,18 +311,21 @@ void main() {
|
||||
expect(blogPostRepository.deleteCalls.single, deleteFixture['id']);
|
||||
});
|
||||
|
||||
testWidgets('should handle loading state during blog post creation', (WidgetTester tester) async {
|
||||
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 is BlogPostCreateStateLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return const Text('Not Loading');
|
||||
},
|
||||
);
|
||||
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));
|
||||
@@ -284,7 +336,6 @@ void main() {
|
||||
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
|
||||
@@ -297,18 +348,21 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('should handle error state during blog post creation', (WidgetTester tester) async {
|
||||
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 is BlogPostCreateStateError) {
|
||||
return Center(child: Text('Error: ${state.errorMessage}'));
|
||||
}
|
||||
return const Text('No 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));
|
||||
@@ -319,7 +373,6 @@ void main() {
|
||||
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 error state
|
||||
@@ -328,16 +381,21 @@ void main() {
|
||||
expect(find.textContaining('Error:'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should handle success state after blog post creation', (WidgetTester tester) async {
|
||||
testWidgets('should handle success state after blog post creation', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget successWidget = BlocBuilder<BlogPostCreateCubit, BlogPostCreateState>(
|
||||
builder: (context, state) {
|
||||
if (state is BlogPostCreateStateLoaded) {
|
||||
return const Center(child: Text('Blog post created successfully!'));
|
||||
}
|
||||
return const Text('Not Loaded');
|
||||
},
|
||||
);
|
||||
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));
|
||||
@@ -348,7 +406,6 @@ void main() {
|
||||
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 success state
|
||||
@@ -357,23 +414,35 @@ void main() {
|
||||
expect(find.text('Blog post created successfully!'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should validate form fields before submission', (WidgetTester tester) async {
|
||||
testWidgets('should validate form fields before submission', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget validationWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', errorText: 'Заголовок не может быть пустым'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Заголовок',
|
||||
errorText: 'Заголовок не может быть пустым',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание', errorText: 'Описание не может быть пустым'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Описание',
|
||||
errorText: 'Описание не может быть пустым',
|
||||
),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')),
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () {},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -386,7 +455,9 @@ void main() {
|
||||
expect(find.text('Описание не может быть пустым'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should handle image picker interaction', (WidgetTester tester) async {
|
||||
testWidgets('should handle image picker interaction', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
bool imagePickerCalled = false;
|
||||
Widget imagePickerWidget = GestureDetector(
|
||||
@@ -425,16 +496,25 @@ void main() {
|
||||
expect(imagePickerCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('should handle video URL validation', (WidgetTester tester) async {
|
||||
testWidgets('should handle video URL validation', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget videoValidationWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео', errorText: 'Введите корректную ссылку на видео'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ссылка на видео',
|
||||
errorText: 'Введите корректную ссылку на видео',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')),
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () {},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -446,7 +526,9 @@ void main() {
|
||||
expect(find.text('Введите корректную ссылку на видео'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should handle form submission with empty fields', (WidgetTester tester) async {
|
||||
testWidgets('should handle form submission with empty fields', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget emptyFormWidget = Column(
|
||||
children: [
|
||||
@@ -465,7 +547,11 @@ void main() {
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () async {
|
||||
await blogPostCreateCubit.createBlogPost(title: '', videoUrl: '', description: '', image: null);
|
||||
await blogPostCreateCubit.createBlogPost(
|
||||
title: '',
|
||||
videoUrl: '',
|
||||
description: '',
|
||||
);
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
@@ -479,14 +565,14 @@ void main() {
|
||||
await tester.tap(find.byKey(AppKeys.blogPostCreateScreenCreateButton));
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
blogPostRepository.createPayloads.single.toJson(),
|
||||
{'title': '', 'description_md': '', 'video_url': '', 'image_name': null},
|
||||
);
|
||||
// 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 {
|
||||
testWidgets('should handle blog post creation with long content', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
const longContent =
|
||||
'Это очень длинное описание блог поста, которое содержит много текста и должно быть правильно обработано системой. '
|
||||
@@ -510,7 +596,11 @@ void main() {
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () async {
|
||||
await blogPostCreateCubit.createBlogPost(title: 'Test Title', videoUrl: null, description: longContent, image: null);
|
||||
await blogPostCreateCubit.createBlogPost(
|
||||
title: 'Test Title',
|
||||
videoUrl: null,
|
||||
description: longContent,
|
||||
);
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
@@ -521,23 +611,26 @@ void main() {
|
||||
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.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,
|
||||
},
|
||||
);
|
||||
expect(blogPostRepository.createPayloads.single.toJson(), {
|
||||
'title': 'Test Title',
|
||||
'description_md': longContent,
|
||||
'video_url': null,
|
||||
'image_name': null,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
blogPostRepository = FixtureBlogPostRepository();
|
||||
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: blogPostRepository);
|
||||
blogPostCreateCubit = BlogPostCreateCubit(
|
||||
blogPostRepository: blogPostRepository,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
@@ -31,7 +33,9 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('should create BlogPostCreateCubit successfully', (WidgetTester tester) async {
|
||||
testWidgets('should create BlogPostCreateCubit successfully', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange & Act
|
||||
final cubit = BlogPostCreateCubit(blogPostRepository: blogPostRepository);
|
||||
|
||||
@@ -41,13 +45,18 @@ void main() {
|
||||
cubit.close();
|
||||
});
|
||||
|
||||
testWidgets('should display basic blog post form fields', (WidgetTester tester) async {
|
||||
testWidgets('should display basic blog post form fields', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget formWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', hintText: 'Введите заголовок поста'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Заголовок',
|
||||
hintText: 'Введите заголовок поста',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
@@ -62,7 +71,11 @@ void main() {
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')),
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () {},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -71,17 +84,34 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenContentTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenCreateButton), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenContentTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenCreateButton),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should update title when user types in title field', (WidgetTester tester) async {
|
||||
testWidgets('should update title when user types in title field', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget formWidget = TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', hintText: 'Введите заголовок поста'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Заголовок',
|
||||
hintText: 'Введите заголовок поста',
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
@@ -90,36 +120,47 @@ void main() {
|
||||
const testTitle = 'Тестовый заголовок поста';
|
||||
|
||||
// Act
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), testTitle);
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
testTitle,
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(find.text(testTitle), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should update description when user types in description field', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget formWidget = TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
);
|
||||
testWidgets(
|
||||
'should update description when user types in description field',
|
||||
(WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget formWidget = TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
const testDescription = 'Это тестовое описание блог поста';
|
||||
const testDescription = 'Это тестовое описание блог поста';
|
||||
|
||||
// Act
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), testDescription);
|
||||
await tester.pump();
|
||||
// Act
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenContentTextField),
|
||||
testDescription,
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(find.text(testDescription), findsOneWidget);
|
||||
});
|
||||
// Assert
|
||||
expect(find.text(testDescription), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('should update video URL when user types in video field', (WidgetTester tester) async {
|
||||
testWidgets('should update video URL when user types in video field', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget formWidget = TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
|
||||
@@ -132,16 +173,27 @@ void main() {
|
||||
const testVideoUrl = 'https://youtube.com/watch?v=example';
|
||||
|
||||
// Act
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), testVideoUrl);
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
testVideoUrl,
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
expect(find.text(testVideoUrl), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should display form labels correctly', (WidgetTester tester) async {
|
||||
testWidgets('should display form labels correctly', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget formWidget = const Column(children: [Text('Заголовок'), Text('Описание'), Text('Ссылка на видео')]);
|
||||
Widget formWidget = const Column(
|
||||
children: [
|
||||
Text('Заголовок'),
|
||||
Text('Описание'),
|
||||
Text('Ссылка на видео'),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
@@ -153,11 +205,16 @@ void main() {
|
||||
expect(find.text('Ссылка на видео'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should display placeholder text in title field', (WidgetTester tester) async {
|
||||
testWidgets('should display placeholder text in title field', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget formWidget = TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', hintText: 'Введите заголовок поста'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Заголовок',
|
||||
hintText: 'Введите заголовок поста',
|
||||
),
|
||||
);
|
||||
|
||||
// Act
|
||||
@@ -170,14 +227,21 @@ void main() {
|
||||
|
||||
testWidgets('should display create button', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget formWidget = ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать'));
|
||||
Widget formWidget = ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () {},
|
||||
child: const Text('Опубликовать'),
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(formWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenCreateButton), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenCreateButton),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Опубликовать'), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -217,19 +281,26 @@ void main() {
|
||||
|
||||
testWidgets('should display error message', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget errorWidget = const Center(child: Text('Произошла ошибка при создании блог поста'));
|
||||
Widget errorWidget = const Center(
|
||||
child: Text('Произошла ошибка при создании блог поста'),
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(errorWidget));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('Произошла ошибка при создании блог поста'), findsOneWidget);
|
||||
expect(
|
||||
find.text('Произошла ошибка при создании блог поста'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should display success message', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget successWidget = const Center(child: Text('Блог пост успешно создан!'));
|
||||
Widget successWidget = const Center(
|
||||
child: Text('Блог пост успешно создан!'),
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(successWidget));
|
||||
@@ -245,12 +316,18 @@ void main() {
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', errorText: 'Заголовок обязателен'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Заголовок',
|
||||
errorText: 'Заголовок обязателен',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание', errorText: 'Описание обязательно'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Описание',
|
||||
errorText: 'Описание обязательно',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -264,13 +341,18 @@ void main() {
|
||||
expect(find.text('Описание обязательно'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should display form with all required fields', (WidgetTester tester) async {
|
||||
testWidgets('should display form with all required fields', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget completeForm = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', hintText: 'Введите заголовок поста'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Заголовок',
|
||||
hintText: 'Введите заголовок поста',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
@@ -285,7 +367,11 @@ void main() {
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео'),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')),
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () {},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -294,13 +380,27 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenContentTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenCreateButton), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenContentTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenCreateButton),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should display image upload widget', (WidgetTester tester) async {
|
||||
testWidgets('should display image upload widget', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget imageWidget = GestureDetector(
|
||||
key: AppKeys.imageWidget,
|
||||
@@ -373,68 +473,101 @@ void main() {
|
||||
expect(imageUploadTapped, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('should display blog post creation form with image and video options', (WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget completeForm = 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),
|
||||
testWidgets(
|
||||
'should display blog post creation form with image and video options',
|
||||
(WidgetTester tester) async {
|
||||
// Arrange
|
||||
Widget completeForm = Column(
|
||||
children: [
|
||||
const Text('Заголовок'),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Введите заголовок поста',
|
||||
),
|
||||
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.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: () {}, child: const 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: () {},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(completeForm));
|
||||
await tester.pumpAndSettle();
|
||||
// Act
|
||||
await tester.pumpWidget(createTestWidget(completeForm));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Assert
|
||||
expect(find.text('Заголовок'), findsOneWidget);
|
||||
expect(find.text('Описание'), findsOneWidget);
|
||||
expect(find.text('Изображение поста'), findsOneWidget);
|
||||
expect(find.text('Ссылка на видео'), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenContentTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.imageWidget), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenCreateButton), findsOneWidget);
|
||||
});
|
||||
// Assert
|
||||
expect(find.text('Заголовок'), findsOneWidget);
|
||||
expect(find.text('Описание'), findsOneWidget);
|
||||
expect(find.text('Изображение поста'), findsOneWidget);
|
||||
expect(find.text('Ссылка на видео'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenContentTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byKey(AppKeys.imageWidget), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenCreateButton),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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/blog_post_create/validation/blog_post_form_validator.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
@@ -14,6 +16,8 @@ void main() {
|
||||
setUp(() {
|
||||
mockRepository = MockBlogPostRepository();
|
||||
cubit = BlogPostCreateCubit(blogPostRepository: mockRepository);
|
||||
registerFallbackValue(<XFile>[]);
|
||||
registerFallbackValue(<int>[]);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
@@ -21,63 +25,246 @@ void main() {
|
||||
});
|
||||
|
||||
test('начальное состояние - BlogPostCreateState', () {
|
||||
expect(cubit.state, isA<BlogPostCreateState>());
|
||||
expect(cubit.state, const BlogPostCreateState());
|
||||
});
|
||||
|
||||
test('createBlogPost - успешное создание', () async {
|
||||
when(() => mockRepository.createBlogPost(any(), any(), any(), any()))
|
||||
.thenAnswer((_) async => {});
|
||||
test('validateForm - пустая форма заполняет formError', () {
|
||||
final isValid = cubit.validateForm(
|
||||
title: '',
|
||||
description: '',
|
||||
videoUrl: '',
|
||||
imageCount: 0,
|
||||
);
|
||||
|
||||
final result = await cubit.createBlogPost();
|
||||
expect(isValid, false);
|
||||
expect(cubit.state.formError, BlogPostFormValidator.contentRequired);
|
||||
verifyNever(
|
||||
() => mockRepository.createBlogPost(
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
images: any(named: 'images'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('validateForm - невалидное видео заполняет videoUrlError', () {
|
||||
final isValid = cubit.validateForm(
|
||||
title: 'Valid title',
|
||||
description: '',
|
||||
videoUrl: 'https://youtube.com/watch?v=abc',
|
||||
imageCount: 0,
|
||||
);
|
||||
|
||||
expect(isValid, false);
|
||||
expect(
|
||||
cubit.state.videoUrlError,
|
||||
BlogPostFormValidator.videoUrlUnsupportedHost,
|
||||
);
|
||||
expect(cubit.state.formError, isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'createBlogPost - не вызывает репозиторий при ошибке валидации',
|
||||
() async {
|
||||
final result = await cubit.createBlogPost();
|
||||
|
||||
expect(result, false);
|
||||
expect(cubit.state.hasValidationErrors, true);
|
||||
verifyNever(
|
||||
() => mockRepository.createBlogPost(
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
images: any(named: 'images'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('createBlogPost - успешное создание', () async {
|
||||
when(
|
||||
() => mockRepository.createBlogPost(
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
images: any(named: 'images'),
|
||||
),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
final result = await cubit.createBlogPost(
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
);
|
||||
|
||||
expect(result, true);
|
||||
expect(cubit.state, isA<BlogPostCreateStateLoaded>());
|
||||
verify(() => mockRepository.createBlogPost(null, null, null, null)).called(1);
|
||||
expect(cubit.state.isSuccess, true);
|
||||
verify(
|
||||
() => mockRepository.createBlogPost(
|
||||
title: 'Test Title',
|
||||
videoUrl: null,
|
||||
description: 'Test Description',
|
||||
images: const [],
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('createBlogPost - обработка ошибки', () async {
|
||||
when(() => mockRepository.createBlogPost(any(), any(), any(), any()))
|
||||
.thenThrow(Exception('Error'));
|
||||
when(
|
||||
() => mockRepository.createBlogPost(
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
images: any(named: 'images'),
|
||||
),
|
||||
).thenThrow(Exception('Error'));
|
||||
|
||||
final result = await cubit.createBlogPost();
|
||||
final result = await cubit.createBlogPost(
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
);
|
||||
|
||||
expect(result, false);
|
||||
expect(cubit.state, isA<BlogPostCreateStateError>());
|
||||
final errorState = cubit.state as BlogPostCreateStateError;
|
||||
expect(errorState.errorMessage, contains('Error'));
|
||||
expect(cubit.state.errorMessage, contains('Error'));
|
||||
expect(cubit.state.isSuccess, false);
|
||||
});
|
||||
|
||||
test(
|
||||
'updateBlogPost - не вызывает репозиторий при ошибке валидации',
|
||||
() async {
|
||||
final result = await cubit.updateBlogPost(id: 1);
|
||||
|
||||
expect(result, false);
|
||||
expect(cubit.state.hasValidationErrors, true);
|
||||
expect(cubit.state.formError, BlogPostFormValidator.contentRequired);
|
||||
verifyNever(
|
||||
() => mockRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('updateBlogPost - невалидное видео не уходит в репозиторий', () async {
|
||||
final result = await cubit.updateBlogPost(
|
||||
id: 1,
|
||||
title: 'Valid title',
|
||||
videoUrl: 'https://youtube.com/watch?v=abc',
|
||||
existingMediaIds: const [10],
|
||||
);
|
||||
|
||||
expect(result, false);
|
||||
expect(
|
||||
cubit.state.videoUrlError,
|
||||
BlogPostFormValidator.videoUrlUnsupportedHost,
|
||||
);
|
||||
verifyNever(
|
||||
() => mockRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'updateBlogPost - короткий заголовок не уходит в репозиторий',
|
||||
() async {
|
||||
final result = await cubit.updateBlogPost(
|
||||
id: 1,
|
||||
title: 'ab',
|
||||
existingMediaIds: const [10],
|
||||
);
|
||||
|
||||
expect(result, false);
|
||||
expect(cubit.state.titleError, BlogPostFormValidator.titleTooShort);
|
||||
verifyNever(
|
||||
() => mockRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('updateBlogPost - успешное обновление', () async {
|
||||
const id = 1;
|
||||
const title = 'Updated Title';
|
||||
|
||||
when(() => mockRepository.updateBlogPost(any(), any(), any(), any(), any(), any()))
|
||||
.thenAnswer((_) async => {});
|
||||
when(
|
||||
() => mockRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
final result = await cubit.updateBlogPost(id: id, title: title);
|
||||
final result = await cubit.updateBlogPost(
|
||||
id: id,
|
||||
title: title,
|
||||
existingMediaIds: [2, 1],
|
||||
mediaIdsToDelete: [9],
|
||||
);
|
||||
|
||||
expect(result, true);
|
||||
expect(cubit.state, isA<BlogPostCreateStateLoaded>());
|
||||
verify(() => mockRepository.updateBlogPost(id, title, null, null, null, null)).called(1);
|
||||
expect(cubit.state.isSuccess, true);
|
||||
verify(
|
||||
() => mockRepository.updateBlogPost(
|
||||
id: id,
|
||||
title: title,
|
||||
videoUrl: '',
|
||||
description: '',
|
||||
existingMediaIds: [2, 1],
|
||||
newImages: const [],
|
||||
mediaIdsToDelete: [9],
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('updateBlogPost - обработка ошибки', () async {
|
||||
const id = 1;
|
||||
|
||||
when(() => mockRepository.updateBlogPost(any(), any(), any(), any(), any(), any()))
|
||||
.thenThrow(Exception('Error'));
|
||||
when(
|
||||
() => mockRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
).thenThrow(Exception('Error'));
|
||||
|
||||
final result = await cubit.updateBlogPost(id: id);
|
||||
final result = await cubit.updateBlogPost(id: id, title: 'Valid title');
|
||||
|
||||
expect(result, false);
|
||||
expect(cubit.state, isA<BlogPostCreateStateError>());
|
||||
expect(cubit.state.errorMessage, contains('Error'));
|
||||
});
|
||||
|
||||
test('deleteBlogPost - успешное удаление', () async {
|
||||
const id = 1;
|
||||
|
||||
when(() => mockRepository.deleteBlogPost(id))
|
||||
.thenAnswer((_) async => {});
|
||||
when(() => mockRepository.deleteBlogPost(id)).thenAnswer((_) async => {});
|
||||
|
||||
final result = await cubit.deleteBlogPost(id);
|
||||
|
||||
@@ -88,13 +275,27 @@ void main() {
|
||||
test('deleteBlogPost - обработка ошибки', () async {
|
||||
const id = 1;
|
||||
|
||||
when(() => mockRepository.deleteBlogPost(id))
|
||||
.thenThrow(Exception('Error'));
|
||||
when(
|
||||
() => mockRepository.deleteBlogPost(id),
|
||||
).thenThrow(Exception('Error'));
|
||||
|
||||
final result = await cubit.deleteBlogPost(id);
|
||||
|
||||
expect(result, false);
|
||||
});
|
||||
|
||||
test('clearTitleError очищает titleError и formError', () {
|
||||
cubit.validateForm(
|
||||
title: '',
|
||||
description: '',
|
||||
videoUrl: '',
|
||||
imageCount: 0,
|
||||
);
|
||||
expect(cubit.state.formError, isNotNull);
|
||||
|
||||
cubit.clearTitleError();
|
||||
|
||||
expect(cubit.state.formError, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,16 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
blogPostRepository = FixtureBlogPostRepository();
|
||||
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: blogPostRepository);
|
||||
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');
|
||||
fullCreateFixture = FixtureReader.readAsMap(
|
||||
'blog_post_create/create_request_full.json',
|
||||
);
|
||||
updateFixture = FixtureReader.readAsMap(
|
||||
'blog_post_create/update_request.json',
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
@@ -41,11 +47,14 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('should complete full blog post creation flow', (WidgetTester tester) async {
|
||||
testWidgets('should complete full blog post creation flow', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget fullFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
|
||||
Widget
|
||||
fullFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
|
||||
listener: (context, state) {
|
||||
if (state is BlogPostCreateStateLoaded) {
|
||||
if (state.isSuccess) {
|
||||
feedProbe.loadFeedBlogPostList();
|
||||
}
|
||||
},
|
||||
@@ -82,7 +91,11 @@ void main() {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey),
|
||||
Icon(
|
||||
Icons.add_photo_alternate,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text('Добавить изображение'),
|
||||
],
|
||||
@@ -98,7 +111,6 @@ void main() {
|
||||
title: fullCreateFixture['title'] as String?,
|
||||
videoUrl: fullCreateFixture['video_url'] as String?,
|
||||
description: fullCreateFixture['description_md'] as String?,
|
||||
image: null,
|
||||
);
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
@@ -111,27 +123,46 @@ void main() {
|
||||
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.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(
|
||||
blogPostRepository.createPayloads.single.toJson(),
|
||||
fullCreateFixture,
|
||||
);
|
||||
expect(feedProbe.loadRequests, 1);
|
||||
});
|
||||
|
||||
testWidgets('should handle blog post update flow', (WidgetTester tester) async {
|
||||
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),
|
||||
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(),
|
||||
@@ -142,9 +173,10 @@ void main() {
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
|
||||
Widget updateFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
|
||||
Widget
|
||||
updateFlowWidget = BlocListener<BlogPostCreateCubit, BlogPostCreateState>(
|
||||
listener: (context, state) {
|
||||
if (state is BlogPostCreateStateLoaded) {
|
||||
if (state.isSuccess) {
|
||||
feedProbe.loadFeedBlogPostList();
|
||||
}
|
||||
},
|
||||
@@ -161,13 +193,17 @@ void main() {
|
||||
decoration: const InputDecoration(labelText: 'Описание'),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
controller: TextEditingController(text: existingBlogPost.description),
|
||||
controller: TextEditingController(
|
||||
text: existingBlogPost.description,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео'),
|
||||
controller: TextEditingController(text: existingBlogPost.videoUrl),
|
||||
controller: TextEditingController(
|
||||
text: existingBlogPost.videoUrl,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
@@ -178,8 +214,9 @@ void main() {
|
||||
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?,
|
||||
existingMediaIds: List<int>.from(
|
||||
updateFixture['existing_media_ids'] as List,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Обновить'),
|
||||
@@ -192,9 +229,18 @@ void main() {
|
||||
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.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));
|
||||
@@ -205,7 +251,9 @@ void main() {
|
||||
expect(feedProbe.loadRequests, 1);
|
||||
});
|
||||
|
||||
testWidgets('should handle blog post deletion flow', (WidgetTester tester) async {
|
||||
testWidgets('should handle blog post deletion flow', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget deleteFlowWidget = Column(
|
||||
children: [
|
||||
@@ -230,35 +278,44 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
// Assert
|
||||
final deleteFixture = FixtureReader.readAsMap('blog_post_create/delete_request.json');
|
||||
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 {
|
||||
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');
|
||||
},
|
||||
);
|
||||
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));
|
||||
@@ -269,7 +326,6 @@ void main() {
|
||||
title: fullCreateFixture['title'] as String?,
|
||||
videoUrl: null,
|
||||
description: fullCreateFixture['description_md'] as String?,
|
||||
image: null,
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
@@ -278,18 +334,27 @@ void main() {
|
||||
expect(find.byKey(const Key('retryButton')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should handle loading state during blog post creation', (WidgetTester tester) async {
|
||||
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');
|
||||
},
|
||||
);
|
||||
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));
|
||||
@@ -300,7 +365,6 @@ void main() {
|
||||
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
|
||||
@@ -314,29 +378,32 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('should handle success state after blog post creation', (WidgetTester tester) async {
|
||||
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');
|
||||
},
|
||||
),
|
||||
);
|
||||
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));
|
||||
@@ -347,7 +414,6 @@ void main() {
|
||||
title: fullCreateFixture['title'] as String?,
|
||||
videoUrl: null,
|
||||
description: fullCreateFixture['description_md'] as String?,
|
||||
image: null,
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
@@ -357,28 +423,43 @@ void main() {
|
||||
expect(feedProbe.loadRequests, 1);
|
||||
});
|
||||
|
||||
testWidgets('should handle form validation flow', (WidgetTester tester) async {
|
||||
testWidgets('should handle form validation flow', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
// Arrange
|
||||
Widget validationFlowWidget = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenTitleTextField,
|
||||
decoration: const InputDecoration(labelText: 'Заголовок', errorText: 'Заголовок обязателен'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Заголовок',
|
||||
errorText: 'Заголовок обязателен',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenContentTextField,
|
||||
decoration: const InputDecoration(labelText: 'Описание', errorText: 'Описание обязательно'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Описание',
|
||||
errorText: 'Описание обязательно',
|
||||
),
|
||||
maxLines: 6,
|
||||
minLines: 4,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
key: AppKeys.blogPostCreateScreenVideoLinkTextField,
|
||||
decoration: const InputDecoration(labelText: 'Ссылка на видео', errorText: 'Введите корректную ссылку'),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ссылка на видео',
|
||||
errorText: 'Введите корректную ссылку',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(key: AppKeys.blogPostCreateScreenCreateButton, onPressed: () {}, child: const Text('Опубликовать')),
|
||||
ElevatedButton(
|
||||
key: AppKeys.blogPostCreateScreenCreateButton,
|
||||
onPressed: () {},
|
||||
child: const Text('Опубликовать'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -415,7 +496,11 @@ void main() {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add_photo_alternate, size: 48, color: Colors.grey),
|
||||
Icon(
|
||||
Icons.add_photo_alternate,
|
||||
size: 48,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text('Добавить изображение'),
|
||||
],
|
||||
@@ -431,7 +516,6 @@ void main() {
|
||||
title: 'Test Title',
|
||||
videoUrl: null,
|
||||
description: 'Test Description',
|
||||
image: null, // В реальном тесте здесь был бы XFile
|
||||
);
|
||||
},
|
||||
child: const Text('Опубликовать'),
|
||||
@@ -450,90 +534,122 @@ void main() {
|
||||
expect(imageUploadInitiated, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('should handle complete blog post creation with all features', (WidgetTester tester) async {
|
||||
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('Добавить изображение'),
|
||||
],
|
||||
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('Опубликовать'),
|
||||
),
|
||||
],
|
||||
),
|
||||
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.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.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(
|
||||
blogPostRepository.createPayloads.single.toJson(),
|
||||
fullCreateFixture,
|
||||
);
|
||||
expect(feedProbe.loadRequests, 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:eatvio_app/features/blog_post_create/presentation/pages/blog_pos
|
||||
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/post_feed_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:eatvio_app/generated/l10n.dart';
|
||||
@@ -19,14 +20,15 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:network_image_mock/network_image_mock.dart';
|
||||
|
||||
import 'blog_post_create_screen_test.mocks.dart';
|
||||
class MockBlogPostRepository extends Mock implements BlogPostRepository {}
|
||||
|
||||
class MockFeedCubit extends Mock implements FeedCubit {}
|
||||
|
||||
@GenerateMocks([BlogPostRepository, BlogPostCreateCubit, FeedCubit])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@@ -37,13 +39,22 @@ void main() {
|
||||
|
||||
setUp(() async {
|
||||
await sl.reset();
|
||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
||||
dotenv.loadFromString(
|
||||
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
|
||||
);
|
||||
AppConfigRegistration.registerAppConfig(buildType: AppBuildType.tests);
|
||||
registerFallbackValue(<XFile>[]);
|
||||
registerFallbackValue(<int>[]);
|
||||
mockBlogPostRepository = MockBlogPostRepository();
|
||||
blogPostCreateCubit = BlogPostCreateCubit(blogPostRepository: mockBlogPostRepository);
|
||||
blogPostCreateCubit = BlogPostCreateCubit(
|
||||
blogPostRepository: mockBlogPostRepository,
|
||||
);
|
||||
mockFeedCubit = MockFeedCubit();
|
||||
when(mockFeedCubit.state).thenReturn(const FeedState.loading());
|
||||
when(mockFeedCubit.stream).thenAnswer((_) => Stream.value(const FeedState.loading()));
|
||||
when(() => mockFeedCubit.state).thenReturn(const FeedState.loading());
|
||||
when(
|
||||
() => mockFeedCubit.stream,
|
||||
).thenAnswer((_) => Stream.value(const FeedState.loading()));
|
||||
when(() => mockFeedCubit.loadFeedBlogPostList()).thenAnswer((_) async {});
|
||||
sl.registerLazySingleton<FeedCubit>(() => mockFeedCubit);
|
||||
sl.registerLazySingleton<BlogPostCreateCubit>(() => blogPostCreateCubit);
|
||||
});
|
||||
@@ -52,19 +63,25 @@ void main() {
|
||||
blogPostCreateCubit.close();
|
||||
});
|
||||
|
||||
Widget createTestWidget({FeedBlogPostModel? feedBlogPostModel, bool canPop = false}) {
|
||||
Widget createTestWidget({
|
||||
FeedBlogPostModel? feedBlogPostModel,
|
||||
bool canPop = false,
|
||||
}) {
|
||||
final router = GoRouter(
|
||||
initialLocation: '/blog-post-create',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
builder: (context, state) => const Scaffold(body: Center(child: Text('Home'))),
|
||||
builder: (context, state) =>
|
||||
const Scaffold(body: Center(child: Text('Home'))),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/blog-post-create',
|
||||
builder: (context, state) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<BlogPostCreateCubit>.value(value: blogPostCreateCubit),
|
||||
BlocProvider<BlogPostCreateCubit>.value(
|
||||
value: blogPostCreateCubit,
|
||||
),
|
||||
BlocProvider<FeedCubit>.value(value: mockFeedCubit),
|
||||
],
|
||||
child: BlogPostCreateScreen(feedBlogPostModel: feedBlogPostModel),
|
||||
@@ -87,28 +104,46 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('displays all form fields when creating new blog post', (tester) async {
|
||||
testWidgets('displays all form fields when creating new blog post', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Заголовок'), findsOneWidget);
|
||||
expect(find.text('Описание'), findsOneWidget);
|
||||
expect(find.text('Изображение поста'), findsOneWidget);
|
||||
expect(find.textContaining('Изображения'), findsOneWidget);
|
||||
expect(find.text('Ссылка на видео'), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenContentTextField), findsOneWidget);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenContentTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('pre-fills form fields when editing existing blog post', (tester) async {
|
||||
testWidgets('pre-fills form fields when editing existing blog post', (
|
||||
tester,
|
||||
) async {
|
||||
final existingPost = FeedBlogPostModel(
|
||||
id: 1,
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
videoUrl: 'https://youtube.com/watch?v=test',
|
||||
image: '', // Changed: removed image so video field is visible
|
||||
videoUrl: 'https://rutube.ru/video/12345/',
|
||||
image: '',
|
||||
state: 'published',
|
||||
author: const Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'avatar.jpg', authorIsMe: true),
|
||||
author: const Author(
|
||||
authorSlug: 'test-author',
|
||||
authorName: 'Test Author',
|
||||
authorProfileIcon: 'avatar.jpg',
|
||||
authorIsMe: true,
|
||||
),
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
totalLikes: 0,
|
||||
@@ -117,24 +152,299 @@ void main() {
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(feedBlogPostModel: existingPost));
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(feedBlogPostModel: existingPost),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final titleField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenTitleTextField));
|
||||
final titleField = tester.widget<CustomTextFormField>(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
);
|
||||
expect(titleField.controller?.text, 'Test Title');
|
||||
|
||||
final descriptionField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenContentTextField));
|
||||
final descriptionField = tester.widget<CustomTextFormField>(
|
||||
find.byKey(AppKeys.blogPostCreateScreenContentTextField),
|
||||
);
|
||||
expect(descriptionField.controller?.text, 'Test Description');
|
||||
|
||||
final videoFieldFinder = find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField);
|
||||
if (videoFieldFinder.evaluate().isNotEmpty) {
|
||||
final videoField = tester.widget<CustomTextFormField>(videoFieldFinder);
|
||||
expect(videoField.controller?.text, 'https://youtube.com/watch?v=test');
|
||||
}
|
||||
final videoField = tester.widget<CustomTextFormField>(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
);
|
||||
expect(videoField.controller?.text, 'https://rutube.ru/video/12345/');
|
||||
expect(find.text('Сохранить'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hides video field when image is present', (tester) async {
|
||||
testWidgets('shows video and gallery fields together', (tester) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
final existingPost = FeedBlogPostModel(
|
||||
id: 1,
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
videoUrl: 'https://rutube.ru/video/12345/',
|
||||
image: 'http://localhost/storage/test_image.jpg',
|
||||
state: 'published',
|
||||
author: const Author(
|
||||
authorSlug: 'test-author',
|
||||
authorName: 'Test Author',
|
||||
authorProfileIcon: 'http://localhost/storage/avatar.jpg',
|
||||
authorIsMe: true,
|
||||
),
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
totalLikes: 0,
|
||||
totalViews: 0,
|
||||
totalComments: 0,
|
||||
wasLikedByMe: false,
|
||||
images: const [
|
||||
FeedGalleryImage(
|
||||
id: 10,
|
||||
url: 'http://localhost/storage/test_image.jpg',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(feedBlogPostModel: existingPost),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Ссылка на видео'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.textContaining('Изображения'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('allows entering text in title field', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
'New Title',
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final titleField = tester.widget<CustomTextFormField>(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
);
|
||||
expect(titleField.controller?.text, 'New Title');
|
||||
});
|
||||
|
||||
testWidgets('allows entering text in description field', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenContentTextField),
|
||||
'New Description',
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final descriptionField = tester.widget<CustomTextFormField>(
|
||||
find.byKey(AppKeys.blogPostCreateScreenContentTextField),
|
||||
);
|
||||
expect(descriptionField.controller?.text, 'New Description');
|
||||
});
|
||||
|
||||
testWidgets('allows entering text in video URL field', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
'https://youtube.com/watch?v=new',
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final videoField = tester.widget<CustomTextFormField>(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
);
|
||||
expect(videoField.controller?.text, 'https://youtube.com/watch?v=new');
|
||||
});
|
||||
|
||||
testWidgets('displays create button', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Создать'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('does not submit when form is empty', (tester) async {
|
||||
when(
|
||||
() => mockBlogPostRepository.createBlogPost(
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
images: any(named: 'images'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final createButton = find.text('Создать');
|
||||
await tester.ensureVisible(createButton);
|
||||
await tester.tap(createButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verifyNever(
|
||||
() => mockBlogPostRepository.createBlogPost(
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
images: any(named: 'images'),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
find.text(
|
||||
'Добавьте заголовок, описание, изображение или ссылку на видео',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('does not submit when video url is invalid', (tester) async {
|
||||
when(
|
||||
() => mockBlogPostRepository.createBlogPost(
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
images: any(named: 'images'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
'Test Title',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
'https://youtube.com/watch?v=abc',
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final createButton = find.text('Создать');
|
||||
await tester.ensureVisible(createButton);
|
||||
await tester.tap(createButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verifyNever(
|
||||
() => mockBlogPostRepository.createBlogPost(
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
images: any(named: 'images'),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
find.text('Поддерживаются только Rutube, VK и OK'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('shows loading state when creating blog post', (tester) async {
|
||||
when(
|
||||
() => mockBlogPostRepository.createBlogPost(
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
images: any(named: 'images'),
|
||||
),
|
||||
).thenAnswer((_) async {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
return;
|
||||
});
|
||||
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
'Test Title',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenContentTextField),
|
||||
'Test Description',
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final createButton = find.text('Создать');
|
||||
await tester.ensureVisible(createButton);
|
||||
await tester.tap(createButton);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(Loader), findsWidgets);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'calls FeedCubit.loadFeedBlogPostList when blog post is created successfully',
|
||||
(tester) async {
|
||||
when(
|
||||
() => mockBlogPostRepository.createBlogPost(
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
images: any(named: 'images'),
|
||||
),
|
||||
).thenAnswer((_) async {
|
||||
return;
|
||||
});
|
||||
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final titleField = find.byKey(
|
||||
AppKeys.blogPostCreateScreenTitleTextField,
|
||||
);
|
||||
final contentField = find.byKey(
|
||||
AppKeys.blogPostCreateScreenContentTextField,
|
||||
);
|
||||
|
||||
await tester.ensureVisible(titleField);
|
||||
await tester.ensureVisible(contentField);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(titleField, 'Test Title');
|
||||
await tester.enterText(contentField, 'Test Description');
|
||||
await tester.pump();
|
||||
|
||||
final createButton = find.text('Создать');
|
||||
await tester.ensureVisible(createButton);
|
||||
await tester.tap(createButton, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => mockFeedCubit.loadFeedBlogPostList()).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('does not submit update when video url is invalid', (
|
||||
tester,
|
||||
) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
when(
|
||||
() => mockBlogPostRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
final existingPost = FeedBlogPostModel(
|
||||
id: 1,
|
||||
title: 'Test Title',
|
||||
@@ -154,136 +464,140 @@ void main() {
|
||||
totalViews: 0,
|
||||
totalComments: 0,
|
||||
wasLikedByMe: false,
|
||||
images: const [
|
||||
FeedGalleryImage(
|
||||
id: 10,
|
||||
url: 'http://localhost/storage/test_image.jpg',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(feedBlogPostModel: existingPost));
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(feedBlogPostModel: existingPost),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Ссылка на видео'), findsNothing);
|
||||
expect(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), findsNothing);
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField),
|
||||
'https://youtube.com/watch?v=abc',
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final saveButton = find.text('Сохранить');
|
||||
await tester.ensureVisible(saveButton);
|
||||
await tester.tap(saveButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verifyNever(
|
||||
() => mockBlogPostRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
find.text('Поддерживаются только Rutube, VK и OK'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('hides image field when video URL is present', (tester) async {
|
||||
final existingPost = FeedBlogPostModel(
|
||||
id: 1,
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
videoUrl: 'https://youtube.com/watch?v=test',
|
||||
image: '',
|
||||
state: 'published',
|
||||
author: const Author(authorSlug: 'test-author', authorName: 'Test Author', authorProfileIcon: 'avatar.jpg', authorIsMe: true),
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
totalLikes: 0,
|
||||
totalViews: 0,
|
||||
totalComments: 0,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(feedBlogPostModel: existingPost));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Изображение поста'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('allows entering text in title field', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'New Title');
|
||||
await tester.pump();
|
||||
|
||||
final titleField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenTitleTextField));
|
||||
expect(titleField.controller?.text, 'New Title');
|
||||
});
|
||||
|
||||
testWidgets('allows entering text in description field', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'New Description');
|
||||
await tester.pump();
|
||||
|
||||
final descriptionField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenContentTextField));
|
||||
expect(descriptionField.controller?.text, 'New Description');
|
||||
});
|
||||
|
||||
testWidgets('allows entering text in video URL field', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField), 'https://youtube.com/watch?v=new');
|
||||
await tester.pump();
|
||||
|
||||
final videoField = tester.widget<CustomTextFormField>(find.byKey(AppKeys.blogPostCreateScreenVideoLinkTextField));
|
||||
expect(videoField.controller?.text, 'https://youtube.com/watch?v=new');
|
||||
});
|
||||
|
||||
testWidgets('displays create button', (tester) async {
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Создать'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows loading state when creating blog post', (tester) async {
|
||||
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
return;
|
||||
});
|
||||
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenTitleTextField), 'Test Title');
|
||||
await tester.enterText(find.byKey(AppKeys.blogPostCreateScreenContentTextField), 'Test Description');
|
||||
await tester.pump();
|
||||
|
||||
final createButton = find.text('Создать');
|
||||
await tester.ensureVisible(createButton);
|
||||
await tester.tap(createButton);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(Loader), findsWidgets);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
|
||||
testWidgets('calls FeedCubit.loadFeedBlogPostList when blog post is created successfully', (tester) async {
|
||||
when(mockBlogPostRepository.createBlogPost(any, any, any, any)).thenAnswer((_) async {
|
||||
return;
|
||||
});
|
||||
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final titleField = find.byKey(AppKeys.blogPostCreateScreenTitleTextField);
|
||||
final contentField = find.byKey(AppKeys.blogPostCreateScreenContentTextField);
|
||||
|
||||
await tester.ensureVisible(titleField);
|
||||
await tester.ensureVisible(contentField);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(titleField, 'Test Title');
|
||||
await tester.enterText(contentField, 'Test Description');
|
||||
await tester.pump();
|
||||
|
||||
final createButton = find.text('Создать');
|
||||
await tester.ensureVisible(createButton);
|
||||
await tester.tap(createButton, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(mockFeedCubit.loadFeedBlogPostList()).called(1);
|
||||
});
|
||||
|
||||
testWidgets('calls updateBlogPost when editing existing post', (tester) async {
|
||||
testWidgets('does not submit update when form content is cleared', (
|
||||
tester,
|
||||
) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
when(mockBlogPostRepository.updateBlogPost(any, any, any, any, any, any)).thenAnswer((_) async {
|
||||
when(
|
||||
() => mockBlogPostRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
final existingPost = FeedBlogPostModel(
|
||||
id: 1,
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
videoUrl: '',
|
||||
image: '',
|
||||
state: 'published',
|
||||
author: const Author(
|
||||
authorSlug: 'test-author',
|
||||
authorName: 'Test Author',
|
||||
authorProfileIcon: 'http://localhost/storage/avatar.jpg',
|
||||
authorIsMe: true,
|
||||
),
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
totalLikes: 0,
|
||||
totalViews: 0,
|
||||
totalComments: 0,
|
||||
wasLikedByMe: false,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(feedBlogPostModel: existingPost),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenTitleTextField),
|
||||
'',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(AppKeys.blogPostCreateScreenContentTextField),
|
||||
'',
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
final saveButton = find.text('Сохранить');
|
||||
await tester.ensureVisible(saveButton);
|
||||
await tester.tap(saveButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verifyNever(
|
||||
() => mockBlogPostRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
find.text(
|
||||
'Добавьте заголовок, описание, изображение или ссылку на видео',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('calls updateBlogPost when editing existing post', (
|
||||
tester,
|
||||
) async {
|
||||
await mockNetworkImagesFor(() async {
|
||||
when(
|
||||
() => mockBlogPostRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
).thenAnswer((_) async {
|
||||
return;
|
||||
});
|
||||
|
||||
@@ -306,26 +620,46 @@ void main() {
|
||||
totalViews: 0,
|
||||
totalComments: 0,
|
||||
wasLikedByMe: false,
|
||||
images: const [
|
||||
FeedGalleryImage(
|
||||
id: 10,
|
||||
url: 'http://localhost/storage/test_image.jpg',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget(feedBlogPostModel: existingPost, canPop: true));
|
||||
await tester.pumpWidget(
|
||||
createTestWidget(feedBlogPostModel: existingPost, canPop: true),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final titleField = find.byKey(AppKeys.blogPostCreateScreenTitleTextField);
|
||||
final titleField = find.byKey(
|
||||
AppKeys.blogPostCreateScreenTitleTextField,
|
||||
);
|
||||
expect(titleField, findsOneWidget);
|
||||
|
||||
await tester.enterText(titleField, 'Updated Title');
|
||||
await tester.pump();
|
||||
|
||||
final createButton = find.text('Создать');
|
||||
await tester.ensureVisible(createButton);
|
||||
await tester.tap(createButton, warnIfMissed: false);
|
||||
final saveButton = find.text('Сохранить');
|
||||
await tester.ensureVisible(saveButton);
|
||||
await tester.tap(saveButton, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(mockBlogPostRepository.updateBlogPost(any, any, any, any, any, any)).called(1);
|
||||
verify(
|
||||
() => mockBlogPostRepository.updateBlogPost(
|
||||
id: any(named: 'id'),
|
||||
title: any(named: 'title'),
|
||||
videoUrl: any(named: 'videoUrl'),
|
||||
description: any(named: 'description'),
|
||||
existingMediaIds: any(named: 'existingMediaIds'),
|
||||
newImages: any(named: 'newImages'),
|
||||
mediaIdsToDelete: any(named: 'mediaIdsToDelete'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -344,8 +678,9 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('renders error state', (tester) async {
|
||||
|
||||
blogPostCreateCubit.emit(const BlogPostCreateState.error(errorMessage: 'Error creating post'));
|
||||
blogPostCreateCubit.emit(
|
||||
const BlogPostCreateState(errorMessage: 'Error creating post'),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
@@ -354,7 +689,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('renders loading state', (tester) async {
|
||||
blogPostCreateCubit.emit(const BlogPostCreateState.loading());
|
||||
blogPostCreateCubit.emit(const BlogPostCreateState(isLoading: true));
|
||||
|
||||
await tester.pumpWidget(createTestWidget());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -25,204 +25,172 @@ void main() {
|
||||
test('deleteBlogPost вызывает сервис', () async {
|
||||
const id = 1;
|
||||
|
||||
when(() => mockService.deleteBlogPost(id: id)).thenAnswer((_) async => {});
|
||||
when(
|
||||
() => mockService.deleteBlogPost(id: id),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
await repository.deleteBlogPost(id);
|
||||
|
||||
verify(() => mockService.deleteBlogPost(id: id)).called(1);
|
||||
});
|
||||
|
||||
test('deleteBlogPostMedia вызывает сервис', () async {
|
||||
when(
|
||||
() => mockService.deleteBlogPostMedia(postId: 10, mediaId: 20),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
await repository.deleteBlogPostMedia(postId: 10, mediaId: 20);
|
||||
|
||||
verify(
|
||||
() => mockService.deleteBlogPostMedia(postId: 10, mediaId: 20),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('createBlogPost вызывает сервис с FormData', () async {
|
||||
const title = 'Test Title';
|
||||
const description = 'Test Description';
|
||||
|
||||
when(() => mockService.createBlogPost(formData: any(named: 'formData'))).thenAnswer((_) async => {});
|
||||
|
||||
await repository.createBlogPost(title, null, description, null);
|
||||
|
||||
verify(() => mockService.createBlogPost(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('updateBlogPost вызывает сервис с FormData', () async {
|
||||
const id = 1;
|
||||
const title = 'Updated Title';
|
||||
|
||||
when(
|
||||
() => mockService.updateBlogPost(
|
||||
id: id,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
() => mockService.createBlogPost(formData: any(named: 'formData')),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
await repository.updateBlogPost(id, title, null, null, null, null);
|
||||
await repository.createBlogPost(
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
);
|
||||
|
||||
verify(
|
||||
() => mockService.updateBlogPost(
|
||||
id: id,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
final captured =
|
||||
verify(
|
||||
() => mockService.createBlogPost(
|
||||
formData: captureAny(named: 'formData'),
|
||||
),
|
||||
).captured.single
|
||||
as FormData;
|
||||
|
||||
test('createBlogPost - обрабатывает пустые поля', () async {
|
||||
when(() => mockService.createBlogPost(formData: any(named: 'formData'))).thenAnswer((_) async => {});
|
||||
|
||||
await repository.createBlogPost(null, null, null, null);
|
||||
|
||||
verify(() => mockService.createBlogPost(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('createBlogPost - обрабатывает только title', () async {
|
||||
const title = 'Test Title';
|
||||
|
||||
when(() => mockService.createBlogPost(formData: any(named: 'formData'))).thenAnswer((_) async => {});
|
||||
|
||||
await repository.createBlogPost(title, null, null, null);
|
||||
|
||||
verify(() => mockService.createBlogPost(formData: any(named: 'formData'))).called(1);
|
||||
expect(
|
||||
captured.fields.map((e) => '${e.key}=${e.value}'),
|
||||
containsAll(['title=Test Title', 'description_md=Test Description']),
|
||||
);
|
||||
expect(captured.files, isEmpty);
|
||||
});
|
||||
|
||||
test('createBlogPost - обрабатывает только videoUrl', () async {
|
||||
const videoUrl = 'https://example.com/video.mp4';
|
||||
|
||||
when(() => mockService.createBlogPost(formData: any(named: 'formData'))).thenAnswer((_) async => {});
|
||||
|
||||
await repository.createBlogPost(null, videoUrl, null, null);
|
||||
|
||||
verify(() => mockService.createBlogPost(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('createBlogPost - обрабатывает только description', () async {
|
||||
const description = 'Test Description';
|
||||
|
||||
when(() => mockService.createBlogPost(formData: any(named: 'formData'))).thenAnswer((_) async => {});
|
||||
|
||||
await repository.createBlogPost(null, null, description, null);
|
||||
|
||||
verify(() => mockService.createBlogPost(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('updateBlogPost - обрабатывает все поля', () async {
|
||||
const id = 1;
|
||||
const title = 'Updated Title';
|
||||
const videoUrl = 'https://example.com/video.mp4';
|
||||
const description = 'Updated Description';
|
||||
const videoUrl = 'https://rutube.ru/video/12345/';
|
||||
|
||||
when(
|
||||
() => mockService.updateBlogPost(
|
||||
id: id,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
() => mockService.createBlogPost(formData: any(named: 'formData')),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
await repository.updateBlogPost(id, title, videoUrl, description, null, null);
|
||||
await repository.createBlogPost(videoUrl: videoUrl);
|
||||
|
||||
verify(
|
||||
() => mockService.updateBlogPost(
|
||||
id: id,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).called(1);
|
||||
final captured =
|
||||
verify(
|
||||
() => mockService.createBlogPost(
|
||||
formData: captureAny(named: 'formData'),
|
||||
),
|
||||
).captured.single
|
||||
as FormData;
|
||||
|
||||
expect(
|
||||
captured.fields.map((e) => '${e.key}=${e.value}'),
|
||||
contains('video_url=$videoUrl'),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'updateBlogPost отправляет existing_media_ids и удаляет media пачкой',
|
||||
() async {
|
||||
when(
|
||||
() => mockService.deleteBlogPostMedia(
|
||||
postId: 1,
|
||||
mediaId: any(named: 'mediaId'),
|
||||
),
|
||||
).thenAnswer((_) async => {});
|
||||
when(
|
||||
() => mockService.updateBlogPost(
|
||||
id: 1,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
await repository.updateBlogPost(
|
||||
id: 1,
|
||||
title: 'Updated Title',
|
||||
existingMediaIds: [5, 3],
|
||||
mediaIdsToDelete: [9, 8],
|
||||
);
|
||||
|
||||
verify(
|
||||
() => mockService.deleteBlogPostMedia(postId: 1, mediaId: 9),
|
||||
).called(1);
|
||||
verify(
|
||||
() => mockService.deleteBlogPostMedia(postId: 1, mediaId: 8),
|
||||
).called(1);
|
||||
|
||||
final captured =
|
||||
verify(
|
||||
() => mockService.updateBlogPost(
|
||||
id: 1,
|
||||
formData: captureAny(named: 'formData'),
|
||||
),
|
||||
).captured.single
|
||||
as FormData;
|
||||
|
||||
final fields = captured.fields
|
||||
.map((e) => '${e.key}=${e.value}')
|
||||
.toList();
|
||||
expect(fields, contains('title=Updated Title'));
|
||||
expect(fields, contains('description_md='));
|
||||
expect(fields, contains('video_url='));
|
||||
expect(fields, contains('existing_media_ids[]=5'));
|
||||
expect(fields, contains('existing_media_ids[]=3'));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'updateBlogPost отправляет пустые text fields для очистки на бэке',
|
||||
() async {
|
||||
when(
|
||||
() => mockService.updateBlogPost(
|
||||
id: 1,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
await repository.updateBlogPost(
|
||||
id: 1,
|
||||
title: '',
|
||||
description: '',
|
||||
videoUrl: '',
|
||||
existingMediaIds: const [],
|
||||
);
|
||||
|
||||
final captured =
|
||||
verify(
|
||||
() => mockService.updateBlogPost(
|
||||
id: 1,
|
||||
formData: captureAny(named: 'formData'),
|
||||
),
|
||||
).captured.single
|
||||
as FormData;
|
||||
|
||||
final fields = captured.fields
|
||||
.map((e) => '${e.key}=${e.value}')
|
||||
.toList();
|
||||
expect(fields, contains('title='));
|
||||
expect(fields, contains('description_md='));
|
||||
expect(fields, contains('video_url='));
|
||||
},
|
||||
);
|
||||
|
||||
test('updateBlogPost - обрабатывает пустые поля', () async {
|
||||
const id = 1;
|
||||
|
||||
when(
|
||||
() => mockService.updateBlogPost(
|
||||
id: id,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
() =>
|
||||
mockService.updateBlogPost(id: 1, formData: any(named: 'formData')),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
await repository.updateBlogPost(id, null, null, null, null, null);
|
||||
await repository.updateBlogPost(id: 1);
|
||||
|
||||
verify(
|
||||
() => mockService.updateBlogPost(
|
||||
id: id,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('createBlogPost - обрабатывает пустые поля', () async {
|
||||
when(() => mockService.createBlogPost(formData: any(named: 'formData'))).thenAnswer((_) async => {});
|
||||
|
||||
await repository.createBlogPost(null, null, null, null);
|
||||
|
||||
verify(() => mockService.createBlogPost(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('createBlogPost - обрабатывает только title', () async {
|
||||
const title = 'Test Title';
|
||||
|
||||
when(() => mockService.createBlogPost(formData: any(named: 'formData'))).thenAnswer((_) async => {});
|
||||
|
||||
await repository.createBlogPost(title, null, null, null);
|
||||
|
||||
verify(() => mockService.createBlogPost(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('createBlogPost - обрабатывает только videoUrl', () async {
|
||||
const videoUrl = 'https://example.com/video.mp4';
|
||||
|
||||
when(() => mockService.createBlogPost(formData: any(named: 'formData'))).thenAnswer((_) async => {});
|
||||
|
||||
await repository.createBlogPost(null, videoUrl, null, null);
|
||||
|
||||
verify(() => mockService.createBlogPost(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('createBlogPost - обрабатывает только description', () async {
|
||||
const description = 'Test Description';
|
||||
|
||||
when(() => mockService.createBlogPost(formData: any(named: 'formData'))).thenAnswer((_) async => {});
|
||||
|
||||
await repository.createBlogPost(null, null, description, null);
|
||||
|
||||
verify(() => mockService.createBlogPost(formData: any(named: 'formData'))).called(1);
|
||||
});
|
||||
|
||||
test('updateBlogPost - обрабатывает все поля', () async {
|
||||
const id = 1;
|
||||
const title = 'Updated Title';
|
||||
const videoUrl = 'https://example.com/video.mp4';
|
||||
const description = 'Updated Description';
|
||||
|
||||
when(
|
||||
() => mockService.updateBlogPost(
|
||||
id: id,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
await repository.updateBlogPost(id, title, videoUrl, description, null, null);
|
||||
|
||||
verify(
|
||||
() => mockService.updateBlogPost(
|
||||
id: id,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('updateBlogPost - обрабатывает пустые поля', () async {
|
||||
const id = 1;
|
||||
|
||||
when(
|
||||
() => mockService.updateBlogPost(
|
||||
id: id,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
).thenAnswer((_) async => {});
|
||||
|
||||
await repository.updateBlogPost(id, null, null, null, null, null);
|
||||
|
||||
verify(
|
||||
() => mockService.updateBlogPost(
|
||||
id: id,
|
||||
formData: any(named: 'formData'),
|
||||
),
|
||||
() =>
|
||||
mockService.updateBlogPost(id: 1, formData: any(named: 'formData')),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,8 +18,9 @@ void main() {
|
||||
test('createBlogPost - создает пост блога', () async {
|
||||
final formData = FormData.fromMap({});
|
||||
|
||||
when(mockBlogPostService.createBlogPost(formData: formData))
|
||||
.thenAnswer((_) async => Future.value());
|
||||
when(
|
||||
mockBlogPostService.createBlogPost(formData: formData),
|
||||
).thenAnswer((_) async => Future.value());
|
||||
|
||||
await mockBlogPostService.createBlogPost(formData: formData);
|
||||
|
||||
@@ -30,19 +31,35 @@ void main() {
|
||||
const id = 1;
|
||||
final formData = FormData.fromMap({});
|
||||
|
||||
when(mockBlogPostService.updateBlogPost(id: id, formData: formData))
|
||||
.thenAnswer((_) async => Future.value());
|
||||
when(
|
||||
mockBlogPostService.updateBlogPost(id: id, formData: formData),
|
||||
).thenAnswer((_) async => Future.value());
|
||||
|
||||
await mockBlogPostService.updateBlogPost(id: id, formData: formData);
|
||||
|
||||
verify(mockBlogPostService.updateBlogPost(id: id, formData: formData)).called(1);
|
||||
verify(
|
||||
mockBlogPostService.updateBlogPost(id: id, formData: formData),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('deleteBlogPostMedia - удаляет изображение поста', () async {
|
||||
when(
|
||||
mockBlogPostService.deleteBlogPostMedia(postId: 1, mediaId: 2),
|
||||
).thenAnswer((_) async => Future.value());
|
||||
|
||||
await mockBlogPostService.deleteBlogPostMedia(postId: 1, mediaId: 2);
|
||||
|
||||
verify(
|
||||
mockBlogPostService.deleteBlogPostMedia(postId: 1, mediaId: 2),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('deleteBlogPost - удаляет пост блога', () async {
|
||||
const id = 1;
|
||||
|
||||
when(mockBlogPostService.deleteBlogPost(id: id))
|
||||
.thenAnswer((_) async => Future.value());
|
||||
when(
|
||||
mockBlogPostService.deleteBlogPost(id: id),
|
||||
).thenAnswer((_) async => Future.value());
|
||||
|
||||
await mockBlogPostService.deleteBlogPost(id: id);
|
||||
|
||||
@@ -50,4 +67,3 @@ void main() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
import 'package:eatvio_app/features/blog_post_create/validation/blog_post_form_validator.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('BlogPostFormValidator.title', () {
|
||||
test('allows empty', () {
|
||||
expect(BlogPostFormValidator.title(''), isNull);
|
||||
expect(BlogPostFormValidator.title(' '), isNull);
|
||||
expect(BlogPostFormValidator.title(null), isNull);
|
||||
});
|
||||
|
||||
test('rejects shorter than 3 chars', () {
|
||||
expect(
|
||||
BlogPostFormValidator.title('ab'),
|
||||
BlogPostFormValidator.titleTooShort,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.title(' a '),
|
||||
BlogPostFormValidator.titleTooShort,
|
||||
);
|
||||
});
|
||||
|
||||
test('allows 3+ chars', () {
|
||||
expect(BlogPostFormValidator.title('abc'), isNull);
|
||||
expect(BlogPostFormValidator.title(' hello '), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('BlogPostFormValidator.description', () {
|
||||
test('allows empty and rejects short', () {
|
||||
expect(BlogPostFormValidator.description(''), isNull);
|
||||
expect(
|
||||
BlogPostFormValidator.description('xy'),
|
||||
BlogPostFormValidator.descriptionTooShort,
|
||||
);
|
||||
expect(BlogPostFormValidator.description('xyz'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('BlogPostFormValidator.videoUrl', () {
|
||||
test('allows empty', () {
|
||||
expect(BlogPostFormValidator.videoUrl(''), isNull);
|
||||
expect(BlogPostFormValidator.videoUrl(' '), isNull);
|
||||
});
|
||||
|
||||
test('rejects unsupported hosts', () {
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('https://youtube.com/watch?v=abc'),
|
||||
BlogPostFormValidator.videoUrlUnsupportedHost,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('https://rutube.ru.evil.com/video/abc'),
|
||||
BlogPostFormValidator.videoUrlUnsupportedHost,
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts rutube / vk / ok formats', () {
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('https://rutube.ru/video/abc123/'),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('https://www.rutube.ru/shorts/abc123'),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('https://vk.com/video-123_456'),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('https://vkvideo.ru/video123_456/'),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('https://ok.ru/video/12345'),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects invalid path formats for allowed hosts', () {
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('https://rutube.ru/channel/abc'),
|
||||
BlogPostFormValidator.videoUrlRutubeFormat,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('https://vk.com/feed'),
|
||||
BlogPostFormValidator.videoUrlVkFormat,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('https://ok.ru/profile/1'),
|
||||
BlogPostFormValidator.videoUrlOkFormat,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects malformed urls', () {
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('not-a-url'),
|
||||
BlogPostFormValidator.videoUrlInvalid,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.videoUrl('ftp://rutube.ru/video/abc'),
|
||||
BlogPostFormValidator.videoUrlInvalid,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('BlogPostFormValidator.contentRequiredMessage', () {
|
||||
test('requires at least one filled field', () {
|
||||
expect(
|
||||
BlogPostFormValidator.contentRequiredMessage(
|
||||
title: '',
|
||||
description: '',
|
||||
videoUrl: '',
|
||||
imageCount: 0,
|
||||
),
|
||||
BlogPostFormValidator.contentRequired,
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts any valid content source', () {
|
||||
expect(
|
||||
BlogPostFormValidator.contentRequiredMessage(
|
||||
title: 'abc',
|
||||
description: '',
|
||||
videoUrl: '',
|
||||
imageCount: 0,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.contentRequiredMessage(
|
||||
title: '',
|
||||
description: 'abc',
|
||||
videoUrl: '',
|
||||
imageCount: 0,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.contentRequiredMessage(
|
||||
title: '',
|
||||
description: '',
|
||||
videoUrl: '',
|
||||
imageCount: 1,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.contentRequiredMessage(
|
||||
title: '',
|
||||
description: '',
|
||||
videoUrl: 'https://rutube.ru/video/abc123/',
|
||||
imageCount: 0,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not count short title or invalid video as content', () {
|
||||
expect(
|
||||
BlogPostFormValidator.contentRequiredMessage(
|
||||
title: 'ab',
|
||||
description: '',
|
||||
videoUrl: 'https://youtube.com/watch?v=1',
|
||||
imageCount: 0,
|
||||
),
|
||||
BlogPostFormValidator.contentRequired,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('BlogPostFormValidator.imagePaths', () {
|
||||
test('allows jpg/jpeg/png', () {
|
||||
expect(
|
||||
BlogPostFormValidator.imagePaths(['a.jpg', 'b.JPEG', 'c.png']),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects other extensions', () {
|
||||
expect(
|
||||
BlogPostFormValidator.imagePaths(['a.webp']),
|
||||
BlogPostFormValidator.imageTypeInvalid,
|
||||
);
|
||||
expect(
|
||||
BlogPostFormValidator.imagePaths(['noext']),
|
||||
BlogPostFormValidator.imageTypeInvalid,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user