import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:patbond_flutter/app/app_localization.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/app_date_picker.dart'; import 'package:patbond_flutter/features/pets/care_reminder_form_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 '../../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 pumpForm(WidgetTester tester) async { tester.view.physicalSize = const Size(700, 1600); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); await tester.pumpWidget( MaterialApp( theme: buildAppTheme(), // M3.5-01:与生产一致挂 zh-CN delegate。 localizationsDelegates: appLocalizationsDelegates, supportedLocales: appSupportedLocales, locale: appLocale, home: Builder( builder: (context) => Scaffold( body: Center( child: TextButton( onPressed: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => CareReminderFormPage( repository: repository, petId: 'p-1', analytics: analytics, ), ), ), child: const Text('打开表单'), ), ), ), ), ), ); await tester.tap(find.text('打开表单')); await tester.pumpAndSettle(); } Future fillValid(WidgetTester tester) async { 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('确定')); await tester.pumpAndSettle(); } testWidgets('started 去重:首次输入一次;四类类型可选', (tester) async { await pumpForm(tester); await fillValid(tester); await tester.enterText( find.widgetWithText(TextFormField, '提醒内容(如:体内外驱虫)'), '改个名字', ); await tester.pump(); final started = eventsOf('health_record_create_started'); expect(started.single, { 'recordType': 'reminder', 'entryPoint': 'record_list', }); // 四类类型齐备。 for (final label in ['驱虫', '体检', '用药', '其他']) { expect(find.text(label), findsOneWidget); } }); testWidgets('40300 横幅 / 网络 SnackBar 兜底 + 失败事件', (tester) async { var attempt = 0; repository.createCareReminderHandler = (petId, request) async { attempt++; if (attempt == 1) { throw const PetAccessDeniedException(message: '无权限'); } throw const ApiNetworkException('断网'); }; await pumpForm(tester); await fillValid(tester); await tester.tap(find.text('保存提醒')); await tester.pumpAndSettle(); expect(find.text('你没有权限为该宠物添加提醒'), findsOneWidget); await tester.tap(find.text('保存提醒')); await tester.pumpAndSettle(); expect(find.text('网络异常,请检查网络后重试'), findsOneWidget); final failed = eventsOf('health_record_create_failed'); expect(failed.length, 2); expect(failed[0], { 'recordType': 'reminder', 'failureReason': 'permission_denied', 'attemptSeq': 1, 'errorCode': 40300, 'httpStatus': 403, }); expect(failed[1]!['failureReason'], 'network_error'); expect(failed[1]!['attemptSeq'], 2); }); group('M3.5-02 · 到期日期录入(firstDate 就是今天的那一格)', () { testWidgets('未选日期时也有「今天」快捷键;一键落今天并清掉必填校验错', (tester) async { await pumpForm(tester); // 先触发必填校验错。 await tester.tap(find.text('保存提醒')); await tester.pumpAndSettle(); expect(find.text('未选择'), findsOneWidget); await tester.tap(find.text('今天')); await tester.pumpAndSettle(); expect(find.text(dateToJson(DateTime.now())), findsOneWidget); expect(find.text('未选择'), findsNothing); expect(find.text('选择日期'), findsNothing); }); testWidgets('业务约束不变:firstDate 今天(不许补记过去)、lastDate 五年后', (tester) async { await pumpForm(tester); await tester.tap(find.widgetWithText(ListTile, '到期日期')); await tester.pumpAndSettle(); final dialog = tester.widget( find.byType(DatePickerDialog), ); expect(dialog.firstDate, dateOnly(DateTime.now())); expect(dialog.lastDate, DateTime(DateTime.now().year + 5)); expect(find.text('选择日期'), findsOneWidget); }); }); }