feat(insights): added kbju fetching for analitics

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

View File

@@ -0,0 +1,336 @@
import 'package:dio/dio.dart';
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 '../../../../fixtures/doubles/insights_doubles.dart';
/// Серверный режим (`kbjuEnabled: true`): `GET /api/profile/insights*`.
void main() {
late FakeInsightsRepository repository;
late IntraChartModel today;
late IntraChartModel yesterday;
setUp(() {
repository = FakeInsightsRepository();
today = repository.payload.intra!;
yesterday = today.copyWith(
date: '2026-11-08',
dateLabel: '8 ноября',
isToday: false,
);
repository.intraByDate['2026-11-08'] = yesterday;
repository.intraByDate['2026-11-09'] = today;
});
InsightsCubit buildCubit({String? slug}) {
return InsightsCubit(repository: repository, slug: slug, kbjuEnabled: true);
}
InsightsStateLoaded loaded(InsightsCubit cubit) =>
cubit.state as InsightsStateLoaded;
group('load', () {
test('запрашивает серверный payload с дефолтными периодами', () async {
final cubit = buildCubit();
await cubit.load();
expect(repository.serverRequests, [
(
slug: null,
bodyPeriod: BodyPeriod.threeMonths,
dailyPeriod: DailyPeriod.twoWeeks,
intraDate: null,
),
]);
expect(repository.insightsRequests, isEmpty);
expect(loaded(cubit).payload.daily, isNotNull);
expect(loaded(cubit).dailyPeriod, DailyPeriod.twoWeeks);
expect(loaded(cubit).intraDate, isNull);
await cubit.close();
});
test('рост профиля не запрашивается: ИМТ считает сервер', () async {
repository.heightCm = null;
final cubit = buildCubit(slug: 'client');
await cubit.load();
expect(repository.serverRequests.single.slug, 'client');
expect(loaded(cubit).payload.bmi, repository.payload.bmi);
await cubit.close();
});
test('403 скрывает блок', () async {
repository.error = const InsightsForbidden();
final cubit = buildCubit(slug: 'client');
await cubit.load();
expect(cubit.state, isA<InsightsStateHidden>());
await cubit.close();
});
test('ошибка сервера → error', () async {
repository.error = DioException(requestOptions: RequestOptions());
final cubit = buildCubit();
await cubit.load();
expect(cubit.state, isA<InsightsStateError>());
await cubit.close();
});
});
group('setDailyPeriod', () {
test('перезапрашивает все данные, сохраняя период тела и день', () async {
final cubit = buildCubit();
await cubit.load();
await cubit.setBodyPeriod(BodyPeriod.year);
await cubit.setIntraDate(DateTime(2026, 11, 8));
repository.serverRequests.clear();
await cubit.setDailyPeriod(DailyPeriod.month);
expect(repository.serverRequests, [
(
slug: null,
bodyPeriod: BodyPeriod.year,
dailyPeriod: DailyPeriod.month,
intraDate: '2026-11-08',
),
]);
expect(loaded(cubit).dailyPeriod, DailyPeriod.month);
expect(loaded(cubit).isRefreshing, isFalse);
await cubit.close();
});
test('тот же период не перезапрашивается', () async {
final cubit = buildCubit();
await cubit.load();
repository.serverRequests.clear();
await cubit.setDailyPeriod(DailyPeriod.twoWeeks);
expect(repository.serverRequests, isEmpty);
await cubit.close();
});
test('ошибка возвращает прежний период и оставляет данные', () async {
final cubit = buildCubit();
await cubit.load();
final before = loaded(cubit).payload;
repository.error = DioException(requestOptions: RequestOptions());
await cubit.setDailyPeriod(DailyPeriod.week);
expect(loaded(cubit).dailyPeriod, DailyPeriod.twoWeeks);
expect(loaded(cubit).payload, before);
expect(loaded(cubit).isRefreshing, isFalse);
await cubit.close();
});
test('смена периода тела передаёт текущий период КБЖУ', () async {
final cubit = buildCubit();
await cubit.load();
await cubit.setDailyPeriod(DailyPeriod.week);
repository.serverRequests.clear();
await cubit.setBodyPeriod(BodyPeriod.month);
expect(repository.serverRequests.single.dailyPeriod, DailyPeriod.week);
await cubit.close();
});
});
group('«В течение дня»', () {
test(
'setIntraDate запрашивает только intra и подменяет её в payload',
() async {
final cubit = buildCubit();
await cubit.load();
repository.serverRequests.clear();
await cubit.setIntraDate(DateTime(2026, 11, 8));
expect(repository.serverRequests, isEmpty);
expect(repository.intraRequests, [(slug: null, date: '2026-11-08')]);
final state = loaded(cubit);
expect(state.payload.intra, yesterday);
expect(state.payload.weight, repository.payload.weight);
expect(state.intraDate, '2026-11-08');
expect(state.isRefreshing, isFalse);
await cubit.close();
},
);
test('slug передаётся в запрос чужого дня', () async {
final cubit = buildCubit(slug: 'client');
await cubit.load();
await cubit.setIntraDate(DateTime(2026, 11, 8));
expect(repository.intraRequests.single.slug, 'client');
await cubit.close();
});
test('showPreviousDay и showNextDay листают по дню', () async {
final cubit = buildCubit();
await cubit.load();
await cubit.showPreviousDay();
expect(repository.intraRequests.last.date, '2026-11-08');
expect(loaded(cubit).payload.intra!.isToday, isFalse);
await cubit.showNextDay();
expect(repository.intraRequests.last.date, '2026-11-09');
expect(loaded(cubit).payload.intra!.isToday, isTrue);
expect(loaded(cubit).intraDate, isNull);
await cubit.close();
});
test('showPreviousDay переходит через границу месяца', () async {
repository.payload = repository.payload.copyWith(
intra: today.copyWith(date: '2026-11-01', isToday: false),
);
final cubit = buildCubit();
await cubit.load();
await cubit.showPreviousDay();
expect(repository.intraRequests.single.date, '2026-10-31');
await cubit.close();
});
test('showNextDay на сегодняшнем дне ничего не запрашивает', () async {
final cubit = buildCubit();
await cubit.load();
await cubit.showNextDay();
expect(repository.intraRequests, isEmpty);
await cubit.close();
});
test('сервер вернул другую дату (clamp) — берём её', () async {
repository.intraByDate['2026-11-20'] = today;
final cubit = buildCubit();
await cubit.load();
await cubit.setIntraDate(DateTime(2026, 11, 20));
expect(loaded(cubit).intraDate, isNull);
expect(loaded(cubit).payload.intra!.date, '2026-11-09');
await cubit.close();
});
test('пустой день: intra с пустыми сериями остаётся объектом', () async {
final empty = today.copyWith(
date: '2026-11-07',
dateLabel: '7 ноября',
isToday: false,
bars: const [],
satiety: const [],
mood: const [],
wake: null,
sleep: null,
wakeText: null,
sleepText: null,
);
repository.intraByDate['2026-11-07'] = empty;
final cubit = buildCubit();
await cubit.load();
await cubit.setIntraDate(DateTime(2026, 11, 7));
expect(loaded(cubit).payload.intra, empty);
await cubit.close();
});
test('ошибка запроса дня не стирает показанные данные', () async {
final cubit = buildCubit();
await cubit.load();
repository.intraError = DioException(requestOptions: RequestOptions());
await cubit.setIntraDate(DateTime(2026, 11, 8));
final state = loaded(cubit);
expect(state.payload.intra, today);
expect(state.intraDate, isNull);
expect(state.isRefreshing, isFalse);
await cubit.close();
});
test('403 при листании скрывает блок', () async {
final cubit = buildCubit(slug: 'client');
await cubit.load();
repository.intraError = const InsightsForbidden();
await cubit.showPreviousDay();
expect(cubit.state, isA<InsightsStateHidden>());
await cubit.close();
});
test('refresh сохраняет выбранный день, вкладку и серии', () async {
final cubit = buildCubit();
await cubit.load();
cubit.toggleExpanded();
cubit.setPanel(InsightsPanel.intra);
cubit.toggleSleep();
await cubit.setIntraDate(DateTime(2026, 11, 8));
repository.serverRequests.clear();
await cubit.refresh();
expect(repository.serverRequests.single.intraDate, '2026-11-08');
final state = loaded(cubit);
expect(state.panel, InsightsPanel.intra);
expect(state.isExpanded, isTrue);
expect(state.showSleep, isFalse);
expect(state.intraDate, '2026-11-08');
await cubit.close();
});
test('refresh во время первой загрузки не дублирует запрос', () async {
final cubit = buildCubit();
final loading = cubit.load();
await cubit.refresh();
await loading;
expect(repository.serverRequests, hasLength(1));
await cubit.close();
});
test('без флага навигация по дням игнорируется', () async {
final cubit = InsightsCubit(repository: repository, kbjuEnabled: false);
await cubit.load();
await cubit.showPreviousDay();
await cubit.setDailyPeriod(DailyPeriod.week);
expect(repository.intraRequests, isEmpty);
expect(repository.serverRequests, isEmpty);
await cubit.close();
});
});
test('после добавления веса данные обновляются с сервера', () async {
final cubit = buildCubit();
await cubit.load();
repository.serverRequests.clear();
final isSaved = await cubit.addWeight(weight: 72.5);
expect(isSaved, isTrue);
expect(repository.addedWeights, [72.5]);
expect(repository.serverRequests, hasLength(1));
await cubit.close();
});
}

View File

@@ -26,6 +26,7 @@ void main() {
setUpAll(() {
registerFallbackValue(BodyPeriod.month);
registerFallbackValue(DailyPeriod.week);
});
setUp(() {
@@ -40,6 +41,14 @@ void main() {
heightCm: any(named: 'heightCm'),
),
).thenAnswer((_) async => _payload);
when(
() => repository.fetchInsights(
slug: any(named: 'slug'),
bodyPeriod: any(named: 'bodyPeriod'),
dailyPeriod: any(named: 'dailyPeriod'),
intraDate: any(named: 'intraDate'),
),
).thenAnswer((_) async => _payload);
});
InsightsCubit buildCubit({

View File

@@ -1,5 +1,6 @@
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/models/insights_period.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';
@@ -60,7 +61,7 @@ void main() {
expect(find.byType(DailyKbjuChart), findsOneWidget);
expect(find.text('1930'), findsOneWidget);
expect(find.text('ккал в среднем'), findsOneWidget);
expect(find.text('98% от нормы'), findsOneWidget);
expect(find.text('Калораж вырос на 5% за период'), findsOneWidget);
for (var i = 0; i < 4; i++) {
expect(find.byKey(Key('insights_series_$i')), findsOneWidget);
}
@@ -85,6 +86,20 @@ void main() {
expect(chart().series, hasLength(3));
});
testWidgets('пилюли периода активны и перезапрашивают данные', (
tester,
) async {
final repository = FakeInsightsRepository();
await pumpKbjuCard(tester, repository, 'insights_tab_daily');
repository.serverRequests.clear();
await tester.tap(find.byKey(const Key('insights_period_30')));
await tester.pumpAndSettle();
expect(repository.serverRequests.single.dailyPeriod, DailyPeriod.month);
expect(repository.serverRequests.single.bodyPeriod, BodyPeriod.threeMonths);
});
testWidgets('без daily показывает заглушку', (tester) async {
final payload = loadInsightsPayloadFixture().copyWith(daily: null);
await pumpKbjuCard(
@@ -135,26 +150,66 @@ void main() {
}
});
testWidgets(
'кнопка «следующий день» отключена при isToday, «предыдущий» без колбэка тоже',
(tester) async {
await pumpKbjuCard(
tester,
FakeInsightsRepository(),
'insights_tab_intra',
);
testWidgets('на сегодняшнем дне «следующий день» отключён, «предыдущий» нет', (
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),
),
);
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);
},
);
expect(buttonIn('insights_intra_next').onPressed, isNull);
expect(buttonIn('insights_intra_prev').onPressed, isNotNull);
});
testWidgets('«предыдущий день» запрашивает только intra и меняет дату', (
tester,
) async {
final repository = FakeInsightsRepository();
repository.intraByDate['2026-11-08'] = loadInsightsPayloadFixture()
.intra!
.copyWith(date: '2026-11-08', dateLabel: '8 ноября', isToday: false);
await pumpKbjuCard(tester, repository, 'insights_tab_intra');
repository.serverRequests.clear();
await tester.tap(find.byKey(const Key('insights_intra_prev')));
await tester.pumpAndSettle();
expect(repository.intraRequests, [(slug: null, date: '2026-11-08')]);
expect(repository.serverRequests, isEmpty);
expect(find.text('8 ноября'), findsOneWidget);
expect(find.text('Сегодня'), findsNothing);
});
testWidgets('с прошедшего дня можно вернуться «следующим днём»', (
tester,
) async {
final repository = FakeInsightsRepository();
final today = loadInsightsPayloadFixture().intra!;
repository.intraByDate['2026-11-08'] = today.copyWith(
date: '2026-11-08',
dateLabel: '8 ноября',
isToday: false,
);
repository.intraByDate['2026-11-09'] = today;
await pumpKbjuCard(tester, repository, 'insights_tab_intra');
await tester.tap(find.byKey(const Key('insights_intra_prev')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('insights_intra_next')));
await tester.pumpAndSettle();
expect(repository.intraRequests.last.date, '2026-11-09');
expect(find.text('Сегодня'), findsOneWidget);
});
testWidgets('чип «Сон и подъём» убирает затемнение и легенду не трогает', (
tester,