import 'package:flutter_test/flutter_test.dart'; import 'package:qiangbao_accounting/models/health.dart'; /// 「强宝健康」模型的解析测试。 /// /// 这些 fromJson 全部要能扛住后端字段缺失/类型不符 —— 它们被用在 /// 首屏三个卡片上,任何一处抛异常都会让整个首页变成错误态。 /// 所以每个测试都在验证「给脏数据也不炸」。 void main() { group('体重折线图解析', () { test('正常数据:点位、当前体重、差值都对', () { final chart = WeightChart.fromJson({ 'points': [ {'date': '2026-09-01', 'weight': 65.0}, {'date': '2026-09-05', 'weight': 64.2}, ], 'currentWeight': 64.2, 'delta': -0.8, 'maxWeight': 66.0, 'minWeight': 58.0, }); expect(chart.points.length, 2); expect(chart.points.first.weight, 65.0); expect(chart.points.last.date.day, 5); expect(chart.currentWeight, 64.2); expect(chart.delta, -0.8); expect(chart.hasData, isTrue); }); test('空数据不炸,hasData 为 false', () { final chart = WeightChart.fromJson(const {}); expect(chart.points, isEmpty); expect(chart.hasData, isFalse); expect(chart.currentWeight, isNull); expect(chart.delta, isNull); }); test('字段类型不符(字符串数字)也能解析', () { final chart = WeightChart.fromJson({ 'points': [ {'date': '2026-09-01', 'weight': '65.5'}, ], 'currentWeight': '65.5', }); expect(chart.points.first.weight, 65.5); expect(chart.currentWeight, 65.5); }); test('日期解析不出来时不抛异常', () { final chart = WeightChart.fromJson({ 'points': [ {'date': '不是日期', 'weight': 65}, ], }); expect(chart.points.length, 1); expect(chart.points.first.weight, 65); }); test('delta 为 null(只记过一次)时保持 null,不退化成 0', () { // 0 会被显示成「没变化」,而实际含义是「还没有上一次可比」—— // 这两件事在界面上必须能区分 final chart = WeightChart.fromJson({ 'points': [ {'date': '2026-09-01', 'weight': 65}, ], 'delta': null, }); expect(chart.delta, isNull); }); }); group('减重目标解析', () { test('没设过目标时 hasGoal=false,字段全是安全的默认值', () { final goal = WeightGoal.fromJson({'hasGoal': false}); expect(goal.hasGoal, isFalse); expect(goal.idealCurve, isEmpty); expect(goal.weeklyLoss, 0); expect(goal.currentWeight, isNull); }); test('正常目标:每周应减 + 理想曲线 + 进度', () { final goal = WeightGoal.fromJson({ 'hasGoal': true, 'id': 3, 'startWeight': 65.0, 'targetWeight': 58.0, 'startDate': '2026-01-01', 'targetDate': '2026-04-01', 'totalWeeks': 13, 'weeklyLoss': 0.54, 'weeklyLossUnhealthy': false, 'idealCurve': [ {'date': '2026-01-01', 'weight': 65.0}, {'date': '2026-01-08', 'weight': 64.46}, ], 'currentWeight': 63.2, 'lostWeight': 1.8, 'remainWeight': 5.2, 'weightProgress': 0.257, 'timeProgress': 0.4, }); expect(goal.hasGoal, isTrue); expect(goal.weeklyLoss, 0.54); expect(goal.idealCurve.length, 2); expect(goal.idealCurve.first.weight, 65.0); expect(goal.lostWeight, 1.8); expect(goal.weightProgress, closeTo(0.257, 0.001)); expect(goal.weeklyLossUnhealthy, isFalse); }); test('每周超过 1kg 时标成不健康', () { final goal = WeightGoal.fromJson({ 'hasGoal': true, 'startWeight': 65.0, 'targetWeight': 58.0, 'weeklyLoss': 2.33, 'weeklyLossUnhealthy': true, }); expect(goal.weeklyLossUnhealthy, isTrue); }); test('缺 hasGoal 字段时按「没设过目标」处理', () { final goal = WeightGoal.fromJson(const {}); expect(goal.hasGoal, isFalse); }); }); group('食物与份量', () { test('份量列表解析,并保证有兜底', () { final food = FoodItem.fromJson({ 'id': 1, 'name': '米饭(蒸)', 'energyKcal': 116.0, 'portions': [ {'label': '一碗', 'grams': 150}, {'label': '小碗', 'grams': 100}, ], }); expect(food.portions.length, 2); expect(food.portions.first.label, '一碗'); expect(food.portions.first.grams, 150); }); test('kcalFor 按份量换算', () { final food = FoodItem.fromJson({ 'id': 1, 'name': '米饭', 'energyKcal': 116.0, }); // 每 100g 116 kcal,吃 150g 就是 174 kcal expect(food.kcalFor(150), closeTo(174.0, 0.01)); expect(food.kcalFor(100), closeTo(116.0, 0.01)); }); test('kcalFor 遇到 0 / 负数 / 缺能量值都返回 0,不抛不返回 NaN', () { final food = FoodItem.fromJson({'id': 1, 'name': '未知食物'}); expect(food.kcalFor(150), 0); expect(food.kcalFor(0), 0); expect(food.kcalFor(-10), 0); }); test('食物名为空时给占位,不显示空白', () { final food = FoodItem.fromJson({'id': 1}); expect(food.name, '未命名食物'); }); }); group('饮食记录解析', () { test('某天的饮食:合计 + 按餐次分组', () { final day = DietDay.fromJson({ 'date': '2026-09-28', 'totals': { 'energyKcal': 1280.5, 'protein': 62.3, 'fat': 41.0, 'carbohydrate': 168.2, 'dietaryFiber': 12.5, 'sodium': 2100.0, 'itemCount': 5, }, 'meals': [ { 'mealType': 'breakfast', 'totals': {'energyKcal': 300.0, 'itemCount': 2}, 'items': [ { 'id': 1, 'mealType': 'breakfast', 'foodName': '鸡蛋', 'amountGrams': 50, 'energyKcal': 72.0, }, { 'id': 2, 'mealType': 'breakfast', 'foodName': '牛奶', 'amountGrams': 200, 'energyKcal': 228.0, }, ], }, ], }); expect(day.totals.energyKcal, 1280.5); expect(day.totals.itemCount, 5); expect(day.meals.length, 1); expect(day.meals.first.mealType, 'breakfast'); expect(day.meals.first.items.length, 2); expect(day.meals.first.items.first.foodName, '鸡蛋'); }); test('空的一天不炸,合计归零', () { final day = DietDay.fromJson(const {}); expect(day.totals.energyKcal, 0); expect(day.totals.itemCount, 0); expect(day.meals, isEmpty); }); test('明细缺字段时用安全默认值', () { final day = DietDay.fromJson({ 'meals': [ { 'items': [ {'id': 1}, ], }, ], }); final item = day.meals.first.items.first; expect(item.foodName, '未命名食物'); expect(item.energyKcal, 0); expect(item.mealType, 'snack'); }); }); group('餐次标签', () { test('四个餐次都有中文名', () { expect(mealLabel('breakfast'), '早餐'); expect(mealLabel('lunch'), '午餐'); expect(mealLabel('dinner'), '晚餐'); expect(mealLabel('snack'), '加餐'); }); test('未知餐次兜底到加餐,不返回空字符串', () { expect(mealLabel('brunch'), '加餐'); expect(mealLabel(''), '加餐'); }); }); }