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_edit_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, ]; HealthEvent baseEvent({int version = 3}) => buildHealthEvent('e-1', overrides: {'notes': '医院复查', 'version': version}); Future pumpEdit(WidgetTester tester, {HealthEvent? event}) async { tester.view.physicalSize = const Size(700, 1600); 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: (_) => HealthEventEditPage( repository: repository, event: event ?? baseEvent(), analytics: analytics, ), ), ), child: const Text('打开编辑'), ), ), ), ), ), ); await tester.tap(find.text('打开编辑')); await tester.pumpAndSettle(); } testWidgets('预填与身份静态区:类型/发生时间不可编辑,金额以元回显', (tester) async { await pumpEdit(tester); // 身份区:类型文案 + 发生时间(无输入控件)。 expect(find.text('就医'), findsOneWidget); // 预填:标题 / 金额(12850 分 → 128.50 元)/ 备注。 expect(find.text('皮肤检查'), findsOneWidget); expect(find.text('128.50'), findsOneWidget); expect(find.text('医院复查'), findsOneWidget); }); testWidgets('差量提交:只发送改动字段 + version;成功回传并报 edit_succeeded(fieldCount)', ( tester, ) async { UpdateHealthEventRequest? captured; repository.updateHealthEventHandler = (eventId, request) async { captured = request; return buildHealthEvent('e-1', overrides: {'version': 4}); }; await pumpEdit(tester); await tester.enterText(find.widgetWithText(TextFormField, '标题'), '皮肤复查'); await tester.enterText(find.widgetWithText(TextFormField, '金额(元)'), '99'); await tester.tap(find.text('保存修改')); await tester.pumpAndSettle(); // 备注未改不出现;金额 99 元 → 9900 分。 expect(captured!.toJson(), { 'version': 3, 'title': '皮肤复查', 'amountCents': 9900, }); final succeeded = eventsOf('health_record_edit_succeeded'); expect(succeeded.single, {'recordType': 'health_event', 'fieldCount': 2}); expect(find.byType(HealthEventEditPage), findsNothing); }); testWidgets('无变更不发 PATCH 直接返回;清空可选字段视为不变更(契约不支持清空回 null)', (tester) async { var called = false; repository.updateHealthEventHandler = (eventId, request) async { called = true; return baseEvent(); }; await pumpEdit(tester); // 清空金额与备注:契约不支持清空回 null → 视为不变更。 await tester.enterText(find.widgetWithText(TextFormField, '金额(元)'), ''); await tester.enterText(find.widgetWithText(TextFormField, '备注'), ''); await tester.tap(find.text('保存修改')); await tester.pumpAndSettle(); expect(called, isFalse); expect(find.byType(HealthEventEditPage), findsNothing); expect(eventsOf('health_record_edit_succeeded'), isEmpty); }); testWidgets('40902 冲突:明确提示 + 经时间线检索自动取新 version(保留输入),重提成功', (tester) async { final submittedVersions = []; var conflictOnce = true; repository.updateHealthEventHandler = (eventId, request) async { submittedVersions.add(request.version); if (conflictOnce) { conflictOnce = false; throw const PetVersionConflictException(message: '数据已被修改'); } return buildHealthEvent('e-1', overrides: {'version': 8}); }; // 契约无按 id 读取端点:40902 后经时间线分页检索取回最新版本(version 7)。 final listCaptured = []; repository.listHealthEventsHandler = (petId, limit, cursor) async { listCaptured.add(cursor); return CursorPage( items: [buildHealthEvent('e-other'), baseEvent(version: 7)], nextCursor: null, hasMore: false, ); }; await pumpEdit(tester); await tester.enterText(find.widgetWithText(TextFormField, '标题'), '皮肤复查'); await tester.tap(find.text('保存修改')); await tester.pumpAndSettle(); // 明确提示 + 用户输入保留。 expect(find.text('记录已在其他设备被修改,已获取最新版本,请核对后重新保存'), findsOneWidget); expect(find.text('皮肤复查'), findsOneWidget); expect(listCaptured, [null]); // conflict 失败事件(M2 验收「并发冲突明确」数据面)。 final failed = eventsOf('health_record_edit_failed'); expect(failed.single, { 'recordType': 'health_event', 'failureReason': 'conflict', 'errorCode': 40902, 'httpStatus': 409, }); // 重提:自动用检索回的新 version。 await tester.tap(find.text('保存修改')); await tester.pumpAndSettle(); expect(submittedVersions, [3, 7]); expect(find.byType(HealthEventEditPage), findsNothing); }); testWidgets('40402 记录不存在:SnackBar + 返回;40300 横幅;网络 SnackBar', (tester) async { var attempt = 0; repository.updateHealthEventHandler = (eventId, request) async { attempt++; switch (attempt) { case 1: throw const PetAccessDeniedException(message: '无权限'); case 2: throw const ApiNetworkException('断网'); default: throw const PetRecordNotFoundException(message: '不存在'); } }; await pumpEdit(tester); await tester.enterText(find.widgetWithText(TextFormField, '标题'), '皮肤复查'); await tester.tap(find.text('保存修改')); await tester.pumpAndSettle(); expect(find.text('你没有权限修改该记录'), findsOneWidget); await tester.tap(find.text('保存修改')); await tester.pumpAndSettle(); expect(find.text('网络异常,请检查网络后重试'), findsOneWidget); // 经 SnackBar「重试」重提(顺带锁定重试动作接线)→ 40402。 await tester.tap(find.text('重试')); await tester.pumpAndSettle(); expect(find.text('记录不存在或已被删除'), findsOneWidget); expect(find.byType(HealthEventEditPage), findsNothing); final failed = eventsOf('health_record_edit_failed'); expect(failed.length, 3); expect(failed[0]!['failureReason'], 'permission_denied'); expect(failed[1]!['failureReason'], 'network_error'); expect(failed[2]!['failureReason'], 'not_found'); expect(failed[2]!['errorCode'], 40402); }); }