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

@@ -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>()),
);
});
});
}