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'; /// The A0 → A1 → A2 → B1 unlock chain. Crossing out of A0 into A1 additionally /// requires passing the A0 stage assessment; higher levels then chain linearly /// (finishing the previous unit unlocks the next), with no extra assessment. 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 opens the first A1 unit. makeA0Passed(state); expect(state.a0Passed, isTrue); expect(state.isLessonUnlocked(firstA1.id), isTrue); }); test('A1 → A2 → B1 chain linearly by finishing the previous unit', () { 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'); // A1 opened, but only the first unit; the second waits on the first. expect(state.isLessonUnlocked(a1.first.id), isTrue); expect(state.isLessonUnlocked(a1[1].id), isFalse); // Finishing the first A1 unit unlocks the second — no extra gate. state.completedLessonIds.add(a1.first.id); expect(state.isLessonUnlocked(a1[1].id), isTrue); // A2 stays locked until the last A1 unit is finished. expect(state.isLessonUnlocked(a2.first.id), isFalse); for (final lesson in a1) { state.completedLessonIds.add(lesson.id); } expect( state.isLessonUnlocked(a2.first.id), isTrue, reason: 'A1 → A2 chains linearly, no separate assessment', ); // B1 likewise unlocks only after the last A2 unit. expect(state.isLessonUnlocked(b1.first.id), isFalse); for (final lesson in a2) { state.completedLessonIds.add(lesson.id); } expect( state.isLessonUnlocked(b1.first.id), isTrue, reason: 'A2 → B1 chains linearly', ); }); }