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/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/features/pets/vaccination_form_page.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, 2000); 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: (_) => VaccinationFormPage( repository: repository, petId: 'p-1', petSpecies: PetSpecies.dog, analytics: analytics, ), ), ), ); await tester.pumpAndSettle(); } Future selectVaccine(WidgetTester tester, String name) async { await tester.tap(find.byType(DropdownButtonFormField)); await tester.pumpAndSettle(); await tester.tap(find.text(name).last); await tester.pumpAndSettle(); } Future pickDate(WidgetTester tester, String tileLabel) async { await tester.tap(find.text(tileLabel)); await tester.pumpAndSettle(); await tester.tap(find.text('OK')); await tester.pumpAndSettle(); } testWidgets('目录按宠物物种过滤;加载失败可重试恢复', (tester) async { var calls = 0; PetSpecies? capturedSpecies; repository.listVaccineCatalogHandler = (species) async { calls++; capturedSpecies = species; if (calls == 1) throw const ApiNetworkException('断网'); return [VaccineCatalogItem.fromJson(sampleVaccineCatalogJson())]; }; await pumpForm(tester); expect(find.text('疫苗目录加载失败,请重试后选择'), findsOneWidget); await tester.tap(find.text('重试')); await tester.pumpAndSettle(); expect(find.byType(DropdownButtonFormField), findsOneWidget); expect(capturedSpecies, PetSpecies.dog); }); testWidgets('空表单提交:疫苗与计划日期双拦截 + failed(validation_error)', (tester) async { await pumpForm(tester); await tester.tap(find.text('保存登记')); await tester.pumpAndSettle(); expect(find.text('请选择疫苗'), findsOneWidget); expect(find.text('请选择计划接种日期'), findsOneWidget); final failed = eventsOf('health_record_create_failed'); expect(failed.single!['recordType'], 'vaccine'); expect(failed.single!['failureReason'], 'validation_error'); }); testWidgets('选疫苗自动预填系列(目录 code),首次输入触发 started 一次', (tester) async { await pumpForm(tester); expect(eventsOf('health_record_create_started'), isEmpty); await selectVaccine(tester, '狂犬疫苗'); expect( tester .widget(find.widgetWithText(TextFormField, '系列名称')) .controller! .text, 'rabies', ); // 再动一个字段:started 不重复。 await tester.enterText(find.widgetWithText(TextFormField, '剂次'), '2'); await tester.pumpAndSettle(); final started = eventsOf('health_record_create_started'); expect(started, hasLength(1)); expect(started.single, { 'recordType': 'vaccine', 'entryPoint': 'record_list', }); }); testWidgets('状态机拦截 · 未填接种日期就标完成:前端拦截不发请求', (tester) async { var createCalls = 0; repository.createVaccinationHandler = (petId, request) async { createCalls++; return buildVaccination('vx-x'); }; await pumpForm(tester); await selectVaccine(tester, '狂犬疫苗'); await tester.tap(find.text('已完成')); await tester.pumpAndSettle(); // completed 态不渲染计划日期字段(结构上杜绝 scheduled 带 administeredOn 的反向路径)。 expect(find.text('计划接种日期'), findsNothing); expect(find.text('接种日期'), findsOneWidget); await tester.tap(find.text('保存登记')); await tester.pumpAndSettle(); expect(find.text('请选择接种日期'), findsOneWidget); expect(createCalls, 0); }); testWidgets('scheduled 登记成功:请求形状精确对齐契约 + succeeded', (tester) async { CreateVaccinationRequest? captured; repository.createVaccinationHandler = (petId, request) async { expect(petId, 'p-1'); captured = request; return buildVaccination('vx-new'); }; await pumpForm(tester); await selectVaccine(tester, '狂犬疫苗'); await tester.enterText( find.widgetWithText(TextFormField, '剂次标签(可选)'), '第一针', ); await pickDate(tester, '计划接种日期'); await tester.tap(find.text('保存登记')); await tester.pumpAndSettle(); final json = captured!.toJson(); expect(json['vaccineId'], 'v-1'); expect(json['seriesKey'], 'rabies'); expect(json['doseNo'], 1); expect(json['status'], 'scheduled'); expect(json['doseLabel'], '第一针'); expect(json.containsKey('plannedOn'), isTrue); // scheduled 不得携带 administeredOn(契约 42201 规则,结构化保证)。 expect(json.containsKey('administeredOn'), isFalse); expect(json.containsKey('nextDueOn'), isFalse); expect(find.text('列表基底'), findsOneWidget); final succeeded = eventsOf('health_record_create_succeeded'); expect(succeeded.single!['recordType'], 'vaccine'); expect(succeeded.single!['photoCount'], 0); }); testWidgets('completed 登记成功:administeredOn 必填已给、无计划日期字段', (tester) async { CreateVaccinationRequest? captured; repository.createVaccinationHandler = (petId, request) async { captured = request; return buildVaccination('vx-new'); }; await pumpForm(tester); await selectVaccine(tester, '狂犬疫苗'); await tester.tap(find.text('已完成')); await tester.pumpAndSettle(); await pickDate(tester, '接种日期'); await tester.tap(find.text('保存登记')); await tester.pumpAndSettle(); final json = captured!.toJson(); expect(json['status'], 'completed'); expect(json.containsKey('administeredOn'), isTrue); expect(json.containsKey('plannedOn'), isFalse); }); testWidgets('40904 同系列同剂次冲突:兜底横幅 + failed 带码', (tester) async { repository.createVaccinationHandler = (petId, request) async => throw const VaccinationDoseExistsException(message: '已存在'); await pumpForm(tester); await selectVaccine(tester, '狂犬疫苗'); await pickDate(tester, '计划接种日期'); await tester.tap(find.text('保存登记')); await tester.pumpAndSettle(); expect(find.text('该系列该剂次已有记录(40904);如登记有误,可取消原记录后重新登记'), findsOneWidget); expect(find.byType(VaccinationFormPage), findsOneWidget); final failed = eventsOf('health_record_create_failed'); expect(failed.single!['errorCode'], 40904); expect(failed.single!['httpStatus'], 409); }); testWidgets('42201 状态-日期规则后端兜底:横幅提示 + failed 带码', (tester) async { repository.createVaccinationHandler = (petId, request) async => throw const VaccinationRuleException( message: 'scheduled 必须提供 plannedOn', ); await pumpForm(tester); await selectVaccine(tester, '狂犬疫苗'); await pickDate(tester, '计划接种日期'); await tester.tap(find.text('保存登记')); await tester.pumpAndSettle(); expect(find.text('接种状态与日期不符合规则,请核对后重试'), findsOneWidget); final failed = eventsOf('health_record_create_failed'); expect(failed.single!['errorCode'], 42201); expect(failed.single!['httpStatus'], 422); expect(failed.single!['failureReason'], 'validation_error'); }); testWidgets('剂次非法(0)拦截', (tester) async { await pumpForm(tester); await selectVaccine(tester, '狂犬疫苗'); await tester.enterText(find.widgetWithText(TextFormField, '剂次'), '0'); await pickDate(tester, '计划接种日期'); await tester.tap(find.text('保存登记')); await tester.pumpAndSettle(); expect(find.text('剂次需为 1~32767 的整数'), findsOneWidget); }); }