新增:健康事件时间线接入真实数据(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>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
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_edit_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,
|
||||
];
|
||||
|
||||
HealthEvent baseEvent({int version = 3}) =>
|
||||
buildHealthEvent('e-1', overrides: {'notes': '医院复查', 'version': version});
|
||||
|
||||
Future<void> pumpEdit(WidgetTester tester, {HealthEvent? event}) async {
|
||||
tester.view.physicalSize = const Size(700, 1600);
|
||||
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: (_) => HealthEventEditPage(
|
||||
repository: repository,
|
||||
event: event ?? baseEvent(),
|
||||
analytics: analytics,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text('打开编辑'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('打开编辑'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('预填与身份静态区:类型/发生时间不可编辑,金额以元回显', (tester) async {
|
||||
await pumpEdit(tester);
|
||||
|
||||
// 身份区:类型文案 + 发生时间(无输入控件)。
|
||||
expect(find.text('就医'), findsOneWidget);
|
||||
// 预填:标题 / 金额(12850 分 → 128.50 元)/ 备注。
|
||||
expect(find.text('皮肤检查'), findsOneWidget);
|
||||
expect(find.text('128.50'), findsOneWidget);
|
||||
expect(find.text('医院复查'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('差量提交:只发送改动字段 + version;成功回传并报 edit_succeeded(fieldCount)', (
|
||||
tester,
|
||||
) async {
|
||||
UpdateHealthEventRequest? captured;
|
||||
repository.updateHealthEventHandler = (eventId, request) async {
|
||||
captured = request;
|
||||
return buildHealthEvent('e-1', overrides: {'version': 4});
|
||||
};
|
||||
|
||||
await pumpEdit(tester);
|
||||
await tester.enterText(find.widgetWithText(TextFormField, '标题'), '皮肤复查');
|
||||
await tester.enterText(find.widgetWithText(TextFormField, '金额(元)'), '99');
|
||||
await tester.tap(find.text('保存修改'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 备注未改不出现;金额 99 元 → 9900 分。
|
||||
expect(captured!.toJson(), {
|
||||
'version': 3,
|
||||
'title': '皮肤复查',
|
||||
'amountCents': 9900,
|
||||
});
|
||||
final succeeded = eventsOf('health_record_edit_succeeded');
|
||||
expect(succeeded.single, {'recordType': 'health_event', 'fieldCount': 2});
|
||||
expect(find.byType(HealthEventEditPage), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('无变更不发 PATCH 直接返回;清空可选字段视为不变更(契约不支持清空回 null)', (tester) async {
|
||||
var called = false;
|
||||
repository.updateHealthEventHandler = (eventId, request) async {
|
||||
called = true;
|
||||
return baseEvent();
|
||||
};
|
||||
|
||||
await pumpEdit(tester);
|
||||
// 清空金额与备注:契约不支持清空回 null → 视为不变更。
|
||||
await tester.enterText(find.widgetWithText(TextFormField, '金额(元)'), '');
|
||||
await tester.enterText(find.widgetWithText(TextFormField, '备注'), '');
|
||||
await tester.tap(find.text('保存修改'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(called, isFalse);
|
||||
expect(find.byType(HealthEventEditPage), findsNothing);
|
||||
expect(eventsOf('health_record_edit_succeeded'), isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('40902 冲突:明确提示 + 经时间线检索自动取新 version(保留输入),重提成功', (tester) async {
|
||||
final submittedVersions = <int>[];
|
||||
var conflictOnce = true;
|
||||
repository.updateHealthEventHandler = (eventId, request) async {
|
||||
submittedVersions.add(request.version);
|
||||
if (conflictOnce) {
|
||||
conflictOnce = false;
|
||||
throw const PetVersionConflictException(message: '数据已被修改');
|
||||
}
|
||||
return buildHealthEvent('e-1', overrides: {'version': 8});
|
||||
};
|
||||
// 契约无按 id 读取端点:40902 后经时间线分页检索取回最新版本(version 7)。
|
||||
final listCaptured = <String?>[];
|
||||
repository.listHealthEventsHandler = (petId, limit, cursor) async {
|
||||
listCaptured.add(cursor);
|
||||
return CursorPage(
|
||||
items: [buildHealthEvent('e-other'), baseEvent(version: 7)],
|
||||
nextCursor: null,
|
||||
hasMore: false,
|
||||
);
|
||||
};
|
||||
|
||||
await pumpEdit(tester);
|
||||
await tester.enterText(find.widgetWithText(TextFormField, '标题'), '皮肤复查');
|
||||
await tester.tap(find.text('保存修改'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 明确提示 + 用户输入保留。
|
||||
expect(find.text('记录已在其他设备被修改,已获取最新版本,请核对后重新保存'), findsOneWidget);
|
||||
expect(find.text('皮肤复查'), findsOneWidget);
|
||||
expect(listCaptured, [null]);
|
||||
|
||||
// conflict 失败事件(M2 验收「并发冲突明确」数据面)。
|
||||
final failed = eventsOf('health_record_edit_failed');
|
||||
expect(failed.single, {
|
||||
'recordType': 'health_event',
|
||||
'failureReason': 'conflict',
|
||||
'errorCode': 40902,
|
||||
'httpStatus': 409,
|
||||
});
|
||||
|
||||
// 重提:自动用检索回的新 version。
|
||||
await tester.tap(find.text('保存修改'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(submittedVersions, [3, 7]);
|
||||
expect(find.byType(HealthEventEditPage), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('40402 记录不存在:SnackBar + 返回;40300 横幅;网络 SnackBar', (tester) async {
|
||||
var attempt = 0;
|
||||
repository.updateHealthEventHandler = (eventId, request) async {
|
||||
attempt++;
|
||||
switch (attempt) {
|
||||
case 1:
|
||||
throw const PetAccessDeniedException(message: '无权限');
|
||||
case 2:
|
||||
throw const ApiNetworkException('断网');
|
||||
default:
|
||||
throw const PetRecordNotFoundException(message: '不存在');
|
||||
}
|
||||
};
|
||||
|
||||
await pumpEdit(tester);
|
||||
await tester.enterText(find.widgetWithText(TextFormField, '标题'), '皮肤复查');
|
||||
|
||||
await tester.tap(find.text('保存修改'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('你没有权限修改该记录'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('保存修改'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('网络异常,请检查网络后重试'), findsOneWidget);
|
||||
|
||||
// 经 SnackBar「重试」重提(顺带锁定重试动作接线)→ 40402。
|
||||
await tester.tap(find.text('重试'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('记录不存在或已被删除'), findsOneWidget);
|
||||
expect(find.byType(HealthEventEditPage), findsNothing);
|
||||
|
||||
final failed = eventsOf('health_record_edit_failed');
|
||||
expect(failed.length, 3);
|
||||
expect(failed[0]!['failureReason'], 'permission_denied');
|
||||
expect(failed[1]!['failureReason'], 'network_error');
|
||||
expect(failed[2]!['failureReason'], 'not_found');
|
||||
expect(failed[2]!['errorCode'], 40402);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user