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_event_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, 1700); tester.view.devicePixelRatio = 1.0; addTearDown(tester.view.reset); await tester.pumpWidget( MaterialApp( theme: buildAppTheme(), home: Builder( builder: (context) => Scaffold( body: Center( child: TextButton( onPressed: () => Navigator.of(context).push( MaterialPageRoute( builder: (_) => HealthEventFormPage( repository: repository, petId: 'p-1', analytics: analytics, ), ), ), child: const Text('打开表单'), ), ), ), ), ), ); await tester.tap(find.text('打开表单')); await tester.pumpAndSettle(); } Future fillValid(WidgetTester tester, {String? amount}) async { await tester.tap(find.text('就医')); await tester.pump(); await tester.enterText( find.widgetWithText(TextFormField, '标题(如:皮肤检查)'), '皮肤检查', ); if (amount != null) { await tester.enterText( find.widgetWithText(TextFormField, '金额(元,可选)'), amount, ); } await tester.pump(); } testWidgets('本地校验:类型/标题缺失与金额非法拦截,不发请求且报 validation_error', (tester) async { var called = false; repository.createHealthEventHandler = (petId, request) async { called = true; return buildHealthEvent('e-1'); }; await pumpForm(tester); await tester.enterText( find.widgetWithText(TextFormField, '金额(元,可选)'), '12.345', ); await tester.tap(find.text('保存记录')); await tester.pump(); expect(find.text('请选择事件类型'), findsOneWidget); expect(find.text('请输入标题'), findsOneWidget); expect(find.text('金额格式不正确,最多两位小数'), findsOneWidget); expect(called, isFalse); final failed = eventsOf('health_record_create_failed'); expect(failed.single!['recordType'], 'health_event'); expect(failed.single!['failureReason'], 'validation_error'); }); testWidgets('成功请求形状:六类类型、UTC 时间戳、金额元→整数分、无金额时键缺席', (tester) async { CreateHealthEventRequest? captured; repository.createHealthEventHandler = (petId, request) async { captured = request; return buildHealthEvent('e-1'); }; await pumpForm(tester); await fillValid(tester, amount: '128.50'); await tester.tap(find.text('保存记录')); await tester.pumpAndSettle(); final json = captured!.toJson(); expect(json['eventType'], 'medical'); expect(json['title'], '皮肤检查'); // 金额以元录入 → 整数分传输(工单硬项)。 expect(json['amountCents'], 12850); // 今日默认此刻,转 UTC 带 Z 上送。 expect(json['occurredAt'], endsWith('Z')); expect(json.containsKey('notes'), isFalse); final succeeded = eventsOf('health_record_create_succeeded'); expect(succeeded.single!['recordType'], 'health_event'); expect(succeeded.single!['durationMs'], isA()); }); testWidgets('无金额提交:amountCents 键整体缺席(非 0、非 null)', (tester) async { CreateHealthEventRequest? captured; repository.createHealthEventHandler = (petId, request) async { captured = request; return buildHealthEvent('e-1'); }; await pumpForm(tester); await fillValid(tester); await tester.tap(find.text('保存记录')); await tester.pumpAndSettle(); expect(captured!.toJson().containsKey('amountCents'), isFalse); }); 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': 'health_event', 'entryPoint': 'record_list', }); }); testWidgets('40300 横幅 / 40000 横幅 / 网络 SnackBar 三兜底 + 失败事件', (tester) async { var attempt = 0; repository.createHealthEventHandler = (petId, request) async { attempt++; switch (attempt) { case 1: throw const PetAccessDeniedException(message: '无权限'); case 2: throw const ApiBusinessException(code: 40000, message: '参数错误'); default: 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); await tester.tap(find.text('保存记录')); await tester.pumpAndSettle(); expect(find.text('网络异常,请检查网络后重试'), findsOneWidget); final failed = eventsOf('health_record_create_failed'); expect(failed.length, 3); expect(failed[0]!['failureReason'], 'permission_denied'); expect(failed[0]!['errorCode'], 40300); expect(failed[1]!['failureReason'], 'validation_error'); expect(failed[1]!['errorCode'], 40000); expect(failed[2]!['failureReason'], 'network_error'); expect(failed[2]!['attemptSeq'], 3); }); }