Files
patbond-flutter/test/features/pets/vaccination_form_page_test.dart
T
lixi 6038901099 修复:Material 内置组件中文化——挂 zh-CN 三件套 delegate(M3.5-01)
用户 Linux 桌面实测反馈「日期选择器全英文」。根因是 MaterialApp 从一迭代
建起就没配 localizationsDelegates/supportedLocales/locale,pubspec 也没有
flutter_localizations;Flutter 在缺 delegate 时静默回退内置的
DefaultMaterialLocalizations(只有英文),于是业务自绘文案全中文、Material
内置组件全英文,同一弹窗里中英混排:

  Select date → 选择日期
  OK / Cancel → 确定 / 取消
  Enter Date → 输入日期
  Switch to input → 切换到输入模式
  Invalid format. / Out of range. → 格式无效。/ 超出范围。
  mm/dd/yyyy → yyyy/mm/dd(zh 顺序年在前)
  September 2026 → 2026年9月

处置:

- pubspec 加 flutter_localizations(sdk)与 intl(日期符号底座,显式直接
  依赖以锁定与 SDK 一致的版本)。
- 新增 lib/app/app_localization.dart 把 Global{Material,Cupertino,Widgets}
  三件套 delegate、supportedLocales、locale 收在一处常量:widget 测试若只
  pumpWidget(MaterialApp(home: ...)) 而不挂 delegate,测到的「中文」是假的,
  测试直接引用同一份常量,生产与测试不会各配一套而漂移。
- 单语言 zh-CN,不列 en:避免设备语言为英文时回退英文,再造混排。
- pets 域三份 widget 测试的 MaterialApp 同步挂上 delegate,
  tap(find.text('OK')) 相应改 '确定'——让测试与生产一致,而不是在英文兜底下测。

新增 test/app/app_localization_test.dart 守住这一行不被回删:断言根
MaterialApp 实际挂上三件套 + locale zh-CN,并从运行期 MaterialLocalizations
取回中文文案。

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

268 lines
9.5 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/app/app_localization.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(),
// M3.5-01:与生产一致挂 zh-CN delegate,日期选择器等内置组件文案
// 才是用户真机看到的中文。
localizationsDelegates: appLocalizationsDelegates,
supportedLocales: appSupportedLocales,
locale: appLocale,
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('确定'));
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);
});
}