181 lines
5.7 KiB
Dart
181 lines
5.7 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:kouyu_english/core/app_state.dart';
|
|
import 'package:kouyu_english/core/models.dart';
|
|
|
|
void main() {
|
|
test(
|
|
'listening help survives resume and cannot earn independent evidence',
|
|
() {
|
|
final first = AppState()..lessonStep = LessonStep.listening;
|
|
first.recordListeningPlayback(slow: true);
|
|
first.revealListeningTranscript();
|
|
first.recordListeningMiss();
|
|
|
|
final resumed = AppState()..restoreForTest(first.snapshotForTest());
|
|
expect(resumed.listeningAudioPlayed, isTrue);
|
|
expect(resumed.listeningSupport, ListeningSupport.transcript);
|
|
expect(resumed.listeningAttempts, 1);
|
|
|
|
resumed.completeListening(recognized: true);
|
|
final evidence = resumed.attemptEvidence
|
|
.where((item) => item.taskId.endsWith('listening-check'))
|
|
.toList();
|
|
expect(evidence, isNotEmpty);
|
|
expect(
|
|
evidence.every((item) => item.outcome == EvidenceKind.assisted),
|
|
isTrue,
|
|
);
|
|
expect(evidence.first.listeningSupport, ListeningSupport.transcript);
|
|
expect(evidence.first.firstTry, isFalse);
|
|
expect(evidence.first.attemptCount, 2);
|
|
},
|
|
);
|
|
|
|
test('unaided first listening answer earns recognition evidence', () {
|
|
final state = AppState()..lessonStep = LessonStep.listening;
|
|
state.recordListeningPlayback();
|
|
state.completeListening(recognized: true);
|
|
final evidence = state.attemptEvidence
|
|
.where((item) => item.taskId.endsWith('listening-check'))
|
|
.toList();
|
|
expect(evidence, isNotEmpty);
|
|
expect(
|
|
evidence.every((item) => item.outcome == EvidenceKind.independentSuccess),
|
|
isTrue,
|
|
);
|
|
expect(evidence.first.firstTry, isTrue);
|
|
});
|
|
|
|
test('a choice without playable audio cannot earn listening recognition', () {
|
|
final state = AppState()..lessonStep = LessonStep.listening;
|
|
state.completeListening(recognized: true);
|
|
final evidence = state.attemptEvidence
|
|
.where((item) => item.taskId.endsWith('listening-check'))
|
|
.toList();
|
|
expect(
|
|
evidence.every((item) => item.outcome == EvidenceKind.assisted),
|
|
isTrue,
|
|
);
|
|
});
|
|
|
|
test(
|
|
'TTS failure survives resume and gives only assisted listening credit',
|
|
() {
|
|
final state = AppState()..lessonStep = LessonStep.listening;
|
|
state.recordListeningAudioUnavailable();
|
|
final resumed = AppState()..restoreForTest(state.snapshotForTest());
|
|
expect(resumed.listeningAudioUnavailable, isTrue);
|
|
expect(resumed.listeningSupport, ListeningSupport.transcript);
|
|
resumed.completeListening(recognized: true);
|
|
expect(resumed.lessonStep, LessonStep.speaking);
|
|
expect(resumed.listeningAudioUnavailable, isFalse);
|
|
expect(
|
|
resumed.attemptEvidence
|
|
.where((entry) => entry.taskId.endsWith('listening-check'))
|
|
.every((entry) => entry.outcome == EvidenceKind.assisted),
|
|
isTrue,
|
|
);
|
|
},
|
|
);
|
|
|
|
test('skipping follow-reading does not create speaking evidence', () {
|
|
final state = AppState()..lessonStep = LessonStep.speaking;
|
|
state.completeSpeaking();
|
|
expect(state.lessonStep, LessonStep.reading);
|
|
expect(
|
|
state.attemptEvidence.where(
|
|
(entry) => entry.taskId.endsWith('-speaking'),
|
|
),
|
|
isEmpty,
|
|
);
|
|
});
|
|
|
|
test('the fourth checkpoint keeps its 30-day audit interval', () {
|
|
final state = AppState();
|
|
state.reviewQueue.add(
|
|
ReviewItem(
|
|
id: 'A0-P02',
|
|
target: "What's your name?",
|
|
prompt: '问名字',
|
|
hint: "What's your name?",
|
|
dueAt: DateTime.now().subtract(const Duration(days: 1)),
|
|
skill: '回忆表达',
|
|
successfulReviews: 3,
|
|
),
|
|
);
|
|
|
|
state.completeReview(
|
|
state.reviewQueue.single,
|
|
assisted: false,
|
|
firstTry: true,
|
|
attemptCount: 1,
|
|
rawAnswer: "What's your name?",
|
|
);
|
|
final next = state.reviewQueue.single;
|
|
expect(next.successfulReviews, 4);
|
|
expect(
|
|
next.dueAt.difference(DateTime.now()).inDays,
|
|
inInclusiveRange(29, 30),
|
|
);
|
|
expect(state.attemptEvidence.last.firstTry, isTrue);
|
|
expect(state.attemptEvidence.last.attemptCount, 1);
|
|
});
|
|
|
|
test('later clean mastery audits lengthen to 45 days', () {
|
|
final state = AppState();
|
|
state.reviewQueue.add(
|
|
ReviewItem(
|
|
id: 'A0-P02',
|
|
target: "What's your name?",
|
|
prompt: '问名字',
|
|
hint: "What's your name?",
|
|
dueAt: DateTime.now().subtract(const Duration(days: 1)),
|
|
skill: '回忆表达',
|
|
successfulReviews: 4,
|
|
),
|
|
);
|
|
state.completeReview(
|
|
state.reviewQueue.single,
|
|
assisted: false,
|
|
firstTry: true,
|
|
attemptCount: 1,
|
|
rawAnswer: "What's your name?",
|
|
);
|
|
expect(
|
|
state.reviewQueue.single.dueAt.difference(DateTime.now()).inDays,
|
|
inInclusiveRange(44, 45),
|
|
);
|
|
});
|
|
|
|
test(
|
|
'slow dictation cannot advance a checkpoint even if caller says independent',
|
|
() {
|
|
final state = AppState();
|
|
state.reviewQueue.add(
|
|
ReviewItem(
|
|
id: 'A0-P02',
|
|
target: "What's your name?",
|
|
prompt: '听写',
|
|
hint: "What's your name?",
|
|
dueAt: DateTime.now().subtract(const Duration(days: 1)),
|
|
skill: '听写',
|
|
),
|
|
);
|
|
state.completeReview(
|
|
state.reviewQueue.single,
|
|
assisted: false,
|
|
firstTry: true,
|
|
attemptCount: 1,
|
|
listeningSupport: ListeningSupport.slow,
|
|
rawAnswer: "What's your name?",
|
|
);
|
|
expect(state.reviewQueue.single.successfulReviews, 0);
|
|
expect(state.attemptEvidence.last.outcome, EvidenceKind.assisted);
|
|
expect(
|
|
state.attemptEvidence.last.listeningSupport,
|
|
ListeningSupport.slow,
|
|
);
|
|
},
|
|
);
|
|
}
|