Files
eatvio_app/test/features/insights/presentation/widgets/insights_kbju_test.dart
2026-11-11 16:05:11 +03:00

454 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
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('Калораж вырос на 5% за период'), 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('пилюли периода активны и перезапрашивают данные', (
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(
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(
'на сегодняшнем дне «следующий день» отключён, «предыдущий» нет',
(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, 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,
) 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 {
final intra = loadInsightsPayloadFixture().intra!.copyWith(
bars: const [],
satiety: const [],
mood: const [],
);
await pumpKbjuCard(
tester,
FakeInsightsRepository(
payload: loadInsightsPayloadFixture().copyWith(intra: intra),
),
'insights_tab_intra',
);
expect(find.byKey(const Key('insights_intra_chart')), findsOneWidget);
expect(find.text('Недостаточно данных'), findsNothing);
final xAxis =
tester
.widget<SfCartesianChart>(
find.byKey(const Key('insights_intra_chart')),
)
.primaryXAxis
as NumericAxis;
expect(xAxis.plotBands, isNotEmpty);
});
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));
},
);
});
}