import 'package:flutter_test/flutter_test.dart'; import 'package:kouyu_english/core/courses/courses.dart'; import 'package:kouyu_english/core/app_state.dart'; import 'package:kouyu_english/core/models.dart'; import 'package:kouyu_english/core/courses/course_repository.dart'; /// Draft packs are loaded for editing and validation but are not learnable. void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUpAll(() async => CourseRepository.instance.load()); tearDownAll(() => CourseRepository.instance.resetForTest()); List byLevel(String level) => allLessons.where((lesson) => lesson.level == level).toList(); /// Drives [state] into the "A0 passed" state: enough usable/mastered core /// items to sit the assessment, plus two valid passes 24h+ apart. void makeA0Passed(AppState state) { for (final id in a0CoreItems.keys.take(48)) { state.mastery[id] = MasteryItem( id: id, label: a0CoreItems[id]!, status: MasteryStatus.master, evidence: const [EvidenceKind.independentSuccess], ); } final now = DateTime.now(); final allSkillsPass = { for (final skill in AssessmentSkill.values) skill: true, }; state.assessments ..add( AssessmentRecord( packId: 'A0-E1', completedAt: now.subtract(const Duration(days: 2)), results: allSkillsPass, ), ) ..add( AssessmentRecord( packId: 'A0-E2', completedAt: now.subtract(const Duration(hours: 1)), results: allSkillsPass, ), ); } test('the loaded content actually spans A0 → A1 → A2 → B1', () { expect(a0SeedLessons, isNotEmpty); expect(byLevel('A1'), isNotEmpty); expect(byLevel('A2'), isNotEmpty); expect(byLevel('B1'), isNotEmpty); // 48/30 graduation thresholds are only reachable if A0 teaches >=48 items. expect(a0CoreItems.length, greaterThanOrEqualTo(48)); }); test('finishing A0 does not unlock A1 until the A0 assessment is passed', () { final state = AppState(); final firstA1 = byLevel('A1').first; // All A0 lessons done, but the stage assessment not yet passed. for (final lesson in a0SeedLessons) { state.completedLessonIds.add(lesson.id); } expect(state.a0Passed, isFalse); expect( state.isLessonUnlocked(firstA1.id), isFalse, reason: 'A1 stays locked until A0 is passed', ); // Passing the A0 stage assessment does not bypass editorial approval. makeA0Passed(state); expect(state.a0Passed, isTrue); expect(state.isLessonUnlocked(firstA1.id), isFalse); }); test('draft packs stay locked even when prerequisites are complete', () { final state = AppState(); makeA0Passed(state); for (final lesson in a0SeedLessons) { state.completedLessonIds.add(lesson.id); } final a1 = byLevel('A1'); final a2 = byLevel('A2'); final b1 = byLevel('B1'); expect(state.isLessonUnlocked(a1.first.id), isFalse); expect(state.isLessonUnlocked(a1[1].id), isFalse); state.completedLessonIds.add(a1.first.id); expect(state.isLessonUnlocked(a1[1].id), isFalse); expect(state.isLessonUnlocked(a2.first.id), isFalse); for (final lesson in a1) { state.completedLessonIds.add(lesson.id); } expect( state.isLessonUnlocked(a2.first.id), isFalse, reason: 'Completing draft prerequisites must not open another draft', ); expect(state.isLessonUnlocked(b1.first.id), isFalse); for (final lesson in a2) { state.completedLessonIds.add(lesson.id); } expect( state.isLessonUnlocked(b1.first.id), isFalse, reason: 'B1 remains closed until approved', ); expect(state.nextIncompleteLessonId, isNull); expect(state.allLessonsComplete, isTrue); state.setPlacementStartLesson(a1.first.id); expect(state.placementStartLessonId, 'a0-01'); }); }