/// 设置页 widget 测试(P3 计划 Task 3)。 /// /// 覆盖:修改密码入口跳 /password-change;退出登录 → 调 api.logout /// (尽力而为)→ 清空本地会话 → 清栈回 /login。 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/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, { List extraOverrides = const [], }) async { await tester.pumpWidget( ProviderScope( // Task 9 起主壳含队列页(触达 sessionAccountProvider/TaskStore), // 真实平台通道在测试环境挂起,必须注入替身。 overrides: [ ...pageTestOverrides(session: store, 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('退出登录 → api.logout + 清会话 + 清栈回 /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}'); }); await pumpHome(tester, clientOf(adapter)); await tester.tap(find.text('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('语言切换:底部弹层三选一 → 更新档位 + 持久化(P3 Task 10)', (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); expect(find.text('Follow system'), findsOneWidget, reason: '默认档位副标题'); await tester.tap(find.text('Language')); await tester.pumpAndSettle(); // 弹层三选项(语言名以自身语言书写,跨 locale 不变)。 // 「Follow system」出现两次:主档 tile 副标题 + 弹层选项。 expect(find.text('Follow system'), findsNWidgets(2)); expect(find.text('中文'), findsOneWidget); expect(find.text('English'), findsOneWidget); await tester.tap(find.text('中文')); await tester.pumpAndSettle(); expect(find.text('中文'), findsOneWidget, reason: '副标题更新为当前档位'); expect(localeStore.writes, ['zh'], reason: '选择写入 app.locale'); }); }