294fc4a781
用户实测已因日期选择器误录:Flutter 原生日历只给年份网格、月份必须靠 < > 逐月点,从 9 月回 4 月要点 5 次,他把当月(2026-09)的就医记录记成了 2026-04-09,进而误判「本月花费 ¥0」是聚合坏了。 新增 lib/core/widgets/app_date_picker.dart,全仓 7 处裸 showDatePicker 收口 (改造后 lib/ 下 showDatePicker 只出现在该文件内部一次): - pickAppDate(...):calendar 首屏 + 保留头部铅笔切手输;initialDate 自动夹进 [firstDate, lastDate] 防原生越界断言(调用方常传「当前值 ?? 今天」,而 「到期日期」的 firstDate 就是今天,历史值可能已越界);返回值统一抹时分秒; 中文文案一律交给 zh-CN 本地化,不硬编码,避免两处文案漂移。 - AppDateFieldTrailing(...):7 处日期行统一「今天 + 日历图标」。今天越界自动 隐藏按钮、提交中禁用、触控 44×44、primaryStrong 白底 4.49:1。 入口模式取舍:不用 calendarOnly——它恰好会砍掉手输按钮,把「录一个已知日期」 这条唯一快路堵死;也不用 input 首屏——「记今天」这类高频场景敲 8 个数字更慢。 两条路都留着最省事。 「今天」为何放表单行而非弹窗内:原生 showDatePicker 无法注入自定义动作 (builder 只能包裹整个 Dialog,拿不到内部选中态;塞进 Column 还会因 Dialog 在无界高度下贪心布局而溢出)。放表单行反而更快——一键落值连弹窗都不用开, 把容易走错的月份导航整段绕开,且一次实现 7 处形态完全一致。 各调用点原有的 firstDate/lastDate 业务约束原样传入、一字未改(健康事件 不许未来、到期日不许补记过去、疫苗 allowFuture 双态、生日不许未来), 并由 widget 测试直接断言 DatePickerDialog.firstDate/lastDate 防后续悄悄放宽。 顺带修配色:此前从未定制 datePickerTheme,选中日直接吃 ColorScheme.fromSeed 由珊瑚橙派生的暗红棕,与品牌脱节。新增 _datePickerTheme 只复用 05 号规范 (iteration-2/05、iteration-3/05)已审计的色对,不新造色值:头部 surfaceTint + primaryDark 7.98:1(选中 chip 同款)、选中日/年 primaryStrong 实底白字 4.49:1、今日 primaryStrong 1.5px 描边、星期表头 inkSoft 6.59:1、 越界日 muted(DEBT-2 允许的禁用态用途)。headerHeadlineStyle 取 22px (默认 32):中文「9月10日周四」在横屏侧栏头部 26px 起就折行。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
166 lines
5.6 KiB
Dart
166 lines
5.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:patbond_flutter/app/app_localization.dart';
|
|
import 'package:patbond_flutter/core/network/api_exception.dart';
|
|
import 'package:patbond_flutter/core/theme/app_theme.dart';
|
|
import 'package:patbond_flutter/core/widgets/app_date_picker.dart';
|
|
import 'package:patbond_flutter/features/pets/care_reminder_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, 1600);
|
|
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: Builder(
|
|
builder: (context) => Scaffold(
|
|
body: Center(
|
|
child: TextButton(
|
|
onPressed: () => Navigator.of(context).push(
|
|
MaterialPageRoute<CareReminder>(
|
|
builder: (_) => CareReminderFormPage(
|
|
repository: repository,
|
|
petId: 'p-1',
|
|
analytics: analytics,
|
|
),
|
|
),
|
|
),
|
|
child: const Text('打开表单'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text('打开表单'));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
Future<void> fillValid(WidgetTester tester) async {
|
|
await tester.tap(find.text('用药'));
|
|
await tester.pump();
|
|
await tester.enterText(
|
|
find.widgetWithText(TextFormField, '提醒内容(如:体内外驱虫)'),
|
|
'心丝虫预防药',
|
|
);
|
|
await tester.tap(find.text('到期日期'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('确定'));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
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': 'reminder',
|
|
'entryPoint': 'record_list',
|
|
});
|
|
// 四类类型齐备。
|
|
for (final label in ['驱虫', '体检', '用药', '其他']) {
|
|
expect(find.text(label), findsOneWidget);
|
|
}
|
|
});
|
|
|
|
testWidgets('40300 横幅 / 网络 SnackBar 兜底 + 失败事件', (tester) async {
|
|
var attempt = 0;
|
|
repository.createCareReminderHandler = (petId, request) async {
|
|
attempt++;
|
|
if (attempt == 1) {
|
|
throw const PetAccessDeniedException(message: '无权限');
|
|
}
|
|
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);
|
|
|
|
final failed = eventsOf('health_record_create_failed');
|
|
expect(failed.length, 2);
|
|
expect(failed[0], {
|
|
'recordType': 'reminder',
|
|
'failureReason': 'permission_denied',
|
|
'attemptSeq': 1,
|
|
'errorCode': 40300,
|
|
'httpStatus': 403,
|
|
});
|
|
expect(failed[1]!['failureReason'], 'network_error');
|
|
expect(failed[1]!['attemptSeq'], 2);
|
|
});
|
|
|
|
group('M3.5-02 · 到期日期录入(firstDate 就是今天的那一格)', () {
|
|
testWidgets('未选日期时也有「今天」快捷键;一键落今天并清掉必填校验错', (tester) async {
|
|
await pumpForm(tester);
|
|
|
|
// 先触发必填校验错。
|
|
await tester.tap(find.text('保存提醒'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('未选择'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('今天'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text(dateToJson(DateTime.now())), findsOneWidget);
|
|
expect(find.text('未选择'), findsNothing);
|
|
expect(find.text('选择日期'), findsNothing);
|
|
});
|
|
|
|
testWidgets('业务约束不变:firstDate 今天(不许补记过去)、lastDate 五年后', (tester) async {
|
|
await pumpForm(tester);
|
|
|
|
await tester.tap(find.widgetWithText(ListTile, '到期日期'));
|
|
await tester.pumpAndSettle();
|
|
|
|
final dialog = tester.widget<DatePickerDialog>(
|
|
find.byType(DatePickerDialog),
|
|
);
|
|
expect(dialog.firstDate, dateOnly(DateTime.now()));
|
|
expect(dialog.lastDate, DateTime(DateTime.now().year + 5));
|
|
expect(find.text('选择日期'), findsOneWidget);
|
|
});
|
|
});
|
|
}
|