Files
photo-live/apps/mobile/integration_test/p3_flow_test.dart
T
浅念 fb3c278e50 test(mobile): integration suite against real backend on mumu
P3 Task 11 (WRITE & COMPILE phase; device run pending MuMu availability).

- integration_test/p3_flow_test.dart: login -> album (zh+en) -> category
  -> invite -> enqueue 3 real JPEGs -> queue completed -> server history
  (sourceDeviceId) -> ov public API sees 3 -> hide via API + UI sync ->
  DIO fault-gate offline (waitingNetwork -> retry -> 4 completed) ->
  password change (session kept) -> relogin -> in-app locale smoke.
- integration_test/p3_background_test.dart: IT_PHASE=prepare/verify spec
  for HOME-background continuation and force-stop kill-restart recovery
  (2 x ~14MB noise JPEGs give a deterministic mid-flight window).
- helpers: dart-define config, fault-injecting DIO adapter, app harness
  (picker/notification/locale provider overrides), UI step library with
  live-binding polling (no pumpAndSettle).
- scripts/run-mobile-it.sh: device detect (exit 2), backend ensure
  (compose + api/ov/worker/sync, exit 3), idempotent per-run user seed
  (no DB reset), APK build/install + adb push of testdata, phase 1 /
  --background phase 2 orchestration with server-side confirmed-count
  truth via curl.
- services/api seed:it script (create-if-absent staff accounts).
- debug-only network security config: cleartext HTTP permitted to
  10.0.2.2/127.0.0.1/localhost only (release unaffected).
- testdata: 3 x ~120KB + 2 x ~14MB generated JPEGs.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 05:35:16 +08:00

220 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 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 <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, 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<Map<UploadTaskState, int>>(
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);
}