forked from eatvio/eatvio_app
feat(insights): first isights impemetation
This commit is contained in:
@@ -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 {
|
||||
|
||||
285
test/features/insights/domain/insights_text_builder_test.dart
Normal file
285
test/features/insights/domain/insights_text_builder_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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')));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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')));
|
||||
|
||||
Reference in New Issue
Block a user