feat: 完善听力口语辅助状态记录、练习分块与复习抽查间隔机制
This commit is contained in:
@@ -117,33 +117,49 @@ void main() {
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.text('独立完成'), findsOneWidget);
|
||||
expect(find.text('提交独立尝试'), findsOneWidget);
|
||||
await tester.enterText(
|
||||
find.byType(TextField),
|
||||
'My name is Alex. Nice to meet you. I is happy.',
|
||||
);
|
||||
await tester.pump();
|
||||
final aiButton = find.text('提交前 AI 检查语法和拼写(可选)');
|
||||
await tester.ensureVisible(aiButton);
|
||||
await tester.tap(aiButton);
|
||||
final submit = find.text('提交独立尝试');
|
||||
await tester.ensureVisible(submit);
|
||||
await tester.tap(submit);
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 50)),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('· I is 应为 I am'), findsOneWidget);
|
||||
final finish = find.text('带帮助完成');
|
||||
await tester.ensureVisible(finish);
|
||||
await tester.tap(finish);
|
||||
expect(state.lessonStep, LessonStep.independent);
|
||||
await tester.enterText(
|
||||
find.byType(TextField),
|
||||
'My name is Alex. Nice to meet you. I am happy.',
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.ensureVisible(submit);
|
||||
await tester.tap(submit);
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 50)),
|
||||
);
|
||||
await tester.pump();
|
||||
},
|
||||
() => MockClient((request) async {
|
||||
prompts.add(request.body);
|
||||
final accepted = jsonEncode({
|
||||
'schemaVersion': 'writing-feedback-1',
|
||||
'verdict': 'accepted',
|
||||
'feedback': '表达正确。',
|
||||
'suggestion': null,
|
||||
'missing': <String>[],
|
||||
'lessonId': segmentId,
|
||||
});
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'choices': [
|
||||
{
|
||||
'message': {'content': check},
|
||||
'message': {'content': prompts.length == 1 ? check : accepted},
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -153,8 +169,221 @@ void main() {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(prompts.single, contains('I is happy'));
|
||||
expect(prompts, hasLength(2));
|
||||
expect(prompts.first, contains('I is happy'));
|
||||
expect(state.lessonStep, LessonStep.complete);
|
||||
expect(state.independentAttemptAssisted, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('writing submission runs AI check without a separate button', (
|
||||
tester,
|
||||
) async {
|
||||
AiService.instance.setFallbackApiKey('test-key');
|
||||
final state = AppState()
|
||||
..aiProvider = AiProviderType.compatible
|
||||
..aiEndpoint = 'https://api.deepseek.com'
|
||||
..aiModel = 'deepseek-flash'
|
||||
..lessonStep = LessonStep.writing;
|
||||
var requests = 0;
|
||||
await http.runWithClient(
|
||||
() async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: LessonFlow(
|
||||
state: state,
|
||||
onOpenDialogue: () {},
|
||||
onFinish: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('收起示例,自己试一次'));
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(TextField), 'Hello. I am Alex.');
|
||||
await tester.pump();
|
||||
expect(find.text('获取 AI 写作建议(可选)'), findsNothing);
|
||||
final submit = find.text('提交并继续');
|
||||
await tester.ensureVisible(submit);
|
||||
await tester.tap(submit);
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 50)),
|
||||
);
|
||||
await tester.pump();
|
||||
},
|
||||
() => MockClient((request) async {
|
||||
requests++;
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'choices': [
|
||||
{
|
||||
'message': {
|
||||
'content': jsonEncode({
|
||||
'schemaVersion': 'writing-feedback-1',
|
||||
'verdict': 'accepted',
|
||||
'feedback': '表达正确。',
|
||||
'suggestion': null,
|
||||
'missing': <String>[],
|
||||
'lessonId': 'a0-01',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
);
|
||||
}),
|
||||
);
|
||||
expect(requests, 1);
|
||||
expect(state.lessonStep, LessonStep.dialogue);
|
||||
expect(
|
||||
state.attemptEvidence
|
||||
.where((entry) => entry.taskId == 'lesson-a0-01-a-writing')
|
||||
.every((entry) => entry.outcome == EvidenceKind.assisted),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a locally matching bad sentence waits for AI correction', (
|
||||
tester,
|
||||
) async {
|
||||
AiService.instance.setFallbackApiKey('test-key');
|
||||
final state = AppState()
|
||||
..aiProvider = AiProviderType.compatible
|
||||
..aiEndpoint = 'https://api.deepseek.com'
|
||||
..aiModel = 'deepseek-flash'
|
||||
..lessonStep = LessonStep.writing;
|
||||
await http.runWithClient(
|
||||
() async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: LessonFlow(
|
||||
state: state,
|
||||
onOpenDialogue: () {},
|
||||
onFinish: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.enterText(find.byType(TextField), 'Hello. I am is Alex.');
|
||||
await tester.pump();
|
||||
final submit = find.text('提交并继续');
|
||||
await tester.ensureVisible(submit);
|
||||
await tester.tap(submit);
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 50)),
|
||||
);
|
||||
await tester.pump();
|
||||
},
|
||||
() => MockClient(
|
||||
(_) async => http.Response(
|
||||
jsonEncode({
|
||||
'choices': [
|
||||
{
|
||||
'message': {
|
||||
'content': jsonEncode({
|
||||
'schemaVersion': 'writing-feedback-1',
|
||||
'verdict': 'rewrite',
|
||||
'feedback': '这里多了 is。',
|
||||
'suggestion': 'Hello. I am Alex.',
|
||||
'missing': ['删去多余的 is'],
|
||||
'lessonId': 'a0-01',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
200,
|
||||
headers: {'content-type': 'application/json; charset=utf-8'},
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(state.lessonStep, LessonStep.writing);
|
||||
expect(find.text('AI 建议:这里多了 is。'), findsOneWidget);
|
||||
expect(find.text('请按自己的意思修改,再提交检查。看过改正后,本次只算辅助练习。'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('independent submission without AI stays unverified', (
|
||||
tester,
|
||||
) async {
|
||||
final state = AppState()..lessonStep = LessonStep.independent;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: LessonFlow(state: state, onOpenDialogue: () {}, onFinish: () {}),
|
||||
),
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byType(TextField),
|
||||
'My name is Alex. Nice to meet you.',
|
||||
);
|
||||
await tester.pump();
|
||||
final submit = find.text('提交独立尝试');
|
||||
await tester.ensureVisible(submit);
|
||||
await tester.tap(submit);
|
||||
await tester.pump();
|
||||
expect(state.lessonStep, LessonStep.complete);
|
||||
expect(state.independentAttemptPending, isTrue);
|
||||
expect(
|
||||
state.attemptEvidence
|
||||
.where((entry) => entry.taskId == 'lesson-a0-01-a-independent')
|
||||
.every((entry) => entry.outcome == EvidenceKind.pending),
|
||||
isTrue,
|
||||
);
|
||||
expect(find.textContaining('未经 AI 核验'), findsOneWidget);
|
||||
});
|
||||
|
||||
test('unverified completion survives a local snapshot', () {
|
||||
final first = AppState()
|
||||
..completeIndependentAttempt(
|
||||
assisted: false,
|
||||
verified: false,
|
||||
rawAnswer: 'My name is Alex. Nice to meet you.',
|
||||
);
|
||||
final resumed = AppState()..restoreForTest(first.snapshotForTest());
|
||||
expect(resumed.independentAttemptPending, isTrue);
|
||||
expect(resumed.independentAttemptComplete, isTrue);
|
||||
expect(
|
||||
resumed.attemptEvidence
|
||||
.where((entry) => entry.taskId == 'lesson-a0-01-a-independent')
|
||||
.every((entry) => entry.outcome == EvidenceKind.pending),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('AI outage offers an unverified continuation', (tester) async {
|
||||
AiService.instance.setFallbackApiKey('test-key');
|
||||
final state = AppState()
|
||||
..aiProvider = AiProviderType.compatible
|
||||
..aiEndpoint = 'https://api.deepseek.com'
|
||||
..aiModel = 'deepseek-flash'
|
||||
..lessonStep = LessonStep.independent;
|
||||
await http.runWithClient(() async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: LessonFlow(
|
||||
state: state,
|
||||
onOpenDialogue: () {},
|
||||
onFinish: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byType(TextField),
|
||||
'My name is Alex. Nice to meet you.',
|
||||
);
|
||||
await tester.pump();
|
||||
final submit = find.text('提交独立尝试');
|
||||
await tester.ensureVisible(submit);
|
||||
await tester.tap(submit);
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 50)),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(state.lessonStep, LessonStep.independent);
|
||||
final fallback = find.text('未核验,保存练习并继续');
|
||||
await tester.ensureVisible(fallback);
|
||||
await tester.tap(fallback);
|
||||
await tester.pump();
|
||||
}, () => MockClient((_) async => http.Response('{}', 503)));
|
||||
expect(state.lessonStep, LessonStep.complete);
|
||||
expect(state.independentAttemptPending, isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user