新增:首页问候语真实化 + 刻意保留的 demo 占位显式登记(T3.5-10,ADR-022)
CI / flutter-gates (push) Failing after 1s

- 「下午好,豆豆」改真实展示名(`nickname ?? username`,与资料页共用同一个
  ProfileController:一次 /me 供两个消费点,改昵称后两处一起变)。资料未到手
  时退化为不称名的「下午好 👋」,不编造假名。
- 按 ADR-022 决策 D3.5-1 钉死范围,**其余首页 demo 一律不动**,并在代码里逐项
  标注为「刻意保留的 demo 占位」:天气/位置(需接外部服务)、圈子=话题
  (ADR-018 剪出)、促销卡(M5 服务域)、搜索与本地服务段。
  widget 测试同时钉住「保留项仍在」,避免后续以「顺手清理」越出拍板范围。
- 新增 compose 真链路桌面实测脚本 `integration_test/profile_avatar_live_test.dart`
  (默认跳过,`PATBOND_PROFILE_LIVE=1 ... -d linux` 开启):UI 注册 → 登录 →
  资料真实化 → 设昵称 → 传用户头像 → Feed 作者名同步 → 宠物头像,逐步截图。
  只有选图与压缩两层是桌面替身,createUpload / 预签名 PUT / confirm / PATCH
  全为生产实现。两处实测坑写进注释:Feed 作者名有服务端 60s 缓存滞后;
  1×1 的极小图能上传能下载但 Flutter 解码器拒绝,会被误判成「没传上」。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 15:37:48 +08:00
parent eff3526840
commit 694543647f
3 changed files with 667 additions and 6 deletions
+144
View File
@@ -0,0 +1,144 @@
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/auth/auth_models.dart';
import 'package:patbond_flutter/features/community/community_controller.dart';
import 'package:patbond_flutter/features/home/home_page.dart';
import 'package:patbond_flutter/features/profile/profile_controller.dart';
import 'package:patbond_flutter/state/app_state.dart';
import '../../helpers/auth_test_helpers.dart';
import '../../helpers/community_test_helpers.dart';
/// T3.5-10 首页问候语真实化。
///
/// ADR-022 决策 D3.5-1:本迭代**只做问候语**,天气 / 位置 / 圈子 / 促销卡
/// 刻意保留为 demo 占位——本文件同时钉住「保留项仍在」,避免后续有人以
/// 「顺手清理」为名越出拍板范围(真要动它们得先改 ADR)。
void main() {
late FakeCommunityRepository community;
late CommunityController feed;
setUp(() {
community = FakeCommunityRepository();
community.onFeed = (_, _) async => feedPage(const []);
feed = CommunityController(repository: community);
});
Future<ProfileController> pumpHome(
WidgetTester tester, {
required FakeAuthRepository auth,
}) async {
tester.view.physicalSize = const Size(700, 1600);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
final profile = ProfileController(
authRepository: auth,
communityRepository: community,
);
addTearDown(profile.dispose);
await tester.pumpWidget(
MaterialApp(
theme: buildAppTheme(),
home: Scaffold(
body: HomePage(
appState: AppState(),
communityController: feed,
profileController: profile,
onOpenServices: (_) {},
onOpenCompose: () {},
),
),
),
);
return profile;
}
/// 当前时段问候前缀(与页面实现同一套阈值;避免测试跟着挂钟漂移)。
String greetingPrefix([DateTime? now]) {
final hour = (now ?? DateTime.now()).hour;
if (hour < 6) return '夜深了';
if (hour < 11) return '早上好';
if (hour < 14) return '中午好';
if (hour < 18) return '下午好';
return '晚上好';
}
testWidgets('有昵称:问候语用昵称,demo 宠物名「豆豆」不再出现', (tester) async {
final profile = await pumpHome(
tester,
auth: FakeAuthRepository(
meHandler: () async => buildProfile(username: 'llx', nickname: '小柴'),
),
);
await profile.refresh();
await tester.pumpAndSettle();
expect(find.text('${greetingPrefix()},小柴 👋'), findsOneWidget);
expect(find.textContaining('豆豆'), findsNothing);
});
testWidgets('无昵称:问候语回退 username(与资料页同规则)', (tester) async {
final profile = await pumpHome(
tester,
auth: FakeAuthRepository(
meHandler: () async => buildProfile(username: 'llx'),
),
);
await profile.refresh();
await tester.pumpAndSettle();
expect(find.text('${greetingPrefix()}llx 👋'), findsOneWidget);
});
testWidgets('资料未到手:只问候不称名(不编造假名)', (tester) async {
await pumpHome(
tester,
auth: FakeAuthRepository(
meHandler: () async => throw const ApiNetworkException('断网'),
),
);
await tester.pumpAndSettle();
expect(find.text('${greetingPrefix()} 👋'), findsOneWidget);
// 不出现「问候,某某」形态(更不会退回 demo 宠物名)。
expect(find.textContaining('${greetingPrefix()}'), findsNothing);
});
testWidgets('改昵称后首页同步(与资料页共用同一控制器,无需手动刷新)', (tester) async {
final auth = FakeAuthRepository(
meHandler: () async => buildProfile(username: 'llx'),
updateMeHandler: (_) async =>
buildProfile(username: 'llx', nickname: '小柴'),
);
final profile = await pumpHome(tester, auth: auth);
await profile.refresh();
await tester.pumpAndSettle();
expect(find.text('${greetingPrefix()}llx 👋'), findsOneWidget);
await profile.save(
const UpdateMeRequest(nickname: PatchField<String>.value('小柴')),
);
await tester.pumpAndSettle();
expect(find.text('${greetingPrefix()},小柴 👋'), findsOneWidget);
});
testWidgets('刻意保留的 demo 占位仍在(ADR-022 钉死的范围)', (tester) async {
final profile = await pumpHome(
tester,
auth: FakeAuthRepository(meHandler: () async => buildProfile()),
);
await profile.refresh();
await tester.pumpAndSettle();
// 天气 / 位置(外部服务未接)。
expect(find.text('北京 · 朝阳区'), findsOneWidget);
expect(find.text('28°C 晴'), findsOneWidget);
// 圈子 = 话题(ADR-018 剪出)。
expect(find.text('柴犬圈'), findsOneWidget);
// 促销卡(M5 服务域)。
expect(find.text('新用户首单立减 ¥20'), findsOneWidget);
});
}