Files
jizhi/frontend/test/api_models_test.dart
T
2026-07-24 23:11:20 +08:00

138 lines
4.4 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:miaoji_zhang/shared/api/business_api.dart';
import 'package:miaoji_zhang/shared/api/sse_frame_accumulator.dart';
import 'package:miaoji_zhang/shared/services/current_ledger_store.dart';
import 'package:miaoji_zhang/shared/widgets/app_icons.dart';
void main() {
test(
'SSE framing preserves split UTF-8 characters and trailing events',
() async {
const source = 'data: {"t":"收入"}\r\n\r\ndata: {"done":true,"c":"收入100"}';
final bytes = utf8.encode(source);
final chunks = <List<int>>[
bytes.sublist(0, 13),
bytes.sublist(13, 14),
bytes.sublist(14, 21),
bytes.sublist(21),
];
final accumulator = SseFrameAccumulator();
final frames = <String>[];
await for (final chunk in Stream<List<int>>.fromIterable(
chunks,
).transform(const Utf8Decoder())) {
frames.addAll(accumulator.add(chunk));
}
frames.addAll(accumulator.close());
expect(frames, hasLength(2));
expect(frames.first, contains('收入'));
expect(frames.last, contains('"done":true'));
},
);
test('transaction and ledger DTOs retain type, deletion and UTC instant', () {
final transaction = TxItem.fromJson({
'id': 7,
'ledgerId': 3,
'categoryId': 5,
'categoryName': '工资',
'categoryIcon': 'money',
'type': 'income',
'amount': 100,
'note': '七月工资',
'paymentMethod': '银行卡',
'source': 'ai_chat',
'sourceText': '我赚了100',
'occurredAt': '2026-06-30T16:30:00Z',
'isDeleted': true,
});
final ledger = LedgerInfo.fromJson({
'id': 3,
'name': '家庭账本',
'iconKey': 'wallet',
'isDefault': true,
'txCount': 9,
});
expect(transaction.isIncome, isTrue);
expect(transaction.isDeleted, isTrue);
expect(transaction.occurredAt.toUtc(), DateTime.utc(2026, 6, 30, 16, 30));
expect(ledger.name, '家庭账本');
expect(ledger.transactionCount, 9);
});
test('budget refinement DTO keeps conversation summary and constraints', () {
final recommendation = BudgetRecommendations.fromJson({
'year': 2026,
'month': 7,
'suggestedTotal': 130,
'message': '草稿已调整',
'adjustmentSummary': '餐饮预算已降低',
'warnings': ['餐饮不能低于已花 ¥80'],
'items': [
{
'categoryId': 1,
'categoryName': '餐饮',
'categoryIcon': 'food',
'suggestedAmount': 80,
'currentSpent': 80,
'historicalAverage': 120,
'confidence': 'high',
},
{
'categoryId': 2,
'categoryName': '交通',
'categoryIcon': 'transport',
'suggestedAmount': 50,
'currentSpent': 20,
'historicalAverage': 60,
'confidence': 'medium',
},
],
});
expect(recommendation.suggestedTotal, 130);
expect(recommendation.adjustmentSummary, '餐饮预算已降低');
expect(recommendation.warnings, contains('餐饮不能低于已花 ¥80'));
expect(recommendation.items, hasLength(2));
});
test('周报与年报 DTO 保留周期、峰值和 AI 占比', () {
final report = PeriodReport.fromJson({
'periodType': 'yearly',
'periodLabel': '2026年',
'startDate': '2026-01-01T00:00:00',
'endDate': '2027-01-01T00:00:00',
'count': 12,
'income': 8000,
'expense': 5600,
'balance': 2400,
'aiRatio': 75,
'peakLabel': '7月',
'peakAmount': 1300,
'peakNote': '旅行',
'topCategory': '餐饮',
'topCategoryAmount': 1600,
'topCategoryPercent': 28.57,
'commentary': '这一年记录得很完整。',
});
expect(report.periodType, 'yearly');
expect(report.peakLabel, '7月');
expect(report.aiRatio, 75);
expect(report.balance, 2400);
});
test('分类图标目录至少 32 个且键值不重复', () {
final keys = AppIcons.categoryCatalog.map((item) => item.key).toList();
expect(keys.length, greaterThanOrEqualTo(32));
expect(keys.toSet().length, keys.length);
expect(keys, containsAll(['cart', 'metro', 'house', 'money']));
expect(keys, isNot(contains('shopping')));
expect(keys, isNot(contains('transport')));
});
}