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 get loadedLessons => allLessons.where((lesson) => lesson.level != 'A0').toList(); List 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 = { 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'); }); }