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/pet_analytics.dart'; import 'package:patbond_flutter/features/pets/pet_exceptions.dart'; import 'package:patbond_flutter/features/pets/pet_form_page.dart'; import 'package:patbond_flutter/features/pets/pet_models.dart'; import 'package:patbond_flutter/features/pets/pets_controller.dart'; import '../../helpers/pet_test_helpers.dart'; void main() { late FakePetsRepository repository; late PetsController controller; late List<(String, Map?)> events; late PetAnalytics analytics; setUp(() { repository = FakePetsRepository(); controller = PetsController(repository: repository); events = []; analytics = PetAnalytics( (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, Widget form) 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: (_) => form))); await tester.pumpAndSettle(); } Future pumpCreate( WidgetTester tester, { PetCreateEntryPoint entryPoint = PetCreateEntryPoint.profileEmptyState, }) { return pumpForm( tester, PetFormPage.create( controller: controller, analytics: analytics, entryPoint: entryPoint, ), ); } Future selectBreed(WidgetTester tester, String label) async { await tester.tap(find.byType(DropdownButtonFormField)); await tester.pumpAndSettle(); await tester.tap(find.text(label).last); await tester.pumpAndSettle(); } group('创建模式', () { testWidgets('提交空表单:昵称/性别/品种三处校验拦截 + failed(validation_error)', ( tester, ) async { await pumpCreate(tester); await tester.tap(find.text('保存档案')); await tester.pumpAndSettle(); expect(find.text('请输入宠物昵称'), findsOneWidget); expect(find.text('请选择性别'), findsOneWidget); expect(find.text('请选择品种'), findsOneWidget); final failed = eventsOf('pet_create_failed'); expect(failed, hasLength(1)); expect(failed.single!['failureReason'], 'validation_error'); expect(failed.single!['attemptSeq'], 1); }); testWidgets('首次输入触发 pet_create_started(每次进入仅一次)', (tester) async { await pumpCreate(tester); expect(eventsOf('pet_create_started'), isEmpty); await tester.enterText(find.widgetWithText(TextFormField, '宠物昵称'), '咪'); await tester.tap(find.text('女孩')); await tester.pumpAndSettle(); final started = eventsOf('pet_create_started'); expect(started, hasLength(1)); expect(started.single!['entryPoint'], 'profile_empty_state'); }); testWidgets('目录品种建档成功:请求对齐契约、页面回列表、succeeded 三属性', (tester) async { CreatePetRequest? captured; repository.createPetHandler = (request) async { captured = request; return buildPet('p-new', name: '咪咪'); }; await pumpCreate(tester); await tester.enterText(find.widgetWithText(TextFormField, '宠物昵称'), '咪咪'); await tester.tap(find.text('女孩')); await tester.pumpAndSettle(); await selectBreed(tester, '柴犬'); await tester.enterText( find.widgetWithText(TextFormField, '芯片号(可选)'), '985112000000001', ); await tester.tap(find.text('保存档案')); await tester.pumpAndSettle(); expect(captured!.toJson(), { 'name': '咪咪', 'species': 'dog', 'sex': 'female', 'breedId': 'b-1', 'microchipNo': '985112000000001', }); expect(find.text('列表基底'), findsOneWidget); expect(controller.pets.single.id, 'p-new'); final succeeded = eventsOf('pet_create_succeeded'); expect(succeeded, hasLength(1)); expect(succeeded.single!['species'], 'dog'); expect(succeeded.single!['petIndex'], 1); expect(succeeded.single!['durationMs'], isA()); }); testWidgets('自定义品种与目录互斥:只发送 customBreedName', (tester) async { CreatePetRequest? captured; repository.createPetHandler = (request) async { captured = request; return buildPet('p-new'); }; await pumpCreate(tester); await tester.enterText(find.widgetWithText(TextFormField, '宠物昵称'), '大黄'); await tester.tap(find.text('男孩')); await tester.pumpAndSettle(); await selectBreed(tester, '自定义品种…'); expect(find.text('品种名称'), findsOneWidget); await tester.enterText( find.widgetWithText(TextFormField, '品种名称'), '中华田园犬', ); await tester.tap(find.text('保存档案')); await tester.pumpAndSettle(); expect(captured!.toJson()['customBreedName'], '中华田园犬'); expect(captured!.toJson().containsKey('breedId'), isFalse); }); testWidgets('40903 芯片号冲突:字段级报错 + failed 带 errorCode/httpStatus', ( tester, ) async { repository.createPetHandler = (request) async => throw const MicrochipTakenException(message: '已登记'); await pumpCreate(tester); await tester.enterText(find.widgetWithText(TextFormField, '宠物昵称'), '咪咪'); await tester.tap(find.text('女孩')); await tester.pumpAndSettle(); await selectBreed(tester, '柴犬'); await tester.tap(find.text('保存档案')); await tester.pumpAndSettle(); expect(find.text('该芯片号已被登记,请核对后重试'), findsOneWidget); expect(find.byType(PetFormPage), findsOneWidget); final failed = eventsOf('pet_create_failed'); expect(failed.single!['errorCode'], 40903); expect(failed.single!['httpStatus'], 409); expect(failed.single!['failureReason'], 'validation_error'); }); testWidgets('网络异常:SnackBar + 重试动作 + failed(network_error)', (tester) async { repository.createPetHandler = (request) async => throw const ApiNetworkException('断网'); await pumpCreate(tester); await tester.enterText(find.widgetWithText(TextFormField, '宠物昵称'), '咪咪'); await tester.tap(find.text('未知')); await tester.pumpAndSettle(); await selectBreed(tester, '柴犬'); await tester.tap(find.text('保存档案')); await tester.pumpAndSettle(); expect(find.text('网络异常,请检查网络后重试'), findsOneWidget); expect(find.text('重试'), findsOneWidget); expect( eventsOf('pet_create_failed').single!['failureReason'], 'network_error', ); }); testWidgets('品种目录加载失败:回落自定义输入,重试后目录恢复', (tester) async { var calls = 0; repository.listBreedsHandler = (species) async { calls++; if (calls == 1) throw const ApiNetworkException('断网'); return [Breed.fromJson(sampleBreedJson())]; }; await pumpCreate(tester); expect(find.text('品种目录加载失败,可先填写自定义品种'), findsOneWidget); expect(find.text('品种名称'), findsOneWidget); await tester.tap(find.text('重试')); await tester.pumpAndSettle(); expect(find.byType(DropdownButtonFormField), findsOneWidget); expect(find.text('品种目录加载失败,可先填写自定义品种'), findsNothing); }); testWidgets('昵称失焦校验:空值即时 errorText,输入即清除', (tester) async { await pumpCreate(tester); await tester.tap(find.widgetWithText(TextFormField, '宠物昵称')); await tester.pump(); // 焦点移到芯片号 → 昵称失焦触发校验。 await tester.tap(find.widgetWithText(TextFormField, '芯片号(可选)')); await tester.pumpAndSettle(); expect(find.text('请输入宠物昵称'), findsOneWidget); await tester.enterText(find.widgetWithText(TextFormField, '宠物昵称'), '豆'); await tester.pumpAndSettle(); expect(find.text('请输入宠物昵称'), findsNothing); }); }); group('编辑模式', () { testWidgets('预填 + 物种锁定 + 差量提交(只发送改动字段与 version)', (tester) async { UpdatePetRequest? captured; repository.updatePetHandler = (petId, request) async { captured = request; return buildPet('p-1', name: '豆豆二世', version: 4); }; await pumpForm( tester, PetFormPage.edit( controller: controller, pet: buildPet('p-1', version: 3), analytics: analytics, ), ); // 物种锁定为静态文案,无可选分段控件。 expect(find.text('物种(创建后不可修改)'), findsOneWidget); expect(find.byType(SegmentedButton), findsNothing); expect(find.text('编辑宠物资料'), findsOneWidget); await tester.enterText( find.widgetWithText(TextFormField, '宠物昵称'), '豆豆二世', ); await tester.tap(find.text('保存修改')); await tester.pumpAndSettle(); expect(captured!.toJson(), {'version': 3, 'name': '豆豆二世'}); expect(find.text('列表基底'), findsOneWidget); // 编辑不产生 pet_create_* 事件(06 §1.4)。 expect(events, isEmpty); }); testWidgets('40902 版本冲突:明确提示 + 自动取新 version,重提成功', (tester) async { var updateCalls = 0; final versions = []; repository.updatePetHandler = (petId, request) async { updateCalls++; versions.add(request.version); if (updateCalls == 1) { throw const PetVersionConflictException(message: '版本过期'); } return buildPet('p-1', name: '豆豆二世', version: 5); }; repository.getPetHandler = (petId) async => buildPet('p-1', version: 4); await pumpForm( tester, PetFormPage.edit( controller: controller, pet: buildPet('p-1', version: 3), ), ); await tester.enterText( find.widgetWithText(TextFormField, '宠物昵称'), '豆豆二世', ); await tester.tap(find.text('保存修改')); await tester.pumpAndSettle(); // 明确的用户提示 + 刷新路径(已拉取最新版本)。 expect(find.text('资料已在其他设备被修改,已获取最新版本,请核对后重新保存'), findsOneWidget); expect(find.byType(PetFormPage), findsOneWidget); await tester.tap(find.text('保存修改')); await tester.pumpAndSettle(); expect(versions, [3, 4]); expect(find.text('列表基底'), findsOneWidget); }); testWidgets('无任何变更:不发 PATCH 直接返回', (tester) async { var updateCalls = 0; repository.updatePetHandler = (petId, request) async { updateCalls++; return buildPet('p-1'); }; await pumpForm( tester, PetFormPage.edit( controller: controller, pet: buildPet('p-1', version: 3), ), ); await tester.tap(find.text('保存修改')); await tester.pumpAndSettle(); expect(updateCalls, 0); expect(find.text('列表基底'), findsOneWidget); }); }); }