/// 设置页 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 pumpHome( WidgetTester tester, ApiClient client, { TaskStore? taskStore, List extraOverrides = const [], }) 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'); }); }