import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:kouyu_english/core/app_state.dart'; import 'package:kouyu_english/core/courses/courses.dart'; import 'package:kouyu_english/core/models.dart'; import 'package:kouyu_english/features/review/review_page.dart'; /// Knowing what a sentence means is not knowing when to say it. The packs /// carry that as `usage`; these cover what the app does with it — learning /// engine 3.1 (what counts as which evidence) and 3.5 (recognition inside a /// situation). void main() { ReviewItem due(String id) => ReviewItem( id: id, target: coreItemEnglish(id), prompt: coreReviewTemplate(id).prompt, hint: coreItemEnglish(id), dueAt: DateTime.now().subtract(const Duration(hours: 1)), skill: '回忆表达', ); test('the pack says when a pattern is used and what it is confused with', () { final usage = coreUsage('A0-P02')!; expect(usage.when, isNotEmpty); expect(usage.reply, isNotEmpty); expect( usage.confuse.map((entry) => entry.id), contains('A0-P05'), reason: 'What\'s your name? 和 How are you? 正是分不清的那一对', ); }); test('a contrast question asks which sentence the situation calls for', () { final question = coreContrastQuestion('A0-P02')!; expect(question.situation, coreUsage('A0-P02')!.when); expect(question.answer, coreItemSpoken('A0-P02')); expect(question.options, contains(coreItemSpoken('A0-P05'))); expect(question.note, isNotEmpty); }); test('an untaught confusable is left out of the question', () { // `How are you?` is taught two units later than `What's your name?`, so // on the day P02 is first reviewed it is not a choice the learner could // make sense of. Its same-unit partner still is. final early = coreContrastQuestion('A0-P02', isTaught: (_) => false)!; expect(early.options, isNot(contains(coreItemSpoken('A0-P05')))); expect(early.options, contains(coreItemSpoken('A0-P01'))); // Once the learner has met it, it is the option worth offering. final later = coreContrastQuestion( 'A0-P02', isTaught: (id) => id == 'A0-P05', )!; expect(later.options, contains(coreItemSpoken('A0-P05'))); }); test('a contrast question never offers more than three sentences', () { // `What's your name?` is confused with three other taught sentences. expect(coreUsage('A0-P02')!.confuse.length, greaterThan(2)); expect(coreContrastQuestion('A0-P02')!.options, hasLength(3)); }); test('answering a contrast question records 认识, not a checkpoint', () { final state = AppState(); final item = due('A0-P02'); state.reviewQueue.add(item); state.recordContrastAnswer(item, correct: true); final mastery = state.mastery['A0-P02']!; expect(mastery.status, MasteryStatus.recognize); expect(mastery.checkpoint, 0); // It is not recall either: the learner picked the sentence, not said it. expect(mastery.status, isNot(MasteryStatus.recall)); }); test('a contrast answer survives rebuilding mastery from evidence', () { final state = AppState(); final item = due('A0-P02'); state.reviewQueue.add(item); state.recordContrastAnswer(item, correct: true); state.rebuildMasteryFromEvidence(); expect(state.mastery['A0-P02']!.checkpoint, 0); expect(state.mastery['A0-P02']!.status, MasteryStatus.recognize); }); test('the contrast question cannot undo the miss that led to it', () { final state = AppState(); final item = due('A0-P02'); state.reviewQueue.add(item); state.reportReviewFailure(item); expect(state.mastery['A0-P02']!.needsReview, isTrue); state.recordContrastAnswer(item, correct: true); expect(state.mastery['A0-P02']!.needsReview, isTrue); expect(state.mastery['A0-P02']!.checkpoint, 0); }); test('getting the contrast wrong is not a language failure', () { final state = AppState(); final item = due('A0-P02'); state.reviewQueue.add(item); state.recordContrastAnswer(item, correct: false); expect(state.mastery['A0-P02']!.needsReview, isFalse); expect(state.mastery['A0-P02']!.checkpoint, 0); }); testWidgets('a miss opens the explanation, then the contrast question', ( tester, ) async { final messenger = tester.binding.defaultBinaryMessenger; for (final name in const [ 'com.llfbandit.record/messages', 'xyz.luan/audioplayers', 'xyz.luan/audioplayers.global', ]) { messenger.setMockMethodCallHandler(MethodChannel(name), (_) async => null); } messenger.setMockStreamHandler( const EventChannel('xyz.luan/audioplayers.global/events'), MockStreamHandler.inline(onListen: (_, _) {}), ); final reportError = FlutterError.onError; FlutterError.onError = (details) { if (details.exception is! MissingPluginException) { reportError?.call(details); } }; addTearDown(() => FlutterError.onError = reportError); final state = AppState(); state.reviewQueue.add(due('A0-P02')); // Past the first checkpoint, so the review opens on the production task // rather than the recognition question. state.mastery['A0-P02'] = const MasteryItem( id: 'A0-P02', label: "What's your name?", status: MasteryStatus.recall, evidence: [], checkpoint: 1, ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: ReviewPage( state: state, onFinished: () {}, onOpenAdaptiveLesson: () {}, ), ), ), ); final giveUp = find.text('暂时想不起来'); await tester.ensureVisible(giveUp); await tester.tap(giveUp); await tester.pumpAndSettle(); expect(find.text('先弄清楚什么时候用'), findsOneWidget); expect(find.textContaining(coreUsage('A0-P02')!.when), findsWidgets); final checkpointAfterMiss = state.mastery['A0-P02']!.checkpoint; final wrong = find.widgetWithText( OutlinedButton, coreItemSpoken('A0-P01'), ); await tester.ensureVisible(wrong); await tester.tap(wrong); await tester.pumpAndSettle(); // The answer is named and the difference explained, and the miss stands. expect( find.textContaining('这里要说 ${coreItemSpoken('A0-P02')}'), findsOneWidget, ); expect(state.mastery['A0-P02']!.needsReview, isTrue); expect(state.mastery['A0-P02']!.checkpoint, checkpointAfterMiss); }); }