Files
English/kouyu_english/test/sentence_analysis_test.dart
T
shenleiandClaude Opus 5 7e29c5449f feat: 重做阅读找答案题与 AI 情境对话,并归拢本地识别、查词等既有改动
阅读"在对话里找到答案":
- 16 道题全部重写,干扰项真实出现在对话里,靠说话人归属或否定句才能作答
- 选项按题目内容确定性打乱,答案不再固定排第一;听力环节同样处理
- 去掉超纲干扰项、重复题干,收紧自由作答匹配(过去单个字母也能判对)

AI 情境对话:
- 提示词区分"AI 这一句要做什么"与"学习者随后要完成什么",并下发已教词句清单
- JSON 只强制 reply,translation/feedback 可选;不再索要用不上的 slots/evidence
- AI 不可用时页面明确提示当前回复来自内置示范脚本
- 删掉按 stage 下标猜中文翻译的兜底,避免译文与英文对不上
- 整课对话改用逐轮必需表达校验,替换"关键词沾边就算过";修正自由场景正则误伤
- 总结的"完成任务"按实际通过的轮次生成;模型点评只在结束页呈现一次
- 自由场景支持草稿续练(独立存储槽);修正回答轮数文案与永不解锁的场景标注

同时提交此前工作区中累积的改动:SenseVoice 本地识别、查词/句型解析卡、
复习与测评页调整等,并补充对话校验、选项分布和句子解析的测试。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 16:38:23 +09:00

232 lines
8.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.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/widgets/lexicon_lookup.dart';
void main() {
group('Sentence and Phrase Parsing Logic', () {
test('isSentenceQuery correctly detects sentences vs words', () {
expect(isSentenceQuery('apple'), isFalse);
expect(isSentenceQuery('check in'), isFalse);
expect(isSentenceQuery('Where is the subway?'), isTrue);
expect(isSentenceQuery("I'd like to check in."), isTrue);
expect(isSentenceQuery('This is a longer sentence with more than three words'), isTrue);
});
test('extractCourseLexiconPhrases finds embedded course lexicon items', () {
final matches = extractCourseLexiconPhrases("Hello, nice to meet you, where's the taxi?");
expect(matches, isNotEmpty);
final words = matches.map((m) => m.word.toLowerCase()).toList();
expect(words.any((w) => w.contains('nice to meet you')), isTrue);
});
});
group('Models Serialization', () {
test('PhraseBreakdownItem serialization round-trip', () {
const item = PhraseBreakdownItem(
phrase: 'check in',
meaning: '办理入住/值机',
ipa: '/tʃek ɪn/',
usageNote: '连读发音,酒店或机场常用',
);
final json = item.toJson();
final parsed = PhraseBreakdownItem.fromJson(json);
expect(parsed.phrase, 'check in');
expect(parsed.meaning, '办理入住/值机');
expect(parsed.ipa, '/tʃek ɪn/');
expect(parsed.usageNote, '连读发音,酒店或机场常用');
});
test('SentenceAnalysisResult serialization round-trip', () {
final result = SentenceAnalysisResult(
originalText: "I would like to check in, please.",
translation: '我想办理入住手续,谢谢。',
sentencePattern: "I would like to + 动词原形 (礼貌表达需求)",
grammarNote: 'would like 语气委婉客气,适合服务场景。',
pronunciationTips: 'would like 发音轻柔,check in 发生连读。',
phrases: const [
PhraseBreakdownItem(
phrase: 'would like to',
meaning: '想要做某事(委婉)',
),
PhraseBreakdownItem(
phrase: 'check in',
meaning: '办理入住',
ipa: '/tʃek ɪn/',
),
],
provider: 'mock',
model: 'test-model',
createdAt: DateTime.utc(2026, 9, 16, 10, 0, 0),
);
final json = result.toJson();
final parsed = SentenceAnalysisResult.fromJson(json);
expect(parsed.originalText, result.originalText);
expect(parsed.translation, result.translation);
expect(parsed.sentencePattern, result.sentencePattern);
expect(parsed.grammarNote, result.grammarNote);
expect(parsed.pronunciationTips, result.pronunciationTips);
expect(parsed.phrases.length, 2);
expect(parsed.phrases[0].phrase, 'would like to');
expect(parsed.phrases[1].ipa, '/tʃek ɪn/');
expect(parsed.provider, 'mock');
expect(parsed.model, 'test-model');
});
});
group('AiService Mock Sentence Analysis', () {
test('returns structured breakdown for would like', () async {
final analysis = await AiService.instance.analyzeSentence(
provider: AiProviderType.mock,
endpoint: '',
model: 'mock',
text: 'I would like a cup of tea.',
);
expect(analysis, isNotNull);
expect(analysis!.translation, contains('想要'));
expect(analysis.sentencePattern, contains('would like'));
expect(analysis.pronunciationTips, isNotNull);
expect(analysis.phrases, isNotEmpty);
expect(analysis.phrases.any((p) => p.phrase == 'would like'), isTrue);
});
test('returns structured breakdown for where is', () async {
final analysis = await AiService.instance.analyzeSentence(
provider: AiProviderType.mock,
endpoint: '',
model: 'mock',
text: 'Where is the gate?',
);
expect(analysis, isNotNull);
expect(analysis!.translation, contains('在哪里'));
expect(analysis.sentencePattern, contains('Where is'));
expect(analysis.phrases.any((p) => p.phrase == 'where is'), isTrue);
});
test('returns structured fallback for generic sentence', () async {
final analysis = await AiService.instance.analyzeSentence(
provider: AiProviderType.mock,
endpoint: '',
model: 'mock',
text: 'The weather is very sunny today.',
);
expect(analysis, isNotNull);
expect(analysis!.translation, isNotEmpty);
expect(analysis.sentencePattern, isNotEmpty);
expect(analysis.grammarNote, isNotEmpty);
});
});
group('AppState Sentence Analysis & Review Integration', () {
test('caching and retrieving sentence analyses with query normalization', () {
final state = AppState();
final result = SentenceAnalysisResult(
originalText: ' Where is the bus stop? ',
translation: '请问公交站在哪里?',
sentencePattern: 'Where is + 地点',
createdAt: DateTime.now(),
);
state.cacheSentenceAnalysis(result);
expect(state.sentenceAnalysisFor('where is the bus stop?'), isNotNull);
expect(state.sentenceAnalysisFor('WHERE IS THE BUS STOP? '), isNotNull);
expect(state.sentenceAnalysisFor('where is the bus stop?')?.translation, '请问公交站在哪里?');
state.removeSentenceAnalysis('where is the bus stop?');
expect(state.sentenceAnalysisFor('where is the bus stop?'), isNull);
});
test('addPhraseToReview converts phrase breakdown into vocabulary item in review queue', () {
final state = AppState();
state.addPhraseToReview(
phrase: 'check in',
meaning: '办理登机或入住',
ipa: '/tʃek ɪn/',
usageNote: '酒店机场高频词',
contextSentence: "I'd like to check in please.",
);
expect(state.reviewQueue.any((r) => r.target == 'check in'), isTrue);
final saved = state.reviewQueue.firstWhere((r) => r.target == 'check in');
expect(saved.hint, contains('办理登机或入住'));
expect(saved.hint, contains('酒店机场高频词'));
expect(saved.prompt, "I'd like to check in please.");
});
});
group('UI Lookup Sheet Sentence Analysis View', () {
testWidgets('renders sentence analysis result cards when cached', (tester) async {
final state = AppState();
final analysis = SentenceAnalysisResult(
originalText: 'Where is the departure gate?',
translation: '请问登机口在哪里?',
sentencePattern: 'Where is + 目的地',
grammarNote: 'where 引导的疑问句,注意语调。',
pronunciationTips: 'Where 与 is 自然连读。',
phrases: const [
PhraseBreakdownItem(
phrase: 'departure gate',
ipa: '/dɪˈpɑːrtʃər ɡeɪt/',
meaning: '登机口',
usageNote: '机场核心词汇',
),
],
provider: 'mock',
createdAt: DateTime.now(),
);
state.cacheSentenceAnalysis(analysis);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Builder(
builder: (context) => ElevatedButton(
onPressed: () => showLexiconLookup(
context,
state: state,
initialText: 'Where is the departure gate?',
),
child: const Text('查句'),
),
),
),
),
);
await tester.tap(find.text('查句'));
await tester.pumpAndSettle();
expect(find.text('查词与整句深度解析'), findsOneWidget);
expect(find.text('中文整句翻译'), findsOneWidget);
expect(find.text('请问登机口在哪里?'), findsOneWidget);
expect(find.text('核心句型'), findsOneWidget);
expect(find.text('Where is + 目的地'), findsOneWidget);
expect(find.text('口语连读与发音'), findsOneWidget);
expect(find.text('重点短语与搭配 (1)'), findsOneWidget);
expect(find.text('departure gate'), findsOneWidget);
expect(find.text('登机口'), findsOneWidget);
expect(find.text('加复习'), findsOneWidget);
// Scroll to '加复习' and tap
await tester.ensureVisible(find.text('加复习'));
await tester.pumpAndSettle();
await tester.tap(find.text('加复习'));
await tester.pumpAndSettle();
expect(state.reviewQueue.any((r) => r.target == 'departure gate'), isTrue);
expect(find.text('已在复习'), findsOneWidget);
});
});
}