mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-07-04 15:43:25 +02:00
Milestone 1 of the diet-app-as-wise-balloon plan: a phone-native way to log meals away from the PC, sharing the exact on-disk JSON shape diet_guard already uses (same field names, no translation layer). - lib/models/: 1:1 Dart mirrors of the Python dataclasses (Nutrition, FoodEntry, MealItem, FoodBankRecord, Slot), including the per-100g/ amount-eaten portion scaling that matches _resolve.resolve_nutrition's semantics exactly. - lib/services/log_storage_service.dart: plain-JSON persistence to food_log.json's exact shape (no sqflite -- the canonical format already is this JSON). - lib/services/foodbank_service.dart: ports _foodbank.py's upsert/fuzzy search logic for autocomplete. - lib/screens/: log_meal_screen.dart (single-item logging) and meal_builder_screen.dart (composite multi-item meals, logging full per-component macros via the new components field). Verified end-to-end on a physical device (BL9000): built, installed, logged a real meal through the UI. 77 Flutter tests passing, `flutter analyze` clean against very_good_analysis. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FU3f5KQ1GHXsbbSecfVEyF
140 lines
3.5 KiB
Dart
140 lines
3.5 KiB
Dart
import 'package:diet_guard_app/models/nutrition.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
Nutrition _ref({
|
|
double kcal = 200,
|
|
double proteinG = 10,
|
|
double carbsG = 20,
|
|
double fatG = 5,
|
|
double grams = 100,
|
|
String source = 'manual',
|
|
}) => Nutrition(
|
|
kcal: kcal,
|
|
proteinG: proteinG,
|
|
carbsG: carbsG,
|
|
fatG: fatG,
|
|
grams: grams,
|
|
source: source,
|
|
);
|
|
|
|
void main() {
|
|
group('scaleNutrition', () {
|
|
test('scales every macro proportionally to the new weight', () {
|
|
final result = scaleNutrition(_ref(), 150);
|
|
expect(result.kcal, 300);
|
|
expect(result.proteinG, 15);
|
|
expect(result.carbsG, 30);
|
|
expect(result.fatG, 7.5);
|
|
expect(result.grams, 150);
|
|
expect(result.source, 'manual');
|
|
});
|
|
|
|
test('is a no-op when the new weight equals the basis weight', () {
|
|
final result = scaleNutrition(_ref(), 100);
|
|
expect(result.kcal, 200);
|
|
expect(result.proteinG, 10);
|
|
expect(result.grams, 100);
|
|
});
|
|
|
|
test('keeps macros unchanged when the basis weight is unknown', () {
|
|
final result = scaleNutrition(_ref(grams: 0), 150);
|
|
expect(result.kcal, 200);
|
|
expect(result.grams, 150);
|
|
});
|
|
|
|
test('keeps macros and basis weight when the new weight is unknown', () {
|
|
final result = scaleNutrition(_ref(), 0);
|
|
expect(result.kcal, 200);
|
|
expect(result.grams, 100);
|
|
});
|
|
|
|
test('keeps basis weight when both weights are unknown', () {
|
|
final result = scaleNutrition(_ref(grams: 0), 0);
|
|
expect(result.grams, 0);
|
|
});
|
|
});
|
|
|
|
group('nutritionForPortion', () {
|
|
test('scales label macros to the amount actually eaten', () {
|
|
final result = nutritionForPortion(
|
|
kcal: 200,
|
|
proteinG: 10,
|
|
carbsG: 20,
|
|
fatG: 5,
|
|
perGrams: 100,
|
|
ateGrams: 150,
|
|
source: 'manual',
|
|
);
|
|
expect(result.kcal, 300);
|
|
expect(result.proteinG, 15);
|
|
expect(result.carbsG, 30);
|
|
expect(result.fatG, 7.5);
|
|
expect(result.grams, 150);
|
|
});
|
|
|
|
test(
|
|
'treats typed macros as totals when per-grams is left blank '
|
|
'(back-compatible with the original single-grams-field behaviour)',
|
|
() {
|
|
final result = nutritionForPortion(
|
|
kcal: 250,
|
|
proteinG: 12,
|
|
carbsG: 30,
|
|
fatG: 8,
|
|
perGrams: 0,
|
|
ateGrams: 150,
|
|
source: 'manual',
|
|
);
|
|
expect(result.kcal, 250);
|
|
expect(result.proteinG, 12);
|
|
expect(result.grams, 150);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'assumes the eaten amount equals per-grams when amount eaten is '
|
|
'left blank',
|
|
() {
|
|
final result = nutritionForPortion(
|
|
kcal: 200,
|
|
proteinG: 10,
|
|
carbsG: 20,
|
|
fatG: 5,
|
|
perGrams: 100,
|
|
ateGrams: 0,
|
|
source: 'manual',
|
|
);
|
|
expect(result.kcal, 200);
|
|
expect(result.grams, 100);
|
|
},
|
|
);
|
|
|
|
test('keeps macros as typed when both grams fields are blank', () {
|
|
final result = nutritionForPortion(
|
|
kcal: 90,
|
|
proteinG: 3,
|
|
carbsG: 4,
|
|
fatG: 1,
|
|
perGrams: 0,
|
|
ateGrams: 0,
|
|
source: 'manual',
|
|
);
|
|
expect(result.kcal, 90);
|
|
expect(result.grams, 0);
|
|
});
|
|
|
|
test('stamps the requested source', () {
|
|
final result = nutritionForPortion(
|
|
kcal: 100,
|
|
proteinG: 1,
|
|
carbsG: 1,
|
|
fatG: 1,
|
|
perGrams: 100,
|
|
ateGrams: 100,
|
|
source: 'food bank',
|
|
);
|
|
expect(result.source, 'food bank');
|
|
});
|
|
});
|
|
}
|