254 lines
7.6 KiB
Dart
254 lines
7.6 KiB
Dart
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(''), '加餐');
|
|
});
|
|
});
|
|
}
|