c91f18a845
CI / flutter-gates (push) Successful in 1m49s
- VaccinationFormPage:目录按宠物物种过滤(四态)、seriesKey 目录 code
预填、scheduled/completed 状态-日期规则结构化 + 纯函数双重前端拦截,
42201/40904 后端兜底横幅(均有测试)
- VaccinationRecordsPage:按系列分组、三态 TagPill(含 cancelled 留痕)、
四态齐备;viewer 隐藏登记入口
- 档案页数据卡行接 GET /pets/{id}/summary 实时聚合:最新体重/疫苗进度/
下一针三卡,null 语义为空态文案而非 0/0;从记录页返回即重拉摘要
- health_record 埋点装配(app→shell→pets→detail→记录页面族)
- 测试 205 → 224(+19)全绿;analyze 0 问题;format 无 diff
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
262 lines
9.1 KiB
Dart
262 lines
9.1 KiB
Dart
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/vaccination_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, 2000);
|
|
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: (_) => VaccinationFormPage(
|
|
repository: repository,
|
|
petId: 'p-1',
|
|
petSpecies: PetSpecies.dog,
|
|
analytics: analytics,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
Future<void> selectVaccine(WidgetTester tester, String name) async {
|
|
await tester.tap(find.byType(DropdownButtonFormField<String>));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text(name).last);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
Future<void> pickDate(WidgetTester tester, String tileLabel) async {
|
|
await tester.tap(find.text(tileLabel));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('OK'));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('目录按宠物物种过滤;加载失败可重试恢复', (tester) async {
|
|
var calls = 0;
|
|
PetSpecies? capturedSpecies;
|
|
repository.listVaccineCatalogHandler = (species) async {
|
|
calls++;
|
|
capturedSpecies = species;
|
|
if (calls == 1) throw const ApiNetworkException('断网');
|
|
return [VaccineCatalogItem.fromJson(sampleVaccineCatalogJson())];
|
|
};
|
|
|
|
await pumpForm(tester);
|
|
|
|
expect(find.text('疫苗目录加载失败,请重试后选择'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('重试'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byType(DropdownButtonFormField<String>), findsOneWidget);
|
|
expect(capturedSpecies, PetSpecies.dog);
|
|
});
|
|
|
|
testWidgets('空表单提交:疫苗与计划日期双拦截 + failed(validation_error)', (tester) async {
|
|
await pumpForm(tester);
|
|
|
|
await tester.tap(find.text('保存登记'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('请选择疫苗'), findsOneWidget);
|
|
expect(find.text('请选择计划接种日期'), findsOneWidget);
|
|
|
|
final failed = eventsOf('health_record_create_failed');
|
|
expect(failed.single!['recordType'], 'vaccine');
|
|
expect(failed.single!['failureReason'], 'validation_error');
|
|
});
|
|
|
|
testWidgets('选疫苗自动预填系列(目录 code),首次输入触发 started 一次', (tester) async {
|
|
await pumpForm(tester);
|
|
|
|
expect(eventsOf('health_record_create_started'), isEmpty);
|
|
|
|
await selectVaccine(tester, '狂犬疫苗');
|
|
|
|
expect(
|
|
tester
|
|
.widget<TextFormField>(find.widgetWithText(TextFormField, '系列名称'))
|
|
.controller!
|
|
.text,
|
|
'rabies',
|
|
);
|
|
|
|
// 再动一个字段:started 不重复。
|
|
await tester.enterText(find.widgetWithText(TextFormField, '剂次'), '2');
|
|
await tester.pumpAndSettle();
|
|
|
|
final started = eventsOf('health_record_create_started');
|
|
expect(started, hasLength(1));
|
|
expect(started.single, {
|
|
'recordType': 'vaccine',
|
|
'entryPoint': 'record_list',
|
|
});
|
|
});
|
|
|
|
testWidgets('状态机拦截 · 未填接种日期就标完成:前端拦截不发请求', (tester) async {
|
|
var createCalls = 0;
|
|
repository.createVaccinationHandler = (petId, request) async {
|
|
createCalls++;
|
|
return buildVaccination('vx-x');
|
|
};
|
|
await pumpForm(tester);
|
|
await selectVaccine(tester, '狂犬疫苗');
|
|
|
|
await tester.tap(find.text('已完成'));
|
|
await tester.pumpAndSettle();
|
|
// completed 态不渲染计划日期字段(结构上杜绝 scheduled 带 administeredOn 的反向路径)。
|
|
expect(find.text('计划接种日期'), findsNothing);
|
|
expect(find.text('接种日期'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('保存登记'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('请选择接种日期'), findsOneWidget);
|
|
expect(createCalls, 0);
|
|
});
|
|
|
|
testWidgets('scheduled 登记成功:请求形状精确对齐契约 + succeeded', (tester) async {
|
|
CreateVaccinationRequest? captured;
|
|
repository.createVaccinationHandler = (petId, request) async {
|
|
expect(petId, 'p-1');
|
|
captured = request;
|
|
return buildVaccination('vx-new');
|
|
};
|
|
|
|
await pumpForm(tester);
|
|
await selectVaccine(tester, '狂犬疫苗');
|
|
await tester.enterText(
|
|
find.widgetWithText(TextFormField, '剂次标签(可选)'),
|
|
'第一针',
|
|
);
|
|
await pickDate(tester, '计划接种日期');
|
|
|
|
await tester.tap(find.text('保存登记'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final json = captured!.toJson();
|
|
expect(json['vaccineId'], 'v-1');
|
|
expect(json['seriesKey'], 'rabies');
|
|
expect(json['doseNo'], 1);
|
|
expect(json['status'], 'scheduled');
|
|
expect(json['doseLabel'], '第一针');
|
|
expect(json.containsKey('plannedOn'), isTrue);
|
|
// scheduled 不得携带 administeredOn(契约 42201 规则,结构化保证)。
|
|
expect(json.containsKey('administeredOn'), isFalse);
|
|
expect(json.containsKey('nextDueOn'), isFalse);
|
|
|
|
expect(find.text('列表基底'), findsOneWidget);
|
|
final succeeded = eventsOf('health_record_create_succeeded');
|
|
expect(succeeded.single!['recordType'], 'vaccine');
|
|
expect(succeeded.single!['photoCount'], 0);
|
|
});
|
|
|
|
testWidgets('completed 登记成功:administeredOn 必填已给、无计划日期字段', (tester) async {
|
|
CreateVaccinationRequest? captured;
|
|
repository.createVaccinationHandler = (petId, request) async {
|
|
captured = request;
|
|
return buildVaccination('vx-new');
|
|
};
|
|
|
|
await pumpForm(tester);
|
|
await selectVaccine(tester, '狂犬疫苗');
|
|
await tester.tap(find.text('已完成'));
|
|
await tester.pumpAndSettle();
|
|
await pickDate(tester, '接种日期');
|
|
|
|
await tester.tap(find.text('保存登记'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final json = captured!.toJson();
|
|
expect(json['status'], 'completed');
|
|
expect(json.containsKey('administeredOn'), isTrue);
|
|
expect(json.containsKey('plannedOn'), isFalse);
|
|
});
|
|
|
|
testWidgets('40904 同系列同剂次冲突:兜底横幅 + failed 带码', (tester) async {
|
|
repository.createVaccinationHandler = (petId, request) async =>
|
|
throw const VaccinationDoseExistsException(message: '已存在');
|
|
|
|
await pumpForm(tester);
|
|
await selectVaccine(tester, '狂犬疫苗');
|
|
await pickDate(tester, '计划接种日期');
|
|
await tester.tap(find.text('保存登记'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('该系列该剂次已有记录(40904);如登记有误,可取消原记录后重新登记'), findsOneWidget);
|
|
expect(find.byType(VaccinationFormPage), findsOneWidget);
|
|
final failed = eventsOf('health_record_create_failed');
|
|
expect(failed.single!['errorCode'], 40904);
|
|
expect(failed.single!['httpStatus'], 409);
|
|
});
|
|
|
|
testWidgets('42201 状态-日期规则后端兜底:横幅提示 + failed 带码', (tester) async {
|
|
repository.createVaccinationHandler = (petId, request) async =>
|
|
throw const VaccinationRuleException(
|
|
message: 'scheduled 必须提供 plannedOn',
|
|
);
|
|
|
|
await pumpForm(tester);
|
|
await selectVaccine(tester, '狂犬疫苗');
|
|
await pickDate(tester, '计划接种日期');
|
|
await tester.tap(find.text('保存登记'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('接种状态与日期不符合规则,请核对后重试'), findsOneWidget);
|
|
final failed = eventsOf('health_record_create_failed');
|
|
expect(failed.single!['errorCode'], 42201);
|
|
expect(failed.single!['httpStatus'], 422);
|
|
expect(failed.single!['failureReason'], 'validation_error');
|
|
});
|
|
|
|
testWidgets('剂次非法(0)拦截', (tester) async {
|
|
await pumpForm(tester);
|
|
await selectVaccine(tester, '狂犬疫苗');
|
|
await tester.enterText(find.widgetWithText(TextFormField, '剂次'), '0');
|
|
await pickDate(tester, '计划接种日期');
|
|
|
|
await tester.tap(find.text('保存登记'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('剂次需为 1~32767 的整数'), findsOneWidget);
|
|
});
|
|
}
|