feat(insights): first isights impemetation

This commit is contained in:
misha
2026-11-09 15:35:10 +03:00
parent 67920b2016
commit 45d072cff4
63 changed files with 6374 additions and 1465 deletions

View File

@@ -15,6 +15,8 @@ import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
import 'package:eatvio_app/features/diary/models/nutrition_model.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/features/diary/presentation/widgets/meal_widget.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/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
@@ -23,6 +25,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import '../../../../fixtures/doubles/insights_doubles.dart';
import 'diary_screen_smoke_test.mocks.dart';
@GenerateNiceMocks([
@@ -44,6 +47,7 @@ void main() {
AppConfig(buildType: AppBuildType.tests)
..setBaseUrl('http://localhost'),
);
sl.registerLazySingleton<InsightsRepository>(() => FakeInsightsRepository());
});
Widget wrap(Widget child) {
@@ -383,6 +387,76 @@ void main() {
expect(find.byType(Text), findsWidgets);
});
Future<void> pumpDiaryWithInsights(
WidgetTester tester, {
required bool isConsultantView,
String? ownerSlug,
required FakeInsightsRepository insightsRepository,
}) async {
final diaryCubit = MockDiaryCubit();
final state = loadedState(hasChosenPlan: false).copyWith(isConsultantView: isConsultantView);
when(diaryCubit.state).thenReturn(state);
when(diaryCubit.stream).thenAnswer((_) => const Stream<DiaryState>.empty());
when(diaryCubit.close()).thenAnswer((_) async => Future<void>.value());
when(
diaryCubit.fetchDiaryV2(needLoading: anyNamed('needLoading'), diaryId: anyNamed('diaryId')),
).thenAnswer((_) async => true);
final commentsCubit = MockConsultantCommentsCubit();
when(commentsCubit.state).thenReturn(ConsultantCommentsState(commentsModel: ConsultantCommentModel(comment: '')));
when(commentsCubit.stream).thenAnswer((_) => const Stream<ConsultantCommentsState>.empty());
when(commentsCubit.close()).thenAnswer((_) async => Future<void>.value());
sl.registerFactory<DiaryCubit>(() => diaryCubit);
sl.registerFactory<ConsultantCommentsCubit>(() => commentsCubit);
sl.unregister<InsightsRepository>();
sl.registerLazySingleton<InsightsRepository>(() => insightsRepository);
await tester.pumpWidget(
wrap(isConsultantView ? UserDiaryScreen(diaryId: 1, ownerSlug: ownerSlug) : DiaryScreen(date: DateTime(2025, 1, 1))),
);
await tester.pump(const Duration(milliseconds: 200));
}
testWidgets('own diary shows insights card for own data with add button enabled', (tester) async {
final insightsRepository = FakeInsightsRepository();
await pumpDiaryWithInsights(tester, isConsultantView: false, insightsRepository: insightsRepository);
expect(find.byType(InsightsCard), findsOneWidget);
expect(tester.widget<InsightsCard>(find.byType(InsightsCard)).slug, isNull);
expect(tester.widget<InsightsCard>(find.byType(InsightsCard)).canAdd, isTrue);
expect(insightsRepository.insightsRequests.single.slug, isNull);
});
testWidgets('consultant view shows insights card of the client without add button', (tester) async {
final insightsRepository = FakeInsightsRepository();
await pumpDiaryWithInsights(tester, isConsultantView: true, ownerSlug: 'client', insightsRepository: insightsRepository);
final card = tester.widget<InsightsCard>(find.byType(InsightsCard));
expect(card.slug, 'client');
expect(card.canAdd, isFalse);
expect(insightsRepository.insightsRequests.single.slug, 'client');
});
testWidgets('consultant view without owner slug hides insights card', (tester) async {
final insightsRepository = FakeInsightsRepository();
await pumpDiaryWithInsights(tester, isConsultantView: true, insightsRepository: insightsRepository);
expect(find.byType(InsightsCard), findsNothing);
expect(insightsRepository.insightsRequests, isEmpty);
});
testWidgets('consultant view hides insights card on 403', (tester) async {
final insightsRepository = FakeInsightsRepository(error: const InsightsForbidden());
await pumpDiaryWithInsights(tester, isConsultantView: true, ownerSlug: 'client', insightsRepository: insightsRepository);
expect(find.byKey(const Key('insights_card')), findsNothing);
});
testWidgets('shows add button for a finished day inside the edit window', (
tester,
) async {

View File

@@ -0,0 +1,285 @@
import 'package:eatvio_app/features/insights/domain/insights_text_builder.dart';
import 'package:eatvio_app/features/insights/models/insights_period.dart';
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/models/weight_model.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
WeightModel _weight(double weight, {double? bmi, int day = 1}) {
final date = DateTime(2026, 11, day, 12);
return WeightModel(
id: day,
userId: 1,
weight: weight,
createdAt: date,
updatedAt: date,
bmi: bmi,
);
}
CircumferenceModel _circumference({
double waist = 80,
double chest = 95,
double hips = 100,
int day = 1,
}) {
final date = DateTime(2026, 11, day, 12);
return CircumferenceModel(
id: day,
userId: 1,
chestCircumference: chest,
waistCircumference: waist,
hipCircumference: hips,
createdAt: date,
updatedAt: date,
);
}
void main() {
late InsightsTextBuilder builder;
setUpAll(() async {
final s = await S.load(const Locale('ru'));
builder = InsightsTextBuilder(s);
});
group('форматирование', () {
test('formatNumber: один знак без хвостового нуля', () {
expect(builder.formatNumber(3), '3');
expect(builder.formatNumber(3.5), '3.5');
expect(builder.formatNumber(3.04), '3');
expect(builder.formatNumber(3.26), '3.3');
});
test('formatLabel: dd.MM', () {
expect(builder.formatLabel(DateTime(2026, 11, 9, 12)), '09.11');
});
test('periodLabel: все периоды', () {
expect(builder.periodLabel(BodyPeriod.month), 'месяц');
expect(builder.periodLabel(BodyPeriod.threeMonths), '3 месяца');
expect(builder.periodLabel(BodyPeriod.sixMonths), '6 месяцев');
expect(builder.periodLabel(BodyPeriod.year), 'год');
expect(builder.periodLabel(BodyPeriod.all), 'всё время');
});
});
group('вес', () {
test('снижение', () {
final model = builder.buildWeight([
_weight(74.5),
_weight(73.2, day: 9),
], BodyPeriod.month);
expect(model.deltaText, 'Снизили на 1.3 кг за месяц');
expect(model.delta, -1.3);
expect(model.last, 73.2);
expect(model.headline, '73.2');
expect(model.unit, 'кг');
expect(model.labels, ['01.11', '09.11']);
expect(model.values, [74.5, 73.2]);
});
test('набор', () {
final model = builder.buildWeight([
_weight(70),
_weight(72, day: 3),
], BodyPeriod.threeMonths);
expect(model.deltaText, 'Набрали 2 кг за 3 месяца');
expect(model.delta, 2);
});
test('порог 0.05: меньше — не изменился', () {
final model = builder.buildWeight([
_weight(70),
_weight(70.04, day: 3),
], BodyPeriod.year);
expect(model.deltaText, 'Вес не изменился за год');
});
test('порог 0.05: ровно 0.25 — уже изменение', () {
final model = builder.buildWeight([
_weight(70),
_weight(70.25, day: 3),
], BodyPeriod.all);
expect(model.deltaText, 'Набрали 0.3 кг за всё время');
});
test('одна точка: недостаточно данных, но headline есть', () {
final model = builder.buildWeight([_weight(72)], BodyPeriod.month);
expect(model.deltaText, 'Недостаточно данных');
expect(model.delta, isNull);
expect(model.headline, '72.0');
});
test('нет точек', () {
final model = builder.buildWeight([], BodyPeriod.month);
expect(model.deltaText, 'Недостаточно данных');
expect(model.last, isNull);
expect(model.headline, '—');
expect(model.values, isEmpty);
});
});
group('категории ИМТ (границы)', () {
final cases = <double, String>{
15.9: 'выраженный дефицит массы',
16.0: 'недостаточная масса',
18.4: 'недостаточная масса',
18.5: 'норма',
24.9: 'норма',
25.0: 'избыточная масса',
29.9: 'избыточная масса',
30.0: 'ожирение I ст.',
34.9: 'ожирение I ст.',
35.0: 'ожирение II ст.',
39.9: 'ожирение II ст.',
40.0: 'ожирение III ст.',
55.0: 'ожирение III ст.',
};
cases.forEach((bmi, expected) {
test('$bmi → $expected', () {
expect(builder.bmiCategory(bmi), expected);
});
});
});
group('ИМТ', () {
test('без роста: пусто и подсказка', () {
final model = builder.buildBmi(
[_weight(70, bmi: 22)],
BodyPeriod.month,
null,
);
expect(model.values, isEmpty);
expect(model.headline, '—');
expect(model.hint, 'Укажите рост, чтобы посчитать ИМТ.');
expect(model.deltaText, 'Укажите рост, чтобы посчитать ИМТ.');
expect(model.category, isNull);
});
test('нулевой рост считается отсутствующим', () {
final model = builder.buildBmi(
[_weight(70, bmi: 22)],
BodyPeriod.month,
0,
);
expect(model.values, isEmpty);
});
test('ИМТ ниже', () {
final model = builder.buildBmi(
[_weight(80, bmi: 26.5), _weight(75, bmi: 24.2, day: 5)],
BodyPeriod.threeMonths,
176,
);
expect(model.deltaText, 'ИМТ ниже на 2.3 за 3 месяца · норма');
expect(model.category, 'норма');
expect(model.headline, '24.2');
expect(model.last, 24.2);
expect(model.delta, -2.3);
expect(model.hint, contains('Считаем при росте 176 см.'));
});
test('ИМТ выше', () {
final model = builder.buildBmi(
[_weight(70, bmi: 24), _weight(80, bmi: 27, day: 5)],
BodyPeriod.month,
170,
);
expect(model.deltaText, 'ИМТ выше на 3 за месяц · избыточная масса');
});
test('ИМТ не изменился', () {
final model = builder.buildBmi(
[_weight(70, bmi: 22), _weight(70, bmi: 22.01, day: 5)],
BodyPeriod.year,
170,
);
expect(model.deltaText, 'ИМТ не изменился за год · норма');
});
test('точки с bmi = null пропускаются', () {
final model = builder.buildBmi(
[_weight(70, bmi: null), _weight(71, bmi: 22, day: 3)],
BodyPeriod.month,
170,
);
expect(model.values, [22]);
expect(model.labels, ['03.11']);
expect(model.deltaText, 'Недостаточно данных');
});
test('рост есть, точек нет', () {
final model = builder.buildBmi([], BodyPeriod.month, 170);
expect(model.values, isEmpty);
expect(model.deltaText, 'Недостаточно данных');
expect(model.hint, contains('170'));
});
});
group('замеры', () {
test('по каждой части: меньше / больше / без изменений', () {
final model = builder.buildMeasure([
_circumference(waist: 82, chest: 96, hips: 100),
_circumference(waist: 80.5, chest: 98, hips: 100, day: 9),
], BodyPeriod.month);
expect(
model.deltaText,
'Талия меньше на 1.5 см, Грудь больше на 2 см, Бёдра без изменений за месяц',
);
expect(model.headline, '81');
expect(model.waist, [82, 80.5]);
expect(model.labels, ['01.11', '09.11']);
});
test('одна точка — недостаточно данных', () {
final model = builder.buildMeasure([_circumference()], BodyPeriod.month);
expect(model.deltaText, 'Недостаточно данных');
expect(model.headline, '80');
});
test('нет точек', () {
final model = builder.buildMeasure([], BodyPeriod.month);
expect(model.headline, '—');
expect(model.deltaText, 'Недостаточно данных');
});
});
group('build', () {
test('сортирует по дате, daily/intra = null', () {
final payload = builder.build(
weights: [_weight(73, bmi: 23, day: 9), _weight(75, bmi: 24, day: 1)],
circumferences: [
_circumference(waist: 79, day: 9),
_circumference(waist: 81),
],
period: BodyPeriod.month,
heightCm: 175,
);
expect(payload.weight.values, [75, 73]);
expect(payload.bmi.values, [24, 23]);
expect(payload.measure.waist, [81, 79]);
expect(payload.daily, isNull);
expect(payload.intra, isNull);
});
});
}

View File

@@ -0,0 +1,76 @@
import 'dart:convert';
import 'dart:io';
import 'package:eatvio_app/features/insights/models/insights_payload_model.dart';
import 'package:flutter_test/flutter_test.dart';
Map<String, dynamic> _fixture(String name) {
return jsonDecode(File('test/fixtures/insights/$name').readAsStringSync())
as Map<String, dynamic>;
}
void main() {
group('InsightsPayloadModel', () {
test('парсит полный payload (weight/bmi/measure/daily/intra)', () {
final model = InsightsPayloadModel.fromJson(
_fixture('insights_payload.json'),
);
expect(model.weight.values, [74.5, 73.9, 73.2]);
expect(model.weight.last, 73.2);
expect(model.weight.delta, -1.3);
expect(model.weight.unit, 'кг');
expect(model.measure.waist, [82.0, 80.5]);
expect(model.measure.chest, [96.0, 96.0]);
expect(model.daily, isNotNull);
expect(model.daily!.kcal, [1850.0, 2010.0, 1930.0]);
expect(model.daily!.average, 1930);
expect(model.intra, isNotNull);
expect(model.intra!.bars, hasLength(2));
expect(model.intra!.bars.first.x, 8.5);
expect(model.intra!.bars.first.c, 240);
expect(model.intra!.satiety.last.y, 4);
expect(model.intra!.wake, 7.5);
expect(model.intra!.isToday, isTrue);
});
test('bmi.values пустой и last/delta/category = null', () {
final model = InsightsPayloadModel.fromJson(
_fixture('insights_payload.json'),
);
expect(model.bmi.values, isEmpty);
expect(model.bmi.last, isNull);
expect(model.bmi.delta, isNull);
expect(model.bmi.category, isNull);
expect(model.bmi.deltaText, contains('рост'));
});
test('intra без wake/sleep и null в average/percent', () {
final model = InsightsPayloadModel.fromJson(
_fixture('insights_payload_minimal.json'),
);
expect(model.intra!.wake, isNull);
expect(model.intra!.sleep, isNull);
expect(model.intra!.bars, isEmpty);
expect(model.daily!.average, isNull);
expect(model.daily!.percent, isNull);
expect(model.weight.last, isNull);
});
test('daily и intra необязательны', () {
final json = _fixture('insights_payload_minimal.json')
..remove('daily')
..remove('intra');
final model = InsightsPayloadModel.fromJson(json);
expect(model.daily, isNull);
expect(model.intra, isNull);
});
});
}

View File

@@ -0,0 +1,430 @@
import 'package:eatvio_app/features/insights/models/insights_payload_model.dart';
import 'package:eatvio_app/features/insights/models/insights_period.dart';
import 'package:eatvio_app/features/insights/presentation/cubit/insights_cubit.dart';
import 'package:eatvio_app/features/insights/presentation/cubit/insights_panel.dart';
import 'package:eatvio_app/features/insights/presentation/cubit/insights_state.dart';
import 'package:eatvio_app/features/insights/repositories/insights_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class _MockInsightsRepository extends Mock implements InsightsRepository {}
const _payload = InsightsPayloadModel(
weight: WeightChartModel(values: [70, 71], headline: '71.0'),
bmi: BmiChartModel(),
measure: MeasureChartModel(),
);
const _payload2 = InsightsPayloadModel(
weight: WeightChartModel(values: [72], headline: '72.0'),
bmi: BmiChartModel(),
measure: MeasureChartModel(),
);
void main() {
late _MockInsightsRepository repository;
setUpAll(() {
registerFallbackValue(BodyPeriod.month);
});
setUp(() {
repository = _MockInsightsRepository();
when(
() => repository.getHeightCm(slug: any(named: 'slug')),
).thenAnswer((_) async => 176);
when(
() => repository.getInsights(
slug: any(named: 'slug'),
bodyPeriod: any(named: 'bodyPeriod'),
heightCm: any(named: 'heightCm'),
),
).thenAnswer((_) async => _payload);
});
InsightsCubit buildCubit({
String? slug,
bool canAdd = false,
bool kbjuEnabled = false,
}) {
return InsightsCubit(
repository: repository,
slug: slug,
canAdd: canAdd,
kbjuEnabled: kbjuEnabled,
);
}
InsightsStateLoaded loaded(InsightsCubit cubit) =>
cubit.state as InsightsStateLoaded;
test('начальное состояние — initial', () {
expect(buildCubit().state, isA<InsightsStateInitial>());
});
group('load', () {
test('дефолты: период 3М, вкладка «Вес», блок свёрнут', () async {
final cubit = buildCubit();
final states = <InsightsState>[];
cubit.stream.listen(states.add);
await cubit.load();
await Future<void>.delayed(Duration.zero);
expect(states.first, isA<InsightsStateLoading>());
final state = loaded(cubit);
expect(state.bodyPeriod, BodyPeriod.threeMonths);
expect(state.panel, InsightsPanel.weight);
expect(state.isExpanded, isFalse);
expect(state.isRefreshing, isFalse);
expect(state.showSleep, isTrue);
expect(state.payload, _payload);
verify(
() => repository.getInsights(
slug: null,
bodyPeriod: BodyPeriod.threeMonths,
heightCm: 176,
),
).called(1);
await cubit.close();
});
test('пробрасывает slug владельца', () async {
final cubit = buildCubit(slug: 'client');
await cubit.load();
verify(() => repository.getHeightCm(slug: 'client')).called(1);
verify(
() => repository.getInsights(
slug: 'client',
bodyPeriod: BodyPeriod.threeMonths,
heightCm: 176,
),
).called(1);
await cubit.close();
});
test('403 → hidden', () async {
when(
() => repository.getHeightCm(slug: any(named: 'slug')),
).thenThrow(const InsightsForbidden());
final cubit = buildCubit(slug: 'client');
await cubit.load();
expect(cubit.state, isA<InsightsStateHidden>());
await cubit.close();
});
test('403 на запросе точек → hidden', () async {
when(
() => repository.getInsights(
slug: any(named: 'slug'),
bodyPeriod: any(named: 'bodyPeriod'),
heightCm: any(named: 'heightCm'),
),
).thenThrow(const InsightsForbidden());
final cubit = buildCubit(slug: 'client');
await cubit.load();
expect(cubit.state, isA<InsightsStateHidden>());
await cubit.close();
});
test('другая ошибка → error', () async {
when(
() => repository.getInsights(
slug: any(named: 'slug'),
bodyPeriod: any(named: 'bodyPeriod'),
heightCm: any(named: 'heightCm'),
),
).thenThrow(Exception('boom'));
final cubit = buildCubit();
await cubit.load();
expect(cubit.state, isA<InsightsStateError>());
await cubit.close();
});
});
group('setBodyPeriod', () {
test(
'смена периода → перезапрос; рост не запрашивается повторно',
() async {
final cubit = buildCubit();
await cubit.load();
when(
() => repository.getInsights(
slug: null,
bodyPeriod: BodyPeriod.year,
heightCm: 176,
),
).thenAnswer((_) async => _payload2);
await cubit.setBodyPeriod(BodyPeriod.year);
expect(loaded(cubit).bodyPeriod, BodyPeriod.year);
expect(loaded(cubit).payload, _payload2);
expect(loaded(cubit).isRefreshing, isFalse);
verify(() => repository.getHeightCm(slug: null)).called(1);
await cubit.close();
},
);
test('тот же период — без запроса', () async {
final cubit = buildCubit();
await cubit.load();
clearInteractions(repository);
await cubit.setBodyPeriod(BodyPeriod.threeMonths);
verifyNever(
() => repository.getInsights(
slug: any(named: 'slug'),
bodyPeriod: any(named: 'bodyPeriod'),
heightCm: any(named: 'heightCm'),
),
);
await cubit.close();
});
test('ошибка перезапроса возвращает прежний период и данные', () async {
final cubit = buildCubit();
await cubit.load();
when(
() => repository.getInsights(
slug: null,
bodyPeriod: BodyPeriod.all,
heightCm: 176,
),
).thenThrow(Exception('boom'));
await cubit.setBodyPeriod(BodyPeriod.all);
expect(loaded(cubit).bodyPeriod, BodyPeriod.threeMonths);
expect(loaded(cubit).payload, _payload);
expect(loaded(cubit).isRefreshing, isFalse);
await cubit.close();
});
test('403 при перезапросе → hidden', () async {
final cubit = buildCubit(slug: 'c');
await cubit.load();
when(
() => repository.getInsights(
slug: 'c',
bodyPeriod: BodyPeriod.month,
heightCm: 176,
),
).thenThrow(const InsightsForbidden());
await cubit.setBodyPeriod(BodyPeriod.month);
expect(cubit.state, isA<InsightsStateHidden>());
await cubit.close();
});
});
group('панели и серии', () {
test('toggleExpanded раскрывает и сворачивает', () async {
final cubit = buildCubit();
await cubit.load();
cubit.toggleExpanded();
expect(loaded(cubit).isExpanded, isTrue);
cubit.toggleExpanded();
expect(loaded(cubit).isExpanded, isFalse);
await cubit.close();
});
test('setPanel переключает вкладку', () async {
final cubit = buildCubit();
await cubit.load();
cubit.setPanel(InsightsPanel.measures);
expect(loaded(cubit).panel, InsightsPanel.measures);
await cubit.close();
});
test('вкладки КБЖУ недоступны при выключенном флаге', () async {
final cubit = buildCubit();
await cubit.load();
cubit.setPanel(InsightsPanel.daily);
expect(loaded(cubit).panel, InsightsPanel.weight);
expect(cubit.panels, [
InsightsPanel.weight,
InsightsPanel.bmi,
InsightsPanel.measures,
]);
await cubit.close();
});
test('вкладки КБЖУ доступны при включённом флаге', () async {
final cubit = buildCubit(kbjuEnabled: true);
await cubit.load();
cubit.setPanel(InsightsPanel.intra);
expect(loaded(cubit).panel, InsightsPanel.intra);
expect(cubit.panels, InsightsPanel.values);
await cubit.close();
});
test('toggleSeries переключает только выбранную серию', () async {
final cubit = buildCubit();
await cubit.load();
cubit.toggleSeries(InsightsPanel.measures, 1);
expect(loaded(cubit).seriesVisibility.measure, [true, false, true]);
cubit.toggleSeries(InsightsPanel.measures, 1);
expect(loaded(cubit).seriesVisibility.measure, [true, true, true]);
cubit.toggleSeries(InsightsPanel.daily, 0);
expect(loaded(cubit).seriesVisibility.daily, [false, true, true, true]);
expect(loaded(cubit).seriesVisibility.measure, [true, true, true]);
await cubit.close();
});
test(
'toggleSeries игнорирует неверный индекс и вкладку без серий',
() async {
final cubit = buildCubit();
await cubit.load();
final before = loaded(cubit).seriesVisibility;
cubit.toggleSeries(InsightsPanel.measures, 5);
cubit.toggleSeries(InsightsPanel.weight, 0);
expect(loaded(cubit).seriesVisibility, before);
await cubit.close();
},
);
test('toggleSleep', () async {
final cubit = buildCubit();
await cubit.load();
cubit.toggleSleep();
expect(loaded(cubit).showSleep, isFalse);
await cubit.close();
});
});
group('refresh и добавление записей', () {
test('refresh сохраняет вкладку, период, серии и раскрытие', () async {
final cubit = buildCubit();
await cubit.load();
cubit.toggleExpanded();
cubit.setPanel(InsightsPanel.measures);
cubit.toggleSeries(InsightsPanel.measures, 0);
when(
() => repository.getInsights(
slug: null,
bodyPeriod: BodyPeriod.threeMonths,
heightCm: 176,
),
).thenAnswer((_) async => _payload2);
await cubit.refresh();
final state = loaded(cubit);
expect(state.payload, _payload2);
expect(state.panel, InsightsPanel.measures);
expect(state.isExpanded, isTrue);
expect(state.seriesVisibility.measure, [false, true, true]);
expect(state.isRefreshing, isFalse);
await cubit.close();
});
test('addWeight сохраняет и обновляет данные', () async {
when(
() => repository.addWeight(weight: any(named: 'weight')),
).thenAnswer((_) async {});
final cubit = buildCubit(canAdd: true);
await cubit.load();
when(
() => repository.getInsights(
slug: null,
bodyPeriod: BodyPeriod.threeMonths,
heightCm: 176,
),
).thenAnswer((_) async => _payload2);
final isSaved = await cubit.addWeight(weight: 72);
expect(isSaved, isTrue);
verify(() => repository.addWeight(weight: 72)).called(1);
expect(loaded(cubit).payload, _payload2);
await cubit.close();
});
test('addCircumference сохраняет и обновляет данные', () async {
when(
() => repository.addCircumference(
chestCircumference: any(named: 'chestCircumference'),
waistCircumference: any(named: 'waistCircumference'),
hipCircumference: any(named: 'hipCircumference'),
),
).thenAnswer((_) async {});
final cubit = buildCubit(canAdd: true);
await cubit.load();
when(
() => repository.getInsights(
slug: null,
bodyPeriod: BodyPeriod.threeMonths,
heightCm: 176,
),
).thenAnswer((_) async => _payload2);
final isSaved = await cubit.addCircumference(
chestCircumference: 95,
waistCircumference: 80,
hipCircumference: 100,
);
expect(isSaved, isTrue);
verify(
() => repository.addCircumference(
chestCircumference: 95,
waistCircumference: 80,
hipCircumference: 100,
),
).called(1);
expect(loaded(cubit).payload, _payload2);
await cubit.close();
});
test(
'ошибка сохранения не перезагружает данные и возвращает false',
() async {
when(
() => repository.addWeight(weight: any(named: 'weight')),
).thenThrow(Exception('boom'));
final cubit = buildCubit(canAdd: true);
await cubit.load();
clearInteractions(repository);
expect(await cubit.addWeight(weight: 72), isFalse);
verifyNever(
() => repository.getInsights(
slug: any(named: 'slug'),
bodyPeriod: any(named: 'bodyPeriod'),
heightCm: any(named: 'heightCm'),
),
);
expect(loaded(cubit).payload, _payload);
await cubit.close();
},
);
});
}

View File

@@ -0,0 +1,382 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/insights/models/insights_period.dart';
import 'package:eatvio_app/features/insights/presentation/charts/bmi_chart.dart';
import 'package:eatvio_app/features/insights/presentation/charts/measure_chart.dart';
import 'package:eatvio_app/features/insights/presentation/charts/weight_chart.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/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../../fixtures/doubles/insights_doubles.dart';
void main() {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: Scaffold(body: SingleChildScrollView(child: child)),
);
}
Future<void> pumpCard(
WidgetTester tester,
FakeInsightsRepository repository, {
String? slug,
bool canAdd = false,
bool kbjuEnabled = false,
bool expand = true,
}) async {
tester.view.physicalSize = const Size(900, 2400);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(
wrap(
InsightsCard(
slug: slug,
canAdd: canAdd,
repository: repository,
kbjuEnabled: kbjuEnabled,
),
),
);
await tester.pumpAndSettle();
if (expand &&
find.byKey(const Key('insights_toggle')).evaluate().isNotEmpty) {
await tester.tap(find.byKey(const Key('insights_toggle')));
await tester.pumpAndSettle();
}
}
group('свёрнутое состояние', () {
testWidgets('по умолчанию свёрнут: виден заголовок, графиков нет', (
tester,
) async {
await pumpCard(tester, FakeInsightsRepository(), expand: false);
expect(find.text('Аналитика'), findsOneWidget);
expect(find.text('нажмите, чтобы посмотреть прогресс'), findsOneWidget);
expect(find.byType(WeightChart), findsNothing);
expect(find.byKey(const Key('insights_tab_weight')), findsNothing);
});
testWidgets('тап по заголовку раскрывает и сворачивает блок', (
tester,
) async {
await pumpCard(tester, FakeInsightsRepository(), expand: false);
await tester.tap(find.byKey(const Key('insights_toggle')));
await tester.pumpAndSettle();
expect(find.byType(WeightChart), findsOneWidget);
expect(find.text('вес, замеры и питание'), findsOneWidget);
await tester.tap(find.byKey(const Key('insights_toggle')));
await tester.pumpAndSettle();
expect(find.byType(WeightChart), findsNothing);
});
});
group('вкладки', () {
testWidgets(
'показаны Вес, ИМТ, Замеры; вкладки КБЖУ скрыты при выключенном флаге',
(tester) async {
await pumpCard(tester, FakeInsightsRepository());
expect(find.byKey(const Key('insights_tab_weight')), findsOneWidget);
expect(find.byKey(const Key('insights_tab_bmi')), findsOneWidget);
expect(find.byKey(const Key('insights_tab_measures')), findsOneWidget);
expect(find.byKey(const Key('insights_tab_daily')), findsNothing);
expect(find.byKey(const Key('insights_tab_intra')), findsNothing);
},
);
testWidgets('вкладки переключают панели', (tester) async {
await pumpCard(tester, FakeInsightsRepository());
expect(find.byType(WeightChart), findsOneWidget);
expect(find.byKey(const Key('insights_headline')), findsOneWidget);
await tester.tap(find.byKey(const Key('insights_tab_measures')));
await tester.pumpAndSettle();
expect(find.byType(MeasureChart), findsOneWidget);
expect(find.byType(WeightChart), findsNothing);
await tester.tap(find.byKey(const Key('insights_tab_bmi')));
await tester.pumpAndSettle();
expect(find.byType(BmiChart), findsOneWidget);
expect(find.byType(MeasureChart), findsNothing);
await tester.tap(find.byKey(const Key('insights_tab_weight')));
await tester.pumpAndSettle();
expect(find.byType(WeightChart), findsOneWidget);
});
testWidgets('вкладки КБЖУ видны при включённом флаге', (tester) async {
await pumpCard(tester, FakeInsightsRepository(), kbjuEnabled: true);
expect(find.byKey(const Key('insights_tab_daily')), findsOneWidget);
expect(find.byKey(const Key('insights_tab_intra')), findsOneWidget);
});
});
group('вес', () {
testWidgets('показывает крупное число и текст дельты из payload', (
tester,
) async {
await pumpCard(tester, FakeInsightsRepository());
expect(find.text('73.2'), findsOneWidget);
expect(find.text('кг'), findsWidgets);
expect(find.text('Снизили на 1.3 кг за период'), findsOneWidget);
});
});
group('пилюли периодов', () {
testWidgets(
'по умолчанию запрошены 3 месяца; смена периода перезапрашивает данные',
(tester) async {
final repository = FakeInsightsRepository();
await pumpCard(tester, repository);
expect(
repository.insightsRequests.single.period,
BodyPeriod.threeMonths,
);
await tester.tap(find.byKey(const Key('insights_period_12')));
await tester.pumpAndSettle();
expect(repository.insightsRequests.map((e) => e.period), [
BodyPeriod.threeMonths,
BodyPeriod.year,
]);
await tester.tap(find.byKey(const Key('insights_period_all')));
await tester.pumpAndSettle();
expect(repository.insightsRequests.last.period, BodyPeriod.all);
},
);
testWidgets('повторный тап по активной пилюле не делает запрос', (
tester,
) async {
final repository = FakeInsightsRepository();
await pumpCard(tester, repository);
await tester.tap(find.byKey(const Key('insights_period_3')));
await tester.pumpAndSettle();
expect(repository.insightsRequests, hasLength(1));
});
testWidgets('slug пробрасывается в запросы', (tester) async {
final repository = FakeInsightsRepository();
await pumpCard(tester, repository, slug: 'client');
await tester.tap(find.byKey(const Key('insights_period_1')));
await tester.pumpAndSettle();
expect(repository.insightsRequests.map((e) => e.slug), [
'client',
'client',
]);
});
});
group('кнопка «+»', () {
testWidgets('нет при canAdd = false', (tester) async {
await pumpCard(tester, FakeInsightsRepository());
expect(find.byKey(const Key('insights_fab')), findsNothing);
});
testWidgets('есть на вкладках «Вес» и «Замеры», нет на «ИМТ»', (
tester,
) async {
await pumpCard(tester, FakeInsightsRepository(), canAdd: true);
expect(find.byKey(const Key('insights_fab')), findsOneWidget);
await tester.tap(find.byKey(const Key('insights_tab_bmi')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('insights_fab')), findsNothing);
await tester.tap(find.byKey(const Key('insights_tab_measures')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('insights_fab')), findsOneWidget);
});
testWidgets('нет в свёрнутом состоянии', (tester) async {
await pumpCard(
tester,
FakeInsightsRepository(),
canAdd: true,
expand: false,
);
expect(find.byKey(const Key('insights_fab')), findsNothing);
});
testWidgets(
'сохранение веса отправляет значение и перезапрашивает данные',
(tester) async {
final repository = FakeInsightsRepository();
await pumpCard(tester, repository, canAdd: true);
await tester.tap(find.byKey(const Key('insights_fab')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('add_weight_kilo_picker')), findsOneWidget);
await tester.tap(find.byKey(const Key('add_sheet_save')));
await tester.pumpAndSettle();
// Начальное значение шторки — последний вес из payload (73.2).
expect(repository.addedWeights, [73.2]);
expect(repository.insightsRequests, hasLength(2));
},
);
testWidgets(
'сохранение замеров отправляет значения и перезапрашивает данные',
(tester) async {
final repository = FakeInsightsRepository();
await pumpCard(tester, repository, canAdd: true);
await tester.tap(find.byKey(const Key('insights_tab_measures')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('insights_fab')));
await tester.pumpAndSettle();
expect(
find.byKey(const Key('add_circumference_waist_picker')),
findsOneWidget,
);
await tester.tap(find.byKey(const Key('add_sheet_save')));
await tester.pumpAndSettle();
// Последние значения из payload: грудь 96, талия 80.5 → 81 (округление), бёдра 100.
expect(repository.addedCircumferences, [
(chest: 96, waist: 81, hip: 100),
]);
expect(repository.insightsRequests, hasLength(2));
},
);
});
group('серии замеров', () {
testWidgets('чипы переключают видимость серии', (tester) async {
await pumpCard(tester, FakeInsightsRepository());
await tester.tap(find.byKey(const Key('insights_tab_measures')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('insights_series_0')), findsOneWidget);
expect(find.byKey(const Key('insights_series_1')), findsOneWidget);
expect(find.byKey(const Key('insights_series_2')), findsOneWidget);
expect(
tester
.widget<Opacity>(
find
.ancestor(
of: find.byKey(const Key('insights_series_1')),
matching: find.byType(Opacity),
)
.first,
)
.opacity,
1,
);
await tester.tap(find.byKey(const Key('insights_series_1')));
await tester.pumpAndSettle();
expect(
tester
.widget<Opacity>(
find
.ancestor(
of: find.byKey(const Key('insights_series_1')),
matching: find.byType(Opacity),
)
.first,
)
.opacity,
0.45,
);
});
});
group('ИМТ', () {
testWidgets('без значений показывает только deltaText', (tester) async {
await pumpCard(tester, FakeInsightsRepository());
await tester.tap(find.byKey(const Key('insights_tab_bmi')));
await tester.pumpAndSettle();
expect(find.text('Укажите рост, чтобы посчитать ИМТ.'), findsOneWidget);
expect(find.byKey(const Key('insights_bmi_chart')), findsNothing);
expect(find.byKey(const Key('insights_headline')), findsNothing);
});
testWidgets('со значениями рисует график и подсказку', (tester) async {
final payload = loadInsightsPayloadFixture().copyWith.bmi(
labels: ['01.11', '09.11'],
values: [26.1, 24.2],
last: 24.2,
delta: -1.9,
headline: '24.2',
category: 'норма',
deltaText: 'ИМТ ниже на 1.9 за 3 месяца · норма',
hint: 'Считаем при росте 176 см.',
);
await pumpCard(tester, FakeInsightsRepository(payload: payload));
await tester.tap(find.byKey(const Key('insights_tab_bmi')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('insights_bmi_chart')), findsOneWidget);
expect(find.byKey(const Key('insights_bmi_hint')), findsOneWidget);
expect(find.text('ИМТ ниже на 1.9 за 3 месяца · норма'), findsOneWidget);
});
});
group('скрытие и ошибки', () {
testWidgets('403 → блок скрыт целиком', (tester) async {
await pumpCard(
tester,
FakeInsightsRepository(error: const InsightsForbidden()),
slug: 'client',
expand: false,
);
expect(find.byKey(const Key('insights_card')), findsNothing);
expect(find.text('Аналитика'), findsNothing);
});
testWidgets(
'ошибка загрузки показывает кнопку «Повторить», повтор грузит данные',
(tester) async {
final repository = FakeInsightsRepository(error: Exception('boom'));
await pumpCard(tester, repository, expand: false);
expect(find.text('Не удалось загрузить аналитику'), findsOneWidget);
repository.error = null;
await tester.tap(find.byKey(const Key('insights_retry')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('insights_toggle')), findsOneWidget);
expect(find.text('Не удалось загрузить аналитику'), findsNothing);
},
);
});
}

View File

@@ -0,0 +1,367 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/insights/models/insights_payload_model.dart';
import 'package:eatvio_app/features/insights/presentation/charts/daily_kbju_chart.dart';
import 'package:eatvio_app/features/insights/presentation/charts/intra_day_chart.dart';
import 'package:eatvio_app/features/insights/presentation/widgets/insights_card.dart';
import 'package:eatvio_app/features/insights/presentation/widgets/intra_date_nav.dart';
import 'package:eatvio_app/features/insights/presentation/widgets/intra_legend.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import '../../../../fixtures/doubles/insights_doubles.dart';
void main() {
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: Scaffold(body: SingleChildScrollView(child: child)),
);
}
Future<void> pumpKbjuCard(
WidgetTester tester,
FakeInsightsRepository repository,
String tabKey,
) async {
tester.view.physicalSize = const Size(900, 2600);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(
wrap(InsightsCard(repository: repository, kbjuEnabled: true)),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('insights_toggle')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(Key(tabKey)));
await tester.pumpAndSettle();
}
group('КБЖУ по дням', () {
testWidgets('рисует график, шапку со средним и 4 чипа серий', (
tester,
) async {
await pumpKbjuCard(
tester,
FakeInsightsRepository(),
'insights_tab_daily',
);
expect(find.byType(DailyKbjuChart), findsOneWidget);
expect(find.text('1930'), findsOneWidget);
expect(find.text('ккал в среднем'), findsOneWidget);
expect(find.text('98% от нормы'), findsOneWidget);
for (var i = 0; i < 4; i++) {
expect(find.byKey(Key('insights_series_$i')), findsOneWidget);
}
expect(find.byKey(const Key('insights_series_4')), findsNothing);
});
testWidgets('чипы скрывают серии', (tester) async {
await pumpKbjuCard(
tester,
FakeInsightsRepository(),
'insights_tab_daily',
);
SfCartesianChart chart() => tester.widget<SfCartesianChart>(
find.byKey(const Key('insights_daily_chart')),
);
expect(chart().series, hasLength(4));
await tester.tap(find.byKey(const Key('insights_series_1')));
await tester.pumpAndSettle();
expect(chart().series, hasLength(3));
});
testWidgets('без daily показывает заглушку', (tester) async {
final payload = loadInsightsPayloadFixture().copyWith(daily: null);
await pumpKbjuCard(
tester,
FakeInsightsRepository(payload: payload),
'insights_tab_daily',
);
expect(find.byType(DailyKbjuChart), findsNothing);
expect(find.text('Недостаточно данных'), findsOneWidget);
});
testWidgets('пустой daily показывает заглушку вместо графика', (
tester,
) async {
await pumpKbjuCard(
tester,
FakeInsightsRepository(
payload: loadInsightsPayloadFixture('insights_payload_minimal.json'),
),
'insights_tab_daily',
);
expect(find.byKey(const Key('insights_daily_chart')), findsNothing);
expect(find.text('Недостаточно данных'), findsWidgets);
});
});
group('В течение дня', () {
testWidgets('рисует график, навигацию по дням, легенду и подсказку', (
tester,
) async {
await pumpKbjuCard(
tester,
FakeInsightsRepository(),
'insights_tab_intra',
);
expect(find.byType(IntraDayChart), findsOneWidget);
expect(find.byType(IntraDateNav), findsOneWidget);
expect(find.text('Сегодня'), findsOneWidget);
expect(find.text('Подъём в 07:30'), findsOneWidget);
expect(find.text('Отход ко сну в 23:00'), findsOneWidget);
expect(find.text('Затемнение — время сна'), findsOneWidget);
expect(find.byKey(const Key('insights_intra_hint')), findsOneWidget);
for (var i = 0; i < 6; i++) {
expect(find.byKey(Key('insights_series_$i')), findsOneWidget);
}
});
testWidgets(
'кнопка «следующий день» отключена при isToday, «предыдущий» без колбэка тоже',
(tester) async {
await pumpKbjuCard(
tester,
FakeInsightsRepository(),
'insights_tab_intra',
);
IconButton buttonIn(String key) => tester.widget<IconButton>(
find.descendant(
of: find.byKey(Key(key)),
matching: find.byType(IconButton),
),
);
expect(buttonIn('insights_intra_next').onPressed, isNull);
expect(buttonIn('insights_intra_prev').onPressed, isNull);
},
);
testWidgets('чип «Сон и подъём» убирает затемнение и легенду не трогает', (
tester,
) async {
await pumpKbjuCard(
tester,
FakeInsightsRepository(),
'insights_tab_intra',
);
NumericAxis xAxis() =>
tester
.widget<SfCartesianChart>(
find.byKey(const Key('insights_intra_chart')),
)
.primaryXAxis
as NumericAxis;
// wake + sleep: две полосы затемнения и две линии.
expect(xAxis().plotBands, hasLength(4));
await tester.tap(find.byKey(const Key('insights_series_5')));
await tester.pumpAndSettle();
expect(xAxis().plotBands, isEmpty);
expect(find.text('Затемнение — время сна'), findsOneWidget);
});
testWidgets('чипы сытости и настроения скрывают линии', (tester) async {
await pumpKbjuCard(
tester,
FakeInsightsRepository(),
'insights_tab_intra',
);
int seriesCount() => tester
.widget<SfCartesianChart>(
find.byKey(const Key('insights_intra_chart')),
)
.series
.length;
// 3 столбика + сытость + настроение.
expect(seriesCount(), 5);
await tester.tap(find.byKey(const Key('insights_series_3')));
await tester.pumpAndSettle();
expect(seriesCount(), 4);
await tester.tap(find.byKey(const Key('insights_series_4')));
await tester.pumpAndSettle();
expect(seriesCount(), 3);
});
testWidgets('пустые линии не создают серий (без экстраполяции)', (
tester,
) async {
final intra = loadInsightsPayloadFixture().intra!.copyWith(
satiety: const [],
mood: const [],
);
final payload = loadInsightsPayloadFixture().copyWith(intra: intra);
await pumpKbjuCard(
tester,
FakeInsightsRepository(payload: payload),
'insights_tab_intra',
);
final chart = tester.widget<SfCartesianChart>(
find.byKey(const Key('insights_intra_chart')),
);
expect(chart.series, hasLength(3));
});
testWidgets('без wake/sleep затемнения нет', (tester) async {
const intra = IntraChartModel(
bars: [IntraBarModel(x: 12, p: 100, f: 50, c: 200)],
date: '2026-11-09',
maxDate: '2026-11-09',
dateLabel: 'Сегодня',
isToday: true,
);
final payload = loadInsightsPayloadFixture().copyWith(intra: intra);
await pumpKbjuCard(
tester,
FakeInsightsRepository(payload: payload),
'insights_tab_intra',
);
final xAxis =
tester
.widget<SfCartesianChart>(
find.byKey(const Key('insights_intra_chart')),
)
.primaryXAxis
as NumericAxis;
expect(xAxis.plotBands, isEmpty);
expect(find.byKey(const Key('insights_legend_wake')), findsNothing);
expect(find.byKey(const Key('insights_legend_sleep')), findsNothing);
expect(find.byType(IntraLegend), findsOneWidget);
});
testWidgets('пустой день показывает заглушку', (tester) async {
await pumpKbjuCard(
tester,
FakeInsightsRepository(
payload: loadInsightsPayloadFixture('insights_payload_minimal.json'),
),
'insights_tab_intra',
);
expect(find.byKey(const Key('insights_intra_chart')), findsNothing);
expect(find.text('Недостаточно данных'), findsOneWidget);
});
});
group('IntraDateNav', () {
const model = IntraChartModel(
date: '2026-11-08',
maxDate: '2026-11-09',
dateLabel: '8 ноября',
isToday: false,
);
testWidgets('вызывает колбэки предыдущего и следующего дня', (
tester,
) async {
var prev = 0;
var next = 0;
await tester.pumpWidget(
wrap(
IntraDateNav(
model: model,
onPreviousDay: () => prev++,
onNextDay: () => next++,
onDatePicked: (_) {},
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('insights_intra_prev')));
await tester.tap(find.byKey(const Key('insights_intra_next')));
expect(prev, 1);
expect(next, 1);
expect(find.text('8 ноября'), findsOneWidget);
});
testWidgets('«следующий день» отключён, когда выбран сегодняшний день', (
tester,
) async {
var next = 0;
await tester.pumpWidget(
wrap(
IntraDateNav(
model: model.copyWith(isToday: true, dateLabel: 'Сегодня'),
onPreviousDay: () {},
onNextDay: () => next++,
),
),
);
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const Key('insights_intra_next')),
warnIfMissed: false,
);
expect(next, 0);
});
testWidgets(
'выбор даты: календарь ограничен maxDate и возвращает выбранный день',
(tester) async {
DateTime? picked;
await tester.pumpWidget(
wrap(
IntraDateNav(
model: model,
onPreviousDay: () {},
onNextDay: () {},
onDatePicked: (date) => picked = date,
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('insights_intra_pick')));
await tester.pumpAndSettle();
final dialog = tester.widget<CalendarDatePicker>(
find.byType(CalendarDatePicker),
);
expect(dialog.lastDate, DateTime(2026, 11, 9));
expect(dialog.initialDate, DateTime(2026, 11, 8));
await tester.tap(find.text('5'));
await tester.tap(
find.text('OK').evaluate().isNotEmpty
? find.text('OK')
: find.text('ОК'),
);
await tester.pumpAndSettle();
expect(picked, DateTime(2026, 11, 5));
},
);
});
}

View File

@@ -0,0 +1,214 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/insights/domain/insights_text_builder.dart';
import 'package:eatvio_app/features/insights/models/insights_period.dart';
import 'package:eatvio_app/features/insights/repositories/insights_repository.dart';
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/repositories/profile_repository.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class _MockProfileRepository extends Mock implements ProfileRepository {}
ProfileDetailsModel _details({int height = 176}) => ProfileDetailsModel(
firstName: 'A',
lastName: 'B',
birthdate: '1990-01-01',
gender: 'male',
country: 'RU',
city: 'Moscow',
lang: 'ru',
weight: 70,
height: height,
chestCircumference: 90,
waistCircumference: 80,
hipCircumference: 95,
bloodPressureUp: 120,
bloodPressureDown: 80,
hasHypo: false,
hasHyper: false,
aboutAlergy: '',
aboutTakingPils: '',
aboutSportHobbies: '',
favoriteFoods: '',
unfavoriteFoods: '',
additionalInfo: '',
isMoodFillable: false,
isBedtimeFillable: false,
activityCoefficient: 1.2,
);
DioException _dio(int status) => DioException(
requestOptions: RequestOptions(path: '/x'),
response: Response(
requestOptions: RequestOptions(path: '/x'),
statusCode: status,
),
);
void main() {
late _MockProfileRepository profileRepository;
late InsightsRepository repository;
final date1 = DateTime(2026, 11, 1, 12);
final date2 = DateTime(2026, 11, 9, 12);
final weights = [
WeightModel(
id: 1,
userId: 1,
weight: 75,
createdAt: date1,
updatedAt: date1,
bmi: 24,
),
WeightModel(
id: 2,
userId: 1,
weight: 73.5,
createdAt: date2,
updatedAt: date2,
bmi: 23.5,
),
];
final circumferences = [
CircumferenceModel(
id: 1,
userId: 1,
chestCircumference: 95,
waistCircumference: 82,
hipCircumference: 100,
createdAt: date1,
updatedAt: date1,
),
];
setUp(() async {
final s = await S.load(const Locale('ru'));
profileRepository = _MockProfileRepository();
repository = InsightsRepository(
profileRepository: profileRepository,
textBuilderFactory: () => InsightsTextBuilder(s),
);
});
group('getInsights', () {
test('собирает payload из веса и замеров; daily/intra == null', () async {
when(
() => profileRepository.getWeight(period: '3', slug: null),
).thenAnswer((_) async => weights);
when(
() => profileRepository.getCircumferences(period: '3', slug: null),
).thenAnswer((_) async => circumferences);
final payload = await repository.getInsights(
bodyPeriod: BodyPeriod.threeMonths,
heightCm: 176,
);
expect(payload.weight.values, [75, 73.5]);
expect(payload.weight.deltaText, 'Снизили на 1.5 кг за 3 месяца');
expect(payload.bmi.values, [24, 23.5]);
expect(payload.measure.waist, [82]);
expect(payload.daily, isNull);
expect(payload.intra, isNull);
});
test('пробрасывает slug и значение периода', () async {
when(
() => profileRepository.getWeight(period: 'all', slug: 'client'),
).thenAnswer((_) async => weights);
when(
() =>
profileRepository.getCircumferences(period: 'all', slug: 'client'),
).thenAnswer((_) async => circumferences);
await repository.getInsights(slug: 'client', bodyPeriod: BodyPeriod.all);
verify(
() => profileRepository.getWeight(period: 'all', slug: 'client'),
).called(1);
verify(
() =>
profileRepository.getCircumferences(period: 'all', slug: 'client'),
).called(1);
});
test('403 → InsightsForbidden', () async {
when(
() => profileRepository.getWeight(period: '3', slug: 'client'),
).thenThrow(_dio(403));
when(
() => profileRepository.getCircumferences(period: '3', slug: 'client'),
).thenAnswer((_) async => circumferences);
expect(
repository.getInsights(
slug: 'client',
bodyPeriod: BodyPeriod.threeMonths,
),
throwsA(isA<InsightsForbidden>()),
);
});
test('прочие ошибки пробрасываются как есть', () async {
when(
() => profileRepository.getWeight(period: '3', slug: null),
).thenThrow(_dio(500));
when(
() => profileRepository.getCircumferences(period: '3', slug: null),
).thenAnswer((_) async => circumferences);
expect(
repository.getInsights(bodyPeriod: BodyPeriod.threeMonths),
throwsA(isA<DioException>()),
);
});
});
group('getHeightCm', () {
test('свой профиль', () async {
when(
() => profileRepository.getMyProfileDetails(),
).thenAnswer((_) async => _details());
expect(await repository.getHeightCm(), 176);
});
test('профиль клиента по slug', () async {
when(
() => profileRepository.getProfileDetailsBySlug('client'),
).thenAnswer((_) async => _details(height: 168));
expect(await repository.getHeightCm(slug: 'client'), 168);
});
test('нулевой рост → null', () async {
when(
() => profileRepository.getMyProfileDetails(),
).thenAnswer((_) async => _details(height: 0));
expect(await repository.getHeightCm(), isNull);
});
test('ошибка не 403 → null', () async {
when(() => profileRepository.getMyProfileDetails()).thenThrow(_dio(500));
expect(await repository.getHeightCm(), isNull);
});
test('403 → InsightsForbidden', () async {
when(
() => profileRepository.getProfileDetailsBySlug('client'),
).thenThrow(_dio(403));
expect(
repository.getHeightCm(slug: 'client'),
throwsA(isA<InsightsForbidden>()),
);
});
});
}

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/models/weight_model.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -42,6 +43,51 @@ void main() {
expect(json['created_at'], isA<String>());
expect(json['updated_at'], isA<String>());
});
Map<String, dynamic> json({Object? weight = 72.3, Object? bmi = 22.4}) => {
'id': 1,
'user_id': 2,
'weight': weight,
'created_at': '2026-11-01T10:00:00.000000Z',
'updated_at': '2026-11-01T10:00:00.000000Z',
'bmi': bmi,
};
test('парсит дробный weight', () {
final model = WeightModel.fromJson(json());
expect(model.weight, 72.3);
expect(model.bmi, 22.4);
});
test('парсит целый weight как double', () {
final model = WeightModel.fromJson(json(weight: 72));
expect(model.weight, 72.0);
});
test('допускает bmi: null (рост не указан)', () {
final model = WeightModel.fromJson(json(bmi: null));
expect(model.bmi, isNull);
});
});
group('CircumferenceModel', () {
test('парсит целые и дробные значения (прореженные средние)', () {
final model = CircumferenceModel.fromJson({
'id': 1,
'user_id': 2,
'chest_circumference': 95,
'waist_circumference': 80.5,
'hip_circumference': 100,
'created_at': '2026-11-01T10:00:00.000000Z',
'updated_at': '2026-11-01T10:00:00.000000Z',
});
expect(model.chestCircumference, 95.0);
expect(model.waistCircumference, 80.5);
expect(model.hipCircumference, 100.0);
});
});
}

View File

@@ -4,9 +4,8 @@ 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/profile/models/circumference_model.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/models/weight_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/profile_cubit/profile_cubit.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
@@ -45,6 +44,7 @@ void main() {
plansRepository = MockPlansRepository();
sl.registerLazySingleton<ProfileRepository>(() => profileRepository);
sl.registerLazySingleton<InsightsRepository>(() => InsightsRepository(profileRepository: profileRepository));
sl.registerLazySingleton<ChatRepository>(() => chatRepository);
sl.registerLazySingleton<ConsultantRepository>(() => consultantRepository);
sl.registerLazySingleton<PlansRepository>(() => plansRepository);
@@ -117,8 +117,6 @@ void main() {
when(profileRepository.getMyProfile()).thenAnswer((_) async => profile);
when(profileRepository.getMyProfileDetails()).thenAnswer((_) async => profileDetails);
when(profileRepository.getBlogPostsByAuthorSlug(slug: anyNamed('slug'))).thenAnswer((_) async => []);
when(profileRepository.getWeight(period: anyNamed('period'))).thenAnswer((_) async => <WeightModel>[]);
when(profileRepository.getCircumferences(period: anyNamed('period'))).thenAnswer((_) async => <CircumferenceModel>[]);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(const ProfileScreen()));
@@ -190,8 +188,6 @@ void main() {
when(profileRepository.getMyProfileDetails()).thenAnswer((_) async => profileDetails);
when(profileRepository.getProfileDetailsBySlug(any)).thenAnswer((_) async => profileDetails);
when(profileRepository.getBlogPostsByAuthorSlug(slug: anyNamed('slug'))).thenAnswer((_) async => []);
when(profileRepository.getWeight(period: anyNamed('period'))).thenAnswer((_) async => <WeightModel>[]);
when(profileRepository.getCircumferences(period: anyNamed('period'))).thenAnswer((_) async => <CircumferenceModel>[]);
await mockNetworkImagesFor(() async {
await tester.pumpWidget(wrap(const ProfileScreen(slug: 'test-user')));

View File

@@ -1,47 +0,0 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/circumference_chart_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/date_symbol_data_local.dart';
void main() {
group('CircumferenceChartWidget smoke', () {
setUpAll(() async {
await initializeDateFormatting('ru', null);
});
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders circumference chart widget with empty points', (tester) async {
await tester.pumpWidget(wrap(const CircumferenceChartWidget(circumferencePoints: [])));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(CircumferenceChartWidget), findsOneWidget);
});
testWidgets('renders circumference chart widget with points', (tester) async {
final points = [
CircumferenceModel(
id: 1,
userId: 1,
chestCircumference: 90,
waistCircumference: 80,
hipCircumference: 95,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
];
await tester.pumpWidget(wrap(CircumferenceChartWidget(circumferencePoints: points)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(CircumferenceChartWidget), findsOneWidget);
});
});
}

View File

@@ -1,132 +0,0 @@
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/profile/models/weight_model.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/imt_chart_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:intl/date_symbol_data_local.dart';
void main() {
group('ImtChartWidget', () {
setUp(() async {
await sl.reset();
await initializeDateFormatting('ru', null);
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
});
Widget createTestWidget(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Portal(
child: Scaffold(body: SingleChildScrollView(child: child)),
),
);
}
testWidgets('should display chart with empty weight points', (WidgetTester tester) async {
// Arrange
const widget = ImtChartWidget(weightPoints: []);
// Act
await tester.pumpWidget(createTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Assert
expect(find.byType(ImtChartWidget), findsOneWidget);
});
testWidgets('should display chart with single weight point', (WidgetTester tester) async {
// Arrange
final weightPoints = [
WeightModel(
id: 1,
userId: 1,
weight: 70,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
bmi: 23.5,
),
];
final widget = ImtChartWidget(weightPoints: weightPoints);
// Act
await tester.pumpWidget(createTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Assert
expect(find.byType(ImtChartWidget), findsOneWidget);
});
testWidgets('should display chart with multiple weight points and show increase arrow', (WidgetTester tester) async {
// Arrange
final now = DateTime.now();
final weightPoints = [
WeightModel(
id: 1,
userId: 1,
weight: 70,
createdAt: now.subtract(const Duration(days: 7)),
updatedAt: now.subtract(const Duration(days: 7)),
bmi: 23.0,
),
WeightModel(
id: 2,
userId: 1,
weight: 72,
createdAt: now,
updatedAt: now,
bmi: 24.0,
),
];
final widget = ImtChartWidget(weightPoints: weightPoints);
// Act
await tester.pumpWidget(createTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Assert
expect(find.byType(ImtChartWidget), findsOneWidget);
});
testWidgets('should display chart with multiple weight points and show decrease arrow', (WidgetTester tester) async {
// Arrange
final now = DateTime.now();
final weightPoints = [
WeightModel(
id: 1,
userId: 1,
weight: 72,
createdAt: now.subtract(const Duration(days: 7)),
updatedAt: now.subtract(const Duration(days: 7)),
bmi: 24.0,
),
WeightModel(
id: 2,
userId: 1,
weight: 70,
createdAt: now,
updatedAt: now,
bmi: 23.0,
),
];
final widget = ImtChartWidget(weightPoints: weightPoints);
// Act
await tester.pumpWidget(createTestWidget(widget));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Assert
expect(find.byType(ImtChartWidget), findsOneWidget);
});
});
}

View File

@@ -1,64 +0,0 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/profile/models/circumference_model.dart';
import 'package:eatvio_app/features/profile/models/weight_model.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/circumference_chart_widget.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
void main() {
group('Profile chart widgets smoke', () {
setUpAll(() async {
Intl.defaultLocale = 'ru';
await initializeDateFormatting('ru');
});
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: SingleChildScrollView(child: child)),
);
}
testWidgets('WeightChartWidget renders empty state', (tester) async {
await tester.pumpWidget(wrap(const WeightChartWidget(weightPoints: [])));
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.text('Добавьте Ваш вес'), findsOneWidget);
});
testWidgets('WeightChartWidget renders with one point', (tester) async {
final points = [WeightModel(id: 1, userId: 1, weight: 70, createdAt: DateTime(2025, 1, 1), updatedAt: DateTime(2025, 1, 1), bmi: 22.0)];
await tester.pumpWidget(wrap(WeightChartWidget(weightPoints: points)));
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.textContaining('кг'), findsWidgets);
});
testWidgets('CircumferenceChartWidget renders empty state', (tester) async {
await tester.pumpWidget(wrap(const CircumferenceChartWidget(circumferencePoints: [])));
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.text('Добавьте Ваши замеры'), findsOneWidget);
});
testWidgets('CircumferenceChartWidget renders with one point', (tester) async {
final points = [
CircumferenceModel(
id: 1,
userId: 1,
chestCircumference: 100,
waistCircumference: 80,
hipCircumference: 90,
createdAt: DateTime(2025, 1, 1),
updatedAt: DateTime(2025, 1, 1),
),
];
await tester.pumpWidget(wrap(CircumferenceChartWidget(circumferencePoints: points)));
await tester.pumpAndSettle(const Duration(milliseconds: 200));
expect(find.text('100'), findsOneWidget);
expect(find.text('80'), findsOneWidget);
expect(find.text('90'), findsOneWidget);
});
});
}

View File

@@ -1,37 +0,0 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/profile/models/weight_model.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/weight_chart_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/date_symbol_data_local.dart';
void main() {
group('WeightChartWidget smoke', () {
setUpAll(() async {
await initializeDateFormatting('ru', null);
});
Widget wrap(Widget child) {
return MaterialApp(
theme: ThemeHelper().themeData(),
home: Scaffold(body: child),
);
}
testWidgets('renders weight chart widget with empty points', (tester) async {
await tester.pumpWidget(wrap(const WeightChartWidget(weightPoints: [])));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(WeightChartWidget), findsOneWidget);
});
testWidgets('renders weight chart widget with points', (tester) async {
final points = [WeightModel(id: 1, userId: 1, weight: 70, bmi: 22.5, createdAt: DateTime.now(), updatedAt: DateTime.now())];
await tester.pumpWidget(wrap(WeightChartWidget(weightPoints: points)));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(WeightChartWidget), findsOneWidget);
});
});
}

View File

@@ -1,6 +1,4 @@
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/repositories/profile_repository.dart';
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -28,8 +26,6 @@ void main() {
expect(cubit.state.isLoading, isFalse);
expect(cubit.state.error, isNull);
expect(cubit.state.profileDetails, isNull);
expect(cubit.state.weightPoints, isNull);
expect(cubit.state.circumferencePoints, isNull);
});
test('getUserDetails - успешная загрузка деталей пользователя', () async {
@@ -62,37 +58,8 @@ void main() {
activityCoefficient: 1.5,
);
final weightPoints = [
WeightModel(
id: 1,
userId: 1,
weight: 70,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
bmi: 22.9,
),
];
final circumferencePoints = [
CircumferenceModel(
id: 1,
userId: 1,
chestCircumference: 100,
waistCircumference: 80,
hipCircumference: 90,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
];
when(mockProfileRepository.getProfileDetailsBySlug(slug))
.thenAnswer((_) async => profileDetails);
when(mockProfileRepository.getWeight(period: '1', slug: slug))
.thenAnswer((_) async => weightPoints);
when(mockProfileRepository.getCircumferences(period: '1', slug: slug))
.thenAnswer((_) async => circumferencePoints);
cubit.getUserDetails(slug);
@@ -119,75 +86,6 @@ void main() {
expect(cubit.state.error, equals(error.toString()));
expect(cubit.state.isLoading, isFalse);
});
test('getChartData - успешная загрузка данных графика', () async {
const slug = 'test-user';
const period = '1';
final weightPoints = [
WeightModel(
id: 1,
userId: 1,
weight: 70,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
bmi: 22.9,
),
WeightModel(
id: 2,
userId: 1,
weight: 71,
createdAt: DateTime.now().add(const Duration(days: 1)),
updatedAt: DateTime.now().add(const Duration(days: 1)),
bmi: 23.2,
),
];
final circumferencePoints = [
CircumferenceModel(
id: 1,
userId: 1,
chestCircumference: 100,
waistCircumference: 80,
hipCircumference: 90,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
];
when(mockProfileRepository.getWeight(period: period, slug: slug))
.thenAnswer((_) async => weightPoints);
when(mockProfileRepository.getCircumferences(period: period, slug: slug))
.thenAnswer((_) async => circumferencePoints);
cubit.getChartData(period: period, slug: slug);
// Ждем завершения асинхронных операций
await Future.delayed(const Duration(milliseconds: 100));
expect(cubit.state.weightPoints, equals(weightPoints));
expect(cubit.state.circumferencePoints, equals(circumferencePoints));
verify(mockProfileRepository.getWeight(period: period, slug: slug)).called(1);
verify(mockProfileRepository.getCircumferences(period: period, slug: slug)).called(1);
});
test('getChartData - обработка ошибки', () async {
const slug = 'test-user';
const period = '1';
final error = Exception('Ошибка загрузки');
when(mockProfileRepository.getWeight(period: period, slug: slug))
.thenThrow(error);
cubit.getChartData(period: period, slug: slug);
// Ждем завершения асинхронных операций
await Future.delayed(const Duration(milliseconds: 100));
expect(cubit.state.error, equals(error.toString()));
expect(cubit.state.isLoading, isFalse);
});
});
}

View File

@@ -2,6 +2,7 @@ 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/insights/repositories/insights_repository.dart';
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';
@@ -97,10 +98,11 @@ void main() {
final cubit = UserDetailsCubit(profileRepository: profileRepository);
when(profileRepository.getProfileDetailsBySlug('test-slug')).thenAnswer((_) async => mockProfileDetails);
when(profileRepository.getWeight(period: '1', slug: 'test-slug')).thenAnswer((_) async => mockWeightPoints);
when(profileRepository.getCircumferences(period: '1', slug: 'test-slug')).thenAnswer((_) async => mockCircumferencePoints);
when(profileRepository.getWeight(period: '3', slug: 'test-slug')).thenAnswer((_) async => mockWeightPoints);
when(profileRepository.getCircumferences(period: '3', slug: 'test-slug')).thenAnswer((_) async => mockCircumferencePoints);
sl.registerLazySingleton<ProfileRepository>(() => profileRepository);
sl.registerLazySingleton<InsightsRepository>(() => InsightsRepository(profileRepository: profileRepository));
sl.registerLazySingleton<UserDetailsCubit>(() => cubit);
await tester.pumpWidget(wrap(const UserDetailsScreen(slug: 'test-slug')));
@@ -117,10 +119,11 @@ void main() {
final cubit = UserDetailsCubit(profileRepository: profileRepository);
when(profileRepository.getProfileDetailsBySlug('test-slug')).thenAnswer((_) async => mockProfileDetails);
when(profileRepository.getWeight(period: '1', slug: 'test-slug')).thenAnswer((_) async => mockWeightPoints);
when(profileRepository.getCircumferences(period: '1', slug: 'test-slug')).thenAnswer((_) async => mockCircumferencePoints);
when(profileRepository.getWeight(period: '3', slug: 'test-slug')).thenAnswer((_) async => mockWeightPoints);
when(profileRepository.getCircumferences(period: '3', slug: 'test-slug')).thenAnswer((_) async => mockCircumferencePoints);
sl.registerLazySingleton<ProfileRepository>(() => profileRepository);
sl.registerLazySingleton<InsightsRepository>(() => InsightsRepository(profileRepository: profileRepository));
sl.registerLazySingleton<UserDetailsCubit>(() => cubit);
await tester.pumpWidget(wrap(const UserDetailsScreen(slug: 'test-slug')));
@@ -136,6 +139,7 @@ void main() {
when(profileRepository.getProfileDetailsBySlug('test-slug')).thenThrow(Exception('Error loading profile'));
sl.registerLazySingleton<ProfileRepository>(() => profileRepository);
sl.registerLazySingleton<InsightsRepository>(() => InsightsRepository(profileRepository: profileRepository));
sl.registerLazySingleton<UserDetailsCubit>(() => cubit);
await tester.pumpWidget(wrap(const UserDetailsScreen(slug: 'test-slug')));