import 'package:flutter_test/flutter_test.dart'; import 'package:kouyu_english/core/seed_courses.dart'; const _digitWords = { '0': 'zero', '1': 'one', '2': 'two', '3': 'three', '4': 'four', '5': 'five', '6': 'six', '7': 'seven', '8': 'eight', '9': 'nine', }; const _skipWords = {'a', 'an', 'the', 'my', 'is', 'it', 'im', 'to', 'you'}; String _normalize(String text) => text.toLowerCase().replaceAll(RegExp(r'[^a-z0-9一-龥]'), ''); /// 选项在对话里是否真的出现过。数字选项按英文读法展开(138 → onethreeeight), /// 逐字母拼写(S-H-E-N)先去掉连字符。 bool _appearsInReading(String reading, String option) { final haystack = _normalize( reading.replaceAllMapped( RegExp(r'\d'), (match) => _digitWords[match[0]]!, ), ); final tokens = option .replaceAll('-', '') .replaceAllMapped(RegExp(r'\d'), (match) => _digitWords[match[0]]!) .toLowerCase() .split(RegExp(r'[^a-z0-9一-龥]+')) .where((token) => token.length > 1 && !_skipWords.contains(token)); return tokens.isNotEmpty && tokens.every(haystack.contains); } void main() { final activities = { ...a0Activities, ...a0SegmentActivities, }; group('阅读找答案题', () { test('每题三个选项,第一项是标准答案', () { for (final entry in activities.entries) { final activity = entry.value; expect(activity.answers, hasLength(3), reason: entry.key); expect(activity.readingOptions, hasLength(3), reason: entry.key); expect( activity.readingOptions.first, activity.readingAnswer, reason: '${entry.key}:readingOptions 第一项应是标准答案', ); expect( activity.readingOptions.toSet(), hasLength(3), reason: '${entry.key}:选项不能重复', ); } }); test('至少两个选项在对话中出现,必须读懂才能选', () { for (final entry in activities.entries) { final activity = entry.value; final present = activity.readingOptions .where((option) => _appearsInReading(activity.reading, option)) .toList(); expect( _appearsInReading(activity.reading, activity.readingAnswer), isTrue, reason: '${entry.key}:答案必须能在对话里找到', ); expect( present.length, greaterThanOrEqualTo(2), reason: '${entry.key}:只有 ${present.length} 个选项出现在对话中,' '学习者不读对话也能挑出唯一出现过的那个', ); } }); test('选项之间不构成子串,避免判分时误判', () { for (final entry in activities.entries) { final options = entry.value.readingOptions.map(_normalize).toList(); for (var i = 0; i < options.length; i++) { for (var j = i + 1; j < options.length; j++) { expect( options[i].contains(options[j]) || options[j].contains(options[i]), isFalse, reason: '${entry.key}:“${entry.value.readingOptions[i]}”与' '“${entry.value.readingOptions[j]}”互为子串,会被判分逻辑当成同一个答案', ); } } } }); test('打乱后的选项内容不变且顺序稳定', () { for (final entry in activities.entries) { final activity = entry.value; final seed = '${activity.readingQuestion}-reading'; final first = shuffledOptions(activity.readingOptions, seed); final second = shuffledOptions(activity.readingOptions, seed); expect(first, second, reason: entry.key); expect( first.toSet(), activity.readingOptions.toSet(), reason: entry.key, ); } }); test('正确答案在三个位置上都出现,不集中在某一位', () { final readingSlots = {}; final listeningSlots = {}; for (final activity in activities.values) { final reading = shuffledOptions( activity.readingOptions, '${activity.readingQuestion}-reading', ); final readingSlot = reading.indexOf(activity.readingAnswer); readingSlots[readingSlot] = (readingSlots[readingSlot] ?? 0) + 1; final listening = shuffledOptions( activity.answers, '${activity.listening}-listening', ); final listeningSlot = listening.indexOf(activity.answers.first); listeningSlots[listeningSlot] = (listeningSlots[listeningSlot] ?? 0) + 1; } for (final slots in [readingSlots, listeningSlots]) { expect(slots.keys.toSet(), {0, 1, 2}); for (final count in slots.values) { expect(count, lessThan(activities.length ~/ 2)); } } }); test('题干不重复,复习时不会撞题', () { final questions = activities.values .map((activity) => activity.readingQuestion) .toList(); expect(questions.toSet(), hasLength(questions.length)); }); }); }