955f9b13c1
Task 9 (P3 plan): queue tab replaces the stub, continuous capture page enqueues per shot, album detail gains an upload FAB, and the upload engine lifecycle is now bound to the login session. - Engine scope: session account drives UploadEngine start/stop; wired from app boot, login success, logout, and 401 eviction. Tasks stay in the store across logout (accountId hard isolation). - Queue tab: album x state x time filter bar, task rows with thumbnail, state chip, localized error copy, attempt count, per-row progress via a broadcast progress hub, state-dependent actions (re-pick retry, retryNow, cancel, delete), batch retry bar, 200ms-debounced live refresh from drift table updates. - Capture page: camera preview via injected CaptureSource, one enqueue per shutter press, persistent album/category/badges overlay with a switch-only-affects-new-shots hint, gallery multi-pick, exit confirm while uploads are pending, permission-denied and init-failure views. - POST_NOTIFICATIONS runtime permission via a small MethodChannel (photolive/permissions) + PermissionPlugin.kt: single runtime permission need, avoids permission_handler dependency; requested once on first enqueue, silent degradation when denied or unavailable. - Album detail FAB: capture-and-upload / pick-from-gallery sheet. - l10n: ~50 new keys in zh/en arbs; tests: +45 (filter matrix, engine scope lifecycle, permission gate, queue tab, capture page, FAB entry); existing page tests updated to inject session/task-store fakes. Co-Authored-By: Claude Code <noreply@anthropic.com>
60 lines
2.2 KiB
Dart
60 lines
2.2 KiB
Dart
/// App 壳 401 接线测试(P3 计划 Task 3)。
|
||
///
|
||
/// 覆盖:业务端点 401 → ApiClient 清会话并触发 onUnauthorized →
|
||
/// 根导航清栈回 /login(PhotoLiveApp.initState 注册的回调)。
|
||
library;
|
||
|
||
import 'package:dio/dio.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:photo_live/app/app.dart';
|
||
import 'package:photo_live/core/api_client.dart';
|
||
import 'package:photo_live/core/error_codes.dart';
|
||
import 'package:photo_live/core/session_store.dart';
|
||
import 'package:photo_live/pages/login_page.dart';
|
||
|
||
import '../helpers/fakes.dart';
|
||
|
||
void main() {
|
||
testWidgets('业务 401 → onUnauthorized 清栈回 /login', (tester) async {
|
||
final store = FakeSessionStore();
|
||
store.session = Session(
|
||
token: 'tok-live-1',
|
||
expiresAt: DateTime.utc(2099, 1, 1),
|
||
account: 'staff01',
|
||
);
|
||
final adapter = FakeAdapter(
|
||
(o) => (401, errorBody(ErrorCodes.authTokenExpired)),
|
||
);
|
||
final client = ApiClient(
|
||
sessionStore: store,
|
||
dio: Dio(BaseOptions(baseUrl: 'http://test.local'))
|
||
..httpClientAdapter = adapter,
|
||
);
|
||
|
||
// PhotoLiveApp.initState 读取被 override 的客户端并注册 onUnauthorized。
|
||
// Task 9 起根组件 watch 引擎作用域(会话→引擎 start),真实
|
||
// flutter_secure_storage/path_provider 平台通道在测试环境挂起,须替身。
|
||
await tester.pumpWidget(
|
||
ProviderScope(
|
||
overrides: [...pageTestOverrides(session: store, client: client)],
|
||
child: const PhotoLiveApp(),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
|
||
// 先抵 /home 再触发 401:验证清栈(回 /login 而非叠加路由)。
|
||
rootNavigatorKey.currentState!
|
||
.pushNamedAndRemoveUntil('/home', (route) => false);
|
||
await tester.pumpAndSettle();
|
||
|
||
// 触发 401 但不在测试体内 await(fake-async 下时钟由 pump 推进,
|
||
// 阻塞等待会死锁);AppException 由 ignore 吞掉,断言在下方。
|
||
client.listAlbums().ignore();
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(store.session, isNull, reason: '业务 401 清空本地会话');
|
||
expect(find.byType(LoginPage), findsOneWidget, reason: '清栈回登录页');
|
||
});
|
||
}
|