merge upstream

This commit is contained in:
2026-08-04 10:17:32 +00:00
88 changed files with 6449 additions and 1957 deletions

View File

@@ -7,6 +7,32 @@ import 'package:go_router/go_router.dart';
void main() {
group('AppRouter', () {
group('resolveDeepLinkTarget - blog_post', () {
test('ru/blog_post/{id} → /blog-post-screen?id=', () {
final target = AppRouter.resolveDeepLinkTarget(
Uri.parse('https://eatvio.ru/ru/blog_post/123'),
);
expect(target, '/blog-post-screen?id=123');
});
test('en/blog_post/{id} → /blog-post-screen?id=', () {
final target = AppRouter.resolveDeepLinkTarget(
Uri.parse('https://eatvio.com/en/blog_post/456'),
);
expect(target, '/blog-post-screen?id=456');
});
test('eatvio.com/blog_post/{id} without locale → /blog-post-screen?id=', () {
final target = AppRouter.resolveDeepLinkTarget(
Uri.parse('https://eatvio.com/blog_post/789'),
);
expect(target, '/blog-post-screen?id=789');
});
});
group('resolveDeepLinkTarget - finance/alfa/return', () {
const orderId = 'alfa_c29_k15138_t1712345678_r1234';

View File

@@ -51,8 +51,11 @@ void main() {
final blogPostRoute = routes.firstWhere(
(route) => route is GoRoute && route.path == '/blog-post-screen',
orElse: () => throw Exception('Route not found'),
);
) as GoRoute;
expect(blogPostRoute, isA<GoRoute>());
expect(blogPostRoute.path, '/blog-post-screen');
expect(blogPostRoute.name, 'blogPostScreen');
expect(blogPostRoute.builder, isNotNull);
});
test('aboutPlanScreen builder обрабатывает slug из query параметров', () {

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);
@@ -248,11 +221,104 @@ void main() {
await tester.pump();
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.favorite_border_rounded));
final likeIcon = find.byIcon(Icons.favorite_border_rounded);
await tester.ensureVisible(likeIcon);
await tester.pumpAndSettle();
await tester.tap(likeIcon);
await tester.pump();
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

@@ -33,4 +33,3 @@ void main() {
});
});
}

View File

@@ -100,4 +100,3 @@ void main() {
});
});
}

View File

@@ -63,7 +63,10 @@ void main() {
});
test('toJson serializes all fields correctly', () {
const model = CertificatesModel(title: 'Certificate Title', date: '2024-01-15');
const model = CertificatesModel(
title: 'Certificate Title',
date: '2024-01-15',
);
final json = model.toJson();
@@ -105,8 +108,21 @@ void main() {
test('toJson serializes all fields correctly', () {
const socialLinks = SocialLinksModel(vkLink: 'https://vk.com/user');
const certificates = CertificatesModel(title: 'Course 1', date: '2024-01-01');
const model = ConsultantProfileModel(hasMedicalEducation: true, socialLinks: socialLinks, photos: ['photo1.jpg'], certificates: [certificates]);
const certificates = CertificatesModel(
title: 'Course 1',
date: '2024-01-01',
);
const model = ConsultantProfileModel(
hasMedicalEducation: true,
socialLinks: socialLinks,
photos: ['photo1.jpg'],
certificates: [certificates],
legalType: 'individualPerson',
inn: '123456789012',
snils: '123-456-789 00',
agreementAccepted: true,
frdoAgreementAccepted: true,
);
final json = model.toJson();
@@ -114,6 +130,34 @@ void main() {
expect(json['social_links'], isNotNull);
expect(json['cert_photos'], ['photo1.jpg']);
expect(json['completed_courses'], isNotNull);
expect(json['legal_type'], 'individualPerson');
expect(json['inn'], '123456789012');
expect(json['snils'], '123-456-789 00');
expect(json['agreement_accepted'], isTrue);
expect(json['frdo_agreement_accepted'], isTrue);
});
test('hasValidPassportData validates required individualPerson fields', () {
const valid = ConsultantProfileModel(
lastName: 'Иванов',
firstName: 'Иван',
inn: '123456789012',
snils: '123-456-789 00',
passportSeries: '1234',
passportNumber: '567890',
passportIssueDate: '2020-01-15',
passportIssuer: 'ОВД',
passportIssuerCode: '772-053',
birthDate: '1990-05-20',
birthPlace: 'Москва',
registrationAddress: 'Адрес 1',
residenceAddress: 'Адрес 2',
citizenship: 'RU',
);
expect(valid.hasValidPassportData, isTrue);
expect(valid.copyWith(inn: '123').hasValidPassportData, isFalse);
expect(valid.copyWith(lastName: '').hasValidPassportData, isFalse);
});
});
}

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/features/consultant/become_consultant/enums/alfa_scoring_status.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -46,7 +47,11 @@ void main() {
});
test('fromJson handles null additional field', () {
final json = {'title': 'Test title', 'status': 'canceled', 'additional': null};
final json = {
'title': 'Test title',
'status': 'canceled',
'additional': null,
};
final model = CanceledModerationModel.fromJson(json);
@@ -56,7 +61,11 @@ void main() {
});
test('toJson serializes all fields correctly', () {
const model = CanceledModerationModel(title: 'Test title', status: ModerationStatus.canceled, additional: ['reason1', 'reason2']);
const model = CanceledModerationModel(
title: 'Test title',
status: ModerationStatus.canceled,
additional: ['reason1', 'reason2'],
);
final json = model.toJson();
@@ -122,10 +131,67 @@ void main() {
expect(model.canceledModeration!.status, ModerationStatus.canceled);
});
test('fromJson parses legal_entity_resource', () {
final json = {
'rating_place': 5,
'education_resource': {'request_state': 'approved'},
'cert_photos': [],
'legal_entity_resource': {
'id': 1,
'type': 'individualPerson',
'inn': '123456789012',
'snils': '123-456-789 00',
'state': 'created',
'alfa_scoring_status': 'scoring_failed',
'alfa_scoring_request_id': 'abc',
'passport_series': '1234',
'passport_number': '567890',
'passport_issue_date': '2020-01-15T00:00:00.000000Z',
'passport_issuer': 'ОВД',
'passport_issuer_code': '772-053',
'birth_date': '1990-05-20',
'birth_place': 'Москва',
'registration_address': 'Адрес 1',
'residence_address': 'Адрес 2',
},
};
final model = ConsultationRequestModel.fromJson(json);
expect(model.legalEntity, isNotNull);
expect(model.legalEntity!.type, 'individualPerson');
expect(model.legalEntity!.inn, '123456789012');
expect(model.legalEntity!.passportSeries, '1234');
expect(model.legalEntity!.normalizedPassportIssueDate, '2020-01-15');
expect(
model.legalEntity!.alfaScoringStatus,
AlfaScoringStatus.scoringFailed,
);
});
test('normalizeApiDate strips time from DateTime.toString format', () {
expect(
LegalEntityModel.normalizeApiDate('2023-09-22 00:00:00.000'),
'2023-09-22',
);
expect(LegalEntityModel.normalizeApiDate('2023-09-22'), '2023-09-22');
expect(LegalEntityModel.normalizeApiDate(null), isNull);
});
test('toJson serializes ConsultationRequestModel correctly', () {
const educationModel = EducationModel(requestState: ModerationStatus.approved);
const photos = [CertPhotosModel(url: 'https://example.com/photo1.jpg'), CertPhotosModel(url: 'https://example.com/photo2.jpg')];
const model = ConsultationRequestModel(ratingPlace: 5, educationModel: educationModel, photos: photos, canceledModeration: null);
const educationModel = EducationModel(
requestState: ModerationStatus.approved,
);
const photos = [
CertPhotosModel(url: 'https://example.com/photo1.jpg'),
CertPhotosModel(url: 'https://example.com/photo2.jpg'),
];
const model = ConsultationRequestModel(
ratingPlace: 5,
educationModel: educationModel,
photos: photos,
canceledModeration: null,
);
final json = model.toJson();

View File

@@ -12,7 +12,7 @@ void main() {
acceptOffer: false,
loading: false,
requestModel: null,
isUpdating: false
isUpdating: false,
);
expect(state.consultant, consultant);
@@ -20,6 +20,8 @@ void main() {
expect(state.loading, false);
expect(state.requestModel, isNull);
expect(state.certPhotos, isEmpty);
expect(state.awaitingScoringOutcome, isFalse);
expect(state.fieldErrors, isEmpty);
});
test('creates instance with custom values', () {
@@ -38,7 +40,7 @@ void main() {
loading: true,
requestModel: null,
certPhotos: [certPhoto],
isUpdating: false
isUpdating: false,
);
expect(state.acceptOffer, true);
@@ -54,7 +56,7 @@ void main() {
acceptOffer: false,
loading: false,
requestModel: null,
isUpdating: false
isUpdating: false,
);
final updatedState = originalState.copyWith(
@@ -92,7 +94,7 @@ void main() {
loading: false,
requestModel: null,
certPhotos: [certPhoto1],
isUpdating: false
isUpdating: false,
);
final updatedState = originalState.copyWith(
@@ -100,9 +102,14 @@ void main() {
);
expect(updatedState.certPhotos.length, 2);
expect(updatedState.certPhotos.first.url, 'https://example.com/photo1.jpg');
expect(updatedState.certPhotos.last.url, 'https://example.com/photo2.jpg');
expect(
updatedState.certPhotos.first.url,
'https://example.com/photo1.jpg',
);
expect(
updatedState.certPhotos.last.url,
'https://example.com/photo2.jpg',
);
});
});
}

View File

@@ -1,6 +1,9 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/consultant/become_consultant/enums/alfa_scoring_status.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/sbp_bank_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -23,16 +26,50 @@ void main() {
late RegisterConsultantCubit cubit;
late ConsultationRequestModel requestModel;
late List<ConsultantCertPhotoModel> certPhotosFromFixture;
late List<SbpBankModel> sbpBanksFromFixture;
late ConsultantProfileModel consultantProfile;
ConsultationRequestModel requestWithScoring(String scoringStatus) {
final json = FixtureReader.readAsMap(
'consultant/consultation_request.json',
);
final legal = Map<String, dynamic>.from(
json['legal_entity_resource'] as Map,
);
legal['alfa_scoring_status'] = scoringStatus;
json['legal_entity_resource'] = legal;
return ConsultationRequestModel.fromJson(json);
}
setUp(() {
repository = MockConsultantRepository();
cubit = RegisterConsultantCubit(consultantRepository: repository);
requestModel = ConsultationRequestModel.fromJson(FixtureReader.readAsMap('consultant/consultation_request.json'));
certPhotosFromFixture = FixtureReader.readAsList(
'consultant/consultant_cert_photos.json',
).map((item) => ConsultantCertPhotoModel.fromJson(item as Map<String, dynamic>)).toList();
consultantProfile = ConsultantProfileModel.fromJson(FixtureReader.readAsMap('consultant/consultant_profile_request.json'));
cubit = RegisterConsultantCubit(
consultantRepository: repository,
scoringPollInterval: const Duration(milliseconds: 30),
);
requestModel = ConsultationRequestModel.fromJson(
FixtureReader.readAsMap('consultant/consultation_request.json'),
);
certPhotosFromFixture =
FixtureReader.readAsList('consultant/consultant_cert_photos.json')
.map(
(item) => ConsultantCertPhotoModel.fromJson(
item as Map<String, dynamic>,
),
)
.toList();
sbpBanksFromFixture =
FixtureReader.readAsList('consultant/sbp_banks.json')
.map(
(item) => SbpBankModel.fromJson(item as Map<String, dynamic>),
)
.toList();
consultantProfile = ConsultantProfileModel.fromJson(
FixtureReader.readAsMap('consultant/consultant_profile_request.json'),
);
when(
() => repository.getSbpBanks(),
).thenAnswer((_) async => sbpBanksFromFixture);
});
tearDown(() async {
@@ -44,11 +81,16 @@ void main() {
expect(cubit.state.consultant.hasMedicalEducation, isTrue);
expect(cubit.state.acceptOffer, isFalse);
expect(cubit.state.requestModel, isNull);
expect(cubit.state.alfaScoringStatus, AlfaScoringStatus.none);
});
test('getMyRequestStatus hydrates state with backend data', () async {
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel);
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
when(
() => repository.getMyRequestStatus(any<String>()),
).thenAnswer((_) async => requestModel);
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
await cubit.getMyRequestStatus('my-consultant-slug');
@@ -56,53 +98,308 @@ void main() {
expect(state.loading, isFalse);
expect(state.requestModel, equals(requestModel));
expect(state.consultant.certificates, isNotNull);
// Certificates list is built from backend courses; we just ensure it is not empty.
expect(state.consultant.certificates!.length, greaterThan(0));
expect(state.consultant.certificates!.first.title, 'Нутрициология PRO');
expect(state.consultant.socialLinks?.instaLink, isNull, reason: 'empty links are sanitized');
expect(
state.consultant.socialLinks?.instaLink,
isNull,
reason: 'empty links are sanitized',
);
expect(state.consultant.socialLinks?.vkLink, 'https://vk.com/consultant');
// All cert photos from backend are kept in state (visible + hidden).
expect(state.consultant.legalType, 'individualPerson');
expect(state.consultant.inn, '123456789012');
expect(state.consultant.passportSeries, '1234');
expect(state.consultant.passportIssueDate, '2020-01-15');
expect(state.consultant.birthPlace, 'г. Москва');
expect(state.certPhotos, hasLength(3));
expect(state.certPhotos.map((e) => e.id), containsAll(<int>[101, 102, 103]));
verify(() => repository.getMyRequestStatus('my-consultant-slug')).called(1);
expect(
state.certPhotos.map((e) => e.id),
containsAll(<int>[101, 102, 103]),
);
expect(state.sbpBanks, hasLength(2));
expect(state.sbpBanks.first.bankName, 'Сбербанк');
expect(state.alfaScoringStatus, AlfaScoringStatus.scoringFailed);
expect(state.alfaScoringStatus.canRetry, isTrue);
expect(state.isScoringPolling, isFalse);
expect(
state.awaitingScoringOutcome,
isFalse,
reason: 'hydrate must not arm success sheet',
);
verify(
() => repository.getMyRequestStatus('my-consultant-slug'),
).called(1);
verify(() => repository.getConsultantCertPhotos()).called(1);
verify(() => repository.getSbpBanks()).called(1);
});
test(
'getMyRequestStatus with processing status starts scoring poll',
() async {
final pending = requestWithScoring('scoring_pending');
final complete = requestWithScoring('scoring_complete');
var calls = 0;
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((
_,
) async {
calls++;
return calls == 1 ? pending : complete;
});
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
await cubit.getMyRequestStatus('my-consultant-slug');
expect(cubit.state.alfaScoringStatus, AlfaScoringStatus.scoringPending);
expect(cubit.state.isScoringPolling, isTrue);
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(
cubit.state.alfaScoringStatus,
AlfaScoringStatus.scoringComplete,
);
expect(cubit.state.isScoringPolling, isFalse);
},
);
test('disableLoading stops spinner and loads cert photos', () async {
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
await cubit.disableLoading(slug: 'my-consultant-slug');
expect(cubit.state.loading, isFalse);
expect(cubit.state.certPhotos, hasLength(3));
expect(cubit.state.sbpBanks, hasLength(2));
expect(cubit.userSlug, 'my-consultant-slug');
verify(() => repository.getConsultantCertPhotos()).called(1);
verify(() => repository.getSbpBanks()).called(1);
});
test('searchSbpBanks filters banks by name', () async {
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
await cubit.disableLoading();
expect(cubit.state.loading, isFalse);
// disableLoading now keeps all photos from repository.
expect(cubit.state.certPhotos, hasLength(3));
verify(() => repository.getConsultantCertPhotos()).called(1);
final results = cubit.searchSbpBanks('сбер');
expect(results, hasLength(1));
expect(results.first.bankMemberId, '100000000111');
});
test('updateConsultantProfileModel replaces consultant data and offer flag', () {
cubit.updateConsultantProfileModel(consultantProfile, true);
expect(cubit.state.consultant, consultantProfile);
expect(cubit.state.acceptOffer, isTrue);
expect(cubit.state.loading, isFalse);
});
test('registerConsultant forwards payload with update=false by default', () async {
test('findSbpBankById returns matching bank', () async {
when(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
update: any<bool>(named: 'update'),
),
).thenAnswer((_) async {});
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
await cubit.disableLoading();
await cubit.registerConsultant();
final bank = cubit.findSbpBankById('100000000004');
verify(() => repository.registerConsultant(consultant: cubit.state.consultant, update: false)).called(1);
expect(bank?.bankName, 'Т-Банк');
});
test('registerConsultant sends update=true after getMyRequestStatus call', () async {
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel);
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
test(
'updateConsultantProfileModel replaces consultant data and offer flag',
() {
cubit.updateConsultantProfileModel(consultantProfile, true);
expect(cubit.state.consultant, consultantProfile);
expect(cubit.state.acceptOffer, isTrue);
expect(cubit.state.loading, isFalse);
},
);
test(
'registerConsultant forwards payload with update=false by default',
() async {
when(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
update: any<bool>(named: 'update'),
),
).thenAnswer((_) async {});
cubit.updateConsultantProfileModel(consultantProfile);
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
await cubit.registerConsultant();
final captured =
verify(
() => repository.registerConsultant(
consultant: captureAny<ConsultantProfileModel>(
named: 'consultant',
),
update: false,
),
).captured.single
as ConsultantProfileModel;
expect(captured.legalType, 'individualPerson');
expect(captured.agreementAccepted, isTrue);
expect(captured.frdoAgreementAccepted, isTrue);
expect(captured.inn, isNotNull);
expect(
cubit.state.alfaScoringStatus,
AlfaScoringStatus.beneficiaryCreating,
);
expect(
cubit.state.isScoringPolling,
isFalse,
reason: 'no slug for poll',
);
expect(
cubit.state.awaitingScoringOutcome,
isFalse,
reason: 'cannot await scoring without slug',
);
},
);
test(
'getMyRequestStatus with scoring_complete does not arm awaiting flag',
() async {
when(
() => repository.getMyRequestStatus(any<String>()),
).thenAnswer((_) async => requestWithScoring('scoring_complete'));
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
await cubit.getMyRequestStatus('my-consultant-slug');
expect(
cubit.state.alfaScoringStatus,
AlfaScoringStatus.scoringComplete,
);
expect(cubit.state.awaitingScoringOutcome, isFalse);
},
);
test(
'registerConsultant with slug starts scoring poll after submit',
() async {
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
when(
() => repository.getMyRequestStatus(any<String>()),
).thenAnswer((_) async => requestWithScoring('scoring_complete'));
when(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
update: any<bool>(named: 'update'),
),
).thenAnswer((_) async {});
await cubit.disableLoading(slug: 'my-consultant-slug');
cubit.updateConsultantProfileModel(consultantProfile);
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
await cubit.registerConsultant();
expect(
cubit.state.alfaScoringStatus,
AlfaScoringStatus.beneficiaryCreating,
);
expect(cubit.state.isScoringPolling, isTrue);
expect(cubit.state.awaitingScoringOutcome, isTrue);
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(
cubit.state.alfaScoringStatus,
AlfaScoringStatus.scoringComplete,
);
expect(cubit.state.isScoringPolling, isFalse);
expect(
cubit.state.awaitingScoringOutcome,
isFalse,
reason: 'flag clears on terminal scoring for one-shot success sheet',
);
},
);
test(
'scoring poll failed clears awaiting flag without leaving processing',
() async {
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
when(
() => repository.getMyRequestStatus(any<String>()),
).thenAnswer((_) async => requestWithScoring('scoring_failed'));
when(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
update: any<bool>(named: 'update'),
),
).thenAnswer((_) async {});
await cubit.disableLoading(slug: 'my-consultant-slug');
cubit.updateConsultantProfileModel(consultantProfile);
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
await cubit.registerConsultant();
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(cubit.state.alfaScoringStatus, AlfaScoringStatus.scoringFailed);
expect(cubit.state.isScoringPolling, isFalse);
expect(cubit.state.awaitingScoringOutcome, isFalse);
expect(cubit.state.alfaScoringStatus.canRetry, isTrue);
},
);
test(
'scoring poll keeps processing UI while API still returns none',
() async {
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
var calls = 0;
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((
_,
) async {
calls++;
if (calls <= 2) {
return requestWithScoring('none');
}
return requestWithScoring('scoring_complete');
});
when(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
update: any<bool>(named: 'update'),
),
).thenAnswer((_) async {});
await cubit.disableLoading(slug: 'my-consultant-slug');
cubit.updateConsultantProfileModel(consultantProfile);
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
await cubit.registerConsultant();
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(
cubit.state.alfaScoringStatus,
AlfaScoringStatus.beneficiaryCreating,
);
expect(cubit.state.isScoringPolling, isTrue);
await Future<void>.delayed(const Duration(milliseconds: 80));
expect(
cubit.state.alfaScoringStatus,
AlfaScoringStatus.scoringComplete,
);
expect(cubit.state.isScoringPolling, isFalse);
},
);
test('registerConsultant sends legal payload with update=true', () async {
when(
() => repository.getMyRequestStatus(any<String>()),
).thenAnswer((_) async => requestModel);
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
when(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
@@ -113,21 +410,131 @@ void main() {
await cubit.getMyRequestStatus('my-consultant-slug');
await cubit.registerConsultant();
verify(() => repository.registerConsultant(consultant: cubit.state.consultant, update: true)).called(1);
final captured =
verify(
() => repository.registerConsultant(
consultant: captureAny<ConsultantProfileModel>(
named: 'consultant',
),
update: true,
),
).captured.single
as ConsultantProfileModel;
final json = captured.toJson();
expect(json['legal_type'], 'individualPerson');
expect(json['inn'], '123456789012');
expect(json['passport_series'], '1234');
expect(json.containsKey('agreement_accepted'), isFalse);
expect(json.containsKey('frdo_agreement_accepted'), isFalse);
});
test('retryScoring calls update and restarts poll', () async {
when(
() => repository.getMyRequestStatus(any<String>()),
).thenAnswer((_) async => requestModel);
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
when(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
update: any<bool>(named: 'update'),
),
).thenAnswer((_) async {});
await cubit.getMyRequestStatus('my-consultant-slug');
expect(cubit.state.alfaScoringStatus, AlfaScoringStatus.scoringFailed);
when(
() => repository.getMyRequestStatus(any<String>()),
).thenAnswer((_) async => requestWithScoring('scoring_complete'));
await cubit.retryScoring();
verify(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
update: true,
),
).called(1);
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(cubit.state.alfaScoringStatus, AlfaScoringStatus.scoringComplete);
expect(cubit.state.isScoringPolling, isFalse);
});
test('registerConsultant blocks submit without passport data', () async {
when(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
update: any<bool>(named: 'update'),
),
).thenAnswer((_) async {});
await cubit.registerConsultant();
expect(cubit.state.error, 'Заполните паспортные данные');
verifyNever(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
update: any<bool>(named: 'update'),
),
);
});
test('registerConsultant maps Laravel 422 into fieldErrors', () async {
when(
() => repository.registerConsultant(
consultant: any<ConsultantProfileModel>(named: 'consultant'),
update: any<bool>(named: 'update'),
),
).thenThrow(
DioException(
requestOptions: RequestOptions(
path: '/api/v2/consultant_request/registration',
),
response: Response(
requestOptions: RequestOptions(
path: '/api/v2/consultant_request/registration',
),
statusCode: 422,
data: {
'message': 'The given data was invalid.',
'errors': {
'inn': ['The inn format is invalid.'],
},
},
),
type: DioExceptionType.badResponse,
),
);
cubit.updateConsultantProfileModel(consultantProfile);
cubit.updateAgreements(acceptOffer: true, acceptFrdoAgreement: true);
await cubit.registerConsultant();
expect(cubit.state.fieldErrors['inn'], 'Некорректный ИНН');
expect(cubit.state.error, contains('Некорректный ИНН'));
expect(cubit.state.awaitingScoringOutcome, isFalse);
expect(cubit.state.isScoringPolling, isFalse);
});
test('removeCertPhoto marks cert as deleted locally', () async {
when(() => repository.getMyRequestStatus(any<String>())).thenAnswer((_) async => requestModel);
when(() => repository.getConsultantCertPhotos()).thenAnswer((_) async => certPhotosFromFixture);
when(() => repository.hideConsultantCertPhoto(any<int>())).thenAnswer((_) async {});
when(
() => repository.getMyRequestStatus(any<String>()),
).thenAnswer((_) async => requestModel);
when(
() => repository.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotosFromFixture);
when(
() => repository.hideConsultantCertPhoto(any<int>()),
).thenAnswer((_) async {});
await cubit.getMyRequestStatus('my-consultant-slug');
// All photos from backend are present in state.
expect(cubit.state.certPhotos, hasLength(3));
await cubit.removeCertPhoto(101, null);
// Current implementation only tracks deleted IDs; photos list remains unchanged here.
verifyNever(() => repository.hideConsultantCertPhoto(any<int>()));
expect(cubit.state.deletedPhotoIds, contains(101));
expect(cubit.state.certPhotos, hasLength(3));
@@ -136,7 +543,9 @@ void main() {
test('uploadCertPhotos - не загружает если список пуст', () async {
await cubit.addCertPhotos([]);
verifyNever(() => repository.storeConsultantCertPhotos(files: any(named: 'files')));
verifyNever(
() => repository.storeConsultantCertPhotos(files: any(named: 'files')),
);
});
test('updateConsultantProfileModel - обновляет без acceptOffer', () {
@@ -147,4 +556,38 @@ void main() {
expect(cubit.state.loading, isFalse);
});
});
group('AlfaScoringStatus', () {
test('fromString maps known values', () {
expect(
AlfaScoringStatus.fromString('beneficiary_creating'),
AlfaScoringStatus.beneficiaryCreating,
);
expect(
AlfaScoringStatus.fromString('scoring_pending'),
AlfaScoringStatus.scoringPending,
);
expect(
AlfaScoringStatus.fromString('scoring_complete'),
AlfaScoringStatus.scoringComplete,
);
expect(
AlfaScoringStatus.fromString('scoring_failed'),
AlfaScoringStatus.scoringFailed,
);
expect(
AlfaScoringStatus.fromString('beneficiary_failed'),
AlfaScoringStatus.beneficiaryFailed,
);
expect(AlfaScoringStatus.fromString(null), AlfaScoringStatus.none);
expect(AlfaScoringStatus.fromString('unknown'), AlfaScoringStatus.none);
});
test('helpers', () {
expect(AlfaScoringStatus.scoringPending.isProcessing, isTrue);
expect(AlfaScoringStatus.scoringFailed.canRetry, isTrue);
expect(AlfaScoringStatus.scoringComplete.isComplete, isTrue);
expect(AlfaScoringStatus.scoringComplete.isTerminal, isTrue);
});
});
}

View File

@@ -18,15 +18,28 @@ void main() {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: child),
body: BlocProvider<RegisterConsultantCubit>.value(
value: cubit,
child: child,
),
),
);
}
testWidgets('renders additional settings screen', (tester) async {
final cubit = MockRegisterConsultantCubit();
const consultant = ConsultantProfileModel(hasMedicalEducation: null, socialLinks: null, photos: null, certificates: null);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: null,
photos: null,
certificates: null,
);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));

View File

@@ -21,12 +21,18 @@ import 'certificates_and_diplomas_screen_smoke_test.mocks.dart';
void main() {
group('CertificatesAndDiplomasScreen smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
dotenv.loadFromString(
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
);
});
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
sl.registerLazySingleton<AppConfig>(
() =>
AppConfig(buildType: AppBuildType.tests)
..setBaseUrl('http://localhost'),
);
});
Widget wrap(Widget child) {
@@ -45,15 +51,30 @@ void main() {
testWidgets('renders certificates and diplomas screen', (tester) async {
final cubit = MockRegisterConsultantCubit();
const consultant = ConsultantProfileModel(hasMedicalEducation: null, socialLinks: null, photos: null, certificates: null);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: null,
photos: null,
certificates: null,
);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: wrap(const CertificatesAndDiplomasScreen())));
await tester.pumpWidget(
BlocProvider<RegisterConsultantCubit>.value(
value: cubit,
child: wrap(const CertificatesAndDiplomasScreen()),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
@@ -67,16 +88,28 @@ void main() {
hasMedicalEducation: null,
socialLinks: null,
photos: null,
certificates: [CertificatesModel(title: 'Test Certificate', date: '2025-01-01')],
certificates: [
CertificatesModel(title: 'Test Certificate', date: '2025-01-01'),
],
);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: wrap(const CertificatesAndDiplomasScreen())));
await tester.pumpWidget(
BlocProvider<RegisterConsultantCubit>.value(
value: cubit,
child: wrap(const CertificatesAndDiplomasScreen()),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));

View File

@@ -19,43 +19,88 @@ import 'last_step_screen_test.mocks.dart';
void main() {
group('LastStepScreen smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
dotenv.loadFromString(
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
);
});
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
sl.registerLazySingleton<AppConfig>(
() =>
AppConfig(buildType: AppBuildType.tests)
..setBaseUrl('http://localhost'),
);
});
Widget wrap(Widget child, RegisterConsultantCubit cubit) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: child),
body: BlocProvider<RegisterConsultantCubit>.value(
value: cubit,
child: child,
),
),
);
}
testWidgets('renders last step screen', (tester) async {
testWidgets(
'renders last step screen with both agreements for new registration',
(tester) async {
final cubit = MockRegisterConsultantCubit();
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(
vkLink: null,
tgLink: null,
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(wrap(const LastStepScreen(), cubit));
await tester.pump();
expect(find.byType(LastStepScreen), findsOneWidget);
expect(find.text('Последний шаг'), findsOneWidget);
expect(find.byType(CheckboxListTile), findsNWidgets(2));
expect(find.textContaining('ФРДО'), findsOneWidget);
},
);
testWidgets('hides FRDO checkbox in update mode', (tester) async {
final cubit = MockRegisterConsultantCubit();
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
photos: null,
certificates: null,
const consultant = ConsultantProfileModel();
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: true,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.updateConsultantProfileModel(any, any)).thenReturn(null);
await tester.pumpWidget(wrap(const LastStepScreen(), cubit));
await tester.pump();
expect(find.byType(LastStepScreen), findsOneWidget);
expect(find.text('Последний шаг'), findsOneWidget);
expect(find.byType(CheckboxListTile), findsOneWidget);
expect(find.textContaining('ФРДО'), findsNothing);
});
});
}

View File

@@ -0,0 +1,64 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/passport_data_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockConsultantRepository extends Mock implements ConsultantRepository {}
void main() {
group('PassportDataScreen smoke', () {
late RegisterConsultantCubit cubit;
setUp(() {
cubit = RegisterConsultantCubit(
consultantRepository: MockConsultantRepository(),
);
cubit.emit(
const RegisterConsultantState(
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel()),
acceptOffer: false,
loading: false,
isUpdating: false,
),
);
});
tearDown(() async {
await cubit.close();
});
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(
body: BlocProvider<RegisterConsultantCubit>.value(
value: cubit,
child: child,
),
),
);
}
testWidgets('renders passport data fields', (tester) async {
await tester.pumpWidget(wrap(const PassportDataScreen()));
await tester.pump();
expect(find.byType(PassportDataScreen), findsOneWidget);
expect(find.text('Паспортные данные'), findsOneWidget);
expect(find.text('Фамилия'), findsOneWidget);
expect(find.text('Имя'), findsOneWidget);
expect(find.text('ИНН (12 цифр)'), findsOneWidget);
expect(find.text('СНИЛС'), findsOneWidget);
expect(find.text('Гражданство (ISO, например RU)'), findsOneWidget);
expect(find.text('Телефон для выплат через СБП'), findsOneWidget);
expect(find.text('Банк получатель (поиск)'), findsOneWidget);
expect(find.text('Я являюсь налоговым резидентом США'), findsOneWidget);
});
});
}

View File

@@ -19,12 +19,18 @@ import 'stepper_become_consultant_screen_smoke_test.mocks.dart';
void main() {
group('PhotoOrScansScreen smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
dotenv.loadFromString(
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
);
});
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
sl.registerLazySingleton<AppConfig>(
() =>
AppConfig(buildType: AppBuildType.tests)
..setBaseUrl('http://localhost'),
);
});
Widget wrap(Widget child, RegisterConsultantCubit cubit) {
@@ -38,7 +44,10 @@ void main() {
],
supportedLocales: const [Locale('ru')],
home: Scaffold(
body: BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: child),
body: BlocProvider<RegisterConsultantCubit>.value(
value: cubit,
child: child,
),
),
);
}
@@ -49,11 +58,22 @@ void main() {
final cubit = RegisterConsultantCubit(consultantRepository: repository);
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
socialLinks: SocialLinksModel(
vkLink: null,
tgLink: null,
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
cubit.emit(state);
@@ -61,7 +81,10 @@ void main() {
await tester.pumpAndSettle();
expect(find.byType(PhotoOrScansScreen), findsOneWidget);
expect(find.text('Загрузите фото или сканы дипломов и сертификатов'), findsOneWidget);
expect(
find.text('Загрузите фото или сканы дипломов и сертификатов'),
findsOneWidget,
);
});
});
@@ -71,11 +94,22 @@ void main() {
final cubit = RegisterConsultantCubit(consultantRepository: repository);
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
socialLinks: SocialLinksModel(
vkLink: null,
tgLink: null,
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
cubit.emit(state);

View File

@@ -1,24 +1,59 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/registration_as_screen.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockConsultantRepository extends Mock implements ConsultantRepository {}
void main() {
group('RegistrationAsScreen smoke', () {
late RegisterConsultantCubit cubit;
setUp(() {
cubit = RegisterConsultantCubit(
consultantRepository: MockConsultantRepository(),
);
cubit.emit(
const RegisterConsultantState(
consultant: ConsultantProfileModel(socialLinks: SocialLinksModel()),
acceptOffer: false,
loading: false,
isUpdating: false,
),
);
});
tearDown(() async {
await cubit.close();
});
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
home: Scaffold(
body: BlocProvider<RegisterConsultantCubit>.value(
value: cubit,
child: child,
),
),
);
}
testWidgets('renders registration as screen', (tester) async {
await tester.pumpWidget(wrap(const RegistrationAsScreen()));
await tester.pump();
await tester.pump();
expect(find.byType(RegistrationAsScreen), findsOneWidget);
expect(find.text('Регистрация в качестве'), findsOneWidget);
expect(find.text('Физического лица'), findsOneWidget);
expect(cubit.state.consultant.legalType, 'individualPerson');
});
});
}

View File

@@ -21,12 +21,18 @@ import 'social_networks_screen_smoke_test.mocks.dart';
void main() {
group('SocialNetworksScreen smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
dotenv.loadFromString(
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
);
});
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
sl.registerLazySingleton<AppConfig>(
() =>
AppConfig(buildType: AppBuildType.tests)
..setBaseUrl('http://localhost'),
);
});
Widget wrap(Widget child) {
@@ -47,18 +53,34 @@ void main() {
final cubit = MockRegisterConsultantCubit();
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
socialLinks: SocialLinksModel(
vkLink: null,
tgLink: null,
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: wrap(const SocialNetworksScreen())));
await tester.pumpWidget(
BlocProvider<RegisterConsultantCubit>.value(
value: cubit,
child: wrap(const SocialNetworksScreen()),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
@@ -69,18 +91,34 @@ void main() {
final cubit = MockRegisterConsultantCubit();
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: 'https://vk.com/test', tgLink: 'https://t.me/test', fbLink: null, instaLink: null, personalSiteLink: null),
socialLinks: SocialLinksModel(
vkLink: 'https://vk.com/test',
tgLink: 'https://t.me/test',
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
when(cubit.state).thenReturn(state);
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.updateConsultantProfileModel(any)).thenReturn(null);
await tester.pumpWidget(BlocProvider<RegisterConsultantCubit>.value(value: cubit, child: wrap(const SocialNetworksScreen())));
await tester.pumpWidget(
BlocProvider<RegisterConsultantCubit>.value(
value: cubit,
child: wrap(const SocialNetworksScreen()),
),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));

View File

@@ -21,12 +21,18 @@ import 'stepper_become_consultant_screen_smoke_test.mocks.dart';
void main() {
group('StepperBecomeConsultantScreen smoke', () {
setUpAll(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
dotenv.loadFromString(
envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost',
);
});
setUp(() async {
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
sl.registerLazySingleton<AppConfig>(
() =>
AppConfig(buildType: AppBuildType.tests)
..setBaseUrl('http://localhost'),
);
});
Widget wrap(Widget child) {
@@ -48,11 +54,22 @@ void main() {
final cubit = RegisterConsultantCubit(consultantRepository: repository);
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
socialLinks: SocialLinksModel(
vkLink: null,
tgLink: null,
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
cubit.emit(state);
@@ -71,11 +88,22 @@ void main() {
final cubit = RegisterConsultantCubit(consultantRepository: repository);
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
socialLinks: SocialLinksModel(
vkLink: null,
tgLink: null,
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
cubit.emit(state);
@@ -86,7 +114,7 @@ void main() {
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('шаг 1 из 6'), findsOneWidget);
expect(find.text('шаг 1 из 7'), findsOneWidget);
});
testWidgets('displays progress bar', (tester) async {
@@ -94,11 +122,22 @@ void main() {
final cubit = RegisterConsultantCubit(consultantRepository: repository);
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
socialLinks: SocialLinksModel(
vkLink: null,
tgLink: null,
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
cubit.emit(state);
@@ -117,11 +156,22 @@ void main() {
final cubit = RegisterConsultantCubit(consultantRepository: repository);
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
socialLinks: SocialLinksModel(
vkLink: null,
tgLink: null,
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
cubit.emit(state);
@@ -140,11 +190,22 @@ void main() {
final cubit = RegisterConsultantCubit(consultantRepository: repository);
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
socialLinks: SocialLinksModel(
vkLink: null,
tgLink: null,
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
cubit.emit(state);
@@ -163,11 +224,22 @@ void main() {
final cubit = RegisterConsultantCubit(consultantRepository: repository);
const consultant = ConsultantProfileModel(
hasMedicalEducation: null,
socialLinks: SocialLinksModel(vkLink: null, tgLink: null, fbLink: null, instaLink: null, personalSiteLink: null),
socialLinks: SocialLinksModel(
vkLink: null,
tgLink: null,
fbLink: null,
instaLink: null,
personalSiteLink: null,
),
photos: null,
certificates: null,
);
const state = RegisterConsultantState(consultant: consultant, acceptOffer: false, loading: false, isUpdating: false);
const state = RegisterConsultantState(
consultant: consultant,
acceptOffer: false,
loading: false,
isUpdating: false,
);
cubit.emit(state);

View File

@@ -5,6 +5,7 @@ import 'package:dio/dio.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_cert_photo_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/models/sbp_bank_model.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
import 'package:eatvio_app/features/consultant/become_consultant/services/consultant_service.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -36,42 +37,99 @@ void main() {
setUp(() {
mockService = MockConsultantService();
repository = ConsultantRepository(consultantService: mockService);
consultantProfile = ConsultantProfileModel.fromJson(FixtureReader.readAsMap('consultant/consultant_profile_request.json'));
consultationRequest = ConsultationRequestModel.fromJson(FixtureReader.readAsMap('consultant/consultation_request.json'));
certPhotos = FixtureReader.readAsList('consultant/consultant_cert_photos.json')
.map((item) => ConsultantCertPhotoModel.fromJson(item as Map<String, dynamic>))
.toList();
consultantProfile = ConsultantProfileModel.fromJson(
FixtureReader.readAsMap('consultant/consultant_profile_request.json'),
);
consultationRequest = ConsultationRequestModel.fromJson(
FixtureReader.readAsMap('consultant/consultation_request.json'),
);
certPhotos =
FixtureReader.readAsList('consultant/consultant_cert_photos.json')
.map(
(item) => ConsultantCertPhotoModel.fromJson(
item as Map<String, dynamic>,
),
)
.toList();
});
test('registerConsultant delegates to consultantRegistration when update=false', () async {
when(() => mockService.consultantRegistration(consultantModel: any<ConsultantProfileModel>(named: 'consultantModel'))).thenAnswer((_) async {});
test(
'registerConsultant delegates to consultantRegistration when update=false',
() async {
when(
() => mockService.consultantRegistration(
consultantModel: any<ConsultantProfileModel>(
named: 'consultantModel',
),
),
).thenAnswer((_) async {});
await repository.registerConsultant(consultant: consultantProfile);
await repository.registerConsultant(consultant: consultantProfile);
verify(() => mockService.consultantRegistration(consultantModel: consultantProfile)).called(1);
verifyNever(() => mockService.consultantUpdate(consultantModel: any<ConsultantProfileModel>(named: 'consultantModel')));
});
verify(
() => mockService.consultantRegistration(
consultantModel: consultantProfile,
),
).called(1);
verifyNever(
() => mockService.consultantUpdate(
consultantModel: any<ConsultantProfileModel>(
named: 'consultantModel',
),
),
);
},
);
test('registerConsultant delegates to consultantUpdate when update=true', () async {
when(() => mockService.consultantUpdate(consultantModel: any<ConsultantProfileModel>(named: 'consultantModel'))).thenAnswer((_) async {});
test(
'registerConsultant delegates to consultantUpdate when update=true',
() async {
when(
() => mockService.consultantUpdate(
consultantModel: any<ConsultantProfileModel>(
named: 'consultantModel',
),
),
).thenAnswer((_) async {});
await repository.registerConsultant(consultant: consultantProfile, update: true);
await repository.registerConsultant(
consultant: consultantProfile,
update: true,
);
verify(() => mockService.consultantUpdate(consultantModel: consultantProfile)).called(1);
verifyNever(() => mockService.consultantRegistration(consultantModel: any<ConsultantProfileModel>(named: 'consultantModel')));
});
verify(
() =>
mockService.consultantUpdate(consultantModel: consultantProfile),
).called(1);
verifyNever(
() => mockService.consultantRegistration(
consultantModel: any<ConsultantProfileModel>(
named: 'consultantModel',
),
),
);
},
);
test('getConsultantRequestStatus returns request from service', () async {
when(() => mockService.getConsultantRequestStatus(slug: any<String>(named: 'slug'))).thenAnswer((_) async => consultationRequest);
when(
() => mockService.getConsultantRequestStatus(
slug: any<String>(named: 'slug'),
),
).thenAnswer((_) async => consultationRequest);
final result = await repository.getConsultantRequestStatus('user-slug');
expect(result, equals(consultationRequest));
verify(() => mockService.getConsultantRequestStatus(slug: 'user-slug')).called(1);
verify(
() => mockService.getConsultantRequestStatus(slug: 'user-slug'),
).called(1);
});
test('getMyRequestStatus returns request from service', () async {
when(() => mockService.getMyRequestStatus(slug: any<String>(named: 'slug'))).thenAnswer((_) async => consultationRequest);
when(
() => mockService.getMyRequestStatus(slug: any<String>(named: 'slug')),
).thenAnswer((_) async => consultationRequest);
final result = await repository.getMyRequestStatus('my-slug');
@@ -80,7 +138,9 @@ void main() {
});
test('getConsultantCertPhotos returns photos from service', () async {
when(() => mockService.getConsultantCertPhotos()).thenAnswer((_) async => certPhotos);
when(
() => mockService.getConsultantCertPhotos(),
).thenAnswer((_) async => certPhotos);
final result = await repository.getConsultantCertPhotos();
@@ -88,42 +148,92 @@ void main() {
verify(() => mockService.getConsultantCertPhotos()).called(1);
});
test('storeConsultantCertPhotos returns empty list when no files provided', () async {
final result = await repository.storeConsultantCertPhotos(files: []);
test('getSbpBanks returns banks from service', () async {
final banks = FixtureReader.readAsList('consultant/sbp_banks.json')
.map((item) => SbpBankModel.fromJson(item as Map<String, dynamic>))
.toList();
when(() => mockService.getSbpBanks()).thenAnswer((_) async => banks);
expect(result, isEmpty);
verifyNever(() => mockService.storeConsultantCertPhotos(photos: any<List<MultipartFile>>(named: 'photos')));
final result = await repository.getSbpBanks();
expect(result, banks);
verify(() => mockService.getSbpBanks()).called(1);
});
test('storeConsultantCertPhotos uploads prepared files and returns response', () async {
final mockFile = MockFile();
when(() => mockFile.readAsBytes()).thenAnswer((_) async => Uint8List.fromList([1, 2, 3]));
when(() => mockFile.path).thenReturn('/tmp/certificate.png');
when(() => mockService.storeConsultantCertPhotos(photos: any<List<MultipartFile>>(named: 'photos'))).thenAnswer((_) async => certPhotos);
test(
'storeConsultantCertPhotos returns empty list when no files provided',
() async {
final result = await repository.storeConsultantCertPhotos(files: []);
final result = await repository.storeConsultantCertPhotos(files: [mockFile]);
expect(result, isEmpty);
verifyNever(
() => mockService.storeConsultantCertPhotos(
photos: any<List<MultipartFile>>(named: 'photos'),
),
);
},
);
expect(result, certPhotos);
final capturedArgument = verify(() => mockService.storeConsultantCertPhotos(photos: captureAny<List<MultipartFile>>(named: 'photos'))).captured.single as List<MultipartFile>;
expect(capturedArgument, hasLength(1));
expect(capturedArgument.first.filename, 'certificate.png');
});
test(
'storeConsultantCertPhotos uploads prepared files and returns response',
() async {
final mockFile = MockFile();
when(
() => mockFile.readAsBytes(),
).thenAnswer((_) async => Uint8List.fromList([1, 2, 3]));
when(() => mockFile.path).thenReturn('/tmp/certificate.png');
when(
() => mockService.storeConsultantCertPhotos(
photos: any<List<MultipartFile>>(named: 'photos'),
),
).thenAnswer((_) async => certPhotos);
final result = await repository.storeConsultantCertPhotos(
files: [mockFile],
);
expect(result, certPhotos);
final capturedArgument =
verify(
() => mockService.storeConsultantCertPhotos(
photos: captureAny<List<MultipartFile>>(named: 'photos'),
),
).captured.single
as List<MultipartFile>;
expect(capturedArgument, hasLength(1));
expect(capturedArgument.first.filename, 'certificate.png');
},
);
test('hideConsultantCertPhoto sends payload to service', () async {
when(() => mockService.hideConsultantCertPhoto(payload: any<Map<String, dynamic>>(named: 'payload'))).thenAnswer((_) async {});
when(
() => mockService.hideConsultantCertPhoto(
payload: any<Map<String, dynamic>>(named: 'payload'),
),
).thenAnswer((_) async {});
await repository.hideConsultantCertPhoto(42);
verify(() => mockService.hideConsultantCertPhoto(payload: {'cert_photo_id': 42})).called(1);
verify(
() =>
mockService.hideConsultantCertPhoto(payload: {'cert_photo_id': 42}),
).called(1);
});
test('unHideConsultantCertPhoto sends payload to service', () async {
when(() => mockService.unHideConsultantCertPhoto(payload: any<Map<String, dynamic>>(named: 'payload'))).thenAnswer((_) async {});
when(
() => mockService.unHideConsultantCertPhoto(
payload: any<Map<String, dynamic>>(named: 'payload'),
),
).thenAnswer((_) async {});
await repository.unHideConsultantCertPhoto(7);
verify(() => mockService.unHideConsultantCertPhoto(payload: {'cert_photo_id': 7})).called(1);
verify(
() => mockService.unHideConsultantCertPhoto(
payload: {'cert_photo_id': 7},
),
).called(1);
});
});
}

View File

@@ -20,11 +20,14 @@ void main() {
test('getMyRequestStatus - возвращает статус запроса', () async {
const slug = 'test-consultant';
final json = FixtureReader.readAsMap('consultant/consultation_request.json');
final json = FixtureReader.readAsMap(
'consultant/consultation_request.json',
);
final expectedRequest = ConsultationRequestModel.fromJson(json);
when(mockConsultantService.getMyRequestStatus(slug: slug))
.thenAnswer((_) async => expectedRequest);
when(
mockConsultantService.getMyRequestStatus(slug: slug),
).thenAnswer((_) async => expectedRequest);
final result = await mockConsultantService.getMyRequestStatus(slug: slug);
@@ -33,25 +36,36 @@ void main() {
verify(mockConsultantService.getMyRequestStatus(slug: slug)).called(1);
});
test('getConsultantRequestStatus - возвращает статус запроса консультанта', () async {
const slug = 'test-consultant';
final json = FixtureReader.readAsMap('consultant/consultation_request.json');
final expectedRequest = ConsultationRequestModel.fromJson(json);
test(
'getConsultantRequestStatus - возвращает статус запроса консультанта',
() async {
const slug = 'test-consultant';
final json = FixtureReader.readAsMap(
'consultant/consultation_request.json',
);
final expectedRequest = ConsultationRequestModel.fromJson(json);
when(mockConsultantService.getConsultantRequestStatus(slug: slug))
.thenAnswer((_) async => expectedRequest);
when(
mockConsultantService.getConsultantRequestStatus(slug: slug),
).thenAnswer((_) async => expectedRequest);
final result = await mockConsultantService.getConsultantRequestStatus(slug: slug);
final result = await mockConsultantService.getConsultantRequestStatus(
slug: slug,
);
expect(result, isA<ConsultationRequestModel>());
verify(mockConsultantService.getConsultantRequestStatus(slug: slug)).called(1);
});
expect(result, isA<ConsultationRequestModel>());
verify(
mockConsultantService.getConsultantRequestStatus(slug: slug),
).called(1);
},
);
test('getConsultantCertPhotos - возвращает фото сертификатов', () async {
final expectedPhotos = <ConsultantCertPhotoModel>[];
when(mockConsultantService.getConsultantCertPhotos())
.thenAnswer((_) async => expectedPhotos);
when(
mockConsultantService.getConsultantCertPhotos(),
).thenAnswer((_) async => expectedPhotos);
final result = await mockConsultantService.getConsultantCertPhotos();
@@ -60,16 +74,22 @@ void main() {
});
test('consultantRegistration - регистрирует консультанта', () async {
final profileJson = FixtureReader.readAsMap('consultant/consultant_profile_request.json');
final profileJson = FixtureReader.readAsMap(
'consultant/consultant_profile_request.json',
);
final profile = ConsultantProfileModel.fromJson(profileJson);
when(mockConsultantService.consultantRegistration(consultantModel: profile))
.thenAnswer((_) async => Future.value());
when(
mockConsultantService.consultantRegistration(consultantModel: profile),
).thenAnswer((_) async => Future.value());
await mockConsultantService.consultantRegistration(consultantModel: profile);
await mockConsultantService.consultantRegistration(
consultantModel: profile,
);
verify(mockConsultantService.consultantRegistration(consultantModel: profile)).called(1);
verify(
mockConsultantService.consultantRegistration(consultantModel: profile),
).called(1);
});
});
}

View File

@@ -0,0 +1,94 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/consultant/become_consultant/utils/laravel_validation_errors.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('parseLaravelValidationErrors', () {
DioException dio422(Map<String, dynamic> data) {
return DioException(
requestOptions: RequestOptions(path: '/test'),
response: Response(
requestOptions: RequestOptions(path: '/test'),
statusCode: 422,
data: data,
),
type: DioExceptionType.badResponse,
);
}
test('maps unique and format errors to Russian UI texts', () {
final parsed = parseLaravelValidationErrors(
dio422({
'message': 'The given data was invalid.',
'errors': {
'inn': ['The inn format is invalid.'],
'passport_number': ['The passport number has already been taken.'],
'snils': ['The snils has already been taken.'],
},
}),
);
expect(parsed.fieldErrors['inn'], 'Некорректный ИНН');
expect(parsed.fieldErrors['passport_number'], 'Такой паспорт существует');
expect(parsed.fieldErrors['snils'], 'Такой СНИЛС существует');
expect(parsed.summary, isNot(contains('The given data was invalid.')));
expect(parsed.summary, isNot(contains('already been taken')));
expect(parsed.summary, contains('Некорректный ИНН'));
expect(parsed.summary, contains('Такой паспорт существует'));
expect(parsed.hasFieldErrors, isTrue);
});
test('maps required and agreement errors', () {
final parsed = parseLaravelValidationErrors(
dio422({
'errors': {
'last_name': ['The last name field is required.'],
'agreement_accepted': ['The agreement accepted must be accepted.'],
},
}),
);
expect(parsed.fieldErrors['last_name'], 'Укажите фамилию');
expect(parsed.fieldErrors['agreement_accepted'], 'Примите условия оферты');
});
test('falls back to generic message for non-422 DioException', () {
final error = DioException(
requestOptions: RequestOptions(path: '/test'),
message: 'Network failed',
type: DioExceptionType.connectionError,
);
final parsed = parseLaravelValidationErrors(error);
expect(parsed.summary, 'Не удалось отправить данные. Попробуйте ещё раз');
expect(parsed.fieldErrors, isEmpty);
});
test('uses generic text for non-Dio errors', () {
final parsed = parseLaravelValidationErrors(Exception('boom'));
expect(parsed.summary, 'Не удалось отправить данные. Попробуйте ещё раз');
expect(parsed.fieldErrors, isEmpty);
});
});
group('mapConsultantValidationMessage', () {
test('unique inn and snils', () {
expect(
mapConsultantValidationMessage(
fieldKey: 'inn',
rawMessage: 'The inn has already been taken.',
),
'Такой ИНН существует',
);
expect(
mapConsultantValidationMessage(
fieldKey: 'snils',
rawMessage: 'The snils has already been taken.',
),
'Такой СНИЛС существует',
);
});
});
}

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,65 @@ 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('parses profile search_by_author fixture with gallery and video', () {
final posts = FeedFixtures.blogPostsByAuthor();
expect(posts, hasLength(2));
expect(posts.first.images, hasLength(2));
expect(posts.first.images.first.orientation, 'horizontal');
expect(posts.first.images[1].orientation, 'vertical');
expect(posts.first.videoEmbedUrl, contains('rutube.ru/play/embed'));
expect(posts.first.videoUrlThumb, 'media/1/thumb.jpg');
expect(posts[1].images, hasLength(2));
expect(posts[1].videoEmbedUrl, isNull);
});
test('creates FeedBlogPostModel with different states', () {
const author = Author(
authorSlug: 'author',

View File

@@ -0,0 +1,56 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/feed/models/post_feed_mapper.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../fixtures/feed_fixtures.dart';
void main() {
group('PostFeedMapper', () {
setUpAll(() async {
await sl.reset();
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
test('maps fixture item post_id to FeedBlogPostModel.id', () {
final item = FeedFixtures.postFeedItem();
final model = PostFeedMapper.fromPostFeedItem(item, currentUserId: 10);
expect(model.id, 1);
expect(model.author.authorIsMe, isTrue);
expect(model.author.authorSlug, 'anna-ivanova');
expect(model.images, hasLength(2));
expect(model.videoUrl, isEmpty);
expect(model.videoEmbedUrl, isNull);
expect(model.title, 'Завтрак с овсянкой');
expect(model.image, 'http://localhost/storage/posts/oatmeal.jpg');
expect(model.author.isEntitySubscribedByMe, isFalse);
});
test('maps fixture embed video post and likes', () {
final item = FeedFixtures.postFeedItem(index: 1);
final model = PostFeedMapper.fromPostFeedItem(item);
expect(model.id, 2);
expect(model.videoUrl, isEmpty);
expect(model.videoEmbedUrl, 'https://www.youtube.com/embed/dQw4w9WgXcQ');
expect(model.wasLikedByMe, isTrue);
expect(model.totalLikes, 42);
expect(model.author.authorIsMe, isFalse);
expect(model.author.isEntitySubscribedByMe, isTrue);
});
test('maps all fixture posts', () {
final posts = FeedFixtures.mappedFeedPosts(currentUserId: 11);
expect(posts, hasLength(3));
expect(posts[1].author.authorIsMe, isTrue);
expect(posts[1].author.isEntitySubscribedByMe, isTrue);
expect(posts[2].videoEmbedUrl, contains('rutube.ru/play/embed'));
expect(posts[2].author.isEntitySubscribedByMe, isNull);
});
});
}

View File

@@ -0,0 +1,57 @@
import 'package:flutter_test/flutter_test.dart';
import '../../../fixtures/feed_fixtures.dart';
void main() {
group('PostFeedPaginationModel fixtures', () {
test('parses first page with gallery, video and next_cursor', () {
final model = FeedFixtures.postFeedPaginationModel();
expect(model.data, hasLength(3));
expect(model.meta.perPage, 20);
expect(model.meta.nextCursor, 'eyJpZCI6MTAzfQ');
expect(model.meta.prevCursor, isNull);
expect(model.links.next, contains('cursor=eyJpZCI6MTAzfQ'));
final withGallery = model.data[0];
expect(withGallery.postId, 1);
expect(withGallery.images, hasLength(2));
expect(withGallery.images.first.orientation, 'horizontal');
expect(withGallery.videoEmbedUrl, isNull);
expect(withGallery.isEntitySubscribedByMe, isFalse);
expect(withGallery.postAuthorDetails?.slug, 'anna-ivanova');
expect(withGallery.postAuthorDetails?.isConsultant, isTrue);
final withYoutubeEmbed = model.data[1];
expect(withYoutubeEmbed.postId, 2);
expect(withYoutubeEmbed.videoEmbedUrl, 'https://www.youtube.com/embed/dQw4w9WgXcQ');
expect(withYoutubeEmbed.postIsLikedByUser, isTrue);
expect(withYoutubeEmbed.isEntitySubscribedByMe, isTrue);
expect(withYoutubeEmbed.images, isEmpty);
final withRutubeEmbed = model.data[2];
expect(withRutubeEmbed.postId, 3);
expect(withRutubeEmbed.postImageUrl, isNull);
expect(withRutubeEmbed.images, hasLength(1));
expect(withRutubeEmbed.videoEmbedUrl, contains('rutube.ru/play/embed'));
expect(withRutubeEmbed.isEntitySubscribedByMe, isNull); });
test('parses next page without next_cursor', () {
final model = FeedFixtures.postFeedNextPagePaginationModel();
expect(model.data, hasLength(1));
expect(model.data.first.postId, 4);
expect(model.meta.nextCursor, isNull);
expect(model.meta.prevCursor, 'eyJpZCI6MTAzfQ');
expect(model.links.next, isNull);
});
test('parses empty feed', () {
final model = FeedFixtures.postFeedEmptyPaginationModel();
expect(model.data, isEmpty);
expect(model.meta.nextCursor, isNull);
expect(model.links.next, isNull);
});
});
}

View File

@@ -1,7 +1,5 @@
import 'package:eatvio_app/core/models/pagination_model.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/feed_blog_post_settings_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/features/feed/repositories/feed_repository.dart';
@@ -13,211 +11,52 @@ import 'feed_cubit_test.mocks.dart';
@GenerateNiceMocks([MockSpec<FeedRepository>()])
void main() {
group('FeedCubit', () {
FeedCubit? feedCubit;
late MockFeedRepository mockFeedRepository;
late FeedBlogPostSettingsModel feedBlogPostsFixture;
group('FeedCubit cursor flow', () {
late FeedCubit cubit;
late MockFeedRepository repository;
final post = FeedBlogPostModel(
id: 10,
state: 'active',
image: '',
description: 'description',
author: const Author(authorSlug: 'slug', authorName: 'name', authorProfileIcon: '', authorIsMe: false),
videoUrl: '',
createdAt: DateTime(2024, 1, 1),
updatedAt: DateTime(2024, 1, 1),
title: 'title',
totalLikes: 1,
totalViews: 1,
totalComments: 1,
wasLikedByMe: false,
);
setUp(() {
mockFeedRepository = MockFeedRepository();
// Создаём тестовые данные
final testPosts = [
FeedBlogPostModel(
id: 1,
state: 'published',
image: 'https://example.com/image1.jpg',
description: 'Описание поста 1',
author: const Author(
authorSlug: 'author-1',
authorName: 'Автор 1',
authorProfileIcon: 'https://example.com/photo1.jpg',
authorIsMe: false,
isEntitySubscribedByMe: null,
authorPlaceInRating: null,
),
videoUrl: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
title: 'Тестовый пост 1',
totalLikes: 10,
totalViews: 100,
totalComments: 5,
wasLikedByMe: false,
),
FeedBlogPostModel(
id: 2,
state: 'published',
image: 'https://example.com/image2.jpg',
description: 'Описание поста 2',
author: const Author(
authorSlug: 'author-2',
authorName: 'Автор 2',
authorProfileIcon: 'https://example.com/photo2.jpg',
authorIsMe: false,
isEntitySubscribedByMe: null,
authorPlaceInRating: null,
),
videoUrl: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
title: 'Тестовый пост 2',
totalLikes: 20,
totalViews: 200,
totalComments: 10,
wasLikedByMe: true,
),
];
feedBlogPostsFixture = FeedBlogPostSettingsModel(
feedBlogPostList: testPosts,
meta: const Meta(
currentPage: 1,
from: null,
lastPage: 3,
links: [],
path: null,
perPage: 10,
to: null,
total: 25,
totalMinPrice: null,
totalMaxPrice: null,
),
);
repository = MockFeedRepository();
cubit = FeedCubit(feedRepository: repository);
});
tearDown(() async {
if (feedCubit != null && !feedCubit!.isClosed) {
await Future.delayed(const Duration(milliseconds: 100));
feedCubit!.close();
feedCubit = null;
}
await cubit.close();
});
test('начальное состояние - FeedStateLoading', () {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
expect(feedCubit!.state, const FeedState.loading());
test('error state on load failure', () async {
when(repository.getFeed(search: null, cursor: null, currentUserId: null)).thenThrow(Exception('boom'));
await cubit.loadFeedBlogPostList();
expect(cubit.state, isA<FeedStateError>());
});
test('loadFeedBlogPostList - успешная загрузка постов', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
test('updateFeedBlogPostList updates likes in loaded state', () async {
when(repository.getFeed(search: null, cursor: null, currentUserId: null)).thenAnswer((_) async => (posts: [post], nextCursor: null));
await cubit.loadFeedBlogPostList();
when(mockFeedRepository.getFeedBlogPosts(query: null))
.thenAnswer((_) async => feedBlogPostsFixture);
cubit.updateFeedBlogPostList(post, true);
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, isA<FeedStateLoaded>());
final loadedState = feedCubit!.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 2);
expect(loadedState.isLoading, false);
expect(feedCubit!.lastPage, 3);
});
test('loadFeedBlogPostList - загрузка с запросом', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
const query = 'тест';
when(mockFeedRepository.getFeedBlogPosts(query: query))
.thenAnswer((_) async => feedBlogPostsFixture);
await feedCubit!.loadFeedBlogPostList(query: query);
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, isA<FeedStateLoaded>());
verify(mockFeedRepository.getFeedBlogPosts(query: query)).called(1);
});
test('loadFeedBlogPostList - обработка ошибки', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
when(mockFeedRepository.getFeedBlogPosts(query: anyNamed('query')))
.thenThrow(Exception('Ошибка загрузки'));
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, isA<FeedStateError>());
final errorState = feedCubit!.state as FeedStateError;
expect(errorState.errorMessage, contains('Exception'));
});
test('updateFeedBlogPostList - обновление лайка поста', () {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
// Сначала загружаем посты
when(mockFeedRepository.getFeedBlogPosts(query: anyNamed('query')))
.thenAnswer((_) async => feedBlogPostsFixture);
// Устанавливаем состояние loaded вручную для теста
feedCubit!.emit(FeedState.loaded(
feedBlogPostList: feedBlogPostsFixture.feedBlogPostList,
isLoading: false,
));
feedCubit!.lastPage = 3;
final postToUpdate = feedBlogPostsFixture.feedBlogPostList.first;
feedCubit!.updateFeedBlogPostList(postToUpdate, true);
expect(feedCubit!.state, isA<FeedStateLoaded>());
final loadedState = feedCubit!.state as FeedStateLoaded;
final updatedPost = loadedState.feedBlogPostList.firstWhere((p) => p.id == postToUpdate.id);
expect(updatedPost.wasLikedByMe, true);
expect(updatedPost.totalLikes, postToUpdate.totalLikes + 1);
});
test('updateFeedBlogPostList - снятие лайка', () {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
feedCubit!.emit(FeedState.loaded(
feedBlogPostList: feedBlogPostsFixture.feedBlogPostList,
isLoading: false,
));
feedCubit!.lastPage = 3;
final postToUpdate = feedBlogPostsFixture.feedBlogPostList[1]; // У этого wasLikedByMe = true
final initialLikes = postToUpdate.totalLikes;
feedCubit!.updateFeedBlogPostList(postToUpdate, false);
expect(feedCubit!.state, isA<FeedStateLoaded>());
final loadedState = feedCubit!.state as FeedStateLoaded;
final updatedPost = loadedState.feedBlogPostList.firstWhere((p) => p.id == postToUpdate.id);
expect(updatedPost.wasLikedByMe, false);
expect(updatedPost.totalLikes, initialLikes - 1);
});
test('updateFeedBlogPostList - игнорирует обновление если состояние не loaded', () {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
// Состояние loading
feedCubit!.emit(const FeedState.loading());
final postToUpdate = feedBlogPostsFixture.feedBlogPostList.first;
feedCubit!.updateFeedBlogPostList(postToUpdate, true);
// Состояние не должно измениться
expect(feedCubit!.state, const FeedState.loading());
});
test('updateFeedBlogPostList - игнорирует обновление если пост не найден', () {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
feedCubit!.emit(FeedState.loaded(
feedBlogPostList: feedBlogPostsFixture.feedBlogPostList,
isLoading: false,
));
feedCubit!.lastPage = 3;
// Создаём пост с несуществующим ID
final nonExistentPost = feedBlogPostsFixture.feedBlogPostList.first.copyWith(id: 999);
feedCubit!.updateFeedBlogPostList(nonExistentPost, true);
// Список не должен измениться
final loadedState = feedCubit!.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 2);
final loaded = cubit.state as FeedStateLoaded;
expect(loaded.feedBlogPostList.first.wasLikedByMe, true);
expect(loaded.feedBlogPostList.first.totalLikes, 2);
});
});
}

View File

@@ -1,404 +1,66 @@
import 'package:eatvio_app/core/models/pagination_model.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/feed_blog_post_settings_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/features/feed/repositories/feed_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'feed_cubit_test.mocks.dart';
@GenerateNiceMocks([MockSpec<FeedRepository>()])
void main() {
group('FeedCubit', () {
FeedCubit? feedCubit;
late MockFeedRepository mockFeedRepository;
late MockFeedRepository repository;
late FeedCubit cubit;
final testBlogPost = FeedBlogPostModel(
final post = FeedBlogPostModel(
id: 1,
title: 'Test Post',
description: 'Test 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: 'https://example.com/video.mp4',
createdAt: DateTime(2024, 1, 15),
updatedAt: DateTime(2024, 1, 15),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
state: 'active',
image: 'https://example.com/image.png',
description: 'text',
author: const Author(authorSlug: 'a', authorName: 'Author', authorProfileIcon: 'https://example.com/avatar.png', authorIsMe: false),
videoUrl: '',
createdAt: DateTime(2024, 1, 1),
updatedAt: DateTime(2024, 1, 1),
title: 'Title',
totalLikes: 1,
totalViews: 2,
totalComments: 3,
wasLikedByMe: false,
);
final testBlogPost2 = FeedBlogPostModel(
id: 2,
title: 'Test Post 2',
description: 'Test description 2',
image: 'https://example.com/image2.jpg',
state: 'published',
author: const Author(authorSlug: 'author-2', authorName: 'Author Name 2', authorProfileIcon: 'https://example.com/icon2.jpg', authorIsMe: false),
videoUrl: 'https://example.com/video2.mp4',
createdAt: DateTime(2024, 1, 16),
updatedAt: DateTime(2024, 1, 16),
totalLikes: 20,
totalViews: 200,
totalComments: 10,
wasLikedByMe: true,
);
setUp(() {
mockFeedRepository = MockFeedRepository();
repository = MockFeedRepository();
cubit = FeedCubit(feedRepository: repository);
});
tearDown(() async {
if (feedCubit != null && !feedCubit!.isClosed) {
await Future.delayed(const Duration(milliseconds: 100));
feedCubit!.close();
feedCubit = null;
}
await cubit.close();
});
test('начальное состояние - loading', () {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
expect(feedCubit!.state, const FeedState.loading());
test('loadFeedBlogPostList emits loaded from getFeed', () async {
when(repository.getFeed(search: null, cursor: null, currentUserId: null)).thenAnswer((_) async => (posts: [post], nextCursor: 'next'));
await cubit.loadFeedBlogPostList();
expect(cubit.state, isA<FeedStateLoaded>());
final loaded = cubit.state as FeedStateLoaded;
expect(loaded.feedBlogPostList, hasLength(1));
verify(repository.getFeed(search: null, cursor: null, currentUserId: null)).called(1);
});
test('loadFeedBlogPostList - успешно загружает список постов', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
final expectedSettings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost],
meta: const Meta(currentPage: 1, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
test('loadNextPage appends posts', () async {
when(repository.getFeed(search: null, cursor: null, currentUserId: null)).thenAnswer((_) async => (posts: [post], nextCursor: 'next'));
when(repository.getFeed(search: null, cursor: 'next', currentUserId: null)).thenAnswer((_) async => (posts: [post.copyWith(id: 2)], nextCursor: null));
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => expectedSettings);
await cubit.loadFeedBlogPostList();
await cubit.loadNextPage();
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, isA<FeedStateLoaded>());
final loadedState = feedCubit!.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 1);
expect(loadedState.feedBlogPostList.first.id, 1);
expect(loadedState.isLoading, false);
expect(feedCubit!.lastPage, 3);
verify(mockFeedRepository.getFeedBlogPosts(query: null)).called(1);
});
test('loadFeedBlogPostList - загружает список постов с query', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
const query = 'test query';
final expectedSettings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost, testBlogPost2],
meta: const Meta(currentPage: 1, lastPage: 1, total: 2, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockFeedRepository.getFeedBlogPosts(query: query)).thenAnswer((_) async => expectedSettings);
await feedCubit!.loadFeedBlogPostList(query: query);
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, isA<FeedStateLoaded>());
final loadedState = feedCubit!.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 2);
verify(mockFeedRepository.getFeedBlogPosts(query: query)).called(1);
});
test('loadFeedBlogPostList - обрабатывает ошибку', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
final exception = Exception('Network error');
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenThrow(exception);
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, isA<FeedStateError>());
final errorState = feedCubit!.state as FeedStateError;
expect(errorState.errorMessage, contains('Exception: Network error'));
});
test('loadNextPage - загружает следующую страницу', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
final firstPageSettings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost],
meta: const Meta(currentPage: 1, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
final secondPageSettings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost2],
meta: const Meta(currentPage: 2, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => firstPageSettings);
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
when(mockFeedRepository.getFeedBlogPosts(page: 2, query: null)).thenAnswer((_) async => secondPageSettings);
await feedCubit!.loadNextPage();
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, isA<FeedStateLoaded>());
final loadedState = feedCubit!.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.length, 2);
expect(feedCubit!.currentPage, 2);
expect(loadedState.isLoading, false);
verify(mockFeedRepository.getFeedBlogPosts(page: 2, query: null)).called(1);
});
test('loadNextPage - не загружает если уже на последней странице', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
final settings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost],
meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings);
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
await feedCubit!.loadNextPage();
await Future.delayed(const Duration(milliseconds: 100));
verifyNever(mockFeedRepository.getFeedBlogPosts(page: 2, query: anyNamed('query')));
});
test('loadNextPage - не загружает если уже загружается', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
final settings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost],
meta: const Meta(currentPage: 1, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings);
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
feedCubit!.isLoading = true;
await feedCubit!.loadNextPage();
await Future.delayed(const Duration(milliseconds: 100));
verifyNever(mockFeedRepository.getFeedBlogPosts(page: 2, query: anyNamed('query')));
});
test('loadNextPage - обрабатывает ошибку при загрузке следующей страницы', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
final firstPageSettings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost],
meta: const Meta(currentPage: 1, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => firstPageSettings);
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
final exception = Exception('Network error');
when(mockFeedRepository.getFeedBlogPosts(page: 2, query: null)).thenThrow(exception);
await feedCubit!.loadNextPage();
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, isA<FeedStateError>());
final errorState = feedCubit!.state as FeedStateError;
expect(errorState.errorMessage, contains('Exception: Network error'));
});
test('loadNextPage - сохраняет query при загрузке следующей страницы', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
const query = 'test query';
final firstPageSettings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost],
meta: const Meta(currentPage: 1, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
final secondPageSettings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost2],
meta: const Meta(currentPage: 2, lastPage: 3, total: 3, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockFeedRepository.getFeedBlogPosts(query: query)).thenAnswer((_) async => firstPageSettings);
await feedCubit!.loadFeedBlogPostList(query: query);
await Future.delayed(const Duration(milliseconds: 100));
when(mockFeedRepository.getFeedBlogPosts(page: 2, query: query)).thenAnswer((_) async => secondPageSettings);
await feedCubit!.loadNextPage();
await Future.delayed(const Duration(milliseconds: 100));
verify(mockFeedRepository.getFeedBlogPosts(page: 2, query: query)).called(1);
});
test('updateFeedBlogPostList - обновляет лайк поста', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
final settings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost],
meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings);
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
feedCubit!.updateFeedBlogPostList(testBlogPost, true);
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, isA<FeedStateLoaded>());
final loadedState = feedCubit!.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.first.wasLikedByMe, true);
expect(loadedState.feedBlogPostList.first.totalLikes, 11);
});
test('updateFeedBlogPostList - убирает лайк поста', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
final settings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost2],
meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings);
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
feedCubit!.updateFeedBlogPostList(testBlogPost2, false);
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, isA<FeedStateLoaded>());
final loadedState = feedCubit!.state as FeedStateLoaded;
expect(loadedState.feedBlogPostList.first.wasLikedByMe, false);
expect(loadedState.feedBlogPostList.first.totalLikes, 19);
});
test('updateFeedBlogPostList - не обновляет если пост не найден', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
final settings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost],
meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings);
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
final nonExistentPost = testBlogPost.copyWith(id: 999);
final initialState = feedCubit!.state;
feedCubit!.updateFeedBlogPostList(nonExistentPost, true);
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, initialState);
});
test('updateFeedBlogPostList - не обновляет если state не loaded', () {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
feedCubit!.updateFeedBlogPostList(testBlogPost, true);
expect(feedCubit!.state, const FeedState.loading());
});
test('updateFeedBlogPostList - не обновляет если post null', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
final settings = FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost],
meta: const Meta(currentPage: 1, lastPage: 1, total: 1, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer((_) async => settings);
await feedCubit!.loadFeedBlogPostList();
await Future.delayed(const Duration(milliseconds: 100));
final initialState = feedCubit!.state;
feedCubit!.updateFeedBlogPostList(null, true);
await Future.delayed(const Duration(milliseconds: 100));
expect(feedCubit!.state, initialState);
});
test('likePost - лайкает пост', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
const postId = 1;
when(mockFeedRepository.likeBlogPost(id: postId)).thenAnswer((_) async => Future.value());
await feedCubit!.likePost(postId);
verify(mockFeedRepository.likeBlogPost(id: postId)).called(1);
});
test('likePost - обрабатывает ошибку без выбрасывания', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
const postId = 1;
final exception = Exception('Network error');
when(mockFeedRepository.likeBlogPost(id: postId)).thenThrow(exception);
await feedCubit!.likePost(postId);
verify(mockFeedRepository.likeBlogPost(id: postId)).called(1);
});
test('unlikedPost - убирает лайк с поста', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
const postId = 1;
when(mockFeedRepository.unlikedBlogPost(id: postId)).thenAnswer((_) async => Future.value());
await feedCubit!.unlikedPost(postId);
verify(mockFeedRepository.unlikedBlogPost(id: postId)).called(1);
});
test('viewPost - отправляет просмотр поста', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
const postId = 1;
when(mockFeedRepository.viewPost(postId)).thenAnswer((_) async => Future.value());
await feedCubit!.viewPost(postId);
verify(mockFeedRepository.viewPost(postId)).called(1);
});
test('viewRecipe - отправляет просмотр рецепта', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
const slug = 'recipe-1';
when(mockFeedRepository.viewRecipe(slug)).thenAnswer((_) async => Future.value());
await feedCubit!.viewRecipe(slug);
verify(mockFeedRepository.viewRecipe(slug)).called(1);
});
test('viewDiet - отправляет просмотр диеты', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
const dietId = 2;
when(mockFeedRepository.viewDiet(dietId)).thenAnswer((_) async => Future.value());
await feedCubit!.viewDiet(dietId);
verify(mockFeedRepository.viewDiet(dietId)).called(1);
});
test('viewPlan - отправляет просмотр плана', () async {
feedCubit = FeedCubit(feedRepository: mockFeedRepository);
const slug = 'plan-1';
when(mockFeedRepository.viewPlan(slug)).thenAnswer((_) async => Future.value());
await feedCubit!.viewPlan(slug);
verify(mockFeedRepository.viewPlan(slug)).called(1);
final loaded = cubit.state as FeedStateLoaded;
expect(loaded.feedBlogPostList, hasLength(2));
verify(repository.getFeed(search: null, cursor: 'next', currentUserId: null)).called(1);
});
});
}

View File

@@ -1,12 +1,10 @@
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/routing/route_constants.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/feed_blog_post_settings_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/features/feed/presentation/widgets/blog_posts.dart';
@@ -378,12 +376,8 @@ void main() {
// Arrange
feedCubit.emit(FeedState.loaded(feedBlogPostList: [testBlogPost], isLoading: false));
when(mockFeedRepository.getFeedBlogPosts(query: null)).thenAnswer(
(_) async => FeedBlogPostSettingsModel(
feedBlogPostList: [testBlogPost, testBlogPost2],
meta: const Meta(currentPage: 1, lastPage: 1, total: 2, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
),
);
when(mockFeedRepository.getFeed(search: null, cursor: null, currentUserId: null))
.thenAnswer((_) async => (posts: [testBlogPost, testBlogPost2], nextCursor: null));
final widget = BlogPosts(handleVisibility: ({required visibleFraction, required id, required type}) {});

View File

@@ -81,7 +81,7 @@ void main() {
when(cubit.stream).thenAnswer((_) => Stream.value(state));
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await tester.pumpWidget(BlocProvider<FeedCubit>.value(value: cubit, child: wrap(const BlogPostWidget(index: 0))));
await tester.pumpWidget(BlocProvider<FeedCubit>.value(value: cubit, child: wrap(BlogPostWidget(post: mockBlogPost))));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));

View File

@@ -1,8 +1,10 @@
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/features/feed/models/comment_model.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/feed_blog_post_settings_model.dart';
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
@@ -11,9 +13,11 @@ import 'package:eatvio_app/features/feed/models/view_model.dart';
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
import 'package:eatvio_app/features/feed/services/feed_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import '../../../fixtures/feed_fixtures.dart';
import '../../../fixtures/fixture_reader.dart';
import 'feed_repository_test.mocks.dart';
@@ -23,72 +27,57 @@ void main() {
late FeedRepository feedRepository;
late MockFeedService mockFeedService;
setUp(() {
setUp(() async {
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
await sl.reset();
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
mockFeedService = MockFeedService();
feedRepository = FeedRepository(feedService: mockFeedService);
});
test('getFeedBlogPosts - успешное получение постов', () async {
const expectedModel = FeedBlogPostSettingsModel(
feedBlogPostList: [],
meta: Meta(currentPage: 1, from: null, lastPage: 1, links: [], path: null, perPage: 10, to: null, total: 0, totalMinPrice: null, totalMaxPrice: null),
);
test('getFeed - успешное получение постов', () async {
final expectedModel = FeedFixtures.postFeedPaginationModel();
when(mockFeedService.getBlogPostList(page: 1, query: null)).thenAnswer((_) async => expectedModel);
when(mockFeedService.getFeed(search: null, cursor: null)).thenAnswer((_) async => expectedModel);
final result = await feedRepository.getFeedBlogPosts();
final result = await feedRepository.getFeed();
expect(result, isA<FeedBlogPostSettingsModel>());
verify(mockFeedService.getBlogPostList(page: 1, query: null)).called(1);
expect(result.posts, hasLength(3));
expect(result.posts.first.id, 1);
expect(result.posts[1].videoEmbedUrl, 'https://www.youtube.com/embed/dQw4w9WgXcQ');
expect(result.nextCursor, 'eyJpZCI6MTAzfQ');
verify(mockFeedService.getFeed(search: null, cursor: null)).called(1);
});
test('getFeedBlogPosts - с запросом и страницей', () async {
test('getFeed - с запросом и курсором', () async {
const query = 'тест';
const page = 2;
const expectedModel = FeedBlogPostSettingsModel(
feedBlogPostList: [],
meta: Meta(
currentPage: page,
from: null,
lastPage: 2,
links: [],
path: null,
perPage: 10,
to: null,
total: 15,
totalMinPrice: null,
totalMaxPrice: null,
),
);
const cursor = 'eyJpZCI6MTAzfQ';
final expectedModel = FeedFixtures.postFeedNextPagePaginationModel();
when(mockFeedService.getBlogPostList(page: page, query: query)).thenAnswer((_) async => expectedModel);
when(mockFeedService.getFeed(search: query, cursor: cursor)).thenAnswer((_) async => expectedModel);
final result = await feedRepository.getFeedBlogPosts(query: query, page: page);
final result = await feedRepository.getFeed(search: query, cursor: cursor);
expect(result, isA<FeedBlogPostSettingsModel>());
verify(mockFeedService.getBlogPostList(page: page, query: query)).called(1);
expect(result.posts, hasLength(1));
expect(result.posts.first.id, 4);
expect(result.nextCursor, isNull);
verify(mockFeedService.getFeed(search: query, cursor: cursor)).called(1);
});
test('getFeedBlogPosts - возвращает список постов без query', () async {
const page = 1;
const expectedSettings = FeedBlogPostSettingsModel(
feedBlogPostList: [],
meta: Meta(currentPage: 1, lastPage: 1, total: 0, perPage: 10, from: null, links: [Link(url: null, label: null, active: null)], to: null),
);
test('getFeed - пустая лента', () async {
final expectedModel = FeedFixtures.postFeedEmptyPaginationModel();
when(mockFeedService.getBlogPostList(page: page, query: null)).thenAnswer((_) async => expectedSettings);
when(mockFeedService.getFeed(search: null, cursor: null)).thenAnswer((_) async => expectedModel);
final result = await feedRepository.getFeedBlogPosts(page: page);
final result = await feedRepository.getFeed();
expect(result, isA<FeedBlogPostSettingsModel>());
expect(result.feedBlogPostList.isEmpty, true);
verify(mockFeedService.getBlogPostList(page: page, query: null)).called(1);
expect(result.posts, isEmpty);
expect(result.nextCursor, isNull);
});
test('getBlogPost - успешное получение поста', () async {
const postId = 1;
final json = FixtureReader.readAsMap('feed/feed_blog_post_response.json');
final expectedPost = FeedBlogPostModel.fromJson(json);
final expectedPost = FeedFixtures.feedBlogPostModel();
when(mockFeedService.getBlogPost(postId: postId)).thenAnswer((_) async => expectedPost);
@@ -96,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);
});
@@ -529,10 +521,10 @@ void main() {
verify(mockFeedService.viewContent(viewModel: viewModel)).called(1);
});
test('getFeedBlogPosts - пробрасывает исключение', () async {
when(mockFeedService.getBlogPostList(page: 1, query: null)).thenThrow(Exception('Ошибка сети'));
test('getFeed - пробрасывает исключение', () async {
when(mockFeedService.getFeed(search: null, cursor: null)).thenThrow(Exception('Ошибка сети'));
expect(() => feedRepository.getFeedBlogPosts(), throwsException);
expect(() => feedRepository.getFeed(), throwsException);
});
});
}

View File

@@ -13,6 +13,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import '../../../fixtures/feed_fixtures.dart';
import 'feed_service_test.mocks.dart';
@GenerateNiceMocks([MockSpec<FeedService>()])
@@ -55,6 +56,32 @@ void main() {
verify(mockFeedService.getBlogPostList(page: page, query: null)).called(1);
});
test('getFeed returns cursor response', () async {
final expected = FeedFixtures.postFeedPaginationModel();
when(mockFeedService.getFeed(search: null, cursor: null)).thenAnswer((_) async => expected);
final result = await mockFeedService.getFeed(search: null, cursor: null);
expect(result.data, hasLength(3));
expect(result.meta.perPage, 20);
expect(result.meta.nextCursor, 'eyJpZCI6MTAzfQ');
verify(mockFeedService.getFeed(search: null, cursor: null)).called(1);
});
test('getFeed returns next page without cursor', () async {
const cursor = 'eyJpZCI6MTAzfQ';
final expected = FeedFixtures.postFeedNextPagePaginationModel();
when(mockFeedService.getFeed(search: null, cursor: cursor)).thenAnswer((_) async => expected);
final result = await mockFeedService.getFeed(search: null, cursor: cursor);
expect(result.data, hasLength(1));
expect(result.meta.nextCursor, isNull);
verify(mockFeedService.getFeed(search: null, cursor: cursor)).called(1);
});
test('getBlogPost returns post', () async {
const id = 10;
final expected = FeedBlogPostModel(

View File

@@ -1,7 +1,6 @@
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_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/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart';
@@ -10,6 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import '../../../../../fixtures/feed_fixtures.dart';
import 'profile_cubit_test.mocks.dart';
@GenerateNiceMocks([
@@ -267,20 +267,10 @@ void main() {
authorPlaceInRating: null,
);
final blogPost = FeedBlogPostModel(
id: 1,
state: 'published',
title: 'Test Post',
description: 'Test Content',
image: 'https://example.com/image.jpg',
final blogPost = FeedFixtures.feedBlogPostGalleryAndVideo().copyWith(
author: author,
videoUrl: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
totalLikes: 10,
totalComments: 5,
totalViews: 100,
wasLikedByMe: false,
totalLikes: 10,
);
const profileDetails = ProfileDetailsModel(

View File

@@ -5,20 +5,20 @@ import 'package:eatvio_app/core/app_config.dart';
import 'package:eatvio_app/core/app_initializer.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_media_content.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_state.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/profile_blog_post_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:eatvio_app/generated/l10n.dart';
import '../../../../fixtures/feed_fixtures.dart';
import 'profile_blog_post_widget_smoke_test.mocks.dart';
class _FakeHttpClient implements HttpClient {
@@ -35,7 +35,6 @@ class _FakeHttpClient implements HttpClient {
@override
Future<HttpClientRequest> getUrl(Uri url) async => _FakeHttpClientRequest();
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
@@ -51,73 +50,10 @@ class _FakeHttpClientRequest implements HttpClientRequest {
class _FakeHttpClientResponse extends Stream<List<int>> implements HttpClientResponse {
// 1x1 transparent PNG.
static final List<int> _png = <int>[
0x89,
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A,
0x00,
0x00,
0x00,
0x0D,
0x49,
0x48,
0x44,
0x52,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x01,
0x08,
0x06,
0x00,
0x00,
0x00,
0x1F,
0x15,
0xC4,
0x89,
0x00,
0x00,
0x00,
0x0A,
0x49,
0x44,
0x41,
0x54,
0x78,
0x9C,
0x63,
0x00,
0x01,
0x00,
0x00,
0x05,
0x00,
0x01,
0x0D,
0x0A,
0x2D,
0xB4,
0x00,
0x00,
0x00,
0x00,
0x49,
0x45,
0x4E,
0x44,
0xAE,
0x42,
0x60,
0x82,
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41,
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49,
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
];
@override
@@ -150,81 +86,53 @@ void main() {
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
testWidgets('renders blog post card', (tester) async {
final post = FeedBlogPostModel(
id: 1,
state: 'published',
image: 'test.png',
description: 'Hello',
author: const Author(
authorSlug: 'author',
authorName: 'Author',
authorProfileIcon: 'https://example.com/icon.png',
authorIsMe: false,
isEntitySubscribedByMe: false,
),
videoUrl: '',
createdAt: DateTime(2025, 1, 1),
updatedAt: DateTime(2025, 1, 1),
title: 'Title',
totalLikes: 0,
totalViews: 0,
totalComments: 0,
wasLikedByMe: false,
);
const profile = ProfileModel(
id: 1,
name: 'User',
email: 'u@e.com',
phoneConfirmationState: 'confirmed',
slug: 'user',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: '',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
const profile = ProfileModel(
id: 1,
name: 'User',
email: 'u@e.com',
phoneConfirmationState: 'confirmed',
slug: 'user',
isPublicProfile: true,
isBlockedByMe: false,
isBlockedByAuthor: false,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: '',
roles: [],
badges: [],
subscriptions: [],
subscribers: [],
);
const details = ProfileDetailsModel(
firstName: 'Test',
lastName: 'User',
birthdate: '1990-01-01',
gender: 'male',
country: 'Russia',
city: 'Moscow',
lang: 'ru',
weight: 70.0,
height: 175,
chestCircumference: 100,
waistCircumference: 80,
hipCircumference: 90,
bloodPressureUp: 120,
bloodPressureDown: 80,
hasHypo: false,
hasHyper: false,
aboutAlergy: '',
aboutTakingPils: '',
aboutSportHobbies: '',
favoriteFoods: '',
unfavoriteFoods: '',
additionalInfo: '',
isMoodFillable: true,
isBedtimeFillable: true,
activityCoefficient: 1.5,
);
final loadedState = ProfileState.loaded(profile: profile, profileDetails: details, blogPosts: [post], isMyProfile: true, certPhotos: []);
final cubit = MockProfileCubit();
when(cubit.state).thenReturn(loadedState);
when(cubit.stream).thenAnswer((_) => const Stream<ProfileState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
const details = ProfileDetailsModel(
firstName: 'Test',
lastName: 'User',
birthdate: '1990-01-01',
gender: 'male',
country: 'Russia',
city: 'Moscow',
lang: 'ru',
weight: 70.0,
height: 175,
chestCircumference: 100,
waistCircumference: 80,
hipCircumference: 90,
bloodPressureUp: 120,
bloodPressureDown: 80,
hasHypo: false,
hasHyper: false,
aboutAlergy: '',
aboutTakingPils: '',
aboutSportHobbies: '',
favoriteFoods: '',
unfavoriteFoods: '',
additionalInfo: '',
isMoodFillable: true,
isBedtimeFillable: true,
activityCoefficient: 1.5,
);
Future<void> pumpPost(WidgetTester tester, ProfileCubit cubit) async {
await HttpOverrides.runZoned(() async {
await tester.pumpWidget(
MaterialApp(
@@ -236,15 +144,60 @@ void main() {
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: Scaffold(body: ProfileBlogPostWidget(index: 0, profileCubit: cubit)),
home: Scaffold(
body: MediaQuery(
data: const MediaQueryData(size: Size(800, 1600)),
child: SingleChildScrollView(
child: ProfileBlogPostWidget(index: 0, profileCubit: cubit),
),
),
),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
}, createHttpClient: (context) => _FakeHttpClient());
}, createHttpClient: (_) => _FakeHttpClient());
}
expect(find.text('Регистрация'), findsNothing);
expect(find.text('Title'), findsOneWidget);
expect(find.text('Author'), findsOneWidget);
testWidgets('renders gallery and video media content', (tester) async {
final post = FeedFixtures.feedBlogPostGalleryAndVideo();
final loadedState = ProfileState.loaded(profile: profile, profileDetails: details, blogPosts: [post], isMyProfile: true, certPhotos: []);
final cubit = MockProfileCubit();
when(cubit.state).thenReturn(loadedState);
when(cubit.stream).thenAnswer((_) => const Stream<ProfileState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await pumpPost(tester, cubit);
expect(find.byType(BlogPostMediaContent), findsOneWidget);
expect(find.textContaining('Галерея и видео'), findsWidgets);
});
testWidgets('renders gallery-only media content', (tester) async {
final post = FeedFixtures.feedBlogPostWithGallery();
final loadedState = ProfileState.loaded(profile: profile, profileDetails: details, blogPosts: [post], isMyProfile: true, certPhotos: []);
final cubit = MockProfileCubit();
when(cubit.state).thenReturn(loadedState);
when(cubit.stream).thenAnswer((_) => const Stream<ProfileState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await pumpPost(tester, cubit);
expect(find.byType(BlogPostMediaContent), findsOneWidget);
expect(find.text('Галерея'), findsOneWidget);
});
testWidgets('renders video-only media content', (tester) async {
final post = FeedFixtures.feedBlogPostVideoOnly();
final loadedState = ProfileState.loaded(profile: profile, profileDetails: details, blogPosts: [post], isMyProfile: true, certPhotos: []);
final cubit = MockProfileCubit();
when(cubit.state).thenReturn(loadedState);
when(cubit.stream).thenAnswer((_) => const Stream<ProfileState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
await pumpPost(tester, cubit);
expect(find.byType(BlogPostMediaContent), findsOneWidget);
expect(find.text('Видео'), findsOneWidget);
});
});
}

View File

@@ -3,9 +3,11 @@ import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/models/weight_model.dart';
import 'package:eatvio_app/features/profile/services/profile_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../fixtures/feed_fixtures.dart';
import 'profile_service_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ProfileService>()])
@@ -72,9 +74,17 @@ void main() {
expect(await mockProfileService.getProfileDetailsBySlug(slug: 'slug'), isA<ProfileDetailsModel>());
});
test('getBlogPostByAuthorSlug', () async {
when(mockProfileService.getBlogPostByAuthorSlug(slug: '')).thenAnswer((_) async => []);
expect(await mockProfileService.getBlogPostByAuthorSlug(slug: 'slug'), isA<List<FeedBlogPostModel>>());
test('getBlogPostByAuthorSlug returns posts with gallery and video fields', () async {
final posts = FeedFixtures.blogPostsByAuthor();
when(mockProfileService.getBlogPostByAuthorSlug(slug: 'slug')).thenAnswer((_) async => posts);
final result = await mockProfileService.getBlogPostByAuthorSlug(slug: 'slug');
expect(result, isA<List<FeedBlogPostModel>>());
expect(result, hasLength(2));
expect(result.first.images, isNotEmpty);
expect(result.first.videoEmbedUrl, isNotNull);
expect(result.first.videoUrlThumb, isNotNull);
});
test('getWeight', () async {

View File

@@ -20,6 +20,26 @@
"title": "Психология питания",
"date": "2023-06-05"
}
]
],
"legal_type": "individualPerson",
"last_name": "Иванов",
"first_name": "Иван",
"middle_name": "Иванович",
"inn": "123456789012",
"snils": "123-456-789 00",
"passport_series": "1234",
"passport_number": "567890",
"passport_issue_date": "2020-01-15",
"passport_issuer": "ОВД Пресненского района г. Москвы",
"passport_issuer_code": "772-053",
"birth_date": "1990-05-20",
"birth_place": "г. Москва",
"registration_address": "г. Москва, ул. Тверская, д. 1, кв. 1",
"residence_address": "г. Москва, ул. Тверская, д. 1, кв. 1",
"citizenship": "RU",
"tax_resident_us": false,
"foreign_tax_resident": false,
"sbp_phone": "+79991234567",
"agreement_accepted": true,
"frdo_agreement_accepted": true
}

View File

@@ -23,6 +23,31 @@
"personal_site_link": "https://consultant.eatvio.app",
"request_state": "approved"
},
"legal_entity_resource": {
"id": 1,
"type": "individualPerson",
"last_name": "Иванов",
"first_name": "Иван",
"middle_name": "Иванович",
"inn": "123456789012",
"snils": "123-456-789 00",
"state": "created",
"alfa_scoring_status": "scoring_failed",
"alfa_scoring_request_id": "abc-123",
"passport_series": "1234",
"passport_number": "567890",
"passport_issue_date": "2020-01-15",
"passport_issuer": "ОВД Пресненского района г. Москвы",
"passport_issuer_code": "772-053",
"birth_date": "1990-05-20",
"birth_place": "г. Москва",
"registration_address": "г. Москва, ул. Тверская, д. 1, кв. 1",
"residence_address": "г. Москва, ул. Тверская, д. 1, кв. 1",
"citizenship": "RU",
"tax_resident_us": false,
"foreign_tax_resident": false,
"sbp_phone": "+79991234567"
},
"cert_photos": [
{
"url": "https://cdn.eatvio.app/certificates/visible.jpg"
@@ -40,4 +65,3 @@
]
}
}

16
test/fixtures/consultant/sbp_banks.json vendored Normal file
View File

@@ -0,0 +1,16 @@
[
{
"bank_member_id": "100000000111",
"bank_name": "Сбербанк",
"short_name": "Сбер",
"logo_url": "https://example.com/sber.png",
"bic": "044525225"
},
{
"bank_member_id": "100000000004",
"bank_name": "Т-Банк",
"short_name": "ТБанк",
"logo_url": "https://example.com/tbank.png",
"bic": "044525974"
}
]

View File

@@ -0,0 +1,39 @@
{
"id": 4,
"state": "active",
"image": "media/4/a.jpg",
"images": [
{
"id": 40,
"url": "media/4/a.jpg",
"position": 1,
"orientation": "horizontal"
},
{
"id": 41,
"url": "media/4/b.jpg",
"position": 2,
"orientation": "horizontal"
}
],
"description": "Галерея и видео",
"author": {
"author_slug": "author-slug",
"author_name": "Автор",
"author_profile_icon": "https://example.com/photo.jpg",
"author_is_me": false,
"is_entity_subscribed_by_me": true,
"author_place_in_rating": null
},
"video_url": "https://rutube.ru/video/6a62f217aa99daf00af8fa6cebebf218/",
"video_url_embed": "https://rutube.ru/play/embed/6a62f217aa99daf00af8fa6cebebf218/",
"video_url_thumb": "media/4/thumb.jpg",
"created_at": "2024-01-18T12:00:00Z",
"updated_at": "2024-01-18T12:00:00Z",
"title": "Галерея и видео",
"total_likes": 12,
"total_views": 200,
"total_comments": 4,
"was_liked_by_me": false,
"is_favorite": false
}

View File

@@ -0,0 +1,45 @@
{
"id": 2,
"state": "active",
"image": "media/2/a.jpg",
"images": [
{
"id": 20,
"url": "media/2/a.jpg",
"position": 1,
"orientation": "horizontal"
},
{
"id": 21,
"url": "media/2/b.jpg",
"position": 2,
"orientation": "horizontal"
},
{
"id": 22,
"url": "media/2/c.jpg",
"position": 3,
"orientation": "vertical"
}
],
"description": "Пост только с галереей",
"author": {
"author_slug": "author-slug",
"author_name": "Автор",
"author_profile_icon": "https://example.com/photo.jpg",
"author_is_me": false,
"is_entity_subscribed_by_me": false,
"author_place_in_rating": null
},
"video_url": "",
"video_url_embed": null,
"video_url_thumb": null,
"created_at": "2024-01-16T12:00:00Z",
"updated_at": "2024-01-16T12:00:00Z",
"title": "Галерея",
"total_likes": 3,
"total_views": 40,
"total_comments": 1,
"was_liked_by_me": false,
"is_favorite": false
}

View File

@@ -1,7 +1,21 @@
{
"id": 1,
"state": "published",
"image": "https://example.com/image.jpg",
"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,
"orientation": "vertical"
}
],
"description": "Описание поста",
"author": {
"author_slug": "author-slug",
@@ -11,13 +25,15 @@
"is_entity_subscribed_by_me": null,
"author_place_in_rating": null
},
"video_url": "",
"video_url": "https://rutube.ru/video/6a62f217aa99daf00af8fa6cebebf218/",
"video_url_embed": "https://rutube.ru/play/embed/6a62f217aa99daf00af8fa6cebebf218/",
"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
"was_liked_by_me": false,
"is_favorite": false
}

View File

@@ -0,0 +1,26 @@
{
"id": 3,
"state": "active",
"image": "images/element-image-placeholder.svg",
"images": [],
"description": "Пост только с видео",
"author": {
"author_slug": "author-slug",
"author_name": "Автор",
"author_profile_icon": "https://example.com/photo.jpg",
"author_is_me": false,
"is_entity_subscribed_by_me": false,
"author_place_in_rating": null
},
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"video_url_embed": "https://www.youtube.com/embed/dQw4w9WgXcQ",
"video_url_thumb": "media/3/thumb.jpg",
"created_at": "2024-01-17T12:00:00Z",
"updated_at": "2024-01-17T12:00:00Z",
"title": "Видео",
"total_likes": 8,
"total_views": 90,
"total_comments": 2,
"was_liked_by_me": true,
"is_favorite": false
}

View File

@@ -0,0 +1,15 @@
{
"data": [],
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
},
"meta": {
"path": "/api/feed",
"per_page": 20,
"next_cursor": null,
"prev_cursor": null
}
}

View File

@@ -0,0 +1,45 @@
{
"data": [
{
"id": 104,
"post_id": 4,
"post_author_id": 13,
"post_author_name": "Олег Кузнецов",
"post_author_image_url": "/storage/avatars/oleg.png",
"post_author_profile_url": "/ru/profiles/oleg-kuznetsov",
"post_created_at": "2024-06-04T11:00:00.000000Z",
"post_title": "Ужин за 20 минут",
"post_short_description": "Быстрый ужин после работы",
"post_text_md": "Курица с овощами на сковороде.",
"post_image_url": "/storage/posts/dinner.jpg",
"images": [],
"video_embed_url": null,
"post_views_count": 88,
"post_likes_count": 7,
"post_comments_count": 2,
"post_is_liked_by_user": false,
"is_entity_subscribed_by_me": false,
"post_url": "/ru/blog_post/4",
"video_url_thumb": null,
"post_state": "active",
"post_author_detailz": {
"slug": "oleg-kuznetsov",
"is_consultant": false
},
"created_at": "2024-06-04T11:05:00.000000Z",
"updated_at": "2024-06-04T11:05:00.000000Z"
}
],
"links": {
"first": null,
"last": null,
"prev": "https://example.com/api/feed?cursor=eyJpZCI6MTAzfQ",
"next": null
},
"meta": {
"path": "/api/feed",
"per_page": 20,
"next_cursor": null,
"prev_cursor": "eyJpZCI6MTAzfQ"
}
}

View File

@@ -0,0 +1,115 @@
{
"data": [
{
"id": 101,
"post_id": 1,
"post_author_id": 10,
"post_author_name": "Анна Иванова",
"post_author_image_url": "/storage/avatars/anna.png",
"post_author_profile_url": "/ru/profiles/anna-ivanova",
"post_created_at": "2024-06-01T10:00:00.000000Z",
"post_title": "Завтрак с овсянкой",
"post_short_description": "Простой и полезный завтрак",
"post_text_md": "Сегодня приготовила **овсянку** с ягодами.",
"post_image_url": "/storage/posts/oatmeal.jpg",
"images": [
{
"url": "/storage/posts/oatmeal-1.jpg",
"orientation": "horizontal"
},
{
"url": "/storage/posts/oatmeal-2.jpg",
"orientation": "vertical"
}
],
"video_embed_url": null,
"post_views_count": 120,
"post_likes_count": 15,
"post_comments_count": 4,
"post_is_liked_by_user": false,
"is_entity_subscribed_by_me": false,
"post_url": "/ru/blog_post/1",
"post_state": "active",
"video_url_thumb": null,
"post_author_detailz": {
"slug": "anna-ivanova",
"is_consultant": true
},
"created_at": "2024-06-01T10:05:00.000000Z",
"updated_at": "2024-06-01T10:05:00.000000Z"
},
{
"id": 102,
"post_id": 2,
"post_author_id": 11,
"post_author_name": "Игорь Петров",
"post_author_image_url": "/storage/avatars/igor.png",
"post_author_profile_url": "/ru/profiles/igor-petrov",
"post_created_at": "2024-06-02T08:30:00.000000Z",
"post_title": "Тренировка дома",
"post_short_description": "Короткая тренировка без оборудования",
"post_text_md": "15 минут кардио и растяжка.",
"post_image_url": "/storage/posts/workout.jpg",
"images": [],
"video_embed_url": "https://www.youtube.com/embed/dQw4w9WgXcQ",
"post_views_count": 340,
"post_likes_count": 42,
"post_comments_count": 9,
"post_is_liked_by_user": true,
"is_entity_subscribed_by_me": true,
"post_url": "/ru/blog_post/2",
"post_state": "active",
"post_author_detailz": {
"slug": "igor-petrov",
"is_consultant": false
},
"created_at": "2024-06-02T08:35:00.000000Z",
"updated_at": "2024-06-02T09:00:00.000000Z"
},
{
"id": 103,
"post_id": 3,
"post_author_id": 12,
"post_author_name": "Мария Смирнова",
"post_author_image_url": null,
"post_author_profile_url": "/ru/profiles/maria-smirnova",
"post_created_at": "2024-06-03T14:15:00.000000Z",
"post_title": "Рецепт смузи",
"post_short_description": null,
"post_text_md": null,
"post_image_url": null,
"images": [
{
"url": "/storage/posts/smoothie.jpg",
"orientation": "horizontal"
}
],
"video_embed_url": "https://rutube.ru/play/embed/6a62f217aa99daf00af8fa6cebebf218/",
"post_views_count": 50,
"post_likes_count": 3,
"post_comments_count": 1,
"post_is_liked_by_user": false,
"is_entity_subscribed_by_me": null,
"post_url": "/ru/blog_post/3",
"post_state": "active",
"post_author_detailz": {
"slug": "maria-smirnova",
"is_consultant": false
},
"created_at": "2024-06-03T14:20:00.000000Z",
"updated_at": "2024-06-03T14:20:00.000000Z"
}
],
"links": {
"first": null,
"last": null,
"prev": null,
"next": "https://example.com/api/feed?cursor=eyJpZCI6MTAzfQ"
},
"meta": {
"path": "/api/feed",
"per_page": 20,
"next_cursor": "eyJpZCI6MTAzfQ",
"prev_cursor": null
}
}

81
test/fixtures/feed_fixtures.dart vendored Normal file
View File

@@ -0,0 +1,81 @@
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
import 'package:eatvio_app/features/feed/models/post_feed_mapper.dart';
import 'package:eatvio_app/features/feed/models/post_feed_pagination_model.dart';
import 'fixture_reader.dart';
class FeedFixtures {
FeedFixtures._();
static Map<String, dynamic> postFeedResponseJson() {
return FixtureReader.readAsMap('feed/post_feed_response.json');
}
static Map<String, dynamic> postFeedNextPageResponseJson() {
return FixtureReader.readAsMap('feed/post_feed_next_page_response.json');
}
static Map<String, dynamic> postFeedEmptyResponseJson() {
return FixtureReader.readAsMap('feed/post_feed_empty_response.json');
}
static Map<String, dynamic> feedBlogPostResponseJson() {
return FixtureReader.readAsMap('feed/feed_blog_post_response.json');
}
static Map<String, dynamic> feedBlogPostGalleryResponseJson() {
return FixtureReader.readAsMap('feed/feed_blog_post_gallery_response.json');
}
static Map<String, dynamic> feedBlogPostVideoOnlyResponseJson() {
return FixtureReader.readAsMap('feed/feed_blog_post_video_only_response.json');
}
static Map<String, dynamic> feedBlogPostGalleryAndVideoResponseJson() {
return FixtureReader.readAsMap('feed/feed_blog_post_gallery_and_video_response.json');
}
static PostFeedPaginationModel postFeedPaginationModel() {
return PostFeedPaginationModel.fromJson(postFeedResponseJson());
}
static PostFeedPaginationModel postFeedNextPagePaginationModel() {
return PostFeedPaginationModel.fromJson(postFeedNextPageResponseJson());
}
static PostFeedPaginationModel postFeedEmptyPaginationModel() {
return PostFeedPaginationModel.fromJson(postFeedEmptyResponseJson());
}
static PostFeedItemModel postFeedItem({int index = 0}) {
return postFeedPaginationModel().data[index];
}
static List<FeedBlogPostModel> mappedFeedPosts({int? currentUserId}) {
return postFeedPaginationModel().data
.map((item) => PostFeedMapper.fromPostFeedItem(item, currentUserId: currentUserId))
.toList();
}
static FeedBlogPostModel feedBlogPostModel() {
return FeedBlogPostModel.fromJson(feedBlogPostResponseJson());
}
static FeedBlogPostModel feedBlogPostWithGallery() {
return FeedBlogPostModel.fromJson(feedBlogPostGalleryResponseJson());
}
static FeedBlogPostModel feedBlogPostVideoOnly() {
return FeedBlogPostModel.fromJson(feedBlogPostVideoOnlyResponseJson());
}
static FeedBlogPostModel feedBlogPostGalleryAndVideo() {
return FeedBlogPostModel.fromJson(feedBlogPostGalleryAndVideoResponseJson());
}
static List<FeedBlogPostModel> blogPostsByAuthor() {
return FixtureReader.readAsList('profile/blog_posts_by_author_response.json')
.map((item) => FeedBlogPostModel.fromJson(item as Map<String, dynamic>))
.toList();
}
}

View File

@@ -0,0 +1,80 @@
[
{
"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,
"orientation": "vertical"
}
],
"description": "Описание поста профиля",
"author": {
"author_slug": "test-user",
"author_name": "Test User",
"author_profile_icon": "https://example.com/photo.jpg",
"author_is_me": true,
"is_entity_subscribed_by_me": false,
"author_place_in_rating": null
},
"video_url": "https://rutube.ru/video/6a62f217aa99daf00af8fa6cebebf218/",
"video_url_embed": "https://rutube.ru/play/embed/6a62f217aa99daf00af8fa6cebebf218/",
"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,
"is_favorite": false
},
{
"id": 2,
"state": "active",
"image": "media/2/a.jpg",
"images": [
{
"id": 20,
"url": "media/2/a.jpg",
"position": 1,
"orientation": "horizontal"
},
{
"id": 21,
"url": "media/2/b.jpg",
"position": 2,
"orientation": "horizontal"
}
],
"description": "Галерея без видео",
"author": {
"author_slug": "test-user",
"author_name": "Test User",
"author_profile_icon": "https://example.com/photo.jpg",
"author_is_me": true,
"is_entity_subscribed_by_me": false,
"author_place_in_rating": null
},
"video_url": "",
"video_url_embed": null,
"video_url_thumb": null,
"created_at": "2024-01-16T12:00:00Z",
"updated_at": "2024-01-16T12:00:00Z",
"title": "Галерея",
"total_likes": 3,
"total_views": 40,
"total_comments": 1,
"was_liked_by_me": false,
"is_favorite": false
}
]