173 lines
6.8 KiB
Dart
173 lines
6.8 KiB
Dart
|
|
/// 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);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|