import 'package:flutter/material.dart'; import 'package:flutter/services.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/generated_content.dart'; import 'package:kouyu_english/core/models.dart'; import 'package:kouyu_english/core/review_feedback.dart'; import 'package:kouyu_english/features/review/review_page.dart'; const _writing = GeneratedLessonTask( taskId: 'w', skill: 'writing', type: 'writeAnswer', prompt: '写出你来自哪里。', stimulus: '你来自哪里?', answer: 'I am from Hong Kong.', targetItemIds: ['A0-P12'], answerSpec: GeneratedAnswerSpec( requiredAnyPhrases: [ ["i'm from", 'i am from'], ['hong kong', 'shanghai'], ], acceptedAnswers: ['I am from Hong Kong.'], ), ); void ignoreMissingPlugins(WidgetTester tester) { 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); } void main() { group('adaptiveAnswerShortfall', () { test('is null for a correct answer', () { expect(adaptiveAnswerShortfall(_writing, "I'm from Shanghai."), isNull); }); test('names the missing phrase group', () { final message = adaptiveAnswerShortfall(_writing, 'I am from.'); expect(message, contains('hong kong')); expect(message, isNot(contains('i am from'))); }); test('only counts what a listening answer lacks', () { const listening = GeneratedLessonTask( taskId: 'l', skill: 'listening', type: 'listenChoice', prompt: '听。', stimulus: 'I am from Hong Kong.', answer: 'Hong Kong', targetItemIds: ['A0-P12'], ); final message = adaptiveAnswerShortfall(listening, 'London'); expect(message, contains('还缺 1 处')); expect(message, isNot(contains('Hong Kong'))); }); }); test('a failed dictation reports the words it missed', () { final sentence = coreDictationSentences['A0-W02']!.sentence; final result = ReviewFeedback.check( ReviewItem( id: 'A0-W02', target: sentence, prompt: '听写', hint: '', dueAt: DateTime(2026), skill: dictationSkill, ), 'My numbr is one-two-three.', ); expect(result.complete, isFalse); expect(result.dictation!.missedWords, ['number']); expect(result.dictation!.extraWords, ['numbr']); }); testWidgets('a correct review answer is confirmed with the reference', ( tester, ) async { ignoreMissingPlugins(tester); final state = AppState(); state.reviewQueue.add( ReviewItem( id: 'A0-P12', target: "I'm from …", prompt: '告诉对方你来自哪里。', hint: "I'm from Shanghai.", dueAt: DateTime.now().subtract(const Duration(hours: 1)), skill: '回忆表达', ), ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: ReviewPage( state: state, onFinished: () {}, onOpenAdaptiveLesson: () {}, ), ), ), ); await tester.enterText(find.byType(TextField), 'I am from Hong Kong.'); await tester.pump(); final submit = find.text('我能独立回答'); await tester.ensureVisible(submit); await tester.tap(submit); await tester.pump(); expect(find.text('答对了!'), findsOneWidget); expect(find.text('你的回答:I am from Hong Kong.'), findsOneWidget); expect(find.textContaining("参考说法:I'm from …"), findsOneWidget); await tester.tap(find.text('完成今天的复习')); await tester.pump(); expect(find.text('今日复习已完成'), findsOneWidget); }); testWidgets( 'the adaptive lesson confirms a correct answer before moving on', (tester) async { ignoreMissingPlugins(tester); final state = AppState(); state.cacheApprovedAdaptiveLesson( const GeneratedLesson( lessonId: 'ai-a0-p12-1', revision: 1, stageVersion: 'A0-1.0', abilityIds: ['A0-C06'], prerequisiteIds: ['a0-05'], targetItemIds: ['A0-P12'], receptiveChunks: [], previewItemIds: [], estimatedMinutes: 10, tasks: [ _writing, GeneratedLessonTask( taskId: 'l', skill: 'listening', type: 'listenChoice', prompt: '听。', stimulus: 'I am from Hong Kong.', answer: 'Hong Kong', targetItemIds: ['A0-P12'], ), GeneratedLessonTask( taskId: 's', skill: 'speaking', type: 'repeat', prompt: '说。', stimulus: 'I am from Hong Kong.', answer: 'I am from Hong Kong.', targetItemIds: ['A0-P12'], ), GeneratedLessonTask( taskId: 'r', skill: 'reading', type: 'readAnswer', prompt: '读。', stimulus: 'I am from Hong Kong.', answer: 'Hong Kong', targetItemIds: ['A0-P12'], ), ], ), auditor: 'test', ); await tester.pumpWidget( MaterialApp( home: AdaptiveLessonPage(state: state, onFinished: () {}), ), ); await tester.enterText(find.byType(TextField), 'I am from.'); await tester.pump(); final check = find.text('检查答案'); await tester.ensureVisible(check); await tester.tap(check); await tester.pump(); expect(find.textContaining('还缺:hong kong / shanghai'), findsOneWidget); await tester.enterText(find.byType(TextField), 'I am from Shanghai.'); await tester.pump(); await tester.ensureVisible(check); await tester.tap(check); await tester.pump(); expect(find.text('答对了!'), findsOneWidget); expect(find.text('AI 补练 · 1/4'), findsOneWidget); final next = find.text('下一项'); await tester.ensureVisible(next); await tester.tap(next); await tester.pump(); expect(find.text('AI 补练 · 2/4'), findsOneWidget); expect(find.text('答对了!'), findsNothing); }, ); }