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_detail_page.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 'package:patbond_flutter/features/pets/vaccination_records_page.dart'; import 'package:patbond_flutter/features/pets/weight_records_page.dart'; import '../../helpers/pet_test_helpers.dart'; void main() { late FakePetsRepository repository; late PetsController controller; setUp(() { repository = FakePetsRepository(); controller = PetsController(repository: repository); }); Future pumpDetail(WidgetTester tester, {String petId = 'p-1'}) async { // 详情页自 T2-13 增加健康数据卡行,加高视口保证底部按钮在栏内。 tester.view.physicalSize = const Size(700, 1800); 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: (_) => PetDetailPage(controller: controller, petId: petId), ), ), ); await tester.pump(); } testWidgets('ready:档案字段齐备,owner 可见编辑入口', (tester) async { repository.getPetHandler = (petId) async => buildPet( 'p-1', overrides: { 'microchipNo': '985112000000001', 'personality': '活泼', 'sterilizedOn': '2025-06-01', }, ); await pumpDetail(tester); await tester.pumpAndSettle(); expect(find.text('豆豆'), findsOneWidget); expect(find.text('基本资料'), findsOneWidget); expect(find.text('柴犬'), findsOneWidget); expect(find.text('男孩'), findsOneWidget); expect(find.text('2024-03-15'), findsOneWidget); expect(find.text('985112000000001'), findsOneWidget); expect(find.text('活泼'), findsOneWidget); expect(find.text('2025-06-01'), findsOneWidget); expect(find.byIcon(Icons.edit_outlined), findsWidgets); expect(find.text('编辑资料'), findsOneWidget); }); testWidgets('生日估算标记与未填写字段兜底', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1', overrides: {'birthDateEstimated': true}); await pumpDetail(tester); await tester.pumpAndSettle(); expect(find.text('2024-03-15(估算)'), findsOneWidget); // 芯片号 / 性格 / 绝育日期未填。 expect(find.text('未填写'), findsNWidgets(3)); }); testWidgets('四态 · loading:无内存副本时居中转圈', (tester) async { final completer = Completer(); repository.getPetHandler = (petId) => completer.future; await pumpDetail(tester); await tester.pump(); expect(find.byType(CircularProgressIndicator), findsOneWidget); completer.complete(buildPet('p-1')); await tester.pumpAndSettle(); expect(find.text('豆豆'), findsOneWidget); }); testWidgets('四态 · error/retry:无副本加载失败给横幅与重试', (tester) async { var calls = 0; repository.getPetHandler = (petId) async { calls++; if (calls == 1) throw const ApiNetworkException('断网'); return buildPet('p-1'); }; await pumpDetail(tester); await tester.pumpAndSettle(); expect(find.text('网络异常,请检查网络后重试'), findsOneWidget); await tester.tap(find.text('重试')); await tester.pumpAndSettle(); expect(find.text('豆豆'), findsOneWidget); }); testWidgets('四态 · notFound(40401):提示不存在,返回列表并触发刷新', (tester) async { repository.getPetHandler = (petId) async => throw const PetNotFoundException(message: '不存在'); var listCalls = 0; repository.listPetsHandler = () async { listCalls++; return const []; }; await pumpDetail(tester); await tester.pumpAndSettle(); expect(find.text('宠物不存在或已被删除'), findsOneWidget); await tester.tap(find.text('返回列表')); await tester.pumpAndSettle(); expect(find.byType(PetDetailPage), findsNothing); expect(find.text('列表基底'), findsOneWidget); expect(listCalls, 1); }); testWidgets('有内存副本时先渲染副本;后台刷新失败降级 SnackBar', (tester) async { repository.listPetsHandler = () async => [buildPet('p-1')]; await controller.refresh(); final completer = Completer(); repository.getPetHandler = (petId) => completer.future; await pumpDetail(tester); await tester.pump(); // 副本即时渲染,无整页 loading。 expect(find.text('豆豆'), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsNothing); completer.completeError(const ApiNetworkException('断网')); await tester.pumpAndSettle(); expect(find.text('豆豆'), findsOneWidget); expect(find.text('网络异常,请检查网络后重试'), findsOneWidget); }); testWidgets('viewer 角色隐藏全部编辑入口(40300 语义)', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1', overrides: {'myRole': 'viewer'}); await pumpDetail(tester); await tester.pumpAndSettle(); expect(find.byIcon(Icons.edit_outlined), findsNothing); expect(find.text('编辑资料'), findsNothing); expect(find.byIcon(Icons.edit), findsNothing); }); testWidgets('owner 点编辑 → 进入编辑表单并预填', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1'); await pumpDetail(tester); await tester.pumpAndSettle(); await tester.tap(find.text('编辑资料')); await tester.pumpAndSettle(); expect(find.byType(PetFormPage), findsOneWidget); expect(find.text('编辑宠物资料'), findsOneWidget); expect(find.text('豆豆'), findsOneWidget); }); group('T2-13 · 数据卡行接 summary', () { testWidgets('三卡取数:最新体重 / 疫苗进度 / 下一针(含疫苗名)', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1'); String? capturedTz; repository.getPetSummaryHandler = (petId, tz) async { capturedTz = tz; return buildSummary(); }; await pumpDetail(tester); await tester.pumpAndSettle(); expect(find.text('健康数据'), findsOneWidget); expect(find.text('4.35 kg'), findsOneWidget); expect(find.text('最新体重'), findsOneWidget); expect(find.text('2/3 针'), findsOneWidget); expect(find.text('疫苗进度'), findsOneWidget); expect(find.text('2026-08-01'), findsOneWidget); expect(find.text('下一针·狂犬疫苗'), findsOneWidget); // 本单不消费 monthlyExpense(T2-14),tz 不传走服务端缺省 UTC。 expect(capturedTz, isNull); }); testWidgets('null 语义:无登记显示空态而非 0/0', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1'); repository.getPetSummaryHandler = (petId, tz) async => buildSummary( overrides: { 'latestWeight': null, 'vaccinationProgress': null, 'nextVaccination': null, }, ); await pumpDetail(tester); await tester.pumpAndSettle(); expect(find.text('暂无记录'), findsOneWidget); expect(find.text('未登记'), findsOneWidget); expect(find.text('暂无安排'), findsOneWidget); expect(find.textContaining('0/0'), findsNothing); expect(find.text('下一针'), findsOneWidget); }); testWidgets('摘要加载失败:档案主链路不受阻,行内重试恢复', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1'); var calls = 0; repository.getPetSummaryHandler = (petId, tz) async { calls++; if (calls == 1) throw const ApiNetworkException('断网'); return buildSummary(); }; await pumpDetail(tester); await tester.pumpAndSettle(); // 基本资料照常渲染,摘要行内错误 + 重试。 expect(find.text('豆豆'), findsOneWidget); expect(find.text('健康数据加载失败'), findsOneWidget); await tester.tap(find.text('重试')); await tester.pumpAndSettle(); expect(find.text('4.35 kg'), findsOneWidget); expect(find.text('健康数据加载失败'), findsNothing); }); testWidgets('点体重卡 → 体重历史页;返回后重拉摘要', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1'); var summaryCalls = 0; repository.getPetSummaryHandler = (petId, tz) async { summaryCalls++; return buildSummary(); }; repository.listWeightsHandler = (petId, limit, cursor) async => const CursorPage(items: [], nextCursor: null, hasMore: false); await pumpDetail(tester); await tester.pumpAndSettle(); await tester.tap(find.text('最新体重')); await tester.pumpAndSettle(); expect(find.byType(WeightRecordsPage), findsOneWidget); expect(find.text('体重记录'), findsOneWidget); await tester.pageBack(); await tester.pumpAndSettle(); expect(summaryCalls, 2); }); testWidgets('点疫苗卡 → 疫苗记录页(viewer 权限透传隐藏登记入口)', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1', overrides: {'myRole': 'viewer'}); repository.getPetSummaryHandler = (petId, tz) async => buildSummary(); repository.listVaccinationsHandler = (petId) async => const []; await pumpDetail(tester); await tester.pumpAndSettle(); await tester.tap(find.text('疫苗进度')); await tester.pumpAndSettle(); expect(find.byType(VaccinationRecordsPage), findsOneWidget); // viewer:空态无 CTA、AppBar 无添加入口。 expect(find.text('登记第一针'), findsNothing); expect(find.byIcon(Icons.add), findsNothing); }); }); }