Files
patbond-flutter/test/features/pets/weight_form_page_test.dart
T
lixi 5b34fa336b 新增:体重录入与历史列表接入真实数据(T2-13 体重半边)
- WeightRecordsPage:cursor 分页(加载更多/末页收起/翻页失败保留重试)、
  loading/empty/error/retry 四态、下拉刷新;viewer 隐藏录入入口
- WeightFormPage:weightKg 契约区间 (0,500] 与两位小数前端校验 + 40000
  兜底;称重时间转 UTC 上送;错误三层分层沿用登录纵切
- health_record 域埋点强类型封装(06 §1.4:create_started/succeeded/
  failed + viewed,httpStatus 由业务码推导;viewed 按工单口径取列表曝光)
- health_record_display 纯函数(体重解析/展示、疫苗状态映射、42201
  规则前端拦截函数);PetsController 暴露 repository(22 号 §7 交接)
- 测试 177 → 205(+28)全绿

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 13:17:17 +08:00

196 lines
6.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String, dynamic>?)> events;
late HealthRecordAnalytics analytics;
setUp(() {
repository = FakePetsRepository();
events = [];
analytics = HealthRecordAnalytics(
(name, [props]) async => events.add((name, props)),
);
});
List<Map<String, dynamic>?> eventsOf(String name) => [
for (final e in events)
if (e.$1 == name) e.$2,
];
Future<void> 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<NavigatorState>(find.byType(Navigator));
unawaited(
navigator.push(
MaterialPageRoute<void>(
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<int>());
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',
);
});
}