import 'package:flutter_test/flutter_test.dart'; import 'package:qiangbao_accounting/models/bill.dart'; import 'package:qiangbao_accounting/utils/amount_util.dart'; import 'package:qiangbao_accounting/utils/bill_grouping.dart'; import 'package:qiangbao_accounting/utils/date_util.dart'; Bill bill({ required int id, required String date, required double amount, required int type, String description = '', }) => Bill( id: id, categoryId: 1, categoryName: '餐饮', categoryIcon: '🍔', amount: amount, description: description, billDate: date, type: type, ); void main() { group('月份/周界工具', () { test('monthStart 取当月 1 日', () { expect(formatDate(monthStart(DateTime(2026, 9, 21))), '2026-09-01'); }); test('monthEnd 处理 28/30/31 天与闰年 2 月', () { expect(formatDate(monthEnd(DateTime(2026, 9, 5))), '2026-09-30'); expect(formatDate(monthEnd(DateTime(2026, 2, 5))), '2026-02-28'); expect(formatDate(monthEnd(DateTime(2028, 2, 5))), '2028-02-29'); // 闰年 expect(formatDate(monthEnd(DateTime(2026, 12, 5))), '2026-12-31'); }); test('shiftMonth 跨年正确', () { expect(formatDate(shiftMonth(DateTime(2026, 1, 1), -1)), '2025-12-01'); expect(formatDate(shiftMonth(DateTime(2026, 12, 1), 1)), '2027-01-01'); }); test('weekStart 返回本周一', () { // 2026-09-21 是周一 expect(formatDate(weekStart(DateTime(2026, 9, 21))), '2026-09-21'); // 周日应回到本周一(9/20 是周日 → 9/14) expect(formatDate(weekStart(DateTime(2026, 9, 20))), '2026-09-14'); // 跨月:2026-10-01 是周四 → 本周一 9/28 expect(formatDate(weekStart(DateTime(2026, 10, 1))), '2026-09-28'); }); test('isSameMonth', () { expect(isSameMonth(DateTime(2026, 9, 1), DateTime(2026, 9, 30)), isTrue); expect(isSameMonth(DateTime(2026, 9, 1), DateTime(2026, 10, 1)), isFalse); }); test('parseBillDate 兼容 LocalDateTime', () { expect(parseBillDate('2026-09-21T12:30:00'), '2026-09-21'); expect(parseBillDate('2026-09-21 12:30:00'), '2026-09-21'); expect(parseBillDate('2026-09-21'), '2026-09-21'); }); }); group('金额工具', () { test('formatAmount 两位小数并挡掉非法值', () { expect(formatAmount(12.5), '12.50'); expect(formatAmount(null), '0.00'); expect(formatAmount(double.nan), '0.00'); expect(formatAmount(double.infinity), '0.00'); }); test('formatSignedAmount 按类型给符号(不看金额正负)', () { expect(formatSignedAmount(12.0, isExpense: true), '-¥12.00'); expect(formatSignedAmount(12.0, isExpense: false), '+¥12.00'); }); test('percentOf 防除零', () { expect(percentOf(25, 100), 25); expect(percentOf(25, 0), 0); expect(percentOf(0, 0), 0); }); }); group('账单分组', () { test('按日期分组并分别累计支出/收入', () { final groups = groupBillsByDate([ bill(id: 1, date: '2026-09-21', amount: 30, type: 1), bill(id: 2, date: '2026-09-21', amount: 20, type: 1), bill(id: 3, date: '2026-09-21', amount: 5000, type: 2), bill(id: 4, date: '2026-09-20', amount: 15.5, type: 1), ]); expect(groups.length, 2); // 保持输入顺序(后端已按日期倒序) expect(groups[0].date, '2026-09-21'); expect(groups[0].bills.length, 3); expect(groups[0].expenseTotal, 50); expect(groups[0].incomeTotal, 5000); // 同日内不重排,保持后端 createTime 倒序 expect(groups[0].bills.map((b) => b.id).toList(), [1, 2, 3]); expect(groups[1].date, '2026-09-20'); expect(groups[1].expenseTotal, 15.5); expect(groups[1].incomeTotal, 0); }); test('空列表返回空分组', () { expect(groupBillsByDate([]), isEmpty); }); test('金额恒为正数时按 type 判断收支方向', () { final expense = bill(id: 1, date: '2026-09-21', amount: 30, type: 1); final income = bill(id: 2, date: '2026-09-21', amount: 30, type: 2); expect(expense.isExpense, isTrue); expect(income.isExpense, isFalse); expect(expense.absAmount, 30); }); }); group('Bill.fromJson 防御性', () { test('字段缺失/类型不符不抛异常', () { final b = Bill.fromJson({ 'id': '12', // 字符串数字 'amount': '35.5', // 字符串金额 'billDate': '2026-09-21T10:00:00', // categoryName / categoryIcon / type 缺失 }); expect(b.id, 12); expect(b.amount, 35.5); expect(b.billDate, '2026-09-21'); expect(b.categoryName, '未分类'); // 图标兜底值是「图标名」不再是 emoji(2026-09-24 换成自绘图标集) expect(b.categoryIcon, 'other'); expect(b.type, 1); // 缺省按支出 }); test('type 为 2 时是收入', () { final b = Bill.fromJson({'id': 1, 'amount': 100, 'billDate': '2026-09-21', 'type': 2}); expect(b.isExpense, isFalse); }); }); }