/// App 内语言切换测试(P3 计划 Task 10)。 /// /// 覆盖: /// - 启动:持久档位注入 Notifier → 首帧即对应语言(无闪变); /// - 切换:setChoice → MaterialApp 即刻重建(登录页文案随切)+ 写持久层; /// - 解析矩阵(system 档):系统 zh → zh、系统 en → en、系统其他 → /// 回退模板语言 zh(l10n.yaml template=app_zh.arb); /// - 显式选择优先于系统 locale; /// - 持久值解析容错(null/非法值/存储异常 → system)。 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/app.dart'; import 'package:photo_live/app/locale_controller.dart'; import 'package:photo_live/core/api_client.dart'; import '../helpers/fakes.dart'; /// 写入即抛的存储替身(持久化失败容错断言用)。 class ThrowingWriteLocaleStore extends FakeLocaleStore { @override Future write(String value) async => throw StateError('disk full'); } void main() { group('持久值解析(纯逻辑)', () { test('fromPersisted:合法值/空值/非法值', () { expect(AppLocaleChoice.fromPersisted('system'), AppLocaleChoice.system); expect(AppLocaleChoice.fromPersisted('zh'), AppLocaleChoice.zh); expect(AppLocaleChoice.fromPersisted('en'), AppLocaleChoice.en); expect(AppLocaleChoice.fromPersisted(null), AppLocaleChoice.system); expect(AppLocaleChoice.fromPersisted('fr'), AppLocaleChoice.system, reason: '未来档位/手改损坏的旧值一律回退 system'); }); test('readPersisted:读取异常 → system(启动不因存储故障卡死)', () async { final store = ThrowingWriteLocaleStore(); final choice = await LocaleSettings.readPersisted(store); expect(choice, AppLocaleChoice.system); }); test('setChoice 写失败不回滚本次会话状态', () async { final container = ProviderContainer( overrides: [ localeSettingsProvider.overrideWith( () => LocaleSettings(store: ThrowingWriteLocaleStore()), ), ], ); addTearDown(container.dispose); await container .read(localeSettingsProvider.notifier) .setChoice(AppLocaleChoice.en); expect(container.read(localeSettingsProvider), AppLocaleChoice.en); }); }); group('PhotoLiveApp 语言接线(widget)', () { late FakeSessionStore session; late ApiClient client; setUp(() { // 无会话:App 停在登录页(文案断言载体);客户端拒绝一切请求 //(语言测试不触网,仅为满足 Provider 依赖图)。 session = FakeSessionStore(); client = ApiClient( sessionStore: session, dio: Dio(BaseOptions(baseUrl: 'http://test.local')) ..httpClientAdapter = FakeAdapter((o) => throw StateError('不应发起请求: ${o.path}')), ); }); Future pumpApp( WidgetTester tester, { AppLocaleChoice initial = AppLocaleChoice.system, FakeLocaleStore? localeStore, }) async { final store = localeStore ?? FakeLocaleStore(); await tester.pumpWidget( ProviderScope( overrides: [ ...pageTestOverrides(session: session, client: client), localeSettingsProvider.overrideWith( () => LocaleSettings(initial: initial, store: store), ), ], child: const PhotoLiveApp(), ), ); await tester.pumpAndSettle(); return ProviderScope.containerOf( tester.element(find.byType(PhotoLiveApp)), ); } testWidgets('持久档位 zh → 首帧中文文案', (tester) async { final store = FakeLocaleStore()..value = 'zh'; await pumpApp(tester, initial: AppLocaleChoice.zh, localeStore: store); expect(find.text('登录'), findsOneWidget); expect(find.text('Sign in'), findsNothing); }); testWidgets('切换 en → 登录页即刻英文 + 持久化 en', (tester) async { final store = FakeLocaleStore(); final container = await pumpApp(tester, initial: AppLocaleChoice.zh, localeStore: store); expect(find.text('登录'), findsOneWidget); await container .read(localeSettingsProvider.notifier) .setChoice(AppLocaleChoice.en); await tester.pumpAndSettle(); expect(find.text('Sign in'), findsOneWidget, reason: 'MaterialApp 已随 provider 重建'); expect(find.text('登录'), findsNothing); expect(store.writes, ['en'], reason: '写入持久层'); }); testWidgets('切换回 system → 交还系统 locale', (tester) async { tester.platformDispatcher.localesTestValue = const [Locale('en')]; addTearDown(tester.platformDispatcher.clearLocalesTestValue); final container = await pumpApp(tester, initial: AppLocaleChoice.zh); expect(find.text('登录'), findsOneWidget); await container .read(localeSettingsProvider.notifier) .setChoice(AppLocaleChoice.system); await tester.pumpAndSettle(); expect(find.text('Sign in'), findsOneWidget, reason: '系统 en 接管'); }); testWidgets('解析矩阵:system + 系统 zh → zh', (tester) async { tester.platformDispatcher.localesTestValue = const [Locale('zh')]; addTearDown(tester.platformDispatcher.clearLocalesTestValue); await pumpApp(tester); expect(find.text('登录'), findsOneWidget); }); testWidgets('解析矩阵:system + 系统 en → en', (tester) async { tester.platformDispatcher.localesTestValue = const [Locale('en')]; addTearDown(tester.platformDispatcher.clearLocalesTestValue); await pumpApp(tester); expect(find.text('Sign in'), findsOneWidget); }); testWidgets('解析矩阵:system + 系统其他(fr)→ 回退模板语言 zh', (tester) async { // Localizations 解析用 platformDispatcher.locales(首选语言列表), // 单值 localeTestValue 不影响它——须整体注入列表。 tester.platformDispatcher.localesTestValue = const [Locale('fr')]; addTearDown(tester.platformDispatcher.clearLocalesTestValue); await pumpApp(tester); expect(find.text('登录'), findsOneWidget, reason: '不依赖 supportedLocales 排序(默认解析会回退 en)'); }); testWidgets('显式选择优先于系统 locale(选 en + 系统 zh → en)', (tester) async { tester.platformDispatcher.localesTestValue = const [Locale('zh')]; addTearDown(tester.platformDispatcher.clearLocalesTestValue); await pumpApp(tester, initial: AppLocaleChoice.en); expect(find.text('Sign in'), findsOneWidget); }); }); }