Files
photo-live/apps/mobile/integration_test/p3_background_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

129 lines
5.6 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 集成测试 · 后台延续与杀进程恢复(A04/A05,模拟器级)。
///
/// 由 `scripts/run-mobile-it.sh --background` 编排,两阶段经
/// `--dart-define IT_PHASE=` 区分(App 无法自己 HOME/杀自己——设备动作
/// 由脚本在见到 MARKER 后执行,属计划明示的 adb 编排场景):
///
/// - **prepare**:登录(本次运行专属账号)→ API 建相册 → 直接经准备器
/// 入队 2 张大图(~15MB,保证传输有可被中断的时间窗)→ 等任务进入
/// uploading → 打 `MARKER:UPLOADING`(脚本此刻 HOME 或 force-stop)→
/// 就地等待全部 completed(HOME 场景:WorkManager 后台续传完成后本
/// 测试自然通过;KILL 场景:进程被杀,本测试非零退出是预期)。
/// - **verify**:全新进程登录同账号 → 断言 2 张全部 completed(KILL 场景
/// 覆盖 A05:drift 任务库持久 + 崩溃恢复 uploading→waitingNetwork→
/// 幂等重传)→ 服务端历史可见。
library;
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:photo_live/core/api_client.dart';
import 'package:photo_live/features/albums/models.dart';
import 'package:photo_live/features/uploads/providers.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 phase = const String.fromEnvironment('IT_PHASE', defaultValue: '');
if (phase.isEmpty) {
fail('缺少 --dart-define IT_PHASE=prepare|verify(经 run-mobile-it.sh 运行)');
}
final harness = ItAppHarness.create();
testWidgets(
'P3 后台延续/杀进程恢复 [$phase]',
(tester) async {
final account = ItConfig.mainAccount;
if (phase == 'prepare') {
await tester.pumpWidget(harness.appWidget());
await loginViaUi(tester, account, ItConfig.mainPassword);
// 相册走 API(建相册的 UI 驱动已在主流程覆盖;本场景主题是传输)。
final api = harness.container.read(apiClientProvider);
final album = await api.createAlbum(
AlbumCreateRequest(
translations: [
AlbumTranslation(
locale: 'zh-CN',
title: '后台测试相册 ${ItConfig.runStamp}',
),
AlbumTranslation(
locale: 'en',
title: 'BG Album ${ItConfig.runStamp}',
),
],
defaultLocale: 'zh-CN',
startAt: DateTime.now().toUtc(),
),
);
debugPrint('[IT] 后台场景相册:${album.id}');
// 大图经准备器直接入队(选图 UI 已在主流程覆盖)。
final support = await getApplicationSupportDirectory();
final dir = await resolveImageDir(support.path);
final largeFiles = [ItConfig.largeFileA, ItConfig.largeFileB]
.map((name) => p.join(dir, name))
.toList();
for (final file in largeFiles) {
final task = await harness
.container
.read(uploadPreparerProvider)
.prepare(
accountId: account,
albumId: album.id,
sourcePath: file,
);
expect(task.state, UploadTaskState.queued, reason: '入队 $file');
}
debugPrint('[IT] 2 张大图已入队:$largeFiles');
// 等首个任务进入 uploading(引擎已领取、原生桥接已注册)。
await pollUntil<Map<UploadTaskState, int>>(
tester,
() => queueSnapshot(harness, account),
(counts) =>
(counts[UploadTaskState.uploading] ?? 0) >= 1 ||
(counts[UploadTaskState.uploadedConfirming] ?? 0) >= 1 ||
(counts[UploadTaskState.completed] ?? 0) >= 1,
timeout: const Duration(seconds: 60),
because: '任务进入传输中(脚本随后执行 HOME/force-stop)',
);
// 脚本协调点:轮询本行出现在 flutter test 输出后立即动作。
debugPrint('[IT] MARKER:UPLOADING');
// HOME 场景:后台完成 → 本断言通过;KILL 场景:进程死于此循环
// (非零退出是预期,脚本以 || true 容忍并以 verify 阶段收口)。
await waitAllCompleted(tester, harness, account, 2);
debugPrint('[IT] MARKER:BG_COMPLETED');
} else if (phase == 'verify') {
await tester.pumpWidget(harness.appWidget());
await loginViaUi(tester, account, ItConfig.mainPassword);
// 崩溃恢复收敛断言:in-flight 残留态经 waitingNetwork 重传幂等收口。
await waitAllCompleted(tester, harness, account, 2);
debugPrint('[IT] verify:2 张大图在持久任务库中均为 completed');
// 服务端侧旁证:跨设备上传历史 2 条已确认记录。
final api = harness.container.read(apiClientProvider);
final page = await api.listUploads(limit: 20);
final confirmed = page.items
.where((record) => record.albumId.isNotEmpty)
.length;
expect(confirmed, greaterThanOrEqualTo(2));
debugPrint('[IT] verify:服务端上传历史 $confirmed 条 OK');
} else {
fail('未知 IT_PHASE:$phase');
}
},
timeout: const Timeout(Duration(minutes: 10)),
);
tearDownAll(harness.container.dispose);
}