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?)> events; late HealthRecordAnalytics analytics; setUp(() { repository = FakePetsRepository(); events = []; analytics = HealthRecordAnalytics( (name, [props]) async => events.add((name, props)), ); }); List?> eventsOf(String name) => [ for (final e in events) if (e.$1 == name) e.$2, ]; Future 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(find.byType(Navigator)); unawaited( navigator.push( MaterialPageRoute( builder: (_) => CareRemindersPage( repository: repository, petId: 'p-1', canWrite: canWrite, analytics: analytics, ), ), ), ); await tester.pump(); } /// 相对当前时刻的待办样本(逾期判定依赖真实 now)。 CareReminder pendingIn( String id, Duration offset, { Map 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>(); final captured = []; 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)>[]; 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 dismissed,completedAt 键缺席。 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('viewer(canWrite=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); }); }