Files
photo-live/apps/mobile/test/pages/server_history_page_test.dart
T
2026-09-21 17:20:43 +08:00

200 lines
6.8 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 6,A19 语义)。
///
/// 覆盖:A19 数据源区分横幅、行内容(clientTaskId 短码/KB/状态标签/
/// 设备短码 + 「本设备」标记)、游标加载更多、错误重试。
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/device_id.dart';
import 'package:photo_live/core/session_store.dart';
import 'package:photo_live/l10n/app_localizations.dart';
import 'package:photo_live/pages/server_history_page.dart';
import '../helpers/fakes.dart';
const String _kOwnDevice = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const String _kOtherDevice = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
class _FixedDeviceIdStore implements DeviceIdStore {
_FixedDeviceIdStore(this._value);
final String _value;
@override
Future<String?> read() async => _value;
@override
Future<void> write(String value) async {}
}
/// 契约 `UploadSessionRecord` 响应体(GET /uploads 条目)。
Map<String, dynamic> sessionJson({
String uploadId = 'upl-0001',
String albumId = 'alb-1',
String clientTaskId = 'clienttask000000000000000000000001',
String state = 'confirmed',
int expectedBytes = 2048,
String createdAt = '2026-09-19T08:00:00Z',
String photoId = 'pho-0001',
String processingState = 'ready',
String? sourceDeviceId = _kOwnDevice,
}) =>
<String, dynamic>{
'uploadId': uploadId,
'albumId': albumId,
'clientTaskId': clientTaskId,
'state': state,
'expectedBytes': expectedBytes,
'contentType': 'image/jpeg',
'createdAt': createdAt,
'sourceDeviceId': sourceDeviceId,
'photo': <String, dynamic>{
'photoId': photoId,
'processingState': processingState,
'visibility': 'published',
},
};
void main() {
late FakeSessionStore sessionStore;
setUp(() {
sessionStore = FakeSessionStore()
..session = Session(token: 'tok-1', account: 'alice');
});
Future<void> pumpPage(WidgetTester tester, ApiClient client) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
apiClientProvider.overrideWith((ref) => client),
deviceIdProvider.overrideWith(
(ref) => DeviceId(store: _FixedDeviceIdStore(_kOwnDevice)),
),
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const ServerHistoryPage(),
),
),
);
await tester.pumpAndSettle();
}
ApiClient clientOf(FakeAdapter adapter) => ApiClient(
sessionStore: sessionStore,
dio: Dio(BaseOptions(baseUrl: 'http://test.local'))
..httpClientAdapter = adapter,
);
testWidgets('A19 横幅 + 行内容(短码/KB/状态标签/设备短码/本设备标记)',
(tester) async {
final adapter = FakeAdapter((o) {
if (o.method == 'GET' && o.path == '/v1/uploads') {
return (
200,
cursorPageBody([
sessionJson(clientTaskId: 'owndevice0000000000000000000001'),
sessionJson(
clientTaskId: 'otherdevice0000000000000000000002',
state: 'pending',
processingState: 'processing',
expectedBytes: 5 * 1024 * 1024,
sourceDeviceId: _kOtherDevice,
),
]),
);
}
throw StateError('unexpected ${o.method} ${o.path}');
});
await pumpPage(tester, clientOf(adapter));
// A19 数据源区分横幅(UI-01:本机队列入口文案指向「上传」页)。
expect(find.textContaining('cross-device'), findsOneWidget);
expect(find.textContaining('Uploads tab'), findsOneWidget);
// 行:clientTaskId 前 8 位 + KB(2048B → 2 KB;5MB → 5120 KB)。
expect(find.textContaining('owndevic'), findsOneWidget);
expect(find.textContaining('2 KB'), findsOneWidget);
expect(find.textContaining('otherdev'), findsOneWidget);
expect(find.textContaining('5120 KB'), findsOneWidget);
// 状态标签(会话状态 + 处理状态)。
expect(find.text('Confirmed'), findsOneWidget);
expect(find.text('Pending'), findsOneWidget);
expect(find.text('Ready'), findsOneWidget);
expect(find.text('Processing'), findsOneWidget);
// 设备短码(本机设备aaaaaaaa + 「本设备」;他机无标记)。
expect(find.textContaining(_kOwnDevice.substring(0, 8)), findsOneWidget);
expect(find.textContaining(_kOtherDevice.substring(0, 8)), findsOneWidget);
expect(find.textContaining('(This device)'), findsOneWidget);
// listUploads 请求带分页 limit=20。
expect(adapter.requests.single.uri.queryParameters['limit'], '20');
});
testWidgets('游标加载更多(滚动触底 → 第二页带 cursor)', (tester) async {
final page1 = List<Map<String, dynamic>>.generate(
20,
(i) => sessionJson(
uploadId: 'upl-p1-$i',
clientTaskId: 'page1row$i.padLeft-000000000000000000001',
),
);
final adapter = FakeAdapter((o) {
final cursor = o.uri.queryParameters['cursor'];
if (cursor == null) {
return (200, cursorPageBody(page1, nextCursor: 'hc1'));
}
if (cursor == 'hc1') {
return (
200,
cursorPageBody(
[sessionJson(clientTaskId: 'page2item000000000000000000000001')],
),
);
}
throw StateError('unexpected cursor $cursor');
});
await pumpPage(tester, clientOf(adapter));
expect(adapter.requests, hasLength(1), reason: '首页无 cursor');
await tester.drag(find.byType(ListView), const Offset(0, -6000));
await tester.pumpAndSettle();
expect(adapter.requests.last.uri.queryParameters['cursor'], 'hc1');
expect(find.textContaining('page2ite'), findsOneWidget);
});
testWidgets('加载失败展示错误与重试;恢复后重试成功', (tester) async {
var failFirst = true;
final adapter = FakeAdapter((o) {
if (failFirst) {
throw DioException(
requestOptions: RequestOptions(path: o.path),
type: DioExceptionType.connectionError,
);
}
return (200, cursorPageBody([sessionJson()]));
});
await pumpPage(tester, clientOf(adapter));
expect(find.textContaining('Network error'), findsOneWidget);
expect(find.byIcon(Icons.cloud_off), findsOneWidget);
failFirst = false;
await tester.tap(find.text('Retry'));
await tester.pumpAndSettle();
expect(find.textContaining('Network error'), findsNothing);
expect(find.textContaining('clientta'), findsOneWidget, reason: '重试后行渲染');
});
}