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/weight_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, 1600); 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: (_) => WeightFormPage( repository: repository, petId: 'p-1', analytics: analytics, ), ), ), ); await tester.pumpAndSettle(); } Finder weightField() => find.widgetWithText(TextFormField, '体重(公斤)'); testWidgets('空值提交拦截 + failed(validation_error)', (tester) async { await pumpForm(tester); await tester.tap(find.text('保存记录')); await tester.pumpAndSettle(); expect(find.text('请输入体重'), findsOneWidget); final failed = eventsOf('health_record_create_failed'); expect(failed.single!['recordType'], 'weight'); expect(failed.single!['failureReason'], 'validation_error'); expect(failed.single!['attemptSeq'], 1); }); testWidgets('契约区间前端拦截:>500 与三位小数均不放行、不发请求', (tester) async { var createCalls = 0; repository.createWeightHandler = (petId, request) async { createCalls++; return buildWeight('w-x'); }; await pumpForm(tester); await tester.enterText(weightField(), '500.5'); await tester.tap(find.text('保存记录')); await tester.pumpAndSettle(); expect(find.text('体重需大于 0 且不超过 500 公斤,最多两位小数'), findsOneWidget); await tester.enterText(weightField(), '4.356'); await tester.tap(find.text('保存记录')); await tester.pumpAndSettle(); expect(find.text('体重需大于 0 且不超过 500 公斤,最多两位小数'), findsOneWidget); expect(createCalls, 0); expect(eventsOf('health_record_create_failed'), hasLength(2)); }); testWidgets('首次输入触发 started(每次进入仅一次,recordType=weight)', (tester) async { await pumpForm(tester); expect(eventsOf('health_record_create_started'), isEmpty); await tester.enterText(weightField(), '4'); await tester.enterText(weightField(), '4.3'); await tester.pumpAndSettle(); final started = eventsOf('health_record_create_started'); expect(started, hasLength(1)); expect(started.single, { 'recordType': 'weight', 'entryPoint': 'record_list', }); }); testWidgets('保存成功:请求对齐契约(UTC 时间戳、note 可选)、pop 回列表、succeeded', (tester) async { CreateWeightRequest? captured; repository.createWeightHandler = (petId, request) async { expect(petId, 'p-1'); captured = request; return buildWeight('w-new', weightKg: 4.35); }; await pumpForm(tester); await tester.enterText(weightField(), '4.35'); await tester.enterText( find.widgetWithText(TextFormField, '备注(可选,如:饭后称重)'), '饭后', ); await tester.tap(find.text('保存记录')); await tester.pumpAndSettle(); final json = captured!.toJson(); expect(json['weightKg'], 4.35); expect(json['note'], '饭后'); // 今日称重取此刻并转 UTC(ISO 带 Z 后缀,避免服务端时区歧义)。 expect(json['measuredAt'], endsWith('Z')); expect(json.containsKey('source'), isFalse); expect(find.text('列表基底'), findsOneWidget); final succeeded = eventsOf('health_record_create_succeeded'); expect(succeeded.single!['recordType'], 'weight'); expect(succeeded.single!['durationMs'], isA()); expect(succeeded.single!['photoCount'], 0); }); testWidgets('40000 后端兜底:横幅提示 + failed 带 errorCode/httpStatus', ( tester, ) async { repository.createWeightHandler = (petId, request) async => throw const ApiBusinessException(code: 40000, message: '参数校验失败'); await pumpForm(tester); await tester.enterText(weightField(), '4.35'); await tester.tap(find.text('保存记录')); await tester.pumpAndSettle(); expect(find.text('请检查填写内容后重试'), findsOneWidget); expect(find.byType(WeightFormPage), findsOneWidget); final failed = eventsOf('health_record_create_failed'); expect(failed.single!['errorCode'], 40000); expect(failed.single!['httpStatus'], 400); expect(failed.single!['failureReason'], 'validation_error'); }); testWidgets('40300 viewer 越权兜底:横幅 + failed(permission_denied)', ( tester, ) async { repository.createWeightHandler = (petId, request) async => throw const PetAccessDeniedException(message: '无权限'); await pumpForm(tester); await tester.enterText(weightField(), '4.35'); await tester.tap(find.text('保存记录')); await tester.pumpAndSettle(); expect(find.text('你没有权限为该宠物添加记录'), findsOneWidget); expect( eventsOf('health_record_create_failed').single!['failureReason'], 'permission_denied', ); }); testWidgets('网络异常:SnackBar + 重试动作 + failed(network_error)', (tester) async { repository.createWeightHandler = (petId, request) async => throw const ApiNetworkException('断网'); await pumpForm(tester); await tester.enterText(weightField(), '4.35'); await tester.tap(find.text('保存记录')); await tester.pumpAndSettle(); expect(find.text('网络异常,请检查网络后重试'), findsOneWidget); expect(find.text('重试'), findsOneWidget); expect( eventsOf('health_record_create_failed').single!['failureReason'], 'network_error', ); }); }