feat(personalized_feed): added video and multiple images handling to detailed blog post screen

This commit is contained in:
misha
2026-07-22 14:41:26 +03:00
parent dcf7b25989
commit 2cccc9e253
17 changed files with 595 additions and 246 deletions

View File

@@ -6,6 +6,8 @@ import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import '../../../../fixtures/feed_fixtures.dart';
class MockFeedRepository extends Mock implements FeedRepository {}
void main() {
@@ -29,21 +31,7 @@ void main() {
test('getBlogPost - успешная загрузка поста', () async {
const postId = 1;
final mockPost = FeedBlogPostModel(
id: postId,
title: 'Test Post',
state: 'active',
image: '',
description: 'Test Content',
videoUrl: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
totalLikes: 0,
totalViews: 0,
totalComments: 0,
wasLikedByMe: false,
author: const Author(authorName: 'Test Author', authorSlug: 'test-author', authorProfileIcon: '', authorIsMe: false),
);
final mockPost = FeedFixtures.feedBlogPostModel();
when(() => mockRepository.getBlogPost(id: postId)).thenAnswer((_) async => mockPost);
@@ -51,6 +39,8 @@ void main() {
expect(cubit.state.isLoading, false);
expect(cubit.state.blogPostModel, mockPost);
expect(cubit.state.blogPostModel?.images, hasLength(2));
expect(cubit.state.blogPostModel?.videoEmbedUrl, contains('rutube.ru/play/embed'));
verify(() => mockRepository.getBlogPost(id: postId)).called(1);
});

View File

@@ -7,9 +7,8 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_cubit.dart';
import 'package:eatvio_app/features/blog_post/presentation/cubit/blog_post_state.dart';
import 'package:eatvio_app/features/blog_post/presentation/pages/blog_post_screen.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/presentation/bloc/feed_cubit/feed_cubit.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_media_content.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -21,6 +20,7 @@ import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:network_image_mock/network_image_mock.dart';
import '../../../../fixtures/feed_fixtures.dart';
import 'blog_post_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<BlogPostCubit>(), MockSpec<FeedCubit>(), MockSpec<CommentsCubit>()])
@@ -167,21 +167,7 @@ void main() {
final blogPostCubit = MockBlogPostCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
final blogPostModel = FeedBlogPostModel(
id: 1,
title: 'Test Post Title',
description: 'Test post description',
image: 'https://example.com/image.jpg',
state: 'published',
author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
videoUrl: '',
createdAt: DateTime(2024, 1, 15),
updatedAt: DateTime(2024, 1, 15),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
wasLikedByMe: false,
);
final blogPostModel = FeedFixtures.feedBlogPostModel();
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
when(blogPostCubit.state).thenReturn(state);
@@ -202,7 +188,8 @@ void main() {
await tester.pumpAndSettle();
expect(find.byType(BlogPostScreen), findsOneWidget);
expect(find.text('Test Post Title'), findsOneWidget);
expect(find.text('Заголовок'), findsOneWidget);
expect(find.byType(BlogPostMediaContent), findsOneWidget);
});
});
@@ -211,21 +198,7 @@ void main() {
final blogPostCubit = MockBlogPostCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
final blogPostModel = FeedBlogPostModel(
id: 1,
title: 'Test Post Title',
description: 'Test post description',
image: 'https://example.com/image.jpg',
state: 'published',
author: const Author(authorSlug: 'author-1', authorName: 'Author Name', authorProfileIcon: 'https://example.com/icon.jpg', authorIsMe: false),
videoUrl: '',
createdAt: DateTime(2024, 1, 15),
updatedAt: DateTime(2024, 1, 15),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
wasLikedByMe: false,
);
final blogPostModel = FeedFixtures.feedBlogPostModel();
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
when(blogPostCubit.state).thenReturn(state);
@@ -254,5 +227,95 @@ void main() {
verify(blogPostCubit.toggleLike()).called(1);
});
});
testWidgets('renders gallery from fixture', (tester) async {
await mockNetworkImagesFor(() async {
final blogPostCubit = MockBlogPostCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
final blogPostModel = FeedFixtures.feedBlogPostWithGallery();
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
when(blogPostCubit.state).thenReturn(state);
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(blogPostCubit, commentsCubit);
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, BlogPostScreen(id: blogPostModel.id)));
await tester.pump();
await tester.pumpAndSettle();
expect(find.byType(BlogPostMediaContent), findsOneWidget);
expect(find.text('Галерея'), findsOneWidget);
});
});
testWidgets('renders video-only post', (tester) async {
await mockNetworkImagesFor(() async {
final blogPostCubit = MockBlogPostCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
final blogPostModel = FeedFixtures.feedBlogPostVideoOnly();
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
when(blogPostCubit.state).thenReturn(state);
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(blogPostCubit, commentsCubit);
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, BlogPostScreen(id: blogPostModel.id)));
await tester.pump();
await tester.pumpAndSettle();
expect(find.byType(BlogPostMediaContent), findsOneWidget);
expect(find.text('Видео'), findsOneWidget);
});
});
testWidgets('renders gallery and video post', (tester) async {
await mockNetworkImagesFor(() async {
final blogPostCubit = MockBlogPostCubit();
final feedCubit = MockFeedCubit();
final commentsCubit = MockCommentsCubit();
final blogPostModel = FeedFixtures.feedBlogPostGalleryAndVideo();
final state = BlogPostState(isLoading: false, blogPostModel: blogPostModel, error: null);
when(blogPostCubit.state).thenReturn(state);
when(blogPostCubit.stream).thenAnswer((_) => Stream.value(state));
when(blogPostCubit.close()).thenAnswer((_) async => Future<void>.value());
when(feedCubit.viewPost(any)).thenAnswer((_) async => Future<void>.value());
when(feedCubit.stream).thenAnswer((_) => const Stream.empty());
when(feedCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.state).thenReturn(const CommentsState());
when(commentsCubit.stream).thenAnswer((_) => const Stream<CommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
when(commentsCubit.getComments(any, any, any)).thenAnswer((_) async => Future<void>.value());
registerMocks(blogPostCubit, commentsCubit);
await tester.pumpWidget(wrap(blogPostCubit, feedCubit, commentsCubit, BlogPostScreen(id: blogPostModel.id)));
await tester.pump();
await tester.pumpAndSettle();
expect(find.byType(BlogPostMediaContent), findsOneWidget);
expect(find.text('Галерея и видео'), findsWidgets);
});
});
});
}

View File

@@ -2,6 +2,8 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../fixtures/feed_fixtures.dart';
void main() {
group('FeedBlogPostModel', () {
test('creates FeedBlogPostModel with all parameters', () {
@@ -102,6 +104,52 @@ void main() {
expect(model.totalLikes, 10);
});
test('parses v2 show JSON with gallery images and video_url_embed', () {
final model = FeedBlogPostModel.fromJson({
'id': 1,
'state': 'active',
'image': 'media/1/cover.jpg',
'images': [
{'id': 10, 'url': 'media/1/a.jpg', 'position': 1, 'orientation': 'horizontal'},
{'id': 11, 'url': 'media/1/b.jpg', 'position': 2},
],
'description': 'Описание',
'author': {
'author_slug': 'author-slug',
'author_name': 'Автор',
'author_profile_icon': 'icon.jpg',
'author_is_me': false,
},
'video_url': 'https://rutube.ru/video/abc/',
'video_url_embed': 'https://rutube.ru/play/embed/abc/',
'video_url_thumb': 'media/1/thumb.jpg',
'created_at': '2024-01-15T12:00:00Z',
'updated_at': '2024-01-15T12:00:00Z',
'title': 'Заголовок',
'total_likes': 10,
'total_views': 100,
'total_comments': 5,
'was_liked_by_me': false,
});
expect(model.images, hasLength(2));
expect(model.images.first.id, 10);
expect(model.images.first.url, 'media/1/a.jpg');
expect(model.images.first.orientation, 'horizontal');
expect(model.images[1].orientation, 'horizontal');
expect(model.images[1].position, 2);
expect(model.videoEmbedUrl, 'https://rutube.ru/play/embed/abc/');
expect(model.videoUrlThumb, 'media/1/thumb.jpg');
});
test('parses fixture feedBlogPostModel with gallery and embed', () {
final model = FeedFixtures.feedBlogPostModel();
expect(model.images, hasLength(2));
expect(model.videoEmbedUrl, contains('rutube.ru/play/embed'));
expect(model.videoUrlThumb, 'media/1/thumb.jpg');
});
test('creates FeedBlogPostModel with different states', () {
const author = Author(
authorSlug: 'author',

View File

@@ -85,6 +85,9 @@ void main() {
expect(result, isA<FeedBlogPostModel>());
expect(result.id, postId);
expect(result.images, hasLength(2));
expect(result.videoEmbedUrl, contains('rutube.ru/play/embed'));
expect(result.videoUrlThumb, isNotEmpty);
verify(mockFeedService.getBlogPost(postId: postId)).called(1);
});