133 lines
4.9 KiB
Dart
133 lines
4.9 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:kouyu_english/core/courses/courses.dart';
|
|
import 'package:kouyu_english/core/courses/course_repository.dart';
|
|
|
|
List<SeedLesson> get loadedLessons =>
|
|
allLessons.where((lesson) => lesson.level != 'A0').toList();
|
|
List<DialogueScene> get loadedScenes =>
|
|
allScenes.where((scene) => !scene.id.startsWith('a0-')).toList();
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
setUp(() => CourseRepository.instance.resetForTest());
|
|
tearDown(() => CourseRepository.instance.resetForTest());
|
|
|
|
test('loads every unit and adapts it into the runtime registries', () async {
|
|
await CourseRepository.instance.load();
|
|
final units = CourseRepository.instance.units;
|
|
|
|
// The course map lists 10 A0 + 21 A1 + 15 A2 + 14 B1 units.
|
|
expect(units.length, 60);
|
|
expect(units.where((u) => u.level == 'A0').length, 10);
|
|
expect(units.where((u) => u.level == 'A1').length, 21);
|
|
expect(units.where((u) => u.level == 'A2').length, 15);
|
|
expect(units.where((u) => u.level == 'B1').length, 14);
|
|
|
|
// Unit ids are unique.
|
|
expect(units.map((u) => u.id).toSet().length, units.length);
|
|
|
|
// Adapted lessons are appended after A0 with continuous numbering.
|
|
expect(loadedLessons.length, 50);
|
|
expect(loadedLessons.first.number, a0SeedLessons.length + 1);
|
|
expect(loadedLessons.last.number, a0SeedLessons.length + 50);
|
|
expect(allLessons.length, a0SeedLessons.length + 50);
|
|
|
|
for (final lesson in loadedLessons) {
|
|
expect(
|
|
lesson.segments,
|
|
isNotEmpty,
|
|
reason: '${lesson.id} has no segments',
|
|
);
|
|
expect(lesson.targetItemIds, isNotEmpty);
|
|
expect(lesson.vocabulary, isNotEmpty);
|
|
// Every adapted lesson resolves through the shared accessors.
|
|
expect(lessonById(lesson.id).id, lesson.id);
|
|
for (final segment in lesson.segments) {
|
|
expect(findLessonSegment(segment.id)?.id, segment.id);
|
|
final activity = activityBySegmentId(segment.id);
|
|
expect(
|
|
activity.listening.isNotEmpty || activity.reading.isNotEmpty,
|
|
isTrue,
|
|
reason: '${segment.id} has empty listening and reading',
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test(
|
|
'core items register their labels, mastery flag and match rules',
|
|
() async {
|
|
await CourseRepository.instance.load();
|
|
final unit = CourseRepository.instance.unitById('A1-U01')!;
|
|
expect(unit.coreItems, isNotEmpty);
|
|
for (final item in unit.coreItems) {
|
|
expect(isCoreItem(item.id), isTrue);
|
|
expect(coreItemLabel(item.id), item.zh);
|
|
expect(coreItemEnglish(item.id), item.en);
|
|
expect(coreItemMatch[item.id], item.match);
|
|
expect(coreReviewTemplates[item.id], isNotNull);
|
|
}
|
|
},
|
|
);
|
|
|
|
test('each unit main scene becomes a free-practice scene', () async {
|
|
await CourseRepository.instance.load();
|
|
final expected = CourseRepository.instance.units
|
|
.where((u) => u.level != 'A0' && u.mainScene != null)
|
|
.length;
|
|
expect(loadedScenes.length, expected);
|
|
for (final scene in loadedScenes) {
|
|
expect(scene.unlockAfterLessonId, isNotNull);
|
|
expect(scene.script.prompts, isNotEmpty);
|
|
// The scene is reachable through the shared accessor.
|
|
expect(sceneById(scene.id).id, scene.id);
|
|
}
|
|
});
|
|
|
|
test('listening/reading materials parse with answerable questions', () async {
|
|
await CourseRepository.instance.load();
|
|
final withMaterials = CourseRepository.instance.units
|
|
.where((u) => u.materials.isNotEmpty)
|
|
.toList();
|
|
expect(withMaterials, isNotEmpty);
|
|
for (final unit in withMaterials) {
|
|
for (final material in unit.materials) {
|
|
expect(material.text, isNotEmpty);
|
|
for (final question in material.questions) {
|
|
expect(question.options, isNotEmpty);
|
|
// The first option is the correct answer by contract.
|
|
expect(question.answer, question.options.first);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('load is idempotent', () async {
|
|
await CourseRepository.instance.load();
|
|
final count = loadedLessons.length;
|
|
await CourseRepository.instance.load();
|
|
expect(loadedLessons.length, count);
|
|
expect(CourseRepository.instance.units.length, 60);
|
|
});
|
|
|
|
test('AI word list covers only the recent units, in English', () async {
|
|
await CourseRepository.instance.load();
|
|
final b1 = loadedLessons.last;
|
|
final words = taughtLanguageUpTo(b1.id);
|
|
final window = loadedLessons.sublist(
|
|
loadedLessons.length - taughtLanguageUnitWindow,
|
|
);
|
|
final expected = <String>{
|
|
for (final lesson in window)
|
|
for (final id in lesson.targetItemIds) coreItemEnglish(id),
|
|
};
|
|
expect(words, expected.toList());
|
|
expect(words.any((w) => RegExp(r'[\u4e00-\u9fff]').hasMatch(w)), isFalse);
|
|
// The first A1 unit does not pull in A0.
|
|
final firstA1 = taughtLanguageUpTo(loadedLessons.first.id);
|
|
expect(firstA1.any(a0CoreItems.values.contains), isFalse);
|
|
expect(lessonLevel(b1.id), 'B1');
|
|
});
|
|
}
|