Files
eatvio_app/test/features/insights/presentation/widgets/insights_kbju_test.dart
2026-11-09 15:35:10 +03:00

368 lines
12 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/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));
},
);
});
}