2026-09-19 05:35:16 +08:00
|
|
|
|
/// 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';
|
2026-09-20 15:03:17 +08:00
|
|
|
|
import 'package:photo_live/features/uploads/models.dart';
|
2026-09-19 05:35:16 +08:00
|
|
|
|
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);
|
2026-09-20 15:03:17 +08:00
|
|
|
|
// 服务端权威判定:verify 是全新进程,测试装配的任务库为内存实例
|
|
|
|
|
|
// (跨进程本就不共享);设备侧真实 drift 持久化由 prepare 阶段的
|
|
|
|
|
|
// App 进程行使。收敛断言 = 服务端 2 条 confirmed(幂等重传的最终
|
|
|
|
|
|
// 落点,A04/A05 语义)。
|
2026-09-19 05:35:16 +08:00
|
|
|
|
final api = harness.container.read(apiClientProvider);
|
2026-09-20 15:03:17 +08:00
|
|
|
|
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');
|
2026-09-19 05:35:16 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
fail('未知 IT_PHASE:$phase');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
timeout: const Timeout(Duration(minutes: 10)),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
tearDownAll(harness.container.dispose);
|
|
|
|
|
|
}
|