新增:首页 Feed 接入真实数据——四态/尾部三态/聚合曝光埋点,社区 demo 消亡第一页(T3-14)
CI / flutter-gates (push) Successful in 2m41s
CI / flutter-gates (push) Successful in 2m41s
- home_page Feed segment 改接 CommunityController:骨架屏首载、空态 CTA、 错误横幅重试、下拉刷新(失败保留旧列表 + SnackBar)、触底游标翻页 (失败态只走显式重试,不随滚动风暴自动重打) - PostCard 三形态(单图通栏 4:3 / 多图折叠封面 +N 角标 / 纯文字 6 行)+ PostMediaGrid(列数规则 + ink 80% scrim 精算)+ LikeButton(error 族修订 Colors.red,T3-14 纯展示禁用态,ToggleSync 接线留 T3-15/16)+ FeedSkeleton (呼吸动效尊重减弱动态设置);降级作者统一「宠友」占位 - SignedNetworkImage:预签名 URL 缓存 key 剥离 X-Amz-* 签名参数, RemoteImage 全仓换用(同图不同签名命中同一缓存条目) - feed 域埋点:feed_viewed 聚合曝光(浏览段开/结算于切 Tab、切分段、 退后台、销毁;≥50% 可见 ≥500ms 段内按帖去重,postId 不上报)+ feed_load_failed(refresh/load_more 双路,会话失效不报) - integration_test/feed_live_test.dart:compose 六容器 Linux 桌面真链路 实测入口(PATBOND_FEED_LIVE=1 门控,默认跳过) - 测试 379 → 421(+42):四态/尾部三态/翻页不丢不重/降级作者/曝光结算 时机/缓存 key/事件属性全覆盖;analyze 0,format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.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/widgets/post_card.dart';
|
||||
import 'package:patbond_flutter/features/auth/session_manager.dart';
|
||||
|
||||
/// 内存 token 存储(桌面实测环境无 keyring;不落任何持久化)。
|
||||
class _InMemoryTokenStore implements TokenStore {
|
||||
final Map<String, String> _values = {};
|
||||
|
||||
@override
|
||||
Future<String?> read(String key) async => _values[key];
|
||||
|
||||
@override
|
||||
Future<void> write(String key, String value) async => _values[key] = value;
|
||||
|
||||
@override
|
||||
Future<void> delete(String key) async => _values.remove(key);
|
||||
}
|
||||
|
||||
/// T3-14 compose 真链路桌面实测(默认跳过,不计入常规测试套件):
|
||||
///
|
||||
/// ```bash
|
||||
/// # 先起后端六容器(patbond-api 仓库根)并用 scratch 种子脚本发帖,再:
|
||||
/// PATBOND_FEED_LIVE=1 flutter test integration_test/feed_live_test.dart -d linux
|
||||
/// ```
|
||||
///
|
||||
/// 驱动**真实 App**(Linux 桌面渲染管线 + 真实 HTTP + MinIO 预签名图片):
|
||||
/// 注册账号 → UI 登录 → Feed 首屏真数据 → 滚动触底游标翻页到底 →
|
||||
/// 下拉刷新。会话存储注入内存实现(桌面环境无 keyring,不动生产配置)。
|
||||
void main() {
|
||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
|
||||
|
||||
final enabled = Platform.environment['PATBOND_FEED_LIVE'] == '1';
|
||||
const authBase = 'http://127.0.0.1:8081';
|
||||
|
||||
Future<void> pumpUntil(
|
||||
WidgetTester tester,
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 20),
|
||||
}) 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');
|
||||
}
|
||||
|
||||
testWidgets('Feed 桌面真链路:登录 → 首屏 → 游标翻页到底 → 下拉刷新', (tester) async {
|
||||
// ---- 注册一次性账号(随机凭据,不落持久化)----
|
||||
final seed = DateTime.now().millisecondsSinceEpoch;
|
||||
final username = 'feedlive$seed';
|
||||
final password = 'Live1234!$seed';
|
||||
final client = HttpClient();
|
||||
final request = await client.postUrl(
|
||||
Uri.parse('$authBase/api/v1/auth/register'),
|
||||
);
|
||||
request.headers.contentType = ContentType.json;
|
||||
request.add(
|
||||
utf8.encode(
|
||||
jsonEncode({
|
||||
'username': username,
|
||||
'phone': '+86137${(seed % 100000000).toString().padLeft(8, '0')}',
|
||||
'password': password,
|
||||
}),
|
||||
),
|
||||
);
|
||||
final response = await request.close();
|
||||
expect(response.statusCode, 200, reason: '注册测试账号失败');
|
||||
client.close();
|
||||
|
||||
// ---- 启动真实 App(仅注入内存会话存储,其余全为生产实现)----
|
||||
await tester.pumpWidget(
|
||||
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('登录'));
|
||||
|
||||
// ---- Feed 首屏:真实 getFeed + 卡片渲染 ----
|
||||
await pumpUntil(tester, find.byType(PostCard));
|
||||
expect(find.textContaining('compose 实测'), findsWidgets);
|
||||
|
||||
// ---- 触底翻页到「没有更多了」(26 帖 / 服务端页长 20 → 两页取齐)----
|
||||
final list = find.byType(ListView).first;
|
||||
for (var i = 0; i < 12; i++) {
|
||||
await tester.fling(list, const Offset(0, -700), 1500);
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
if (find.text('没有更多了').evaluate().isNotEmpty) break;
|
||||
}
|
||||
await pumpUntil(tester, find.text('没有更多了'));
|
||||
// 最早一帖(#1)在第二页尾部——游标翻页取齐的直接证据。
|
||||
expect(find.textContaining('#1:'), findsOneWidget);
|
||||
|
||||
// ---- 回顶下拉刷新:整体替换后首屏仍在 ----
|
||||
for (var i = 0; i < 12; i++) {
|
||||
await tester.fling(list, const Offset(0, 700), 1500);
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
}
|
||||
await tester.fling(list, const Offset(0, 400), 1000);
|
||||
await pumpUntil(tester, find.byType(PostCard));
|
||||
expect(find.textContaining('compose 实测'), findsWidgets);
|
||||
}, skip: !enabled);
|
||||
}
|
||||
Reference in New Issue
Block a user