2026-09-22 02:06:23 +08:00
|
|
|
|
/// 服务端上传历史页 widget 测试(UI-03 页面 16;A19 语义)。
|
2026-09-19 02:50:40 +08:00
|
|
|
|
///
|
2026-09-22 02:06:23 +08:00
|
|
|
|
/// 覆盖:标题/副标题(跨设备口径)与 A19 数据源区分横幅、行内容
|
|
|
|
|
|
/// (clientTaskId 短码/KB/状态徽章/时间 · 设备来源安全回退)、相册与
|
|
|
|
|
|
/// 日期客户端筛选、下拉刷新、游标加载更多、加载更多失败页脚重试、
|
|
|
|
|
|
/// 首屏错误重试。
|
2026-09-19 02:50:40 +08:00
|
|
|
|
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');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-22 02:06:23 +08:00
|
|
|
|
/// 页面 watch albumListProvider(相册筛选候选)——适配器统一供给
|
|
|
|
|
|
/// GET /v1/albums;/v1/uploads 路由由各用例注入。
|
|
|
|
|
|
FakeAdapter adapterOf(
|
|
|
|
|
|
(int, Object?) Function(RequestOptions options) uploads, {
|
|
|
|
|
|
List<Map<String, dynamic>> albums = const <Map<String, dynamic>>[],
|
|
|
|
|
|
}) =>
|
|
|
|
|
|
FakeAdapter((o) {
|
|
|
|
|
|
if (o.method == 'GET' && o.path == '/v1/albums') {
|
|
|
|
|
|
return (200, cursorPageBody(albums));
|
|
|
|
|
|
}
|
|
|
|
|
|
return uploads(o);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-19 02:50:40 +08:00
|
|
|
|
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,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-09-22 02:06:23 +08:00
|
|
|
|
/// 仅 /v1/uploads 请求(剥离相册列表请求)。
|
|
|
|
|
|
Iterable<RecordedRequest> uploadsRequests(FakeAdapter adapter) =>
|
|
|
|
|
|
adapter.requests.where((r) => r.path == '/v1/uploads');
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('标题/副标题(跨设备口径)+ A19 横幅 + 行内容与设备来源回退',
|
2026-09-19 02:50:40 +08:00
|
|
|
|
(tester) async {
|
2026-09-22 02:06:23 +08:00
|
|
|
|
final adapter = adapterOf((o) {
|
2026-09-19 02:50:40 +08:00
|
|
|
|
if (o.method == 'GET' && o.path == '/v1/uploads') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
200,
|
|
|
|
|
|
cursorPageBody([
|
|
|
|
|
|
sessionJson(clientTaskId: 'owndevice0000000000000000000001'),
|
|
|
|
|
|
sessionJson(
|
2026-09-22 02:06:23 +08:00
|
|
|
|
uploadId: 'upl-0002',
|
|
|
|
|
|
clientTaskId: 'otherdev000000000000000000000002',
|
2026-09-19 02:50:40 +08:00
|
|
|
|
state: 'pending',
|
|
|
|
|
|
processingState: 'processing',
|
|
|
|
|
|
expectedBytes: 5 * 1024 * 1024,
|
|
|
|
|
|
sourceDeviceId: _kOtherDevice,
|
|
|
|
|
|
),
|
2026-09-22 02:06:23 +08:00
|
|
|
|
sessionJson(
|
|
|
|
|
|
uploadId: 'upl-0003',
|
|
|
|
|
|
clientTaskId: 'nodevice0000000000000000000000003',
|
|
|
|
|
|
state: 'expired',
|
|
|
|
|
|
processingState: 'processing_failed',
|
|
|
|
|
|
expectedBytes: 4096,
|
|
|
|
|
|
sourceDeviceId: null,
|
|
|
|
|
|
),
|
2026-09-19 02:50:40 +08:00
|
|
|
|
]),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
throw StateError('unexpected ${o.method} ${o.path}');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await pumpPage(tester, clientOf(adapter));
|
|
|
|
|
|
|
2026-09-22 02:06:23 +08:00
|
|
|
|
// 页面 16:标题 + 跨设备副标题(与本地队列口径区分)。
|
|
|
|
|
|
expect(find.text('Server records'), findsOneWidget);
|
|
|
|
|
|
expect(find.text('All devices · Your account'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
// A19 数据源区分横幅(本机待传队列入口指向「上传」页)。
|
|
|
|
|
|
expect(find.textContaining('all of your devices'), findsOneWidget);
|
2026-09-21 17:20:43 +08:00
|
|
|
|
expect(find.textContaining('Uploads tab'), findsOneWidget);
|
2026-09-19 02:50:40 +08:00
|
|
|
|
|
|
|
|
|
|
// 行:clientTaskId 前 8 位 + KB(2048B → 2 KB;5MB → 5120 KB)。
|
2026-09-22 02:06:23 +08:00
|
|
|
|
expect(find.textContaining('owndevic ·'), findsOneWidget);
|
2026-09-19 02:50:40 +08:00
|
|
|
|
expect(find.textContaining('2 KB'), findsOneWidget);
|
2026-09-22 02:06:23 +08:00
|
|
|
|
expect(find.textContaining('otherdev ·'), findsOneWidget);
|
2026-09-19 02:50:40 +08:00
|
|
|
|
expect(find.textContaining('5120 KB'), findsOneWidget);
|
2026-09-22 02:06:23 +08:00
|
|
|
|
expect(find.textContaining('nodevice · 4 KB'), findsOneWidget);
|
2026-09-19 02:50:40 +08:00
|
|
|
|
|
2026-09-22 02:06:23 +08:00
|
|
|
|
// 状态徽章(会话状态 + 处理状态;页面 16 专用文案)。
|
2026-09-19 02:50:40 +08:00
|
|
|
|
expect(find.text('Confirmed'), findsOneWidget);
|
|
|
|
|
|
expect(find.text('Pending'), findsOneWidget);
|
|
|
|
|
|
expect(find.text('Ready'), findsOneWidget);
|
|
|
|
|
|
expect(find.text('Processing'), findsOneWidget);
|
2026-09-22 02:06:23 +08:00
|
|
|
|
expect(find.text('Expired'), findsOneWidget);
|
|
|
|
|
|
expect(find.text('Processing failed'), findsOneWidget);
|
2026-09-19 02:50:40 +08:00
|
|
|
|
|
2026-09-22 02:06:23 +08:00
|
|
|
|
// 设备来源安全回退:本机 → This device;他机/缺失 → Unknown device
|
|
|
|
|
|
//(不展示裸设备 ID)。
|
|
|
|
|
|
expect(find.textContaining('This device'), findsOneWidget);
|
|
|
|
|
|
expect(find.textContaining('Unknown device'), findsNWidgets(2));
|
|
|
|
|
|
expect(
|
|
|
|
|
|
find.textContaining(_kOtherDevice.substring(0, 8)),
|
|
|
|
|
|
findsNothing,
|
|
|
|
|
|
reason: '契约无设备名——不展示裸设备 ID',
|
|
|
|
|
|
);
|
2026-09-19 02:50:40 +08:00
|
|
|
|
|
|
|
|
|
|
// listUploads 请求带分页 limit=20。
|
2026-09-22 02:06:23 +08:00
|
|
|
|
expect(
|
|
|
|
|
|
uploadsRequests(adapter).single.uri.queryParameters['limit'],
|
|
|
|
|
|
'20',
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('相册筛选(候选来自相册列表;客户端过滤已加载记录)',
|
|
|
|
|
|
(tester) async {
|
|
|
|
|
|
final adapter = adapterOf(
|
|
|
|
|
|
(o) {
|
|
|
|
|
|
if (o.method == 'GET' && o.path == '/v1/uploads') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
200,
|
|
|
|
|
|
cursorPageBody([
|
|
|
|
|
|
sessionJson(
|
|
|
|
|
|
albumId: 'alb-1',
|
|
|
|
|
|
clientTaskId: 'galarow00000000000000000000000001',
|
|
|
|
|
|
),
|
|
|
|
|
|
sessionJson(
|
|
|
|
|
|
albumId: 'alb-2',
|
|
|
|
|
|
clientTaskId: 'wedrow000000000000000000000000002',
|
|
|
|
|
|
),
|
|
|
|
|
|
]),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
throw StateError('unexpected ${o.method} ${o.path}');
|
|
|
|
|
|
},
|
|
|
|
|
|
albums: [
|
|
|
|
|
|
albumJson(),
|
|
|
|
|
|
albumJson(
|
|
|
|
|
|
id: 'alb-2',
|
|
|
|
|
|
publicToken: 'sharetoken000000000002',
|
|
|
|
|
|
translations: [
|
|
|
|
|
|
<String, dynamic>{
|
|
|
|
|
|
'locale': 'zh-CN',
|
|
|
|
|
|
'title': '婚礼当天',
|
|
|
|
|
|
'description': null,
|
|
|
|
|
|
'venue': null,
|
|
|
|
|
|
'address': null,
|
|
|
|
|
|
},
|
|
|
|
|
|
<String, dynamic>{
|
|
|
|
|
|
'locale': 'en',
|
|
|
|
|
|
'title': 'Wedding Day',
|
|
|
|
|
|
'description': null,
|
|
|
|
|
|
'venue': null,
|
|
|
|
|
|
'address': null,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
await pumpPage(tester, clientOf(adapter));
|
|
|
|
|
|
expect(find.textContaining('galarow'), findsOneWidget);
|
|
|
|
|
|
expect(find.textContaining('wedrow'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
// 打开相册筛选菜单(默认 All albums)→ 选择 Wedding Day。
|
|
|
|
|
|
await tester.tap(find.text('All albums'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
await tester.tap(find.text('Wedding Day'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
// 仅保留 alb-2 记录;筛选入口标签随选择更新。
|
|
|
|
|
|
expect(find.textContaining('wedrow'), findsOneWidget);
|
|
|
|
|
|
expect(find.textContaining('galarow'), findsNothing);
|
|
|
|
|
|
expect(find.text('Wedding Day'), findsOneWidget, reason: '药丸标签');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('日期筛选(Today:仅保留今天创建的记录)', (tester) async {
|
|
|
|
|
|
final now = DateTime.now().toUtc().toIso8601String();
|
|
|
|
|
|
final adapter = adapterOf((o) {
|
|
|
|
|
|
if (o.method == 'GET' && o.path == '/v1/uploads') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
200,
|
|
|
|
|
|
cursorPageBody([
|
|
|
|
|
|
sessionJson(
|
|
|
|
|
|
clientTaskId: 'todayrow0000000000000000000000001',
|
|
|
|
|
|
createdAt: now,
|
|
|
|
|
|
),
|
|
|
|
|
|
sessionJson(
|
|
|
|
|
|
uploadId: 'upl-0002',
|
|
|
|
|
|
clientTaskId: 'oldrow00000000000000000000000002',
|
|
|
|
|
|
createdAt: '2026-09-01T00:00:00Z',
|
|
|
|
|
|
),
|
|
|
|
|
|
]),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
throw StateError('unexpected ${o.method} ${o.path}');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await pumpPage(tester, clientOf(adapter));
|
|
|
|
|
|
expect(find.textContaining('todayrow'), findsOneWidget);
|
|
|
|
|
|
expect(find.textContaining('oldrow'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
// 打开日期筛选菜单(默认 Any time)→ 选择 Today。
|
|
|
|
|
|
await tester.tap(find.text('Any time'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
await tester.tap(find.text('Today'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.textContaining('todayrow'), findsOneWidget);
|
|
|
|
|
|
expect(find.textContaining('oldrow'), findsNothing);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('下拉刷新重取第一页', (tester) async {
|
|
|
|
|
|
var first = true;
|
|
|
|
|
|
final adapter = adapterOf((o) {
|
|
|
|
|
|
if (o.method == 'GET' && o.path == '/v1/uploads') {
|
|
|
|
|
|
return first
|
|
|
|
|
|
? (
|
|
|
|
|
|
200,
|
|
|
|
|
|
cursorPageBody([
|
|
|
|
|
|
sessionJson(
|
|
|
|
|
|
clientTaskId: 'firstrow000000000000000000000001',
|
|
|
|
|
|
),
|
|
|
|
|
|
]),
|
|
|
|
|
|
)
|
|
|
|
|
|
: (
|
|
|
|
|
|
200,
|
|
|
|
|
|
cursorPageBody([
|
|
|
|
|
|
sessionJson(
|
|
|
|
|
|
clientTaskId: 'refreshr000000000000000000000002',
|
|
|
|
|
|
),
|
|
|
|
|
|
]),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
throw StateError('unexpected ${o.method} ${o.path}');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await pumpPage(tester, clientOf(adapter));
|
|
|
|
|
|
expect(find.textContaining('firstrow'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
first = false;
|
|
|
|
|
|
await tester.fling(
|
|
|
|
|
|
find.byType(ListView),
|
|
|
|
|
|
const Offset(0, 300),
|
|
|
|
|
|
600,
|
|
|
|
|
|
);
|
|
|
|
|
|
await tester.pump();
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.textContaining('refreshr'), findsOneWidget);
|
|
|
|
|
|
expect(find.textContaining('firstrow'), findsNothing);
|
2026-09-19 02:50:40 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('游标加载更多(滚动触底 → 第二页带 cursor)', (tester) async {
|
|
|
|
|
|
final page1 = List<Map<String, dynamic>>.generate(
|
|
|
|
|
|
20,
|
|
|
|
|
|
(i) => sessionJson(
|
|
|
|
|
|
uploadId: 'upl-p1-$i',
|
|
|
|
|
|
clientTaskId: 'page1row$i.padLeft-000000000000000000001',
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
2026-09-22 02:06:23 +08:00
|
|
|
|
final adapter = adapterOf((o) {
|
2026-09-19 02:50:40 +08:00
|
|
|
|
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));
|
2026-09-22 02:06:23 +08:00
|
|
|
|
expect(uploadsRequests(adapter), hasLength(1), reason: '首页无 cursor');
|
2026-09-19 02:50:40 +08:00
|
|
|
|
|
|
|
|
|
|
await tester.drag(find.byType(ListView), const Offset(0, -6000));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
2026-09-22 02:06:23 +08:00
|
|
|
|
expect(
|
|
|
|
|
|
uploadsRequests(adapter).last.uri.queryParameters['cursor'],
|
|
|
|
|
|
'hc1',
|
|
|
|
|
|
);
|
2026-09-19 02:50:40 +08:00
|
|
|
|
expect(find.textContaining('page2ite'), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-22 02:06:23 +08:00
|
|
|
|
testWidgets('加载更多失败 → 页脚重试行;重试恢复追加第二页', (tester) async {
|
|
|
|
|
|
final page1 = List<Map<String, dynamic>>.generate(
|
|
|
|
|
|
20,
|
|
|
|
|
|
(i) => sessionJson(
|
|
|
|
|
|
uploadId: 'upl-p1-$i',
|
|
|
|
|
|
clientTaskId: 'page1row$i.padLeft-000000000000000000001',
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
var failMore = true;
|
|
|
|
|
|
final adapter = adapterOf((o) {
|
|
|
|
|
|
final cursor = o.uri.queryParameters['cursor'];
|
|
|
|
|
|
if (cursor == null) {
|
|
|
|
|
|
return (200, cursorPageBody(page1, nextCursor: 'hc1'));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (failMore) {
|
|
|
|
|
|
failMore = false;
|
2026-09-19 02:50:40 +08:00
|
|
|
|
throw DioException(
|
|
|
|
|
|
requestOptions: RequestOptions(path: o.path),
|
|
|
|
|
|
type: DioExceptionType.connectionError,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-09-22 02:06:23 +08:00
|
|
|
|
return (
|
|
|
|
|
|
200,
|
|
|
|
|
|
cursorPageBody(
|
|
|
|
|
|
[sessionJson(clientTaskId: 'page2item000000000000000000000001')],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await pumpPage(tester, clientOf(adapter));
|
|
|
|
|
|
|
|
|
|
|
|
await tester.drag(find.byType(ListView), const Offset(0, -6000));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
// 首屏成功不整体报错:仅页脚错误说明 + 重试(页面 16)。
|
|
|
|
|
|
expect(find.textContaining('Network error'), findsOneWidget);
|
|
|
|
|
|
expect(find.byIcon(Icons.cloud_off), findsOneWidget);
|
|
|
|
|
|
expect(find.text('Retry'), findsOneWidget);
|
|
|
|
|
|
|
|
|
|
|
|
await tester.tap(find.text('Retry'));
|
|
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
|
|
|
|
|
|
|
|
expect(find.textContaining('Network error'), findsNothing);
|
|
|
|
|
|
expect(find.textContaining('page2ite'), findsOneWidget);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
testWidgets('首屏加载失败展示错误与重试;恢复后重试成功', (tester) async {
|
|
|
|
|
|
var failFirst = true;
|
|
|
|
|
|
final adapter = adapterOf((o) {
|
|
|
|
|
|
if (o.method == 'GET' && o.path == '/v1/uploads') {
|
|
|
|
|
|
if (failFirst) {
|
|
|
|
|
|
throw DioException(
|
|
|
|
|
|
requestOptions: RequestOptions(path: o.path),
|
|
|
|
|
|
type: DioExceptionType.connectionError,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return (200, cursorPageBody([sessionJson()]));
|
|
|
|
|
|
}
|
|
|
|
|
|
throw StateError('unexpected ${o.method} ${o.path}');
|
2026-09-19 02:50:40 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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: '重试后行渲染');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|