Files
photo-live/apps/mobile/test/app/locale_test.dart
T

173 lines
6.8 KiB
Dart
Raw Normal View History

2026-09-19 05:00:11 +08:00
/// 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<void> 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<ProviderContainer> 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);
});
});
}