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

186 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.
/// 相册列表页签 widget 测试(P3 Task 4;UI-01 Task 3 起覆盖封面卡片样板)。
///
/// 覆盖:封面卡片样板(封面占位/标题回退/时间场地行/公开与上传徽章/
/// 令牌与四开关移除)、关闭状态徽章、封面加载失败替代图、游标加载更多
/// (第二页携带 cursor)、空态文案、首屏错误 + 重试恢复。
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/core/api_client.dart';
import 'package:photo_live/core/error_codes.dart';
import 'package:photo_live/features/albums/providers.dart';
import 'package:photo_live/l10n/app_localizations.dart';
import 'package:photo_live/pages/albums_tab.dart';
import 'package:photo_live/ui/components/photo_thumbnail.dart';
import '../helpers/fakes.dart';
void main() {
late FakeSessionStore store;
setUp(() {
store = FakeSessionStore();
});
ApiClient clientOf(FakeAdapter adapter) => ApiClient(
sessionStore: store,
dio: Dio(BaseOptions(baseUrl: 'http://test.local'))
..httpClientAdapter = adapter,
);
Future<void> pumpTab(WidgetTester tester, ApiClient client) async {
await tester.pumpWidget(
ProviderScope(
overrides: [apiClientProvider.overrideWith((ref) => client)],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const AlbumsTab(),
),
),
);
await tester.pumpAndSettle();
}
testWidgets('封面卡片样板:封面占位 + 标题回退 + 时间场地 + 公开/上传徽章', (tester) async {
final adapter = FakeAdapter(
(o) => o.path == '/v1/albums'
? (200, cursorPageBody([albumJson()]))
: throw StateError('unexpected ${o.path}'),
);
await pumpTab(tester, clientOf(adapter));
// 标题按界面语言回退(en → en 翻译),最多两行。
expect(find.text('Opening Gala'), findsOneWidget);
// 时间用相册时区(上海墙钟 14:30)+ 场地(en 未填场地回退 zh-CN)。
expect(find.text('2026/9/18 14:30 ~ Not set · 大宴会厅'), findsOneWidget);
// 封面未配置 → 固定 16:9 中性占位(非转圈)。
expect(find.byType(PhotoThumbnail), findsOneWidget);
expect(find.byIcon(Icons.image_outlined), findsOneWidget);
// 公开/上传状态徽章(图标 + 文案,不只靠颜色)。
expect(find.text('Public'), findsOneWidget);
expect(find.byIcon(Icons.info_outline), findsOneWidget);
expect(find.text('Uploads on'), findsOneWidget);
expect(find.byIcon(Icons.check_circle), findsOneWidget);
// publicToken 与四开关堆叠已从主卡片移除(完整配置在详情设置)。
expect(find.text('sharetoken…'), findsNothing);
expect(find.text('Download'), findsNothing);
expect(find.text('Original'), findsNothing);
expect(find.byIcon(Icons.add), findsOneWidget, reason: 'FAB 新建入口');
});
testWidgets('关闭状态徽章与封面加载失败替代图', (tester) async {
final adapter = FakeAdapter(
(o) => o.path == '/v1/albums'
? (
200,
cursorPageBody([
albumJson(
publicEnabled: false,
uploadEnabled: false,
cover: {
'url': 'http://test.local/cover.jpg',
'expiresAt': '2026-09-22T00:00:00Z',
},
),
]),
)
: throw StateError('unexpected ${o.path}'),
);
await pumpTab(tester, clientOf(adapter));
// 公开关闭 / 上传关闭 → 中性徽章。
expect(find.text('Not public'), findsOneWidget);
expect(find.text('Uploads off'), findsOneWidget);
// 封面 URL 加载失败(测试环境网络请求 400)→ 中性块 + 小图标,不显示破图。
expect(find.byType(PhotoThumbnail), findsOneWidget);
expect(find.byIcon(Icons.broken_image_outlined), findsOneWidget);
expect(find.byIcon(Icons.image_outlined), findsNothing);
});
testWidgets('滚动接近底部 → 游标加载第二页(携带 cursor)', (tester) async {
final page1 = List<Map<String, dynamic>>.generate(
20,
(i) => albumJson(id: 'alb-${i + 1}'),
);
final page2 = List<Map<String, dynamic>>.generate(
10,
(i) => albumJson(id: 'alb-${i + 21}'),
);
final adapter = FakeAdapter((o) {
if (o.path != '/v1/albums') throw StateError('unexpected ${o.path}');
final cursor = o.uri.queryParameters['cursor'];
if (cursor == null) return (200, cursorPageBody(page1, nextCursor: 'c1'));
if (cursor == 'c1') return (200, cursorPageBody(page2));
throw StateError('unexpected cursor $cursor');
});
await pumpTab(tester, clientOf(adapter));
// 首屏只拉第一页(20 条入 state;ListView 懒构建,不逐条断言渲染)。
expect(adapter.requests, hasLength(1));
final container = ProviderScope.containerOf(
tester.element(find.byType(AlbumsTab)),
);
expect(container.read(albumListProvider).items, hasLength(20));
// 上滑触发加载更多(封面卡片较高,分段滚动直至触发第二页请求)。
for (var i = 0; i < 10 && adapter.requests.length < 2; i++) {
await tester.drag(find.byType(ListView), const Offset(0, -1200));
await tester.pump();
}
await tester.pumpAndSettle();
expect(adapter.requests, hasLength(2));
expect(
adapter.requests.last.uri.queryParameters['cursor'],
'c1',
reason: '第二页携带游标 c1',
);
expect(
container.read(albumListProvider).items,
hasLength(30),
reason: '两页累积',
);
});
testWidgets('空列表 → 空态文案', (tester) async {
final adapter = FakeAdapter(
(o) => o.path == '/v1/albums'
? (200, cursorPageBody(<Map<String, dynamic>>[]))
: throw StateError('unexpected ${o.path}'),
);
await pumpTab(tester, clientOf(adapter));
expect(
find.text('No albums yet. Tap "Create album" to get started'),
findsOneWidget,
);
});
testWidgets('首屏错误 → 错误视图,重试后恢复', (tester) async {
var failFirst = true;
final adapter = FakeAdapter((o) {
if (o.path != '/v1/albums') throw StateError('unexpected ${o.path}');
if (failFirst) {
failFirst = false;
return (500, errorBody(ErrorCodes.internalError));
}
return (200, cursorPageBody([albumJson()]));
});
await pumpTab(tester, clientOf(adapter));
expect(
find.text('Service temporarily unavailable. Please retry later'),
findsOneWidget,
);
await tester.tap(find.text('Retry'));
await tester.pumpAndSettle();
expect(find.text('Opening Gala'), findsOneWidget);
});
}