From 7d5c84d06dd052089c120709ddccf90c2b1d8638 Mon Sep 17 00:00:00 2001 From: Lixi20 Date: Thu, 10 Sep 2026 18:00:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E8=8A=B1=E8=B4=B9?= =?UTF-8?q?=E5=8D=A1=E5=B1=95=E7=A4=BA=E5=AE=9E=E9=99=85=E6=9C=88=E4=BB=BD?= =?UTF-8?q?=20+=20=E5=9B=9B=E5=BC=A0=E6=95=B0=E6=8D=AE=E5=8D=A1=E8=A1=A5?= =?UTF-8?q?=E5=8F=AF=E7=82=B9=E6=8F=90=E7=A4=BA=EF=BC=88M3.5-03=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 卡片标签硬编码「本月花费」,而服务端 summary.monthlyExpense 本就返回 month (ISO year-month,按 tz 归月)。用户看不到实际月份,所以无法自证「我这条记录 到底落在哪个月」,把正确的 ¥0 当成统计故障。 已核实不改后端聚合:用户记录落在 2026-04-09、当天是 2026-09-10, 「本月花费 ¥0」是正确行为。本单只让客户端把口径亮出来。 - health_record_display 新增纯函数 monthlyExpenseCardLabel(month, {now}): 同年「9 月花费」(窄卡一行放得下,故不取「2026-09 花费」);跨年(服务端 归月年份 ≠ 设备当前年份)「2026/12 花费」补年份消歧;串非法退回 「本月花费」不崩不显示脏值。 - 可点提示:_SummaryCard 在 onTap 非空时右上角补 chevron_right。四张卡本都 可点进明细页却无任何视觉提示(用户反馈不知道能点),提示形态沿用项目既有 可点行/卡(宠物列表卡、健康提醒卡、资料页设置行)的 chevron_right,不自创。 - 整卡包 MergeSemantics:读屏一次读全「¥128.50,9 月花费,按钮」而非两段 孤立文字。没有用 excludeSemantics——那会连带丢掉 InkWell 的可激活性。 既有测试口径调整:pet_detail_page_test 两处断言改为实际月份,且样本 monthlyExpense.month 改用当月串使断言不随年份漂移(跨年格式由纯函数单测覆盖)。 新增 integration_test/client_ux_live_test.dart(环境变量门控,默认 skip, 沿用 M3 既有 live 测试形态):compose 六容器 + 真实 App 走完三项修复—— 日期选择器全中文/品牌配色/手输录入/一键今天,以及花费卡实际月份 + chevron。 本机是 Wayland 会话、X11 import -window root 取不到根窗口,改为把整棵 App 包 一层 RepaintBoundary 后 toImage() 直出真实渲染像素落 build/ux-live/。 Co-Authored-By: Claude Fable 5 --- integration_test/client_ux_live_test.dart | 243 ++++++++++++++++++ lib/features/pets/health_record_display.dart | 21 ++ lib/features/pets/pet_detail_page.dart | 83 +++--- .../pets/health_record_display_test.dart | 26 ++ test/features/pets/pet_detail_page_test.dart | 67 ++++- 5 files changed, 405 insertions(+), 35 deletions(-) create mode 100644 integration_test/client_ux_live_test.dart diff --git a/integration_test/client_ux_live_test.dart b/integration_test/client_ux_live_test.dart new file mode 100644 index 0000000..9e1fbcf --- /dev/null +++ b/integration_test/client_ux_live_test.dart @@ -0,0 +1,243 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui' show ImageByteFormat; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:patbond_flutter/app/app.dart'; +import 'package:patbond_flutter/core/theme/app_theme.dart'; +import 'package:patbond_flutter/features/auth/session_manager.dart'; +import 'package:patbond_flutter/features/pets/pet_detail_page.dart'; +import 'package:patbond_flutter/features/pets/pet_form_page.dart'; + +/// 内存 token 存储(桌面实测环境无 keyring;不落任何持久化)。 +class _InMemoryTokenStore implements TokenStore { + final Map _values = {}; + + @override + Future read(String key) async => _values[key]; + + @override + Future write(String key, String value) async => _values[key] = value; + + @override + Future delete(String key) async => _values.remove(key); +} + +/// M3.5-01/02/03 compose 真链路桌面实测(默认跳过,不计入常规测试套件): +/// +/// ```bash +/// # 先起后端六容器(patbond-api 仓库根 docker compose up -d --build),再: +/// PATBOND_UX_LIVE=1 flutter test integration_test/client_ux_live_test.dart -d linux +/// ``` +/// +/// 驱动**真实 App**(Linux 桌面 GTK 渲染管线 + 真实 HTTP)走用户实测那条路: +/// 注册 → UI 登录 → 档案页建档 → 打开生日选择器(校验全中文 + 品牌配色 + +/// 手输可用 + 一键今天)→ 保存进详情页(校验花费卡展示实际月份 + chevron)。 +/// 逐步截图落到 `build/ux-live/`:本机是 Wayland 会话,X11 的 +/// `import -window root` 取不到根窗口,改为把整棵 App 包一层 +/// [RepaintBoundary] 后 `toImage()` 直出真实渲染像素(含 Overlay 里的弹窗)。 +void main() { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + + final enabled = Platform.environment['PATBOND_UX_LIVE'] == '1'; + const authBase = 'http://127.0.0.1:8081'; + const petBase = 'http://127.0.0.1:8083'; + final shotDir = Directory('build/ux-live'); + const rootKey = ValueKey('ux-live-root'); + + Future shot(WidgetTester tester, String name) async { + final boundary = tester.renderObject( + find.byKey(rootKey), + ); + await tester.runAsync(() async { + if (!shotDir.existsSync()) shotDir.createSync(recursive: true); + final image = await boundary.toImage(pixelRatio: 1.5); + final bytes = await image.toByteData(format: ImageByteFormat.png); + final path = '${shotDir.path}/$name.png'; + File(path).writeAsBytesSync(bytes!.buffer.asUint8List()); + debugPrint('[shot] $name → $path (${bytes.lengthInBytes} bytes)'); + }); + } + + Future pumpUntil( + WidgetTester tester, + Finder finder, { + Duration timeout = const Duration(seconds: 25), + }) async { + final deadline = DateTime.now().add(timeout); + while (DateTime.now().isBefore(deadline)) { + await tester.pump(const Duration(milliseconds: 250)); + if (finder.evaluate().isNotEmpty) return; + } + fail('等待超时:$finder'); + } + + Future pumpUntilGone( + WidgetTester tester, + Finder finder, { + Duration timeout = const Duration(seconds: 25), + }) async { + final deadline = DateTime.now().add(timeout); + while (DateTime.now().isBefore(deadline)) { + await tester.pump(const Duration(milliseconds: 250)); + if (finder.evaluate().isEmpty) return; + } + fail('等待消失超时:$finder'); + } + + /// 直连服务端下单个 JSON 请求(种子数据走真实 HTTP,不绕 DB)。 + Future> postJson( + String url, + Map body, { + String? bearer, + }) async { + final client = HttpClient(); + final request = await client.postUrl(Uri.parse(url)); + request.headers.contentType = ContentType.json; + if (bearer != null) { + request.headers.set(HttpHeaders.authorizationHeader, 'Bearer $bearer'); + } + request.add(utf8.encode(jsonEncode(body))); + final response = await request.close(); + final text = await response.transform(utf8.decoder).join(); + client.close(); + expect(response.statusCode, anyOf(200, 201), reason: 'POST $url 失败:$text'); + final envelope = jsonDecode(text) as Map; + expect(envelope['code'], 0, reason: 'POST $url 业务码非 0:$text'); + return envelope['data'] as Map; + } + + testWidgets('桌面真链路:日期选择器中文/品牌色/手输/今天 + 花费卡实际月份', (tester) async { + // ---- 注册一次性账号 + 种子数据(宠物 + 当月一笔就医支出)---- + final seed = DateTime.now().millisecondsSinceEpoch; + final username = 'uxlive$seed'; + final password = 'Live1234!$seed'; + await postJson('$authBase/api/v1/auth/register', { + 'username': username, + 'phone': '+86137${(seed % 100000000).toString().padLeft(8, '0')}', + 'password': password, + }); + final tokens = await postJson('$authBase/api/v1/auth/login', { + 'username': username, + 'password': password, + }); + final accessToken = tokens['accessToken'] as String; + final pet = await postJson('$petBase/api/v1/pets', { + 'name': '实测豆豆', + 'species': 'dog', + 'sex': 'male', + 'customBreedName': '中华田园犬', + }, bearer: accessToken); + final petId = pet['id'] as String; + // 当月一笔 128.50 元就医支出:花费卡应显示「N 月花费 ¥128.50」—— + // 用户那次误录(记到 4 月)看到 ¥0 正是因为记录不在当月窗口内。 + await postJson('$petBase/api/v1/pets/$petId/health-events', { + 'eventType': 'medical', + 'occurredAt': DateTime.now().toUtc().toIso8601String(), + 'title': '皮肤检查', + 'amountCents': 12850, + }, bearer: accessToken); + + // ---- 启动真实 App ---- + await tester.pumpWidget( + RepaintBoundary( + key: rootKey, + child: App( + sessionManager: SessionManager(store: _InMemoryTokenStore()), + ), + ), + ); + await pumpUntil(tester, find.text('登录')); + + await tester.enterText(find.byType(TextField).at(0), username); + await tester.enterText(find.byType(TextField).at(1), password); + await tester.tap(find.text('登录')); + + // ---- 档案 Tab → 建档表单 ---- + await pumpUntil(tester, find.text('档案')); + await tester.tap(find.text('档案')); + // 列表已有种子宠物 → 走虚线「+ 添加宠物」卡(空态 CTA 文案不同)。 + await pumpUntil(tester, find.textContaining('添加宠物')); + await tester.tap(find.textContaining('添加宠物').first); + await pumpUntil(tester, find.byType(PetFormPage)); + await shot(tester, '01-pet-form'); + + // ---- 生日日期选择器:全中文 + 品牌配色 ---- + await tester.tap(find.widgetWithText(ListTile, '生日(可选)')); + await pumpUntil(tester, find.byType(DatePickerDialog)); + await tester.pump(const Duration(milliseconds: 600)); + await shot(tester, '02-date-picker-zh'); + + expect(find.text('选择日期'), findsOneWidget, reason: '标题非中文'); + expect(find.text('确定'), findsOneWidget); + expect(find.text('取消'), findsOneWidget); + expect(find.text('Select date'), findsNothing); + expect(find.text('OK'), findsNothing); + + final inkColors = tester + .widgetList(find.byType(Ink)) + .map((ink) => ink.decoration) + .whereType() + .map((d) => d.color) + .toList(); + expect( + inkColors, + contains(AppColors.primaryStrong), + reason: '选中日仍是 fromSeed 派生的暗红棕,未取品牌 primaryStrong', + ); + + // ---- 手输模式:中文标签 + 敲入已知日期 ---- + await tester.tap(find.byIcon(Icons.edit_outlined)); + await pumpUntil(tester, find.text('输入日期')); + await shot(tester, '03-date-input-zh'); + final field = find.descendant( + of: find.byType(DatePickerDialog), + matching: find.byType(TextField), + ); + await tester.enterText(field, '2024/03/15'); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(find.text('确定')); + await pumpUntil(tester, find.text('2024-03-15')); + await shot(tester, '04-date-typed'); + + // ---- 「今天」快捷键:一键落今天,不开弹窗 ---- + final todayText = + '${DateTime.now().year}-' + '${DateTime.now().month.toString().padLeft(2, '0')}-' + '${DateTime.now().day.toString().padLeft(2, '0')}'; + await tester.tap(find.widgetWithText(TextButton, '今天')); + await pumpUntil(tester, find.text(todayText)); + expect(find.byType(DatePickerDialog), findsNothing, reason: '「今天」不该开弹窗'); + await shot(tester, '05-today-shortcut'); + + // ---- 退出建档表单(本单不验建档链路,宠物与支出已由 API 种下)---- + await tester.tap(find.byIcon(Icons.arrow_back)); + await pumpUntilGone(tester, find.byType(PetFormPage)); + + // ---- 详情页:花费卡展示实际月份 + chevron 可点提示 ---- + await pumpUntil(tester, find.text('实测豆豆')); + await shot(tester, '06-pets-list'); + await tester.tap(find.text('实测豆豆')); + await pumpUntil(tester, find.byType(PetDetailPage)); + final monthLabel = '${DateTime.now().month} 月花费'; + await pumpUntil(tester, find.text(monthLabel)); + await tester.pump(const Duration(milliseconds: 600)); + await shot(tester, '07-pet-detail-expense-card'); + + expect(find.text('本月花费'), findsNothing, reason: '仍是硬编码「本月花费」'); + // 当月真有支出 → 卡片给出金额(月份口径自证:记录落在当月才计入)。 + expect(find.text('¥128.50'), findsOneWidget); + expect( + find.ancestor( + of: find.text(monthLabel), + matching: find.widgetWithIcon(Card, Icons.chevron_right), + ), + findsOneWidget, + reason: '花费卡缺 chevron 可点提示', + ); + }, skip: !enabled); +} diff --git a/lib/features/pets/health_record_display.dart b/lib/features/pets/health_record_display.dart index d656cf5..c00b9a9 100644 --- a/lib/features/pets/health_record_display.dart +++ b/lib/features/pets/health_record_display.dart @@ -125,6 +125,27 @@ String healthEventMonthHeader(DateTime occurredAt) { return '${local.year} 年 ${local.month} 月'; } +/// 月度花费卡标签(M3.5-03)。 +/// +/// 根因备忘:卡片标签此前硬编码「本月花费」,用户无法自证「本月」到底是 +/// 哪个月——他把当月记录误录到 2026-04 后看到「本月花费 ¥0」,以为聚合坏了。 +/// 服务端 `summary.monthlyExpense.month` 本就返回 ISO year-month(如 `2026-09`, +/// 按 `tz` 归月),直接展示即可自查,无需改后端。 +/// +/// [month] 形如 `2026-09`。窄卡(一行四卡)只放得下 4~5 个字,故取「9 月花费」 +/// 而非「2026-09 花费」;跨年(服务端归月的年份与设备当前年份不一致,如设备 +/// 已跨到 1 月而窗口仍是去年 12 月)时补年份消歧。解析失败退回「本月花费」。 +String monthlyExpenseCardLabel(String month, {DateTime? now}) { + final match = RegExp(r'^(\d{4})-(\d{2})$').firstMatch(month); + if (match == null) return '本月花费'; + final year = int.parse(match.group(1)!); + final monthNo = int.parse(match.group(2)!); + if (monthNo < 1 || monthNo > 12) return '本月花费'; + final currentYear = (now ?? DateTime.now()).year; + if (year != currentYear) return '$year/$monthNo 花费'; + return '$monthNo 月花费'; +} + /// 设备时区 → summary `tz` 参数(契约接受固定偏移形如 `+08:00`; /// Flutter 无 IANA 名可取,固定偏移语义等价——只作用于月度窗口)。 String tzOffsetQueryValue(Duration offset) { diff --git a/lib/features/pets/pet_detail_page.dart b/lib/features/pets/pet_detail_page.dart index 6849886..4be2788 100644 --- a/lib/features/pets/pet_detail_page.dart +++ b/lib/features/pets/pet_detail_page.dart @@ -532,7 +532,9 @@ class _PetDetailPageState extends State { // monthlyExpense 恒非 null(契约);金额整数分 → 元展示。 value: '¥${formatCentsAsYuan(expense.amountCents)}', emphasized: expense.amountCents > 0, - label: '本月花费', + // M3.5-03:标签展示服务端归月的实际月份(此前硬编码 + // 「本月花费」,用户无法自证记录落在哪个月)。 + label: monthlyExpenseCardLabel(expense.month), onTap: () => _openTimeline(pet), ), ), @@ -545,6 +547,13 @@ class _PetDetailPageState extends State { /// 数据卡(正典 stat-card 形态):图标 + 数值 15/w800 + 标签 12 inkSoft; /// 空态数值降级 inkSoft 常规字重(区分「有数据」与「空态」两种视觉)。 +/// +/// M3.5-03:可点卡([onTap] 非空)右上角补 `chevron_right`——四张卡本都可点进 +/// 明细页,但此前无任何视觉提示,用户实测反馈不知道能点。提示形态沿用项目 +/// 既有可点行/卡(宠物列表卡、健康提醒卡、资料页设置行)的 `chevron_right`, +/// 不自创。同时 [MergeSemantics] 把「数值 + 标签」并进 InkWell 的 button 节点, +/// 读屏一次读全「¥0,9 月花费,按钮」,而不是两段孤立文字(tap 动作仍在 +/// InkWell 上,不用 `excludeSemantics` 以免连带丢掉可激活性)。 class _SummaryCard extends StatelessWidget { const _SummaryCard({ required this.icon, @@ -564,35 +573,51 @@ class _SummaryCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(AppRadius.xl), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(icon, size: 18, color: iconColor), - const SizedBox(height: 8), - Text( - value, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: emphasized ? AppColors.ink : AppColors.inkSoft, - fontSize: emphasized ? 15 : 13, - fontWeight: emphasized ? FontWeight.w800 : FontWeight.w600, + return MergeSemantics( + child: Card( + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(AppRadius.xl), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 18, color: iconColor), + const Spacer(), + if (onTap != null) + const Icon( + Icons.chevron_right, + size: 16, + color: AppColors.muted, + ), + ], ), - ), - const SizedBox(height: 4), - Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(color: AppColors.inkSoft, fontSize: 11), - ), - ], + const SizedBox(height: 8), + Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: emphasized ? AppColors.ink : AppColors.inkSoft, + fontSize: emphasized ? 15 : 13, + fontWeight: emphasized ? FontWeight.w800 : FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.inkSoft, + fontSize: 11, + ), + ), + ], + ), ), ), ), diff --git a/test/features/pets/health_record_display_test.dart b/test/features/pets/health_record_display_test.dart index 167b94e..604e4da 100644 --- a/test/features/pets/health_record_display_test.dart +++ b/test/features/pets/health_record_display_test.dart @@ -277,4 +277,30 @@ void main() { expect(reminderStatusColor(dismissed, now), AppColors.muted); }); }); + + group('M3.5-03 · monthlyExpenseCardLabel(花费卡展示实际月份)', () { + test('同年只给月份,卡片一行放得下(不再硬编码「本月花费」)', () { + final now = DateTime(2026, 9, 10); + expect(monthlyExpenseCardLabel('2026-09', now: now), '9 月花费'); + // 用户误录场景:记录落在 4 月,卡片就该明说是 4 月,而不是「本月」。 + expect(monthlyExpenseCardLabel('2026-04', now: now), '4 月花费'); + expect(monthlyExpenseCardLabel('2026-12', now: now), '12 月花费'); + expect(monthlyExpenseCardLabel('2026-01', now: now), '1 月花费'); + }); + + test('跨年(服务端归月年份 ≠ 设备当前年份)补年份消歧', () { + // 设备已跨到 2027-01,服务端 tz 窗口仍落在 2026-12。 + final now = DateTime(2027, 1, 1); + expect(monthlyExpenseCardLabel('2026-12', now: now), '2026/12 花费'); + }); + + test('月份串非法退回「本月花费」(不崩、不显示脏值)', () { + final now = DateTime(2026, 9, 10); + expect(monthlyExpenseCardLabel('', now: now), '本月花费'); + expect(monthlyExpenseCardLabel('2026-9', now: now), '本月花费'); + expect(monthlyExpenseCardLabel('2026-13', now: now), '本月花费'); + expect(monthlyExpenseCardLabel('2026-00', now: now), '本月花费'); + expect(monthlyExpenseCardLabel('2026-09-01', now: now), '本月花费'); + }); + }); } diff --git a/test/features/pets/pet_detail_page_test.dart b/test/features/pets/pet_detail_page_test.dart index 3458507..b1de2bf 100644 --- a/test/features/pets/pet_detail_page_test.dart +++ b/test/features/pets/pet_detail_page_test.dart @@ -188,12 +188,24 @@ void main() { }); group('T2-13 · 数据卡行接 summary', () { - testWidgets('四卡取数:最新体重 / 疫苗进度 / 下一针 / 本月花费(tz 透传)', (tester) async { + testWidgets('四卡取数:最新体重 / 疫苗进度 / 下一针 / 当月花费(tz 透传)', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1'); String? capturedTz; + // M3.5-03:花费卡标签展示服务端归月的实际月份,故样本月份取「本月」, + // 断言不随年份漂移(跨年格式另由 monthlyExpenseCardLabel 单测覆盖)。 + final now = DateTime.now(); + final thisMonth = '${now.year}-${now.month.toString().padLeft(2, '0')}'; repository.getPetSummaryHandler = (petId, tz) async { capturedTz = tz; - return buildSummary(); + return buildSummary( + overrides: { + 'monthlyExpense': { + 'month': thisMonth, + 'timezone': 'Asia/Shanghai', + 'amountCents': 12850, + }, + }, + ); }; await pumpDetail(tester); @@ -208,12 +220,45 @@ void main() { expect(find.text('下一针·狂犬疫苗'), findsOneWidget); // T2-14:月度花费卡接 monthlyExpense(12850 分 → 元展示)。 expect(find.text('¥128.50'), findsOneWidget); - expect(find.text('本月花费'), findsOneWidget); + // M3.5-03:不再硬编码「本月花费」,展示实际月份便于用户自查。 + expect(find.text('${now.month} 月花费'), findsOneWidget); + expect(find.text('本月花费'), findsNothing); // T2-13 遗留③:tz 透传设备时区固定偏移(月度窗口随设备时区)。 expect(capturedTz, tzOffsetQueryValue(DateTime.now().timeZoneOffset)); expect(capturedTz, matches(RegExp(r'^[+-]\d{2}:\d{2}$'))); }); + testWidgets('M3.5-03 · 四张数据卡均有 chevron 可点提示', (tester) async { + repository.getPetHandler = (petId) async => buildPet('p-1'); + final now = DateTime.now(); + final thisMonth = '${now.year}-${now.month.toString().padLeft(2, '0')}'; + repository.getPetSummaryHandler = (petId, tz) async => buildSummary( + overrides: { + 'monthlyExpense': { + 'month': thisMonth, + 'timezone': 'Asia/Shanghai', + 'amountCents': 0, + }, + }, + ); + + await pumpDetail(tester); + await tester.pumpAndSettle(); + + // 四张 stat 卡都挂了 onTap,此前无任何视觉提示;现统一补 + // chevron_right(与宠物列表卡 / 健康提醒卡同款)。 + for (final label in ['最新体重', '疫苗进度', '下一针·狂犬疫苗', '${now.month} 月花费']) { + expect( + find.ancestor( + of: find.text(label), + matching: find.widgetWithIcon(Card, Icons.chevron_right), + ), + findsOneWidget, + reason: '「$label」卡缺可点提示', + ); + } + }); + testWidgets('null 语义:无登记显示空态而非 0/0', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1'); repository.getPetSummaryHandler = (petId, tz) async => buildSummary( @@ -302,12 +347,22 @@ void main() { }); group('T2-14 · 时间线入口', () { - testWidgets('点本月花费卡 → 健康时间线页;返回后重拉摘要', (tester) async { + testWidgets('点当月花费卡 → 健康时间线页;返回后重拉摘要', (tester) async { repository.getPetHandler = (petId) async => buildPet('p-1'); var summaryCalls = 0; + final now = DateTime.now(); + final thisMonth = '${now.year}-${now.month.toString().padLeft(2, '0')}'; repository.getPetSummaryHandler = (petId, tz) async { summaryCalls++; - return buildSummary(); + return buildSummary( + overrides: { + 'monthlyExpense': { + 'month': thisMonth, + 'timezone': 'Asia/Shanghai', + 'amountCents': 12850, + }, + }, + ); }; repository.listHealthEventsHandler = (petId, limit, cursor) async => const CursorPage(items: [], nextCursor: null, hasMore: false); @@ -315,7 +370,7 @@ void main() { await pumpDetail(tester); await tester.pumpAndSettle(); - await tester.tap(find.text('本月花费')); + await tester.tap(find.text('${now.month} 月花费')); await tester.pumpAndSettle(); expect(find.byType(HealthEventsPage), findsOneWidget);