Files
photo-live/apps/mobile/test/pages/settings_tab_test.dart
T
2026-09-22 00:28:39 +08:00

227 lines
8.7 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 3;UI-03 页面 02 重设计后更新)。
///
/// 覆盖:修改密码入口跳 /password-change;退出登录(有进行中任务 →
/// 确认弹窗任务保留说明)→ 调 api.logout(尽力而为)→ 清空本地会话 →
/// 清栈回 /login;账号摘要卡 + 通用分组 + 上传帮助面板 + 版本;
/// 语言行打开共享面板 Done 提交。
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/locale_controller.dart';
import 'package:photo_live/core/api_client.dart';
import 'package:photo_live/core/session_store.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/home_page.dart';
import 'package:photo_live/pages/login_page.dart';
import 'package:photo_live/pages/password_change_page.dart';
import '../helpers/fakes.dart';
void main() {
late FakeSessionStore store;
setUp(() {
store = FakeSessionStore();
store.session = Session(
token: 'tok-live-1',
expiresAt: DateTime.utc(2099, 1, 1),
account: 'staff01',
);
});
ApiClient clientOf(FakeAdapter adapter) => ApiClient(
sessionStore: store,
dio: Dio(BaseOptions(baseUrl: 'http://test.local'))
..httpClientAdapter = adapter,
);
Future<void> pumpHome(
WidgetTester tester,
ApiClient client, {
TaskStore? taskStore,
List<dynamic> extraOverrides = const <dynamic>[],
}) async {
await tester.pumpWidget(
ProviderScope(
// Task 9 起主壳含队列页(触达 sessionAccountProvider/TaskStore),
// 真实平台通道在测试环境挂起,必须注入替身。
overrides: [
...pageTestOverrides(
session: store,
taskStore: taskStore,
client: client,
),
...extraOverrides,
],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomePage(),
routes: {
'/login': (context) => const LoginPage(),
'/password-change': (context) => const PasswordChangePage(),
},
),
),
);
await tester.pumpAndSettle();
// 切到设置 tab(底部导航第三项)。
await tester.tap(find.byIcon(Icons.settings_outlined));
await tester.pumpAndSettle();
}
testWidgets('修改密码入口 → 跳 /password-change', (tester) async {
final adapter = FakeAdapter((_) => throw StateError('不应发起请求'));
await pumpHome(tester, clientOf(adapter));
await tester.tap(find.text('Change password'));
await tester.pumpAndSettle();
expect(find.byType(PasswordChangePage), findsOneWidget);
});
testWidgets('退出登录:确认弹窗(本机任务保留说明)→ 确认后清会话回 /login',
(tester) async {
// 服务端撤销失败(500)也照样本地登出——尽力而为语义。
// 首页「相册」页签(Task 4)进入即自动拉列表,需一并兜底路由。
final adapter = FakeAdapter((o) {
if (o.path == '/v1/auth/logout') return (500, errorBody('INTERNAL_ERROR'));
if (o.path == '/v1/albums') return (500, errorBody('INTERNAL_ERROR'));
throw StateError('unexpected ${o.path}');
});
// 一个排队中任务:退出确认走「任务保留」文案(UI-03 有任务分支)。
final taskStore = TaskStore.memory();
final task = await taskStore.create(UploadTaskDraft(
id: 't-1',
accountId: 'staff01',
albumId: 'alb-1',
clientTaskId: 'ct-t-1',
sourceFilePath: '/tmp/t-1.jpg',
filename: 't-1.jpg',
bytes: 10,
contentType: 'image/jpeg',
));
await taskStore.updateState(task.id, UploadTaskState.queued);
await pumpHome(tester, clientOf(adapter), taskStore: taskStore);
// 等任务计数防抖载入(未结束任务 → 退出确认带保留说明)。
await tester.pump(const Duration(milliseconds: 450));
// 点列表项 → 先弹确认(明确本机任务保留,不承诺后台继续)。
await tester.ensureVisible(find.text('Sign out'));
await tester.pumpAndSettle();
await tester.tap(find.text('Sign out'));
await tester.pumpAndSettle();
expect(find.text('Sign out?'), findsOneWidget);
expect(
find.textContaining('preserved on this device'),
findsOneWidget,
reason: '确认文案明确本地任务保留、重新登录后继续',
);
// 相册页签进入即拉列表(见上注释),此处只约束未发生 logout。
expect(
adapter.requests.where((r) => r.path == '/v1/auth/logout'),
isEmpty,
reason: '未确认不调 logout',
);
// 确认(弹窗内 FilledButton 与列表项同名,按按钮类型定位)。
await tester.tap(find.widgetWithText(FilledButton, 'Sign out'));
await tester.pumpAndSettle();
final logout = adapter.requests.singleWhere(
(r) => r.path == '/v1/auth/logout',
);
expect(logout.method, 'POST');
expect(store.session, isNull, reason: '本地会话必须清除');
expect(find.byType(LoginPage), findsOneWidget);
expect(find.byType(HomePage), findsNothing, reason: '主壳已出栈');
});
testWidgets('账号摘要卡 + 通用分组 + 上传帮助面板 + 版本(UI-03 页面 02)',
(tester) async {
final adapter = FakeAdapter((_) => throw StateError('不应发起请求'));
await pumpHome(tester, clientOf(adapter));
// 两个分组标题(SectionHeader 文案)。
for (final section in ['Account', 'General']) {
expect(find.text(section), findsOneWidget, reason: '分组「$section」');
}
// 账号摘要卡:当前账号 + 说明副标题(非编辑入口)。
expect(find.text('staff01'), findsOneWidget);
expect(find.text('Currently signed-in account'), findsOneWidget);
expect(find.descendant(
of: find.byType(CircleAvatar),
matching: find.text('S'),
), findsOneWidget, reason: '首字母头像占位');
// 通用分组四行。
expect(find.text('Language'), findsOneWidget);
expect(find.text('Change password'), findsOneWidget);
expect(find.text('Upload help'), findsOneWidget);
expect(find.text('Version'), findsOneWidget);
expect(find.text(kAppVersion), findsOneWidget);
// 上传帮助:底部面板说明系统限制 / 强退 / 网络等待(不承诺
// 「后台上传已开启」)。
await tester.tap(find.text('Upload help'));
await tester.pumpAndSettle();
expect(find.text('How background uploads work'), findsOneWidget);
expect(find.textContaining('battery limits'), findsOneWidget);
expect(
find.textContaining('start automatically once a connection is available'),
findsOneWidget,
);
});
testWidgets('语言切换:共享面板临时选中 + Done 提交 → 更新档位 + 持久化',
(tester) async {
final adapter = FakeAdapter((_) => throw StateError('不应发起请求'));
final localeStore = FakeLocaleStore();
await pumpHome(
tester,
clientOf(adapter),
extraOverrides: [
localeSettingsProvider.overrideWith(
() => LocaleSettings(store: localeStore),
),
],
);
// 本 harness 的 MaterialApp 固定 en(live 全 App 重建在
// app/locale_test.dart 的 PhotoLiveApp 用例覆盖)。
expect(find.text('Language'), findsOneWidget, reason: '语言行标题');
expect(
find.text('Follow system'),
findsOneWidget,
reason: '默认档位(行尾当前值)',
);
// 点语言行打开共享弹层。
await tester.tap(find.text('Language'));
await tester.pumpAndSettle();
// 弹层三选项(语言名以自身语言书写,跨 locale 不变)。
// 「Follow system」出现两次:语言行尾当前值 + 弹层选项。
expect(find.text('Follow system'), findsNWidgets(2));
expect(find.text('中文'), findsOneWidget);
expect(find.text('English'), findsOneWidget);
expect(find.text('Done'), findsOneWidget);
// 临时选中 + Done 提交(未 Done 前不落档位)。
await tester.tap(find.text('中文'));
await tester.pump();
expect(localeStore.writes, isEmpty, reason: 'Done 前不写持久层');
await tester.tap(find.text('Done'));
await tester.pumpAndSettle();
expect(find.text('中文'), findsOneWidget, reason: '行尾更新为当前档位');
expect(localeStore.writes, ['zh'], reason: '选择写入 app.locale');
});
}