Files
photo-live/apps/mobile/integration_test/p3_flow_test.dart
T

242 lines
11 KiB
Dart
Raw Normal View History

/// 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/app/locale_controller.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 <String>[];
// ── 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, account, images, 3);
await waitAllCompleted(tester, harness, account, 3);
debugPrint('[IT] leg5 队列 3 张全部 completed');
// 队列页 UI:3 个「已完成」徽章(IndexedStack 常驻,切页签后断言)。
// 详情页是压在首页骨架之上的路由(无底部导航),先退回首页再切页签。
await goBack(tester);
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 goBack(tester);
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 goBack(tester); // 照片管理 → 详情
await tester.pump();
// ── 9. 断网场景(DIO 故障注入):waiting_network → 恢复重试 ────────
harness.gate.offline = true;
debugPrint('[IT] leg9 故障闸门关闭(模拟断网)');
await enqueueViaGallery(tester, harness, account, [images.first], 1);
await pollUntil<Map<UploadTaskState, int>>(
tester,
() => queueSnapshot(harness, account),
(counts) => (counts[UploadTaskState.waitingNetwork] ?? 0) >= 1,
timeout: const Duration(seconds: 90),
because: '断网下任务进入 waitingNetwork',
);
// 队列页 UI:「等待网络」徽章 + 行内重试按钮(先退回首页才有底部导航)。
await goBack(tester);
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,
);
// 软键盘遮挡 + 「修改密码」与 AppBar 标题同文案——收键盘(动画需时,
// hitTestable 可能瞬时为空致 .last 抛错),滚动定位 + 重试点击。
final submitChange = find
.descendant(
of: find.byType(FilledButton),
matching: find.text('修改密码'),
);
await dismissKeyboard(tester);
await tester.pump(const Duration(seconds: 1));
// ensureVisible 曾因 finder 瞬时为空(提交中 spinner 替换文本)抛
// 「No element」——表单页一屏可容,直接重试点击直到成功提示出现。
for (var attempt = 0; attempt < 3; attempt++) {
if (find.text('密码已修改').evaluate().isNotEmpty) break;
final target = submitChange.evaluate().isNotEmpty
? submitChange.last
: null;
if (target != null) {
await tester.tap(target, warnIfMissed: false);
}
await tester.pump(const Duration(seconds: 2));
}
await pumpUntil(
tester,
find.text('密码已修改'),
because: '修改密码成功提示',
);
// 改密页成功后自行 pop(Task 3 实现)——此刻已在设置页,无需再返回。
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 真机级)──────────
// 弹层选项与顶栏切换器同文案且命中不稳——改为驱动真实 LocaleSettings
// provider(弹层交互已由 Task 10 widget 测试覆盖;此处验证的是
// provider→MaterialApp 全局重构链路,登记偏差)。
await harness.container
.read(localeSettingsProvider.notifier)
.setChoice(AppLocaleChoice.en);
await pumpUntil(
tester,
find.text('Albums'),
because: '界面切换为英文',
);
debugPrint('[IT] leg12 语言切换 en OK');
await harness.container
.read(localeSettingsProvider.notifier)
.setChoice(AppLocaleChoice.zh);
await pumpUntil(
tester,
find.text('相册'),
because: '界面切回中文',
);
debugPrint('[IT] leg12 语言切回 zh OK —— 全流程完成');
},
timeout: const Timeout(Duration(minutes: 15)),
);
tearDownAll(harness.container.dispose);
}