Files
English/kouyu_english/test/unlock_chain_test.dart

123 lines
4.1 KiB
Dart

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<SeedLesson> 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',
);
});
}