123 lines
4.1 KiB
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',
|
|
);
|
|
});
|
|
}
|