* feat: fetch languages directly from CMS - Switch language_repo.dart to fetch from CMS REST API (public, no auth) - Parse CMS paginated response format (docs[] envelope) - Rename getLanguages URL to cmsLanguages in urls.dart - Add 15 unit tests for CMS response parsing - Add design docs: course-plans, layout instructions * formatting --------- Co-authored-by: ggurdin <ggurdin@gmail.com>
216 lines
6.6 KiB
Dart
216 lines
6.6 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:fluffychat/pangea/languages/l2_support_enum.dart';
|
|
import 'package:fluffychat/pangea/languages/language_constants.dart';
|
|
import 'package:fluffychat/pangea/languages/language_model.dart';
|
|
|
|
void main() {
|
|
group('LanguageModel.fromJson', () {
|
|
test('parses full CMS response with all fields', () {
|
|
final json = {
|
|
'id': 42,
|
|
'language_code': 'es',
|
|
'language_name': 'Spanish',
|
|
'locale_emoji': '🇪🇸',
|
|
'l2_support': 'full',
|
|
'script': 'Latn',
|
|
'voices': ['es-ES-Standard-A', 'es-ES-Wavenet-B'],
|
|
'createdAt': '2026-01-01T00:00:00.000Z',
|
|
'updatedAt': '2026-01-01T00:00:00.000Z',
|
|
};
|
|
|
|
final model = LanguageModel.fromJson(json);
|
|
|
|
expect(model.langCode, 'es');
|
|
expect(model.displayName, 'Spanish');
|
|
expect(model.localeEmoji, '🇪🇸');
|
|
expect(model.l2Support, L2SupportEnum.full);
|
|
expect(model.script, 'Latn');
|
|
expect(model.voices, ['es-ES-Standard-A', 'es-ES-Wavenet-B']);
|
|
});
|
|
|
|
test('parses minimal fields (only required)', () {
|
|
final json = {'language_code': 'en', 'language_name': 'English'};
|
|
|
|
final model = LanguageModel.fromJson(json);
|
|
|
|
expect(model.langCode, 'en');
|
|
expect(model.displayName, 'English');
|
|
expect(model.l2Support, L2SupportEnum.na);
|
|
expect(model.script, LanguageKeys.unknownLanguage);
|
|
expect(model.localeEmoji, isNull);
|
|
expect(model.voices, isEmpty);
|
|
});
|
|
|
|
test('parses each l2_support level', () {
|
|
for (final entry in {
|
|
'full': L2SupportEnum.full,
|
|
'beta': L2SupportEnum.beta,
|
|
'alpha': L2SupportEnum.alpha,
|
|
'na': L2SupportEnum.na,
|
|
}.entries) {
|
|
final json = {
|
|
'language_code': 'xx',
|
|
'language_name': 'Test',
|
|
'l2_support': entry.key,
|
|
};
|
|
final model = LanguageModel.fromJson(json);
|
|
expect(model.l2Support, entry.value, reason: 'l2_support=${entry.key}');
|
|
}
|
|
});
|
|
|
|
test('defaults l2_support to na when null', () {
|
|
final json = {
|
|
'language_code': 'xx',
|
|
'language_name': 'Test',
|
|
'l2_support': null,
|
|
};
|
|
final model = LanguageModel.fromJson(json);
|
|
expect(model.l2Support, L2SupportEnum.na);
|
|
});
|
|
|
|
test('handles missing locale_emoji', () {
|
|
final json = {
|
|
'language_code': 'yue-HK',
|
|
'language_name': 'Cantonese',
|
|
'l2_support': 'beta',
|
|
'script': 'Hant',
|
|
};
|
|
final model = LanguageModel.fromJson(json);
|
|
expect(model.localeEmoji, isNull);
|
|
});
|
|
|
|
test('handles missing voices', () {
|
|
final json = {'language_code': 'fr', 'language_name': 'French'};
|
|
final model = LanguageModel.fromJson(json);
|
|
expect(model.voices, isEmpty);
|
|
});
|
|
|
|
test('handles null voices', () {
|
|
final json = {
|
|
'language_code': 'fr',
|
|
'language_name': 'French',
|
|
'voices': null,
|
|
};
|
|
final model = LanguageModel.fromJson(json);
|
|
expect(model.voices, isEmpty);
|
|
});
|
|
|
|
test('handles empty voices list', () {
|
|
final json = {
|
|
'language_code': 'fr',
|
|
'language_name': 'French',
|
|
'voices': <dynamic>[],
|
|
};
|
|
final model = LanguageModel.fromJson(json);
|
|
expect(model.voices, isEmpty);
|
|
});
|
|
|
|
test('parses text_direction when present', () {
|
|
final json = {
|
|
'language_code': 'ar',
|
|
'language_name': 'Arabic',
|
|
'text_direction': 'rtl',
|
|
'script': 'Arab',
|
|
};
|
|
// RTL should be parsed — TextDirection.rtl
|
|
final model = LanguageModel.fromJson(json);
|
|
expect(model.langCode, 'ar');
|
|
expect(model.script, 'Arab');
|
|
});
|
|
|
|
test('ignores extra CMS metadata fields', () {
|
|
// CMS returns id, createdAt, updatedAt, createdBy, updatedBy
|
|
// LanguageModel.fromJson should not break on them
|
|
final json = {
|
|
'id': 99,
|
|
'language_code': 'de',
|
|
'language_name': 'German',
|
|
'l2_support': 'full',
|
|
'script': 'Latn',
|
|
'locale_emoji': '🇩🇪',
|
|
'c_p_w': 5.2,
|
|
'createdAt': '2026-01-01T00:00:00.000Z',
|
|
'updatedAt': '2026-01-15T00:00:00.000Z',
|
|
'createdBy': {'id': 1, 'email': 'admin@example.com'},
|
|
'updatedBy': {'id': 1, 'email': 'admin@example.com'},
|
|
};
|
|
|
|
expect(() => LanguageModel.fromJson(json), returnsNormally);
|
|
final model = LanguageModel.fromJson(json);
|
|
expect(model.langCode, 'de');
|
|
expect(model.displayName, 'German');
|
|
});
|
|
});
|
|
|
|
group('LanguageModel.toJson', () {
|
|
test('round-trips through fromJson → toJson', () {
|
|
final original = {
|
|
'language_code': 'ja',
|
|
'language_name': 'Japanese',
|
|
'l2_support': 'beta',
|
|
'script': 'Jpan',
|
|
'locale_emoji': '🇯🇵',
|
|
'voices': ['ja-JP-Standard-A'],
|
|
};
|
|
|
|
final model = LanguageModel.fromJson(original);
|
|
final serialized = model.toJson();
|
|
|
|
expect(serialized['language_code'], 'ja');
|
|
expect(serialized['language_name'], 'Japanese');
|
|
expect(serialized['l2_support'], 'beta');
|
|
expect(serialized['script'], 'Jpan');
|
|
expect(serialized['locale_emoji'], '🇯🇵');
|
|
expect(serialized['voices'], ['ja-JP-Standard-A']);
|
|
});
|
|
|
|
test('serializes default values correctly', () {
|
|
final model = LanguageModel(langCode: 'sw', displayName: 'Swahili');
|
|
final json = model.toJson();
|
|
|
|
expect(json['language_code'], 'sw');
|
|
expect(json['language_name'], 'Swahili');
|
|
expect(json['l2_support'], 'na');
|
|
expect(json['script'], LanguageKeys.unknownLanguage);
|
|
expect(json['locale_emoji'], isNull);
|
|
expect(json['voices'], isEmpty);
|
|
});
|
|
});
|
|
|
|
group('LanguageModel properties', () {
|
|
test('l2 returns true for non-na support levels', () {
|
|
for (final level in [
|
|
L2SupportEnum.full,
|
|
L2SupportEnum.beta,
|
|
L2SupportEnum.alpha,
|
|
]) {
|
|
final model = LanguageModel(
|
|
langCode: 'xx',
|
|
displayName: 'Test',
|
|
l2Support: level,
|
|
);
|
|
expect(model.l2, isTrue, reason: 'l2Support=$level should be l2=true');
|
|
}
|
|
});
|
|
|
|
test('l2 returns false for na', () {
|
|
final model = LanguageModel(
|
|
langCode: 'xx',
|
|
displayName: 'Test',
|
|
l2Support: L2SupportEnum.na,
|
|
);
|
|
expect(model.l2, isFalse);
|
|
});
|
|
});
|
|
|
|
group('LanguageModel.unknown', () {
|
|
test('has correct defaults', () {
|
|
final unknown = LanguageModel.unknown;
|
|
expect(unknown.langCode, LanguageKeys.unknownLanguage);
|
|
expect(unknown.displayName, 'Unknown');
|
|
expect(unknown.l2Support, L2SupportEnum.na);
|
|
expect(unknown.l2, isFalse);
|
|
});
|
|
});
|
|
}
|