앱 단위·화면·통합 테스트
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fancake/core/i18n/app_locale.dart';
|
||||
import 'package:fancake/features/my/my_data.dart';
|
||||
import 'package:fancake/features/my/settings_screen.dart';
|
||||
import 'package:fancake/l10n/app_localizations.dart'
|
||||
show lookupAppLocalizations;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../tool/gen_l10n_remote.dart';
|
||||
|
||||
Map<String, dynamic> _arb(String lang) =>
|
||||
jsonDecode(File('lib/l10n/app_$lang.arb').readAsStringSync())
|
||||
as Map<String, dynamic>;
|
||||
|
||||
Set<String> _keys(Map<String, dynamic> arb) =>
|
||||
arb.keys.where((k) => !k.startsWith('@')).toSet();
|
||||
|
||||
Widget _app(LocaleController c, Widget home) => ListenableBuilder(
|
||||
listenable: c,
|
||||
builder: (_, _) => LocaleScope(
|
||||
controller: c,
|
||||
child: MaterialApp(
|
||||
locale: c.locale,
|
||||
supportedLocales: appLocales,
|
||||
localizationsDelegates: [
|
||||
RemoteL10nDelegate(c.remote),
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
home: home,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void main() {
|
||||
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||
|
||||
group('지원 언어 (원장 B-76)', () {
|
||||
test('supportedLocales 가 정확히 ko · en · ja · zh-CN · es 다', () {
|
||||
expect(appLocales.map(localeTag).toList(), [
|
||||
'ko',
|
||||
'en',
|
||||
'ja',
|
||||
'zh-CN',
|
||||
'es',
|
||||
]);
|
||||
});
|
||||
|
||||
test('단말 언어로 기본값을 고르고 zh-TW·zh-Hant 는 zh-CN · 미지원 언어는 en 으로 폴백한다', () {
|
||||
expect(resolveAppLocale(const [Locale('ja', 'JP')]), const Locale('ja'));
|
||||
expect(
|
||||
resolveAppLocale(const [Locale('zh', 'TW')]),
|
||||
const Locale('zh', 'CN'),
|
||||
);
|
||||
expect(
|
||||
resolveAppLocale(const [
|
||||
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'),
|
||||
]),
|
||||
const Locale('zh', 'CN'),
|
||||
);
|
||||
expect(resolveAppLocale(const [Locale('fr', 'FR')]), const Locale('en'));
|
||||
expect(
|
||||
resolveAppLocale(const [Locale('fr'), Locale('es', 'MX')]),
|
||||
const Locale('es'),
|
||||
);
|
||||
});
|
||||
|
||||
test('5개 언어 arb 가 같은 키를 전부 가진다(QA 8-2 문자열 누락 0)', () {
|
||||
final ko = _keys(_arb('ko'));
|
||||
for (final lang in const ['en', 'ja', 'zh', 'es']) {
|
||||
final other = _keys(_arb(lang));
|
||||
expect(ko.difference(other), isEmpty, reason: '$lang 누락');
|
||||
expect(other.difference(ko), isEmpty, reason: '$lang 초과');
|
||||
for (final k in other) {
|
||||
expect(
|
||||
(_arb(lang)[k] as String).trim(),
|
||||
isNotEmpty,
|
||||
reason: '$lang.$k',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('원격 덮어쓰기 층이 arb 의 모든 키를 덮는다(생성물이 최신)', () {
|
||||
final generated = File(
|
||||
'lib/l10n/remote_app_localizations.dart',
|
||||
).readAsStringSync();
|
||||
final expected = generateRemoteL10n(_arb('ko'));
|
||||
for (final key in _keys(_arb('ko'))) {
|
||||
expect(expected, contains(' $key'));
|
||||
expect(generated, contains(' $key'), reason: '재생성 필요: $key');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group('언어 전환 · 원격 문자열 (FR-I18N-01)', () {
|
||||
testWidgets('설정의 언어 시트에서 English 를 고르면 화면 문자열이 바뀌고 기기에 남는다', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(1080, 2400);
|
||||
tester.view.devicePixelRatio = 2;
|
||||
addTearDown(tester.view.reset);
|
||||
String? saved;
|
||||
final c = LocaleController(
|
||||
initial: fallbackLocale,
|
||||
onSaved: (tag) async => saved = tag,
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
_app(c, SettingsScreen(repository: SampleMyRepository())),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('설정'), findsOneWidget);
|
||||
|
||||
await tester.scrollUntilVisible(find.text('한국어'), 200);
|
||||
await tester.tap(find.text('한국어'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('English'));
|
||||
await tester.tap(find.text('선택'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(c.locale, const Locale('en'));
|
||||
expect(find.text('Settings'), findsOneWidget);
|
||||
expect(saved, 'en');
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
expect(prefs.getString(LocaleController.prefsKey), 'en');
|
||||
});
|
||||
|
||||
testWidgets('원격 문서의 키는 번들 문자열을 덮고, 없는 키는 번들을 쓴다', (tester) async {
|
||||
final remote = StreamController<Map<String, Object?>>();
|
||||
addTearDown(remote.close);
|
||||
final c = LocaleController(
|
||||
initial: const Locale('ja'),
|
||||
remoteSource: (tag) {
|
||||
expect(tag, 'ja');
|
||||
return remote.stream;
|
||||
},
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
_app(
|
||||
c,
|
||||
Builder(
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
Text(context.l10n.settingsTitle),
|
||||
Text(context.l10n.commonNotYet('X')),
|
||||
Text(context.l10n.commonCancel),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('設定'), findsOneWidget);
|
||||
|
||||
remote.add({
|
||||
'settingsTitle': '設定(更新)',
|
||||
'commonNotYet': '{where} まもなく',
|
||||
'ignored': 3,
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('設定(更新)'), findsOneWidget);
|
||||
expect(find.text('X まもなく'), findsOneWidget);
|
||||
expect(find.text('キャンセル'), findsOneWidget);
|
||||
});
|
||||
|
||||
test('원격 문자열은 인자 값 안의 자리표시자를 다시 치환하지 않는다', () {
|
||||
final t = RemoteL10nDelegate(const {
|
||||
'linkTitle': '{name} 연동',
|
||||
'settingsLinkNotReady': '{name}: {items}',
|
||||
});
|
||||
return t.load(const Locale('ko')).then((l10n) {
|
||||
expect(l10n.linkTitle('{name}'), '{name} 연동');
|
||||
expect(l10n.settingsLinkNotReady('{items}', 'A'), '{items}: A');
|
||||
});
|
||||
});
|
||||
|
||||
test('언어를 복원하면 이전 언어의 원격 문자열을 버린다', () async {
|
||||
final remote = StreamController<Map<String, Object?>>.broadcast();
|
||||
addTearDown(remote.close);
|
||||
final c = LocaleController(
|
||||
initial: const Locale('ja'),
|
||||
remoteSource: (_) => remote.stream,
|
||||
);
|
||||
remote.add({'settingsTitle': '設定(更新)'});
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(c.remote, isNotEmpty);
|
||||
SharedPreferences.setMockInitialValues({LocaleController.prefsKey: 'es'});
|
||||
await c.restore();
|
||||
expect(c.locale, const Locale('es'));
|
||||
expect(c.remote, isEmpty);
|
||||
});
|
||||
|
||||
test('로그인 때 이 기기에서 고른 언어가 없으면 프로필 언어를 따른다', () async {
|
||||
final c = LocaleController(initial: const Locale('en'));
|
||||
await c.adoptProfile('ja');
|
||||
expect(c.locale, const Locale('ja'));
|
||||
});
|
||||
|
||||
test('이 기기에서 고른 언어가 있으면 프로필 언어보다 우선한다', () async {
|
||||
SharedPreferences.setMockInitialValues({LocaleController.prefsKey: 'es'});
|
||||
final c = LocaleController(initial: const Locale('es'));
|
||||
await c.adoptProfile('ja');
|
||||
expect(c.locale, const Locale('es'));
|
||||
});
|
||||
|
||||
test('저장된 언어를 복원한다', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
LocaleController.prefsKey: 'zh-CN',
|
||||
});
|
||||
final c = LocaleController(initial: fallbackLocale);
|
||||
await c.restore();
|
||||
expect(c.locale, const Locale('zh', 'CN'));
|
||||
});
|
||||
|
||||
test('대리자가 없는 트리는 ko 로 폴백한다', () {
|
||||
expect(koL10n.settingsTitle, '설정');
|
||||
expect(
|
||||
lookupAppLocalizations(const Locale('zh', 'CN')).settingsTitle,
|
||||
'设置',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user