e186ba3da9
- 时间线页: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>
196 lines
6.7 KiB
Dart
196 lines
6.7 KiB
Dart
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);
|
||
});
|
||
}
|