Files
patbond-flutter/test/features/pets/care_reminders_page_test.dart
T
lixi ba503327f5
CI / flutter-gates (push) Successful in 2m12s
新增:照护提醒接入与疫苗流转收尾(T2-14 提醒半边 + T2-13 遗留)
- 照护提醒页:status 过滤(服务端白名单视图)、due_at ASC、逾期红标
  双通道标识,四态齐备;创建表单(四类)+ 完成(completedAt 必带、
  支持补记)/ 忽略(禁带 completedAt)流转,42202/40902 兜底重拉
- 档案页:「健康提醒」卡改真实待办数据驱动(取代 demo 硬编码文案,
  最近到期一条 + 逾期警示形态),照护提醒导航入口带待办数副行
- 疫苗列表(25 号报告遗留①②):scheduled 行「标记完成/取消登记」
  PATCH 流转;完成对话框补录厂商/批号(契约可选字段);
  42201/40902/40402 兜底
- 埋点:create 三事件 + viewed(recordType=reminder)挂通;
  edit_succeeded/failed 挂 vaccine 流转(failureReason 含 conflict);
  提醒完成/忽略按 06 §7 缺口 3 既定取舍不埋
- 测试 250 → 272 全绿(+22,较基线 +48);analyze 0 问题;format 无 diff

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 14:01:17 +08:00

351 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
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/core/widgets/empty_state_illustration.dart';
import 'package:patbond_flutter/features/pets/care_reminder_form_page.dart';
import 'package:patbond_flutter/features/pets/care_reminders_page.dart';
import 'package:patbond_flutter/features/pets/health_record_analytics.dart';
import 'package:patbond_flutter/features/pets/pet_exceptions.dart';
import 'package:patbond_flutter/features/pets/pet_models.dart';
import 'package:patbond_flutter/widgets/common.dart';
import '../../helpers/pet_test_helpers.dart';
void main() {
late FakePetsRepository repository;
late List<(String, Map<String, dynamic>?)> events;
late HealthRecordAnalytics analytics;
setUp(() {
repository = FakePetsRepository();
events = [];
analytics = HealthRecordAnalytics(
(name, [props]) async => events.add((name, props)),
);
});
List<Map<String, dynamic>?> eventsOf(String name) => [
for (final e in events)
if (e.$1 == name) e.$2,
];
Future<void> pumpPage(WidgetTester tester, {bool canWrite = true}) async {
tester.view.physicalSize = const Size(700, 1600);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
theme: buildAppTheme(),
home: const Scaffold(body: Text('详情基底')),
),
);
final navigator = tester.state<NavigatorState>(find.byType(Navigator));
unawaited(
navigator.push(
MaterialPageRoute<void>(
builder: (_) => CareRemindersPage(
repository: repository,
petId: 'p-1',
canWrite: canWrite,
analytics: analytics,
),
),
),
);
await tester.pump();
}
/// 相对当前时刻的待办样本(逾期判定依赖真实 now)。
CareReminder pendingIn(
String id,
Duration offset, {
Map<String, Object?> overrides = const {},
}) => buildReminder(
id,
overrides: {
'dueAt': DateTime.now().add(offset).toUtc().toIso8601String(),
...overrides,
},
);
testWidgets('四态 · ready:类型/时间/状态齐备,逾期待办红标;viewed 一次', (tester) async {
repository.listCareRemindersHandler = (petId, status) async => [
pendingIn(
'r-1',
const Duration(days: -3),
overrides: {'title': '体内外驱虫', 'reminderType': 'deworming'},
),
pendingIn('r-2', const Duration(days: 5)),
buildReminder(
'r-3',
overrides: {
'title': '疫苗加强针',
'reminderType': 'medication',
'status': 'completed',
'completedAt': '2026-09-02T10:00:00+08:00',
},
),
];
await pumpPage(tester);
await tester.pumpAndSettle();
expect(find.text('体内外驱虫'), findsOneWidget);
expect(find.text('年度体检'), findsOneWidget);
expect(find.text('疫苗加强针'), findsOneWidget);
// 逾期视觉标识(工单硬项):过期待办标「已逾期」,未到期标「待办」。
expect(find.widgetWithText(TagPill, '已逾期'), findsOneWidget);
expect(find.widgetWithText(TagPill, '待办'), findsOneWidget);
expect(find.widgetWithText(TagPill, '已完成'), findsOneWidget);
// 待办行有完成/忽略动作,终态行没有。
expect(find.text('标记完成'), findsNWidgets(2));
expect(find.text('忽略'), findsNWidgets(2));
final viewed = eventsOf('health_record_viewed');
expect(viewed.single, {'recordType': 'reminder', 'source': 'pet_detail'});
});
testWidgets('四态 · loading / empty:空态插画 + CTA;过滤空态无 CTA', (tester) async {
final completer = Completer<List<CareReminder>>();
final captured = <CareReminderStatus?>[];
repository.listCareRemindersHandler = (petId, status) {
captured.add(status);
if (captured.length == 1) return completer.future;
return Future.value(const []);
};
await pumpPage(tester);
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsOneWidget);
completer.complete(const []);
await tester.pumpAndSettle();
expect(find.byType(EmptyStateIllustration), findsOneWidget);
expect(find.text('还没有照护提醒'), findsOneWidget);
expect(find.text('添加第一条'), findsOneWidget);
// 切「已完成」过滤:status 参数透传服务端;过滤空态不给 CTA。
await tester.tap(find.text('已完成'));
await tester.pumpAndSettle();
expect(captured, [null, CareReminderStatus.completed]);
expect(find.text('暂无「已完成」提醒'), findsOneWidget);
expect(find.text('添加第一条'), findsNothing);
});
testWidgets('四态 · error/retry:横幅 + 重试恢复', (tester) async {
var calls = 0;
repository.listCareRemindersHandler = (petId, status) async {
calls++;
if (calls == 1) throw const ApiNetworkException('断网');
return [pendingIn('r-1', const Duration(days: 5))];
};
await pumpPage(tester);
await tester.pumpAndSettle();
expect(find.text('网络异常,请检查网络后重试'), findsOneWidget);
await tester.tap(find.text('重试'));
await tester.pumpAndSettle();
expect(find.text('年度体检'), findsOneWidget);
});
testWidgets('创建闭环:CTA → 表单(record_form 路由名)→ 请求形状与事件 → 成功重拉', (tester) async {
var listCalls = 0;
repository.listCareRemindersHandler = (petId, status) async {
listCalls++;
return listCalls == 1
? const []
: [
pendingIn(
'r-new',
const Duration(days: 30),
overrides: {'title': '体内外驱虫'},
),
];
};
CreateCareReminderRequest? captured;
repository.createCareReminderHandler = (petId, request) async {
captured = request;
return pendingIn(
'r-new',
const Duration(days: 30),
overrides: {'title': '体内外驱虫'},
);
};
await pumpPage(tester);
await tester.pumpAndSettle();
await tester.tap(find.text('添加第一条'));
await tester.pumpAndSettle();
expect(find.byType(CareReminderFormPage), findsOneWidget);
final route = ModalRoute.of(
tester.element(find.byType(CareReminderFormPage)),
)!;
expect(route.settings.name, 'record_form');
// 类型必选 + 标题必填 + 到期日期必选:先空提交拦截。
await tester.tap(find.text('保存提醒'));
await tester.pump();
expect(find.text('请选择提醒类型'), findsOneWidget);
expect(find.text('请输入提醒内容'), findsOneWidget);
expect(find.text('请选择到期日期'), findsOneWidget);
await tester.tap(find.text('驱虫'));
await tester.pump();
await tester.enterText(
find.widgetWithText(TextFormField, '提醒内容(如:体内外驱虫)'),
'体内外驱虫',
);
await tester.tap(find.text('到期日期'));
await tester.pumpAndSettle();
await tester.tap(find.text('OK'));
await tester.pumpAndSettle();
await tester.tap(find.text('保存提醒'));
await tester.pumpAndSettle();
final json = captured!.toJson();
expect(json['reminderType'], 'deworming');
expect(json['title'], '体内外驱虫');
expect(json['dueAt'], endsWith('Z'));
expect(json.containsKey('status'), isFalse);
expect(find.byType(CareReminderFormPage), findsNothing);
expect(find.text('已添加提醒'), findsOneWidget);
expect(listCalls, 2);
// 创建漏斗事件(recordType=reminder)。
final started = eventsOf('health_record_create_started');
expect(started.single, {
'recordType': 'reminder',
'entryPoint': 'record_list',
});
final failed = eventsOf('health_record_create_failed');
expect(failed.single!['failureReason'], 'validation_error');
final succeeded = eventsOf('health_record_create_succeeded');
expect(succeeded.single!['recordType'], 'reminder');
});
testWidgets('标记完成:completedAt 必带(UTC);忽略:禁带 completedAt;成功重拉', (
tester,
) async {
var listCalls = 0;
repository.listCareRemindersHandler = (petId, status) async {
listCalls++;
return [
pendingIn('r-1', const Duration(days: 5)),
pendingIn(
'r-2',
const Duration(days: 9),
overrides: {'title': '体内外驱虫'},
),
];
};
final captured = <(String, Map<String, Object?>)>[];
repository.updateCareReminderHandler = (reminderId, request) async {
captured.add((reminderId, request.toJson()));
return buildReminder(
reminderId,
overrides: {
'status': request.status.name,
'completedAt': request.completedAt?.toIso8601String(),
},
);
};
await pumpPage(tester);
await tester.pumpAndSettle();
// 完成 r-1:对话框(默认今天)确认 → PATCH completed + completedAt。
await tester.tap(find.text('标记完成').first);
await tester.pumpAndSettle();
await tester.tap(find.text('确认完成'));
await tester.pumpAndSettle();
expect(captured.length, 1);
expect(captured[0].$1, 'r-1');
expect(captured[0].$2['status'], 'completed');
expect(captured[0].$2['completedAt'], endsWith('Z'));
expect(find.text('已标记完成'), findsOneWidget);
expect(listCalls, 2);
// 忽略 r-2:确认对话框 → PATCH dismissedcompletedAt 键缺席。
await tester.tap(find.text('忽略').last);
await tester.pumpAndSettle();
expect(find.text('忽略这条提醒?'), findsOneWidget);
// 对话框主按钮文案与动作同名,用 FilledButton 定位。
await tester.tap(find.widgetWithText(FilledButton, '忽略'));
await tester.pumpAndSettle();
expect(captured.length, 2);
expect(captured[1].$2, {'status': 'dismissed'});
expect(listCalls, 3);
// 提醒完成/忽略不埋事件(06 §7 缺口 3 既定取舍)。
expect(eventsOf('health_record_edit_succeeded'), isEmpty);
});
testWidgets('42202 规则兜底:提示 + 重拉', (tester) async {
var listCalls = 0;
repository.listCareRemindersHandler = (petId, status) async {
listCalls++;
return [pendingIn('r-1', const Duration(days: 5))];
};
repository.updateCareReminderHandler = (reminderId, request) async {
throw const CareReminderRuleException(message: '规则违反');
};
await pumpPage(tester);
await tester.pumpAndSettle();
await tester.tap(find.text('标记完成'));
await tester.pumpAndSettle();
await tester.tap(find.text('确认完成'));
await tester.pumpAndSettle();
expect(find.text('提醒状态不满足流转规则,已刷新,请重试'), findsOneWidget);
expect(listCalls, 2);
});
testWidgets('40902 并发流转抢先:提示已被处理 + 重拉', (tester) async {
var listCalls = 0;
repository.listCareRemindersHandler = (petId, status) async {
listCalls++;
return [pendingIn('r-1', const Duration(days: 5))];
};
repository.updateCareReminderHandler = (reminderId, request) async {
throw const PetVersionConflictException(message: '数据已被修改');
};
await pumpPage(tester);
await tester.pumpAndSettle();
await tester.tap(find.text('标记完成'));
await tester.pumpAndSettle();
await tester.tap(find.text('确认完成'));
await tester.pumpAndSettle();
expect(find.text('提醒已在其他设备被处理,已刷新'), findsOneWidget);
expect(listCalls, 2);
});
testWidgets('viewercanWrite=false):无添加入口、无完成/忽略动作、空态无 CTA', (tester) async {
repository.listCareRemindersHandler = (petId, status) async => [
pendingIn('r-1', const Duration(days: 5)),
];
await pumpPage(tester, canWrite: false);
await tester.pumpAndSettle();
expect(find.byIcon(Icons.add), findsNothing);
expect(find.text('标记完成'), findsNothing);
expect(find.text('忽略'), findsNothing);
});
}