新增:健康事件时间线接入真实数据(T2-14 时间线半边)

- 时间线页:occurred_at DESC cursor 分页 + 月分组组头,四态齐备;
  六类事件经 RecordTypeDot 映射(增补喂养/洗护/测量三型,色族复用
  已审计色对),类型 TagPill 双通道
- 事件录入表单:六类类型选择器、金额以元录入/整数分传输(money.dart
  换算)、UTC 时间戳约定与体重表单一致
- 事件编辑页:顶层 PATCH 差量提交(title/notes/amountCents + version),
  40902 照 T2-12 模式自动经时间线检索取新 version 重提(保留输入)
- 档案页:月度花费卡接 summary.monthlyExpense(分→元展示)、tz 透传
  设备时区固定偏移(T2-13 遗留③)、健康时间线导航入口
- 埋点:health_record 域 create 三事件 + viewed(recordType=health_event)
  挂通;新增 edit_succeeded/failed 封装(failureReason 含 conflict)
- 测试 224 → 250 全绿(+26);analyze 0 问题;format 无 diff

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 13:49:48 +08:00
parent c91f18a845
commit e186ba3da9
15 changed files with 2274 additions and 23 deletions
+61 -5
View File
@@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:patbond_flutter/core/network/api_exception.dart';
import 'package:patbond_flutter/core/theme/app_theme.dart';
import 'package:patbond_flutter/features/pets/health_events_page.dart';
import 'package:patbond_flutter/features/pets/health_record_display.dart';
import 'package:patbond_flutter/features/pets/pet_detail_page.dart';
import 'package:patbond_flutter/features/pets/pet_exceptions.dart';
import 'package:patbond_flutter/features/pets/pet_form_page.dart';
@@ -24,8 +26,9 @@ void main() {
});
Future<void> pumpDetail(WidgetTester tester, {String petId = 'p-1'}) async {
// 详情页自 T2-13 增加健康数据卡行,加高视口保证底部按钮在栏内。
tester.view.physicalSize = const Size(700, 1800);
// 详情页自 T2-13 增加健康数据卡行、T2-14 增加花费卡与记录导航区,
// 加高视口保证底部按钮在栏内。
tester.view.physicalSize = const Size(700, 2200);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
@@ -184,7 +187,7 @@ void main() {
});
group('T2-13 · 数据卡行接 summary', () {
testWidgets('卡取数:最新体重 / 疫苗进度 / 下一针(含疫苗名', (tester) async {
testWidgets('卡取数:最新体重 / 疫苗进度 / 下一针 / 本月花费(tz 透传', (tester) async {
repository.getPetHandler = (petId) async => buildPet('p-1');
String? capturedTz;
repository.getPetSummaryHandler = (petId, tz) async {
@@ -202,8 +205,12 @@ void main() {
expect(find.text('疫苗进度'), findsOneWidget);
expect(find.text('2026-08-01'), findsOneWidget);
expect(find.text('下一针·狂犬疫苗'), findsOneWidget);
// 本单不消费 monthlyExpenseT2-14),tz 不传走服务端缺省 UTC
expect(capturedTz, isNull);
// T2-14:月度花费卡接 monthlyExpense12850 分 → 元展示)
expect(find.text('¥128.50'), findsOneWidget);
expect(find.text('本月花费'), findsOneWidget);
// T2-13 遗留③:tz 透传设备时区固定偏移(月度窗口随设备时区)。
expect(capturedTz, tzOffsetQueryValue(DateTime.now().timeZoneOffset));
expect(capturedTz, matches(RegExp(r'^[+-]\d{2}:\d{2}$')));
});
testWidgets('null 语义:无登记显示空态而非 0/0', (tester) async {
@@ -292,4 +299,53 @@ void main() {
expect(find.byIcon(Icons.add), findsNothing);
});
});
group('T2-14 · 时间线入口', () {
testWidgets('点本月花费卡 → 健康时间线页;返回后重拉摘要', (tester) async {
repository.getPetHandler = (petId) async => buildPet('p-1');
var summaryCalls = 0;
repository.getPetSummaryHandler = (petId, tz) async {
summaryCalls++;
return buildSummary();
};
repository.listHealthEventsHandler = (petId, limit, cursor) async =>
const CursorPage(items: [], nextCursor: null, hasMore: false);
await pumpDetail(tester);
await tester.pumpAndSettle();
await tester.tap(find.text('本月花费'));
await tester.pumpAndSettle();
expect(find.byType(HealthEventsPage), findsOneWidget);
expect(find.text('健康时间线'), findsOneWidget);
await tester.pageBack();
await tester.pumpAndSettle();
expect(summaryCalls, 2);
});
testWidgets('记录导航区「健康时间线」入口可达(viewer 透传隐藏录入)', (tester) async {
repository.getPetHandler = (petId) async =>
buildPet('p-1', overrides: {'myRole': 'viewer'});
repository.getPetSummaryHandler = (petId, tz) async => buildSummary();
repository.listHealthEventsHandler = (petId, limit, cursor) async =>
const CursorPage(items: [], nextCursor: null, hasMore: false);
await pumpDetail(tester);
await tester.pumpAndSettle();
final tile = find.widgetWithText(ListTile, '健康时间线');
expect(tile, findsOneWidget);
await tester.ensureVisible(tile);
await tester.tap(tile);
await tester.pumpAndSettle();
expect(find.byType(HealthEventsPage), findsOneWidget);
// viewer:空态无 CTA、AppBar 无添加入口。
expect(find.text('记录第一条'), findsNothing);
expect(find.byIcon(Icons.add), findsNothing);
});
});
}