新增:疫苗登记/记录列表与档案页摘要卡接入(T2-13 疫苗半边)
CI / flutter-gates (push) Successful in 1m49s

- VaccinationFormPage:目录按宠物物种过滤(四态)、seriesKey 目录 code
  预填、scheduled/completed 状态-日期规则结构化 + 纯函数双重前端拦截,
  42201/40904 后端兜底横幅(均有测试)
- VaccinationRecordsPage:按系列分组、三态 TagPill(含 cancelled 留痕)、
  四态齐备;viewer 隐藏登记入口
- 档案页数据卡行接 GET /pets/{id}/summary 实时聚合:最新体重/疫苗进度/
  下一针三卡,null 语义为空态文案而非 0/0;从记录页返回即重拉摘要
- health_record 埋点装配(app→shell→pets→detail→记录页面族)
- 测试 205 → 224(+19)全绿;analyze 0 问题;format 无 diff

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 13:20:04 +08:00
parent 5b34fa336b
commit c91f18a845
9 changed files with 1570 additions and 3 deletions
@@ -9,6 +9,8 @@ import 'package:patbond_flutter/features/pets/pet_exceptions.dart';
import 'package:patbond_flutter/features/pets/pet_form_page.dart';
import 'package:patbond_flutter/features/pets/pet_models.dart';
import 'package:patbond_flutter/features/pets/pets_controller.dart';
import 'package:patbond_flutter/features/pets/vaccination_records_page.dart';
import 'package:patbond_flutter/features/pets/weight_records_page.dart';
import '../../helpers/pet_test_helpers.dart';
@@ -22,6 +24,10 @@ void main() {
});
Future<void> pumpDetail(WidgetTester tester, {String petId = 'p-1'}) async {
// 详情页自 T2-13 增加健康数据卡行,加高视口保证底部按钮在栏内。
tester.view.physicalSize = const Size(700, 1800);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
theme: buildAppTheme(),
@@ -176,4 +182,114 @@ void main() {
expect(find.text('编辑宠物资料'), findsOneWidget);
expect(find.text('豆豆'), findsOneWidget);
});
group('T2-13 · 数据卡行接 summary', () {
testWidgets('三卡取数:最新体重 / 疫苗进度 / 下一针(含疫苗名)', (tester) async {
repository.getPetHandler = (petId) async => buildPet('p-1');
String? capturedTz;
repository.getPetSummaryHandler = (petId, tz) async {
capturedTz = tz;
return buildSummary();
};
await pumpDetail(tester);
await tester.pumpAndSettle();
expect(find.text('健康数据'), findsOneWidget);
expect(find.text('4.35 kg'), findsOneWidget);
expect(find.text('最新体重'), findsOneWidget);
expect(find.text('2/3 针'), findsOneWidget);
expect(find.text('疫苗进度'), findsOneWidget);
expect(find.text('2026-08-01'), findsOneWidget);
expect(find.text('下一针·狂犬疫苗'), findsOneWidget);
// 本单不消费 monthlyExpenseT2-14),tz 不传走服务端缺省 UTC。
expect(capturedTz, isNull);
});
testWidgets('null 语义:无登记显示空态而非 0/0', (tester) async {
repository.getPetHandler = (petId) async => buildPet('p-1');
repository.getPetSummaryHandler = (petId, tz) async => buildSummary(
overrides: {
'latestWeight': null,
'vaccinationProgress': null,
'nextVaccination': null,
},
);
await pumpDetail(tester);
await tester.pumpAndSettle();
expect(find.text('暂无记录'), findsOneWidget);
expect(find.text('未登记'), findsOneWidget);
expect(find.text('暂无安排'), findsOneWidget);
expect(find.textContaining('0/0'), findsNothing);
expect(find.text('下一针'), findsOneWidget);
});
testWidgets('摘要加载失败:档案主链路不受阻,行内重试恢复', (tester) async {
repository.getPetHandler = (petId) async => buildPet('p-1');
var calls = 0;
repository.getPetSummaryHandler = (petId, tz) async {
calls++;
if (calls == 1) throw const ApiNetworkException('断网');
return buildSummary();
};
await pumpDetail(tester);
await tester.pumpAndSettle();
// 基本资料照常渲染,摘要行内错误 + 重试。
expect(find.text('豆豆'), findsOneWidget);
expect(find.text('健康数据加载失败'), findsOneWidget);
await tester.tap(find.text('重试'));
await tester.pumpAndSettle();
expect(find.text('4.35 kg'), findsOneWidget);
expect(find.text('健康数据加载失败'), findsNothing);
});
testWidgets('点体重卡 → 体重历史页;返回后重拉摘要', (tester) async {
repository.getPetHandler = (petId) async => buildPet('p-1');
var summaryCalls = 0;
repository.getPetSummaryHandler = (petId, tz) async {
summaryCalls++;
return buildSummary();
};
repository.listWeightsHandler = (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(WeightRecordsPage), 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.listVaccinationsHandler = (petId) async => const [];
await pumpDetail(tester);
await tester.pumpAndSettle();
await tester.tap(find.text('疫苗进度'));
await tester.pumpAndSettle();
expect(find.byType(VaccinationRecordsPage), findsOneWidget);
// viewer:空态无 CTA、AppBar 无添加入口。
expect(find.text('登记第一针'), findsNothing);
expect(find.byIcon(Icons.add), findsNothing);
});
});
}