Files
patbond-flutter/test/features/pets/health_event_form_page_test.dart
T
lixi e186ba3da9 新增:健康事件时间线接入真实数据(T2-14 时间线半边)
- 时间线页:occurred_at DESC cursor 分页 + 月分组组头,四态齐备;
  六类事件经 RecordTypeDot 映射(增补喂养/洗护/测量三型,色族复用
  已审计色对),类型 TagPill 双通道
- 事件录入表单:六类类型选择器、金额以元录入/整数分传输(money.dart
  换算)、UTC 时间戳约定与体重表单一致
- 事件编辑页:顶层 PATCH 差量提交(title/notes/amountCents + version),
  40902 照 T2-12 模式自动经时间线检索取新 version 重提(保留输入)
- 档案页:月度花费卡接 summary.monthlyExpense(分→元展示)、tz 透传
  设备时区固定偏移(T2-13 遗留③)、健康时间线导航入口
- 埋点:health_record 域 create 三事件 + viewed(recordType=health_event)
  挂通;新增 edit_succeeded/failed 封装(failureReason 含 conflict)
- 测试 224 → 250 全绿(+26);analyze 0 问题;format 无 diff

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

196 lines
6.7 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 '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_event_form_page.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 '../../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, 1700);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
theme: buildAppTheme(),
home: Builder(
builder: (context) => Scaffold(
body: Center(
child: TextButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<HealthEvent>(
builder: (_) => HealthEventFormPage(
repository: repository,
petId: 'p-1',
analytics: analytics,
),
),
),
child: const Text('打开表单'),
),
),
),
),
),
);
await tester.tap(find.text('打开表单'));
await tester.pumpAndSettle();
}
Future<void> fillValid(WidgetTester tester, {String? amount}) async {
await tester.tap(find.text('就医'));
await tester.pump();
await tester.enterText(
find.widgetWithText(TextFormField, '标题(如:皮肤检查)'),
'皮肤检查',
);
if (amount != null) {
await tester.enterText(
find.widgetWithText(TextFormField, '金额(元,可选)'),
amount,
);
}
await tester.pump();
}
testWidgets('本地校验:类型/标题缺失与金额非法拦截,不发请求且报 validation_error', (tester) async {
var called = false;
repository.createHealthEventHandler = (petId, request) async {
called = true;
return buildHealthEvent('e-1');
};
await pumpForm(tester);
await tester.enterText(
find.widgetWithText(TextFormField, '金额(元,可选)'),
'12.345',
);
await tester.tap(find.text('保存记录'));
await tester.pump();
expect(find.text('请选择事件类型'), findsOneWidget);
expect(find.text('请输入标题'), findsOneWidget);
expect(find.text('金额格式不正确,最多两位小数'), findsOneWidget);
expect(called, isFalse);
final failed = eventsOf('health_record_create_failed');
expect(failed.single!['recordType'], 'health_event');
expect(failed.single!['failureReason'], 'validation_error');
});
testWidgets('成功请求形状:六类类型、UTC 时间戳、金额元→整数分、无金额时键缺席', (tester) async {
CreateHealthEventRequest? captured;
repository.createHealthEventHandler = (petId, request) async {
captured = request;
return buildHealthEvent('e-1');
};
await pumpForm(tester);
await fillValid(tester, amount: '128.50');
await tester.tap(find.text('保存记录'));
await tester.pumpAndSettle();
final json = captured!.toJson();
expect(json['eventType'], 'medical');
expect(json['title'], '皮肤检查');
// 金额以元录入 → 整数分传输(工单硬项)。
expect(json['amountCents'], 12850);
// 今日默认此刻,转 UTC 带 Z 上送。
expect(json['occurredAt'], endsWith('Z'));
expect(json.containsKey('notes'), isFalse);
final succeeded = eventsOf('health_record_create_succeeded');
expect(succeeded.single!['recordType'], 'health_event');
expect(succeeded.single!['durationMs'], isA<int>());
});
testWidgets('无金额提交:amountCents 键整体缺席(非 0、非 null', (tester) async {
CreateHealthEventRequest? captured;
repository.createHealthEventHandler = (petId, request) async {
captured = request;
return buildHealthEvent('e-1');
};
await pumpForm(tester);
await fillValid(tester);
await tester.tap(find.text('保存记录'));
await tester.pumpAndSettle();
expect(captured!.toJson().containsKey('amountCents'), isFalse);
});
testWidgets('started 去重:首次输入一次,后续输入不再上报', (tester) async {
await pumpForm(tester);
await fillValid(tester);
await tester.enterText(
find.widgetWithText(TextFormField, '备注(可选)'),
'换季护理',
);
await tester.pump();
final started = eventsOf('health_record_create_started');
expect(started.single, {
'recordType': 'health_event',
'entryPoint': 'record_list',
});
});
testWidgets('40300 横幅 / 40000 横幅 / 网络 SnackBar 三兜底 + 失败事件', (tester) async {
var attempt = 0;
repository.createHealthEventHandler = (petId, request) async {
attempt++;
switch (attempt) {
case 1:
throw const PetAccessDeniedException(message: '无权限');
case 2:
throw const ApiBusinessException(code: 40000, message: '参数错误');
default:
throw const ApiNetworkException('断网');
}
};
await pumpForm(tester);
await fillValid(tester);
await tester.tap(find.text('保存记录'));
await tester.pumpAndSettle();
expect(find.text('你没有权限为该宠物添加记录'), findsOneWidget);
await tester.tap(find.text('保存记录'));
await tester.pumpAndSettle();
expect(find.text('请检查填写内容后重试'), findsOneWidget);
await tester.tap(find.text('保存记录'));
await tester.pumpAndSettle();
expect(find.text('网络异常,请检查网络后重试'), findsOneWidget);
final failed = eventsOf('health_record_create_failed');
expect(failed.length, 3);
expect(failed[0]!['failureReason'], 'permission_denied');
expect(failed[0]!['errorCode'], 40300);
expect(failed[1]!['failureReason'], 'validation_error');
expect(failed[1]!['errorCode'], 40000);
expect(failed[2]!['failureReason'], 'network_error');
expect(failed[2]!['attemptSeq'], 3);
});
}