import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:kouyu_english/core/ai_service.dart'; import 'package:kouyu_english/core/app_state.dart'; import 'package:kouyu_english/core/models.dart'; import 'package:kouyu_english/features/dialogue/dialogue_flow.dart'; void main() { testWidgets('sending a turn after an AI correction counts as assisted', ( tester, ) async { // The page wires up recording and playback plugins that tests lack. 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: (_, _) {}), ); // Each player also opens its own event channel under a random id. final reportError = FlutterError.onError; FlutterError.onError = (details) { if (details.exception is! MissingPluginException) { reportError?.call(details); } }; addTearDown(() => FlutterError.onError = reportError); AiService.instance.setFallbackApiKey('test-key'); final state = AppState() ..aiProvider = AiProviderType.compatible ..aiEndpoint = 'https://api.deepseek.com' ..aiModel = 'deepseek-flash'; // A free scene only records items the learner has already been taught. state.mastery['A0-P01'] = MasteryItem( id: 'A0-P01', label: '介绍名字', status: MasteryStatus.recognize, evidence: const [], firstTaughtAt: DateTime.now().subtract(const Duration(days: 1)), ); final check = jsonEncode({ 'schemaVersion': 'writing-feedback-1', 'verdict': 'rewrite', 'feedback': '介绍姓名正确,后半句有语法错误。', 'suggestion': 'My name is Alex and I am happy.', 'missing': ['I is 应为 I am'], 'lessonId': 'dialogue-0', }); final prompts = []; http.Response reply(String content) => http.Response( jsonEncode({ 'choices': [ { 'message': {'content': content}, }, ], }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); await http.runWithClient( () async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DialoguePage(state: state, onFinished: (_) {}), ), ), ); await tester.pump(); await tester.enterText( find.byType(TextField), 'My name is Alex and I is happy', ); await tester.pump(); final aiButton = find.text('发送前 AI 检查语法和拼写(可选)'); await tester.ensureVisible(aiButton); await tester.tap(aiButton); await tester.runAsync( () => Future.delayed(const Duration(milliseconds: 50)), ); await tester.pump(); expect(find.text('· I is 应为 I am'), findsOneWidget); expect( find.text('参考改正:My name is Alex and I am happy.'), findsOneWidget, ); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.runAsync( () => Future.delayed(const Duration(milliseconds: 50)), ); await tester.pump(); }, () => MockClient((request) async { final body = request.body; prompts.add(body); // The dialogue reply itself may fall back to the script. return reply(body.contains('writing-feedback-1') ? check : '{}'); }), ); expect(prompts.first, contains('I is happy')); expect(prompts.first, contains('dialogue-0')); final attempt = state.attemptEvidence.singleWhere( (entry) => entry.taskId.startsWith('dialogue-scene-'), ); expect(attempt.itemId, 'A0-P01'); expect(attempt.outcome, EvidenceKind.assisted); expect(state.sceneDialogueDraft?.usedHelp, isTrue); }); }