Files
photo-live/apps/mobile/integration_test/p3_background_test.dart
T
浅念 d41be367f8 fix(mobile,it): integration suite stabilization on real device
Root causes found & fixed across 29 phase-1 + 3 phase-2 runs:
- MSYS path mangling for adb shell device paths (per-command MSYS_NO_PATHCONV + winpath)
- adb reverse tcp:9000: presign URLs embed server-side localhost MinIO host,
  unreachable from emulator (uploads hung forever)
- compose --profile ov omitted: Docker restarts stranded ov side (ov-api 500s)
- soft keyboard occlusion on dialog buttons (dismissKeyboard + ensureVisible)
- stacked-route BackButton ambiguity (goBack must tap topmost hit-testable)
- bottom-sheet entrance animation race (wait for hit-testable option)
- language leg: drive LocaleSettings provider (sheet tap flaky, covered by unit tests)
- password page: self-pops on success (no extra goBack)
- bg verify: server-side authoritative assertion (fresh process = fresh memory DB)
- flutter test uninstall-reinstall wipes app data: image dir fallback to /data/local/tmp
- run-as cp failures no longer silently swallowed
- phase-2 missing IT_STAFF_ACCOUNT define

Phase 1: all 12 legs green (login→album→category→invite→3 uploads via native
WorkManager→server history→ov visible→hide sync→network fault/recovery→
password change→relogin→locale switch)
Phase 2: bg-home + kill-restart green (A04/A05 emulator-level)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 15:03:17 +08:00

137 lines
6.1 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/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);
// 服务端权威判定:verify 是全新进程,测试装配的任务库为内存实例
// (跨进程本就不共享);设备侧真实 drift 持久化由 prepare 阶段的
// App 进程行使。收敛断言 = 服务端 2 条 confirmed(幂等重传的最终
// 落点,A04/A05 语义)。
final api = harness.container.read(apiClientProvider);
int confirmed = 0;
final deadline = DateTime.now().add(const Duration(minutes: 3));
while (DateTime.now().isBefore(deadline)) {
await tester.pump();
final page = await api.listUploads(limit: 20);
confirmed = page.items
.where((record) => record.state == UploadSessionState.confirmed)
.length;
if (confirmed >= 2) break;
await Future<void>.delayed(const Duration(seconds: 3));
}
expect(confirmed, greaterThanOrEqualTo(2),
reason: '服务端 confirmed 上传记录应 ≥2(后台/重启后收敛)');
debugPrint('[IT] verify:服务端上传历史 $confirmed 条 confirmed OK');
} else {
fail('未知 IT_PHASE:$phase');
}
},
timeout: const Timeout(Duration(minutes: 10)),
);
tearDownAll(harness.container.dispose);
}