162 lines
5.4 KiB
Dart
162 lines
5.4 KiB
Dart
import 'package:drift/native.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:glp1_companion/core/database/app_database.dart';
|
|
import 'package:glp1_companion/features/nutrition/data/repositories/nutrition_repository.dart';
|
|
import 'package:glp1_companion/features/nutrition/domain/entities/food_log_entry.dart';
|
|
import 'package:glp1_companion/features/nutrition/domain/entities/hydration_log_entry.dart';
|
|
import 'package:glp1_companion/features/nutrition/domain/entities/meal_type.dart';
|
|
|
|
void main() {
|
|
late AppDatabase database;
|
|
late NutritionRepository repository;
|
|
|
|
final morning = DateTime(2026, 10, 16, 8);
|
|
final noon = DateTime(2026, 10, 16, 12);
|
|
final previousDay = DateTime(2026, 10, 15, 9);
|
|
|
|
setUp(() {
|
|
database = AppDatabase.forTesting(NativeDatabase.memory());
|
|
repository = NutritionRepository(database);
|
|
});
|
|
|
|
tearDown(() => database.close());
|
|
|
|
FoodLogEntry makeFood({
|
|
int? id,
|
|
DateTime? timestamp,
|
|
MealType mealType = MealType.breakfast,
|
|
String description = 'Greek yogurt',
|
|
double proteinGrams = 20,
|
|
int calories = 150,
|
|
double? carbsGrams,
|
|
double? fatGrams,
|
|
}) =>
|
|
FoodLogEntry(
|
|
id: id,
|
|
timestamp: timestamp ?? morning,
|
|
mealType: mealType,
|
|
description: description,
|
|
proteinGrams: proteinGrams,
|
|
calories: calories,
|
|
carbsGrams: carbsGrams,
|
|
fatGrams: fatGrams,
|
|
);
|
|
|
|
group('NutritionRepository food diary', () {
|
|
test('saveFoodLog assigns an id and persists all fields', () async {
|
|
final saved = await repository.saveFoodLog(
|
|
makeFood(
|
|
mealType: MealType.lunch,
|
|
description: 'Chicken breast bowl',
|
|
proteinGrams: 42.5,
|
|
calories: 520,
|
|
carbsGrams: 45,
|
|
fatGrams: 12,
|
|
),
|
|
);
|
|
|
|
expect(saved.id, isNotNull);
|
|
|
|
final fetched = await repository.getFoodLogs();
|
|
expect(fetched, hasLength(1));
|
|
expect(fetched.single.id, saved.id);
|
|
expect(fetched.single.mealType, MealType.lunch);
|
|
expect(fetched.single.description, 'Chicken breast bowl');
|
|
expect(fetched.single.proteinGrams, 42.5);
|
|
expect(fetched.single.calories, 520);
|
|
expect(fetched.single.carbsGrams, 45);
|
|
expect(fetched.single.fatGrams, 12);
|
|
});
|
|
|
|
test('getFoodLogs returns entries newest first', () async {
|
|
await repository.saveFoodLog(makeFood(timestamp: previousDay));
|
|
await repository.saveFoodLog(makeFood(timestamp: noon));
|
|
|
|
final fetched = await repository.getFoodLogs();
|
|
expect(fetched, hasLength(2));
|
|
expect(fetched.first.timestamp, noon);
|
|
expect(fetched.last.timestamp, previousDay);
|
|
});
|
|
|
|
test('getFoodLogs filters by the half-open [from, to) range', () async {
|
|
await repository.saveFoodLog(makeFood(timestamp: previousDay));
|
|
await repository.saveFoodLog(makeFood(timestamp: morning));
|
|
await repository.saveFoodLog(makeFood(timestamp: noon));
|
|
|
|
final dayStart = DateTime(2026, 10, 16);
|
|
final dayEnd = DateTime(2026, 10, 17);
|
|
final todays = await repository.getFoodLogs(from: dayStart, to: dayEnd);
|
|
|
|
expect(todays, hasLength(2));
|
|
expect(
|
|
todays.every((entry) => entry.timestamp.isAfter(previousDay)),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test('deleteFoodLog removes only the targeted row', () async {
|
|
final first = await repository.saveFoodLog(makeFood());
|
|
await repository.saveFoodLog(
|
|
makeFood(mealType: MealType.dinner, description: 'Salmon'),
|
|
);
|
|
|
|
await repository.deleteFoodLog(first.id!);
|
|
|
|
final remaining = await repository.getFoodLogs();
|
|
expect(remaining, hasLength(1));
|
|
expect(remaining.single.description, 'Salmon');
|
|
});
|
|
|
|
test('watchFoodLogs emits on every write', () async {
|
|
final events = <List<FoodLogEntry>>[];
|
|
final subscription = repository.watchFoodLogs().listen(events.add);
|
|
|
|
await repository.saveFoodLog(makeFood());
|
|
await Future<void>.delayed(const Duration(milliseconds: 50));
|
|
|
|
expect(events, isNotEmpty);
|
|
expect(events.last, hasLength(1));
|
|
await subscription.cancel();
|
|
});
|
|
});
|
|
|
|
group('NutritionRepository hydration', () {
|
|
test('hydration entries persist and filter by day', () async {
|
|
await repository.saveHydrationLog(
|
|
HydrationLogEntry(timestamp: morning, volumeOz: 8),
|
|
);
|
|
await repository.saveHydrationLog(
|
|
HydrationLogEntry(timestamp: noon, volumeOz: 16),
|
|
);
|
|
await repository.saveHydrationLog(
|
|
HydrationLogEntry(timestamp: previousDay, volumeOz: 24),
|
|
);
|
|
|
|
final dayStart = DateTime(2026, 10, 16);
|
|
final dayEnd = DateTime(2026, 10, 17);
|
|
final todays =
|
|
await repository.getHydrationLogs(from: dayStart, to: dayEnd);
|
|
|
|
expect(todays, hasLength(2));
|
|
final totalOz = todays.fold<double>(0, (sum, e) => sum + e.volumeOz);
|
|
expect(totalOz, 24);
|
|
|
|
await repository.deleteHydrationLog(todays.first.id!);
|
|
final afterDelete = await repository.getHydrationLogs(
|
|
from: dayStart,
|
|
to: dayEnd,
|
|
);
|
|
expect(afterDelete, hasLength(1));
|
|
});
|
|
});
|
|
|
|
group('MealType persistence keys', () {
|
|
test('tryParse round-trips every value', () {
|
|
for (final type in MealType.values) {
|
|
expect(MealType.tryParse(type.persistenceKey), type);
|
|
}
|
|
expect(MealType.tryParse('brunch'), isNull);
|
|
expect(MealType.tryParse(null), isNull);
|
|
});
|
|
});
|
|
} |