feat(insights): added kbju fetching for analitics

This commit is contained in:
misha
2026-11-10 12:21:53 +03:00
parent 45d072cff4
commit 8c33f033ff
30 changed files with 1910 additions and 118 deletions

View File

@@ -4,9 +4,11 @@ import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
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/insights/presentation/widgets/insights_card.dart';
import 'package:eatvio_app/features/insights/repositories/insights_repository.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart'
as profile_plans;
import 'package:eatvio_app/features/profile/presentation/bloc/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
@@ -20,9 +22,15 @@ import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:network_image_mock/network_image_mock.dart';
import '../../../../fixtures/doubles/insights_doubles.dart';
import 'profile_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ProfileRepository>(), MockSpec<ChatRepository>(), MockSpec<ConsultantRepository>(), MockSpec<PlansRepository>()])
@GenerateNiceMocks([
MockSpec<ProfileRepository>(),
MockSpec<ChatRepository>(),
MockSpec<ConsultantRepository>(),
MockSpec<PlansRepository>(),
])
void main() {
group('ProfileScreen smoke', () {
late MockProfileRepository profileRepository;
@@ -31,12 +39,18 @@ void main() {
late MockPlansRepository plansRepository;
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'),
);
profileRepository = MockProfileRepository();
chatRepository = MockChatRepository();
@@ -44,13 +58,25 @@ void main() {
plansRepository = MockPlansRepository();
sl.registerLazySingleton<ProfileRepository>(() => profileRepository);
sl.registerLazySingleton<InsightsRepository>(() => InsightsRepository(profileRepository: profileRepository));
sl.registerLazySingleton<InsightsRepository>(
() => FakeInsightsRepository(),
);
sl.registerLazySingleton<ChatRepository>(() => chatRepository);
sl.registerLazySingleton<ConsultantRepository>(() => consultantRepository);
sl.registerLazySingleton<ConsultantRepository>(
() => consultantRepository,
);
sl.registerLazySingleton<PlansRepository>(() => plansRepository);
sl.registerFactory<ProfileCubit>(() => ProfileCubit(profileRepository: sl(), chatRepository: sl(), consultantRepository: sl()));
sl.registerFactory<profile_plans.PlansCubit>(() => profile_plans.PlansCubit(plansRepository: sl()));
sl.registerFactory<ProfileCubit>(
() => ProfileCubit(
profileRepository: sl(),
chatRepository: sl(),
consultantRepository: sl(),
),
);
sl.registerFactory<profile_plans.PlansCubit>(
() => profile_plans.PlansCubit(plansRepository: sl()),
);
});
Widget wrap(Widget child) {
@@ -115,8 +141,12 @@ void main() {
);
when(profileRepository.getMyProfile()).thenAnswer((_) async => profile);
when(profileRepository.getMyProfileDetails()).thenAnswer((_) async => profileDetails);
when(profileRepository.getBlogPostsByAuthorSlug(slug: anyNamed('slug'))).thenAnswer((_) async => []);
when(
profileRepository.getMyProfileDetails(),
).thenAnswer((_) async => profileDetails);
when(
profileRepository.getBlogPostsByAuthorSlug(slug: anyNamed('slug')),
).thenAnswer((_) async => []);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(const ProfileScreen()));
@@ -127,8 +157,12 @@ void main() {
});
testWidgets('renders error state', (tester) async {
when(profileRepository.getMyProfile()).thenThrow(Exception('Error loading profile'));
when(profileRepository.getMyProfileDetails()).thenThrow(Exception('Error'));
when(
profileRepository.getMyProfile(),
).thenThrow(Exception('Error loading profile'));
when(
profileRepository.getMyProfileDetails(),
).thenThrow(Exception('Error'));
await tester.pumpWidget(wrap(const ProfileScreen()));
await tester.pumpAndSettle();
@@ -183,11 +217,19 @@ void main() {
activityCoefficient: 1.5,
);
when(profileRepository.getProfileBySlug(any)).thenAnswer((_) async => profile);
when(
profileRepository.getProfileBySlug(any),
).thenAnswer((_) async => profile);
when(profileRepository.getMyProfile()).thenAnswer((_) async => profile);
when(profileRepository.getMyProfileDetails()).thenAnswer((_) async => profileDetails);
when(profileRepository.getProfileDetailsBySlug(any)).thenAnswer((_) async => profileDetails);
when(profileRepository.getBlogPostsByAuthorSlug(slug: anyNamed('slug'))).thenAnswer((_) async => []);
when(
profileRepository.getMyProfileDetails(),
).thenAnswer((_) async => profileDetails);
when(
profileRepository.getProfileDetailsBySlug(any),
).thenAnswer((_) async => profileDetails);
when(
profileRepository.getBlogPostsByAuthorSlug(slug: anyNamed('slug')),
).thenAnswer((_) async => []);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(const ProfileScreen(slug: 'test-user')));
@@ -195,7 +237,159 @@ void main() {
});
expect(find.byType(ProfileScreen), findsOneWidget);
expect(find.text('Пользователь ограничил доступ к своей странице'), findsOneWidget);
expect(
find.text('Пользователь ограничил доступ к своей странице'),
findsOneWidget,
);
});
group('аналитика (InsightsCard)', () {
ProfileModel profileOf(
String slug, {
bool blockedByMe = false,
bool blockedByAuthor = false,
}) => ProfileModel(
id: 1,
name: 'Test User',
email: 'test@example.com',
phoneConfirmationState: 'confirmed',
slug: slug,
isPublicProfile: true,
isBlockedByMe: blockedByMe,
isBlockedByAuthor: blockedByAuthor,
isBlockedByUser: false,
isEntitySubscribedByMe: false,
profilePhotoUrl: '',
roles: const [],
badges: const [],
subscriptions: const [],
subscribers: const [],
);
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<FakeInsightsRepository> pumpProfile(
WidgetTester tester, {
required ProfileModel viewed,
String? slug,
Object? insightsError,
}) async {
final insights = FakeInsightsRepository(error: insightsError);
sl.unregister<InsightsRepository>();
sl.registerLazySingleton<InsightsRepository>(() => insights);
when(
profileRepository.getProfileBySlug(any),
).thenAnswer((_) async => viewed);
when(
profileRepository.getMyProfile(),
).thenAnswer((_) async => profileOf('me'));
when(
profileRepository.getMyProfileDetails(),
).thenAnswer((_) async => details);
when(
profileRepository.getProfileDetailsBySlug(any),
).thenAnswer((_) async => details);
when(
profileRepository.getBlogPostsByAuthorSlug(slug: anyNamed('slug')),
).thenAnswer((_) async => []);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(ProfileScreen(slug: slug)));
await tester.pumpAndSettle();
});
return insights;
}
testWidgets('собственный профиль: свои данные и кнопка добавления', (
tester,
) async {
final insights = await pumpProfile(tester, viewed: profileOf('me'));
final card = tester.widget<InsightsCard>(find.byType(InsightsCard));
expect(card.slug, isNull);
expect(card.canAdd, isTrue);
expect(insights.serverRequests.single.slug, isNull);
});
testWidgets(
'чужой профиль (консультант → клиент): данные по slug без кнопки добавления',
(tester) async {
final insights = await pumpProfile(
tester,
viewed: profileOf('client'),
slug: 'client',
);
final card = tester.widget<InsightsCard>(find.byType(InsightsCard));
expect(card.slug, 'client');
expect(card.canAdd, isFalse);
expect(insights.serverRequests.single.slug, 'client');
expect(find.byKey(const Key('insights_card')), findsOneWidget);
},
);
testWidgets('чужой профиль: 403 скрывает блок', (tester) async {
await pumpProfile(
tester,
viewed: profileOf('client'),
slug: 'client',
insightsError: const InsightsForbidden(),
);
expect(find.byKey(const Key('insights_card')), findsNothing);
});
testWidgets(
'профиль, заблокированный мной, не показывает аналитику и не запрашивает её',
(tester) async {
final insights = await pumpProfile(
tester,
viewed: profileOf('client', blockedByMe: true),
slug: 'client',
);
expect(find.byType(InsightsCard), findsNothing);
expect(insights.serverRequests, isEmpty);
},
);
testWidgets('профиль, ограничивший доступ, не показывает аналитику', (
tester,
) async {
final insights = await pumpProfile(
tester,
viewed: profileOf('client', blockedByAuthor: true),
slug: 'client',
);
expect(find.byType(InsightsCard), findsNothing);
expect(insights.serverRequests, isEmpty);
});
});
});
}