forked from eatvio/eatvio_app
feat(insights): first isights impemetation
This commit is contained in:
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user