forked from eatvio/eatvio_app
feat(insights): first isights impemetation
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
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 'package:mocktail/mocktail.dart';
|
||||
|
||||
class _MockInsightsRepository extends Mock implements InsightsRepository {}
|
||||
|
||||
const _payload = InsightsPayloadModel(
|
||||
weight: WeightChartModel(values: [70, 71], headline: '71.0'),
|
||||
bmi: BmiChartModel(),
|
||||
measure: MeasureChartModel(),
|
||||
);
|
||||
|
||||
const _payload2 = InsightsPayloadModel(
|
||||
weight: WeightChartModel(values: [72], headline: '72.0'),
|
||||
bmi: BmiChartModel(),
|
||||
measure: MeasureChartModel(),
|
||||
);
|
||||
|
||||
void main() {
|
||||
late _MockInsightsRepository repository;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(BodyPeriod.month);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
repository = _MockInsightsRepository();
|
||||
when(
|
||||
() => repository.getHeightCm(slug: any(named: 'slug')),
|
||||
).thenAnswer((_) async => 176);
|
||||
when(
|
||||
() => repository.getInsights(
|
||||
slug: any(named: 'slug'),
|
||||
bodyPeriod: any(named: 'bodyPeriod'),
|
||||
heightCm: any(named: 'heightCm'),
|
||||
),
|
||||
).thenAnswer((_) async => _payload);
|
||||
});
|
||||
|
||||
InsightsCubit buildCubit({
|
||||
String? slug,
|
||||
bool canAdd = false,
|
||||
bool kbjuEnabled = false,
|
||||
}) {
|
||||
return InsightsCubit(
|
||||
repository: repository,
|
||||
slug: slug,
|
||||
canAdd: canAdd,
|
||||
kbjuEnabled: kbjuEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
InsightsStateLoaded loaded(InsightsCubit cubit) =>
|
||||
cubit.state as InsightsStateLoaded;
|
||||
|
||||
test('начальное состояние — initial', () {
|
||||
expect(buildCubit().state, isA<InsightsStateInitial>());
|
||||
});
|
||||
|
||||
group('load', () {
|
||||
test('дефолты: период 3М, вкладка «Вес», блок свёрнут', () async {
|
||||
final cubit = buildCubit();
|
||||
final states = <InsightsState>[];
|
||||
cubit.stream.listen(states.add);
|
||||
|
||||
await cubit.load();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(states.first, isA<InsightsStateLoading>());
|
||||
final state = loaded(cubit);
|
||||
expect(state.bodyPeriod, BodyPeriod.threeMonths);
|
||||
expect(state.panel, InsightsPanel.weight);
|
||||
expect(state.isExpanded, isFalse);
|
||||
expect(state.isRefreshing, isFalse);
|
||||
expect(state.showSleep, isTrue);
|
||||
expect(state.payload, _payload);
|
||||
verify(
|
||||
() => repository.getInsights(
|
||||
slug: null,
|
||||
bodyPeriod: BodyPeriod.threeMonths,
|
||||
heightCm: 176,
|
||||
),
|
||||
).called(1);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('пробрасывает slug владельца', () async {
|
||||
final cubit = buildCubit(slug: 'client');
|
||||
|
||||
await cubit.load();
|
||||
|
||||
verify(() => repository.getHeightCm(slug: 'client')).called(1);
|
||||
verify(
|
||||
() => repository.getInsights(
|
||||
slug: 'client',
|
||||
bodyPeriod: BodyPeriod.threeMonths,
|
||||
heightCm: 176,
|
||||
),
|
||||
).called(1);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('403 → hidden', () async {
|
||||
when(
|
||||
() => repository.getHeightCm(slug: any(named: 'slug')),
|
||||
).thenThrow(const InsightsForbidden());
|
||||
final cubit = buildCubit(slug: 'client');
|
||||
|
||||
await cubit.load();
|
||||
|
||||
expect(cubit.state, isA<InsightsStateHidden>());
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('403 на запросе точек → hidden', () async {
|
||||
when(
|
||||
() => repository.getInsights(
|
||||
slug: any(named: 'slug'),
|
||||
bodyPeriod: any(named: 'bodyPeriod'),
|
||||
heightCm: any(named: 'heightCm'),
|
||||
),
|
||||
).thenThrow(const InsightsForbidden());
|
||||
final cubit = buildCubit(slug: 'client');
|
||||
|
||||
await cubit.load();
|
||||
|
||||
expect(cubit.state, isA<InsightsStateHidden>());
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('другая ошибка → error', () async {
|
||||
when(
|
||||
() => repository.getInsights(
|
||||
slug: any(named: 'slug'),
|
||||
bodyPeriod: any(named: 'bodyPeriod'),
|
||||
heightCm: any(named: 'heightCm'),
|
||||
),
|
||||
).thenThrow(Exception('boom'));
|
||||
final cubit = buildCubit();
|
||||
|
||||
await cubit.load();
|
||||
|
||||
expect(cubit.state, isA<InsightsStateError>());
|
||||
await cubit.close();
|
||||
});
|
||||
});
|
||||
|
||||
group('setBodyPeriod', () {
|
||||
test(
|
||||
'смена периода → перезапрос; рост не запрашивается повторно',
|
||||
() async {
|
||||
final cubit = buildCubit();
|
||||
await cubit.load();
|
||||
when(
|
||||
() => repository.getInsights(
|
||||
slug: null,
|
||||
bodyPeriod: BodyPeriod.year,
|
||||
heightCm: 176,
|
||||
),
|
||||
).thenAnswer((_) async => _payload2);
|
||||
|
||||
await cubit.setBodyPeriod(BodyPeriod.year);
|
||||
|
||||
expect(loaded(cubit).bodyPeriod, BodyPeriod.year);
|
||||
expect(loaded(cubit).payload, _payload2);
|
||||
expect(loaded(cubit).isRefreshing, isFalse);
|
||||
verify(() => repository.getHeightCm(slug: null)).called(1);
|
||||
await cubit.close();
|
||||
},
|
||||
);
|
||||
|
||||
test('тот же период — без запроса', () async {
|
||||
final cubit = buildCubit();
|
||||
await cubit.load();
|
||||
clearInteractions(repository);
|
||||
|
||||
await cubit.setBodyPeriod(BodyPeriod.threeMonths);
|
||||
|
||||
verifyNever(
|
||||
() => repository.getInsights(
|
||||
slug: any(named: 'slug'),
|
||||
bodyPeriod: any(named: 'bodyPeriod'),
|
||||
heightCm: any(named: 'heightCm'),
|
||||
),
|
||||
);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('ошибка перезапроса возвращает прежний период и данные', () async {
|
||||
final cubit = buildCubit();
|
||||
await cubit.load();
|
||||
when(
|
||||
() => repository.getInsights(
|
||||
slug: null,
|
||||
bodyPeriod: BodyPeriod.all,
|
||||
heightCm: 176,
|
||||
),
|
||||
).thenThrow(Exception('boom'));
|
||||
|
||||
await cubit.setBodyPeriod(BodyPeriod.all);
|
||||
|
||||
expect(loaded(cubit).bodyPeriod, BodyPeriod.threeMonths);
|
||||
expect(loaded(cubit).payload, _payload);
|
||||
expect(loaded(cubit).isRefreshing, isFalse);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('403 при перезапросе → hidden', () async {
|
||||
final cubit = buildCubit(slug: 'c');
|
||||
await cubit.load();
|
||||
when(
|
||||
() => repository.getInsights(
|
||||
slug: 'c',
|
||||
bodyPeriod: BodyPeriod.month,
|
||||
heightCm: 176,
|
||||
),
|
||||
).thenThrow(const InsightsForbidden());
|
||||
|
||||
await cubit.setBodyPeriod(BodyPeriod.month);
|
||||
|
||||
expect(cubit.state, isA<InsightsStateHidden>());
|
||||
await cubit.close();
|
||||
});
|
||||
});
|
||||
|
||||
group('панели и серии', () {
|
||||
test('toggleExpanded раскрывает и сворачивает', () async {
|
||||
final cubit = buildCubit();
|
||||
await cubit.load();
|
||||
|
||||
cubit.toggleExpanded();
|
||||
expect(loaded(cubit).isExpanded, isTrue);
|
||||
cubit.toggleExpanded();
|
||||
expect(loaded(cubit).isExpanded, isFalse);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('setPanel переключает вкладку', () async {
|
||||
final cubit = buildCubit();
|
||||
await cubit.load();
|
||||
|
||||
cubit.setPanel(InsightsPanel.measures);
|
||||
|
||||
expect(loaded(cubit).panel, InsightsPanel.measures);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('вкладки КБЖУ недоступны при выключенном флаге', () async {
|
||||
final cubit = buildCubit();
|
||||
await cubit.load();
|
||||
|
||||
cubit.setPanel(InsightsPanel.daily);
|
||||
|
||||
expect(loaded(cubit).panel, InsightsPanel.weight);
|
||||
expect(cubit.panels, [
|
||||
InsightsPanel.weight,
|
||||
InsightsPanel.bmi,
|
||||
InsightsPanel.measures,
|
||||
]);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('вкладки КБЖУ доступны при включённом флаге', () async {
|
||||
final cubit = buildCubit(kbjuEnabled: true);
|
||||
await cubit.load();
|
||||
|
||||
cubit.setPanel(InsightsPanel.intra);
|
||||
|
||||
expect(loaded(cubit).panel, InsightsPanel.intra);
|
||||
expect(cubit.panels, InsightsPanel.values);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('toggleSeries переключает только выбранную серию', () async {
|
||||
final cubit = buildCubit();
|
||||
await cubit.load();
|
||||
|
||||
cubit.toggleSeries(InsightsPanel.measures, 1);
|
||||
expect(loaded(cubit).seriesVisibility.measure, [true, false, true]);
|
||||
|
||||
cubit.toggleSeries(InsightsPanel.measures, 1);
|
||||
expect(loaded(cubit).seriesVisibility.measure, [true, true, true]);
|
||||
|
||||
cubit.toggleSeries(InsightsPanel.daily, 0);
|
||||
expect(loaded(cubit).seriesVisibility.daily, [false, true, true, true]);
|
||||
expect(loaded(cubit).seriesVisibility.measure, [true, true, true]);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test(
|
||||
'toggleSeries игнорирует неверный индекс и вкладку без серий',
|
||||
() async {
|
||||
final cubit = buildCubit();
|
||||
await cubit.load();
|
||||
final before = loaded(cubit).seriesVisibility;
|
||||
|
||||
cubit.toggleSeries(InsightsPanel.measures, 5);
|
||||
cubit.toggleSeries(InsightsPanel.weight, 0);
|
||||
|
||||
expect(loaded(cubit).seriesVisibility, before);
|
||||
await cubit.close();
|
||||
},
|
||||
);
|
||||
|
||||
test('toggleSleep', () async {
|
||||
final cubit = buildCubit();
|
||||
await cubit.load();
|
||||
|
||||
cubit.toggleSleep();
|
||||
|
||||
expect(loaded(cubit).showSleep, isFalse);
|
||||
await cubit.close();
|
||||
});
|
||||
});
|
||||
|
||||
group('refresh и добавление записей', () {
|
||||
test('refresh сохраняет вкладку, период, серии и раскрытие', () async {
|
||||
final cubit = buildCubit();
|
||||
await cubit.load();
|
||||
cubit.toggleExpanded();
|
||||
cubit.setPanel(InsightsPanel.measures);
|
||||
cubit.toggleSeries(InsightsPanel.measures, 0);
|
||||
when(
|
||||
() => repository.getInsights(
|
||||
slug: null,
|
||||
bodyPeriod: BodyPeriod.threeMonths,
|
||||
heightCm: 176,
|
||||
),
|
||||
).thenAnswer((_) async => _payload2);
|
||||
|
||||
await cubit.refresh();
|
||||
|
||||
final state = loaded(cubit);
|
||||
expect(state.payload, _payload2);
|
||||
expect(state.panel, InsightsPanel.measures);
|
||||
expect(state.isExpanded, isTrue);
|
||||
expect(state.seriesVisibility.measure, [false, true, true]);
|
||||
expect(state.isRefreshing, isFalse);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('addWeight сохраняет и обновляет данные', () async {
|
||||
when(
|
||||
() => repository.addWeight(weight: any(named: 'weight')),
|
||||
).thenAnswer((_) async {});
|
||||
final cubit = buildCubit(canAdd: true);
|
||||
await cubit.load();
|
||||
when(
|
||||
() => repository.getInsights(
|
||||
slug: null,
|
||||
bodyPeriod: BodyPeriod.threeMonths,
|
||||
heightCm: 176,
|
||||
),
|
||||
).thenAnswer((_) async => _payload2);
|
||||
|
||||
final isSaved = await cubit.addWeight(weight: 72);
|
||||
|
||||
expect(isSaved, isTrue);
|
||||
verify(() => repository.addWeight(weight: 72)).called(1);
|
||||
expect(loaded(cubit).payload, _payload2);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('addCircumference сохраняет и обновляет данные', () async {
|
||||
when(
|
||||
() => repository.addCircumference(
|
||||
chestCircumference: any(named: 'chestCircumference'),
|
||||
waistCircumference: any(named: 'waistCircumference'),
|
||||
hipCircumference: any(named: 'hipCircumference'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
final cubit = buildCubit(canAdd: true);
|
||||
await cubit.load();
|
||||
when(
|
||||
() => repository.getInsights(
|
||||
slug: null,
|
||||
bodyPeriod: BodyPeriod.threeMonths,
|
||||
heightCm: 176,
|
||||
),
|
||||
).thenAnswer((_) async => _payload2);
|
||||
|
||||
final isSaved = await cubit.addCircumference(
|
||||
chestCircumference: 95,
|
||||
waistCircumference: 80,
|
||||
hipCircumference: 100,
|
||||
);
|
||||
|
||||
expect(isSaved, isTrue);
|
||||
|
||||
verify(
|
||||
() => repository.addCircumference(
|
||||
chestCircumference: 95,
|
||||
waistCircumference: 80,
|
||||
hipCircumference: 100,
|
||||
),
|
||||
).called(1);
|
||||
expect(loaded(cubit).payload, _payload2);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test(
|
||||
'ошибка сохранения не перезагружает данные и возвращает false',
|
||||
() async {
|
||||
when(
|
||||
() => repository.addWeight(weight: any(named: 'weight')),
|
||||
).thenThrow(Exception('boom'));
|
||||
final cubit = buildCubit(canAdd: true);
|
||||
await cubit.load();
|
||||
clearInteractions(repository);
|
||||
|
||||
expect(await cubit.addWeight(weight: 72), isFalse);
|
||||
|
||||
verifyNever(
|
||||
() => repository.getInsights(
|
||||
slug: any(named: 'slug'),
|
||||
bodyPeriod: any(named: 'bodyPeriod'),
|
||||
heightCm: any(named: 'heightCm'),
|
||||
),
|
||||
);
|
||||
expect(loaded(cubit).payload, _payload);
|
||||
await cubit.close();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user