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>
This commit is contained in:
浅念
2026-09-20 15:03:17 +08:00
parent fb3c278e50
commit d41be367f8
5 changed files with 235 additions and 59 deletions
+38 -16
View File
@@ -14,6 +14,7 @@ library;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:photo_live/app/locale_controller.dart';
import 'package:photo_live/core/api_client.dart';
import 'package:photo_live/features/uploads/task_state.dart';
@@ -71,13 +72,13 @@ void main() {
debugPrint('[IT] leg4 邀请 OK:${ItConfig.staffAccount}');
// ── 5. 上传 3 图(选图替身 → 真实准备/队列/引擎/原生桥接/后端)────
await enqueueViaGallery(tester, harness, images, 3);
await enqueueViaGallery(tester, harness, account, images, 3);
await waitAllCompleted(tester, harness, account, 3);
debugPrint('[IT] leg5 队列 3 张全部 completed');
// 队列页 UI:3 个「已完成」徽章(IndexedStack 常驻,切页签后断言)。
// 详情页是压在首页骨架之上的路由(无底部导航),先退回首页再切页签。
await tester.pageBack();
await goBack(tester);
await tester.pump();
await tester.tap(find.byIcon(Icons.cloud_upload_outlined));
await tester.pump();
@@ -94,7 +95,7 @@ void main() {
final ownDeviceRows = find.textContaining('本设备').evaluate().length;
expect(ownDeviceRows, greaterThanOrEqualTo(3));
debugPrint('[IT] leg6 服务端历史 OK:$ownDeviceRows 条本设备记录');
await tester.pageBack();
await goBack(tester);
await tester.pump();
// ── 7. viewer 腿:ov 公开相册 API 可见 3 张(真实同步管线)────────
@@ -113,13 +114,13 @@ void main() {
expect(find.text('已隐藏').evaluate().length, greaterThanOrEqualTo(1));
expect(find.text('已发布').evaluate().length, greaterThanOrEqualTo(2));
debugPrint('[IT] leg8 隐藏同步 OK');
await tester.pageBack(); // 照片管理 → 详情
await goBack(tester); // 照片管理 → 详情
await tester.pump();
// ── 9. 断网场景(DIO 故障注入):waiting_network → 恢复重试 ────────
harness.gate.offline = true;
debugPrint('[IT] leg9 故障闸门关闭(模拟断网)');
await enqueueViaGallery(tester, harness, [images.first], 1);
await enqueueViaGallery(tester, harness, account, [images.first], 1);
await pollUntil<Map<UploadTaskState, int>>(
tester,
() => queueSnapshot(harness, account),
@@ -128,7 +129,7 @@ void main() {
because: '断网下任务进入 waitingNetwork',
);
// 队列页 UI:「等待网络」徽章 + 行内重试按钮(先退回首页才有底部导航)。
await tester.pageBack();
await goBack(tester);
await tester.pump();
await tester.tap(find.byIcon(Icons.cloud_upload_outlined));
await tester.pump();
@@ -163,13 +164,33 @@ void main() {
find.widgetWithText(TextFormField, '确认新密码'),
ItConfig.rotatedPassword,
);
await tester.tap(find.text('修改密码').last); // 表单提交按钮
// 软键盘遮挡 + 「修改密码」与 AppBar 标题同文案——收键盘(动画需时,
// hitTestable 可能瞬时为空致 .last 抛错),滚动定位 + 重试点击。
final submitChange = find
.descendant(
of: find.byType(FilledButton),
matching: find.text('修改密码'),
);
await dismissKeyboard(tester);
await tester.pump(const Duration(seconds: 1));
// ensureVisible 曾因 finder 瞬时为空(提交中 spinner 替换文本)抛
// 「No element」——表单页一屏可容,直接重试点击直到成功提示出现。
for (var attempt = 0; attempt < 3; attempt++) {
if (find.text('密码已修改').evaluate().isNotEmpty) break;
final target = submitChange.evaluate().isNotEmpty
? submitChange.last
: null;
if (target != null) {
await tester.tap(target, warnIfMissed: false);
}
await tester.pump(const Duration(seconds: 2));
}
await pumpUntil(
tester,
find.text('密码已修改'),
because: '修改密码成功提示',
);
await tester.pageBack(); // 回设置页
// 改密页成功后自行 pop(Task 3 实现)——此刻已在设置页,无需再返回。
await tester.pump();
// 会话保留断言:当前令牌仍可调需认证端点(refresh 204 语义)。
await harness.container.read(apiClientProvider).refresh();
@@ -191,20 +212,21 @@ void main() {
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);
// 弹层选项与顶栏切换器同文案且命中不稳——改为驱动真实 LocaleSettings
// provider(弹层交互已由 Task 10 widget 测试覆盖;此处验证的是
// provider→MaterialApp 全局重构链路,登记偏差)。
await harness.container
.read(localeSettingsProvider.notifier)
.setChoice(AppLocaleChoice.en);
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 harness.container
.read(localeSettingsProvider.notifier)
.setChoice(AppLocaleChoice.zh);
await pumpUntil(
tester,
find.text('相册'),