fb3c278e50
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>
129 lines
5.6 KiB
Dart
129 lines
5.6 KiB
Dart
/// 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);
|
||
}
|