Files
photo-live/apps/mobile/test/pages/home_badge_test.dart
T
2026-09-21 14:55:29 +08:00

195 lines
6.9 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.
/// 底部导航与「上传」角标测试(UI-01 Task 3,§4.1)。
///
/// 覆盖:三页签改名(Albums/Uploads/Settings)与 IndexedStack 状态保持、
/// 角标 = 当前账号未结束任务数(countsByState 聚合,写通知防抖刷新)、
/// 他账号任务不计入(账号隔离)、任务全部结束后角标隐藏、
/// [unfinishedCountOf] 纯函数口径(含 retryableFailed)。
library;
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:photo_live/app/theme.dart';
import 'package:photo_live/core/api_client.dart';
import 'package:photo_live/core/session_store.dart';
import 'package:photo_live/features/uploads/queue_controller.dart';
import 'package:photo_live/features/uploads/task_state.dart';
import 'package:photo_live/features/uploads/task_store.dart';
import 'package:photo_live/l10n/app_localizations.dart';
import 'package:photo_live/pages/albums_tab.dart';
import 'package:photo_live/pages/home_page.dart';
import 'package:photo_live/pages/queue_tab.dart';
import '../helpers/fakes.dart';
UploadTaskDraft _draft(String id, {String account = 'staff01'}) =>
UploadTaskDraft(
id: id,
accountId: account,
albumId: 'alb-1',
clientTaskId: 'ct-$id',
sourceFilePath: '/tmp/$id.jpg',
filename: '$id.jpg',
bytes: 10,
contentType: 'image/jpeg',
);
/// 沿合法状态机单步迁移(自迁移非法,须按序调用)。
Future<void> _transition(TaskStore store, String id, UploadTaskState to) =>
store.updateState(id, to);
void main() {
late FakeSessionStore session;
late TaskStore taskStore;
setUp(() {
session = FakeSessionStore()
..session = Session(
token: 'tok-live-1',
expiresAt: DateTime.utc(2099, 1, 1),
account: 'staff01',
);
taskStore = TaskStore.memory();
});
Future<void> pumpHome(WidgetTester tester) async {
// 相册列表页签进入即拉列表;空页兜底即可。
final adapter = FakeAdapter(
(o) => o.path == '/v1/albums'
? (200, cursorPageBody(<Map<String, dynamic>>[]))
: throw StateError('unexpected ${o.path}'),
);
final client = ApiClient(
sessionStore: session,
dio: Dio(BaseOptions(baseUrl: 'http://test.local'))
..httpClientAdapter = adapter,
);
await tester.pumpWidget(
ProviderScope(
overrides: [
...pageTestOverrides(
session: session,
taskStore: taskStore,
client: client,
),
],
child: MaterialApp(
theme: AppTheme.light,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomePage(),
),
),
);
await tester.pumpAndSettle();
}
/// 任务库写通知 → 200ms 防抖重查(kQueueRefreshDebounce)。
///
/// 不用 pumpAndSettle:uploading 任务的行级进度条为不确定态(常驻动画)。
Future<void> pumpCounts(WidgetTester tester) async {
await tester.pump(kQueueRefreshDebounce * 2);
await tester.pump(const Duration(milliseconds: 50));
}
testWidgets('三页签改名 + IndexedStack 保持页面状态', (tester) async {
await pumpHome(tester);
// IndexedStack 常驻三页签(非当前页签 offstage,find 默认跳过,故显式
// 关闭 skipOffstage)。Albums/Settings 各两次(页签 AppBar 标题 + 导航
// 标签);Uploads 仅导航标签(队列页标题为 Upload queue)。
expect(find.text('Albums', skipOffstage: false), findsNWidgets(2));
expect(find.text('Uploads', skipOffstage: false), findsOneWidget);
expect(find.text('Settings', skipOffstage: false), findsNWidgets(2));
expect(
find.text('Queue', skipOffstage: false),
findsNothing,
reason: '旧页签名已移除',
);
expect(find.byType(IndexedStack), findsOneWidget);
expect(find.byType(AlbumsTab, skipOffstage: false), findsOneWidget);
expect(find.byType(QueueTab, skipOffstage: false), findsOneWidget);
expect(find.byType(Badge), findsNothing, reason: '初始无任务无角标');
});
testWidgets('角标 = 当前账号未完成任务数;他账号任务不计入', (tester) async {
await pumpHome(tester);
// 其他账号任务:账号隔离,不显示角标。
final other = await taskStore.create(_draft('t-o1', account: 'staff02'));
expect(other.accountId, 'staff02');
await pumpCounts(tester);
expect(find.byType(Badge), findsNothing, reason: '他账号任务不计入');
// 本账号 3 个未完成(2 queued + 1 uploading)。
final t1 = await taskStore.create(_draft('t-1'));
final t2 = await taskStore.create(_draft('t-2'));
final t3 = await taskStore.create(_draft('t-3'));
await _transition(taskStore, t1.id, UploadTaskState.queued);
await _transition(taskStore, t2.id, UploadTaskState.queued);
await _transition(taskStore, t3.id, UploadTaskState.queued);
await _transition(taskStore, t3.id, UploadTaskState.uploading);
await pumpCounts(tester);
expect(find.byType(Badge), findsOneWidget);
expect(find.text('3'), findsOneWidget, reason: '角标计数 3');
});
testWidgets('任务全部结束 → 角标隐藏', (tester) async {
await pumpHome(tester);
final tasks = <String>[
for (var i = 1; i <= 2; i++) (await taskStore.create(_draft('t-$i'))).id,
];
for (final id in tasks) {
await _transition(taskStore, id, UploadTaskState.queued);
}
await pumpCounts(tester);
expect(find.text('2'), findsOneWidget);
for (final id in tasks) {
await _transition(taskStore, id, UploadTaskState.uploading);
await _transition(taskStore, id, UploadTaskState.uploadedConfirming);
await _transition(taskStore, id, UploadTaskState.completed);
}
await pumpCounts(tester);
expect(find.byType(Badge), findsNothing, reason: '0 隐藏角标');
});
test('unfinishedCountOf 口径:待传 + 等待网络 + 可重试失败', () {
Map<UploadTaskState, int> counts(Map<UploadTaskState, int> patch) =>
<UploadTaskState, int>{
for (final state in UploadTaskState.values) state: patch[state] ?? 0,
};
// 待传口径(pendingCountOf)之外,可重试失败计入;准备失败/取消/完成不计。
expect(
unfinishedCountOf(
counts({UploadTaskState.queued: 2, UploadTaskState.uploading: 1}),
),
3,
);
expect(
unfinishedCountOf(
counts({
UploadTaskState.retryableFailed: 1,
UploadTaskState.waitingNetwork: 2,
}),
),
3,
);
expect(
unfinishedCountOf(
counts({
UploadTaskState.completed: 5,
UploadTaskState.cancelled: 1,
UploadTaskState.preparationFailed: 1,
}),
),
0,
);
});
}