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 _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); } /// 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 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); }