/// P3 Task 11 集成测试 · 主流程(MuMu + 真实 cn-api/MinIO/worker/sync/ov)。 /// /// 覆盖(对照计划 Task 11 第一条): /// 登录 → 建相册(zh+en,Material 日期+时间选择器)→ 加分类 → 邀成员 → /// 传 3 图 → 队列全 completed → 服务端历史可见(sourceDeviceId=本设备)→ /// viewer 腿(ov 公开相册 API 见 3 张,替代网页断言——登记偏差)→ /// 隐藏一张 → App 管理列表状态同步 → 断网(DIO 故障注入)waiting_network → /// 恢复重试 → 修改密码(会话保留)→ 退出重登录 → App 内语言切换冒烟。 /// /// 运行方式(勿直接 flutter test——账号/图片由脚本预置): /// `bash scripts/run-mobile-it.sh` library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:photo_live/core/api_client.dart'; import 'package:photo_live/features/uploads/task_state.dart'; import 'helpers/app_boot.dart'; import 'helpers/it_config.dart'; import 'helpers/ui_steps.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final harness = ItAppHarness.create(); // 单一大步骤串行全流程:会话/队列状态跨腿复用,顺序即语义。 testWidgets( 'P3 全流程:真实后端 登录→相册→邀请→上传→历史→ov→隐藏→断网→改密→重登录', (tester) async { final account = ItConfig.mainAccount; var images = const []; // ── 0. 启动与资源定位 ───────────────────────────────────────────── await tester.pumpWidget(harness.appWidget()); images = await resolveSmallImages(); debugPrint('[IT] 使用测试图片:$images'); // ── 1. 登录(本次运行专属账号)──────────────────────────────────── await loginViaUi(tester, account, ItConfig.mainPassword); debugPrint('[IT] leg1 登录 OK:$account'); // ── 2. 建相册(zh+en 双语标题 + 必填开始时间)────────────────────── await createAlbumViaUi( tester, ItConfig.albumTitleZh, ItConfig.albumTitleEn, ); debugPrint('[IT] leg2 建相册 OK:${ItConfig.albumTitleZh}'); // 相册 id / 公开令牌(后续 API 断言与 ov 检查用)。 final api = harness.container.read(apiClientProvider); final albumsPage = await api.listAlbums(limit: 50); final album = albumsPage.items.firstWhere( (a) => a.translations.any( (t) => t.locale == 'zh-CN' && t.title == ItConfig.albumTitleZh, ), ); final albumId = album.id; final publicToken = album.publicToken; debugPrint('[IT] albumId=$albumId publicToken=$publicToken'); // ── 3. 详情 + 分类 ──────────────────────────────────────────────── await openAlbumDetail(tester, ItConfig.albumTitleZh); await addCategoryViaUi(tester, '全程连拍', 'Burst'); debugPrint('[IT] leg3 分类 OK'); // ── 4. 邀请成员(staff 账号由脚本预建)──────────────────────────── await inviteMemberViaUi(tester, ItConfig.staffAccount); debugPrint('[IT] leg4 邀请 OK:${ItConfig.staffAccount}'); // ── 5. 上传 3 图(选图替身 → 真实准备/队列/引擎/原生桥接/后端)──── await enqueueViaGallery(tester, harness, images, 3); await waitAllCompleted(tester, harness, account, 3); debugPrint('[IT] leg5 队列 3 张全部 completed'); // 队列页 UI:3 个「已完成」徽章(IndexedStack 常驻,切页签后断言)。 // 详情页是压在首页骨架之上的路由(无底部导航),先退回首页再切页签。 await tester.pageBack(); await tester.pump(); await tester.tap(find.byIcon(Icons.cloud_upload_outlined)); await tester.pump(); expect(find.text('已完成').evaluate().length, greaterThanOrEqualTo(3)); // ── 6. 服务端上传历史(跨设备记录 + 本设备标记,A19)─────────────── await tester.tap(find.byTooltip('查看服务端上传历史')); await pumpUntil( tester, find.textContaining('本设备'), timeout: const Duration(seconds: 60), because: '服务端历史出现本设备记录', ); final ownDeviceRows = find.textContaining('本设备').evaluate().length; expect(ownDeviceRows, greaterThanOrEqualTo(3)); debugPrint('[IT] leg6 服务端历史 OK:$ownDeviceRows 条本设备记录'); await tester.pageBack(); await tester.pump(); // ── 7. viewer 腿:ov 公开相册 API 可见 3 张(真实同步管线)──────── final ovCount = await waitOvPublicPhotoCount(publicToken, 3); expect(ovCount, greaterThanOrEqualTo(3)); debugPrint('[IT] leg7 ov 公开相册 OK:$ovCount 张'); // ── 8. 隐藏一张 → App 管理列表同步「已隐藏」─────────────────────── // (App 照片页为 Task 4 占位无操作按钮——经同一 ApiClient 调真实 // 契约端点隐藏,再回 UI 断言状态同步;登记偏差。) await hideFirstPhotoViaApi(harness, albumId); await tester.tap(find.byIcon(Icons.photo_library_outlined)); await tester.pump(); await openAlbumDetail(tester, ItConfig.albumTitleZh); await openPhotosPageAndWait(tester, 2); expect(find.text('已隐藏').evaluate().length, greaterThanOrEqualTo(1)); expect(find.text('已发布').evaluate().length, greaterThanOrEqualTo(2)); debugPrint('[IT] leg8 隐藏同步 OK'); await tester.pageBack(); // 照片管理 → 详情 await tester.pump(); // ── 9. 断网场景(DIO 故障注入):waiting_network → 恢复重试 ──────── harness.gate.offline = true; debugPrint('[IT] leg9 故障闸门关闭(模拟断网)'); await enqueueViaGallery(tester, harness, [images.first], 1); await pollUntil>( tester, () => queueSnapshot(harness, account), (counts) => (counts[UploadTaskState.waitingNetwork] ?? 0) >= 1, timeout: const Duration(seconds: 90), because: '断网下任务进入 waitingNetwork', ); // 队列页 UI:「等待网络」徽章 + 行内重试按钮(先退回首页才有底部导航)。 await tester.pageBack(); await tester.pump(); await tester.tap(find.byIcon(Icons.cloud_upload_outlined)); await tester.pump(); expect(find.text('等待网络').evaluate().length, greaterThanOrEqualTo(1)); debugPrint('[IT] leg9 waitingNetwork 断言 OK(UI+store)'); harness.gate.offline = false; final retryButton = find.byTooltip('重试'); await tester.ensureVisible(retryButton); await tester.tap(retryButton); await waitAllCompleted(tester, harness, account, 4); debugPrint('[IT] leg9 网络恢复重试 OK:4 张全部 completed'); // ── 10. 修改密码(服务端保留当前会话)→ 会话仍有效 ──────────────── await tester.tap(find.byIcon(Icons.settings_outlined)); await tester.pump(); await tester.tap(find.text('修改密码')); // 设置页入口(此时唯一) await pumpUntil( tester, find.widgetWithText(TextFormField, '当前密码'), because: '修改密码表单', ); await tester.enterText( find.widgetWithText(TextFormField, '当前密码'), ItConfig.mainPassword, ); await tester.enterText( find.widgetWithText(TextFormField, '新密码'), ItConfig.rotatedPassword, ); await tester.enterText( find.widgetWithText(TextFormField, '确认新密码'), ItConfig.rotatedPassword, ); await tester.tap(find.text('修改密码').last); // 表单提交按钮 await pumpUntil( tester, find.text('密码已修改'), because: '修改密码成功提示', ); await tester.pageBack(); // 回设置页 await tester.pump(); // 会话保留断言:当前令牌仍可调需认证端点(refresh 204 语义)。 await harness.container.read(apiClientProvider).refresh(); debugPrint('[IT] leg10 修改密码 OK(会话保留,refresh 成功)'); // ── 11. 退出登录 → 新密码重登录 ─────────────────────────────────── await pumpUntil( tester, find.text('退出登录'), because: '设置页退出入口', ); await tester.tap(find.text('退出登录')); await pumpUntil( tester, find.widgetWithText(TextFormField, '账号'), because: '回到登录页', ); await loginViaUi(tester, account, ItConfig.rotatedPassword); debugPrint('[IT] leg11 新密码重登录 OK'); // ── 12. App 内语言切换冒烟(zh→en→zh,P3 Task 10 真机级)────────── await tester.tap(find.byIcon(Icons.settings_outlined)); await tester.pump(); await tester.tap(find.text('语言')); await pumpUntil(tester, find.text('跟随系统'), because: '语言选择弹层'); await tester.tap(find.text('English').last); await pumpUntil( tester, find.text('Albums'), because: '界面切换为英文', ); debugPrint('[IT] leg12 语言切换 en OK'); await tester.tap(find.text('Language')); await pumpUntil(tester, find.text('中文'), because: '语言选择弹层(en)'); await tester.tap(find.text('中文').last); await pumpUntil( tester, find.text('相册'), because: '界面切回中文', ); debugPrint('[IT] leg12 语言切回 zh OK —— 全流程完成'); }, timeout: const Timeout(Duration(minutes: 15)), ); tearDownAll(harness.container.dispose); }