forked from eatvio/eatvio_app
454 lines
15 KiB
Dart
454 lines
15 KiB
Dart
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));
|
||
},
|
||
);
|
||
});
|
||
}
|