Files
English/kouyu_english/test/navigation_back_button_test.dart
T
shenandshen 5b9cba793e feat: 优化口语防重复问答、课程推进流转、键盘输入流畅度与竖屏锁定
- AI口语约束:在对话系统提示词与单轮指导中增加防重复问答约束,避免重复寒暄及索取已提供信息,根据历史自然向前推进
- 课程流转:修复多段课程学完后今日任务卡片流转下一课,及退出重进定位逻辑
- 竖屏锁定:在 Flutter、Android 及 iOS 平台配置仅支持竖屏显示
- 对话交互:优化对话界面文本输入重绘与词汇查询,解决键盘输入卡顿
- 测试用例:补充并更新单轮约束、状态流转与返回键退出测试,212项测试全绿
2026-09-18 07:38:54 -07:00

305 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.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/features/dialogue/dialogue_flow.dart';
import 'package:kouyu_english/features/review/review_page.dart';
import 'package:kouyu_english/features/shell/learning_shell.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:kouyu_english/core/courses/course_repository.dart';
import 'support/app_assets.dart';
void main() {
setUp(() {
SharedPreferences.setMockInitialValues({});
});
testWidgets('Onboarding PlacementPage back button returns to WelcomePage', (
tester,
) async {
await pumpLoadedApp(tester);
await tester.pumpAndSettle();
expect(find.text('每天 20 分钟,\n说出能用的英语。'), findsOneWidget);
await tester.tap(find.text('继续'));
await tester.pumpAndSettle();
expect(find.text('从哪里开始?'), findsOneWidget);
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
expect(find.text('每天 20 分钟,\n说出能用的英语。'), findsOneWidget);
});
testWidgets('Profile sub-page back button returns to 我的', (tester) async {
final state = AppState()..finishOnboarding();
await tester.pumpWidget(MaterialApp(home: LearningShell(state: state)));
await tester.pumpAndSettle();
await tester.tap(find.text('我的'));
await tester.pumpAndSettle();
final preferences = find.text('学习偏好');
await tester.ensureVisible(preferences);
await tester.tap(preferences);
await tester.pumpAndSettle();
expect(find.text('每日学习时间'), findsOneWidget);
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
expect(find.text('每日学习时间'), findsNothing);
expect(find.text('学习偏好'), findsOneWidget);
});
testWidgets('LessonFlow back button returns to tab', (tester) async {
final state = AppState()..finishOnboarding();
await tester.pumpWidget(MaterialApp(home: LearningShell(state: state)));
await tester.pumpAndSettle();
expect(find.text('开始今天的学习'), findsOneWidget);
await tester.tap(find.text('开始今天的学习'));
await tester.pumpAndSettle();
// In LessonFlow
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
// Tap back button
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
expect(find.text('开始今天的学习'), findsOneWidget);
});
testWidgets('学习 tab shows today task and the lesson path', (tester) async {
final state = AppState()..finishOnboarding();
await tester.pumpWidget(MaterialApp(home: LearningShell(state: state)));
await tester.pumpAndSettle();
expect(find.byType(NavigationDestination), findsNWidgets(4));
expect(find.text('今日新课'), findsOneWidget);
expect(find.text('课程路径'), findsOneWidget);
expect(find.textContaining('第 1 课 · '), findsWidgets);
});
testWidgets('DialogueSummaryPage has back button', (tester) async {
var backCalled = false;
await tester.pumpWidget(
MaterialApp(
home: DialogueSummaryPage(
summary: const DialogueSummaryData(
completedTasks: ['介绍姓名'],
personalSentence: 'My name is Shen.',
usedHelp: false,
),
onHome: () {},
onLesson: () {},
onRetry: () {},
onBack: () => backCalled = true,
),
),
);
await tester.pumpAndSettle();
expect(find.text('对话完成'), findsNWidgets(2)); // AppBar title and Eyebrow
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
expect(backCalled, isTrue);
});
testWidgets('AdaptiveLessonPage has back button in all states', (
tester,
) async {
final state = AppState()..finishOnboarding();
// 1. Empty state
var finished = false;
await tester.pumpWidget(
MaterialApp(
home: AdaptiveLessonPage(
state: state,
onFinished: () => finished = true,
),
),
);
await tester.pumpAndSettle();
expect(find.text('AI 四技能补练'), findsNWidgets(2));
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
await tester.tap(find.byIcon(Icons.arrow_back));
expect(finished, isTrue);
// 2. Active lesson state
state.cacheApprovedAdaptiveLesson(
const GeneratedLesson(
lessonId: 'adapt-1',
revision: 1,
stageVersion: 'A0',
abilityIds: ['greeting'],
prerequisiteIds: [],
targetItemIds: ['name'],
receptiveChunks: ['My name is Mia.'],
previewItemIds: ['name'],
estimatedMinutes: 5,
tasks: [
GeneratedLessonTask(
taskId: 't1',
skill: 'listening',
type: 'listen',
prompt: '听并写出名字',
stimulus: 'My name is Mia.',
answer: 'Mia',
targetItemIds: ['name'],
),
],
),
auditor: 'test',
);
finished = false;
await tester.pumpWidget(
MaterialApp(
home: AdaptiveLessonPage(
state: state,
onFinished: () => finished = true,
),
),
);
await tester.pumpAndSettle();
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
await tester.tap(find.byIcon(Icons.arrow_back));
expect(finished, isTrue);
});
testWidgets(
'AssessmentPreparationPage and AssessmentPage have back buttons',
(tester) async {
final state = AppState()..finishOnboarding();
await tester.pumpWidget(MaterialApp(home: LearningShell(state: state)));
await tester.pumpAndSettle();
// Navigate to "我的"
await tester.tap(find.text('我的'));
await tester.pumpAndSettle();
final assessments = find.text('四技能评估');
await tester.ensureVisible(assessments);
await tester.tap(assessments);
await tester.pumpAndSettle();
// Open assessment pack A0-E1
final packAButton = find.text('开始 A0-E1');
await tester.ensureVisible(packAButton);
await tester.pumpAndSettle();
expect(packAButton, findsOneWidget);
await tester.tap(packAButton);
await tester.pumpAndSettle();
// In AssessmentPreparationPage
expect(find.text('评估准备'), findsOneWidget);
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
// Start assessment
await tester.tap(find.text('开始评估'));
await tester.pumpAndSettle();
// In AssessmentPage
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
// Returns to 我的
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
expect(find.text('评估准备'), findsNothing);
expect(find.text('四技能评估'), findsOneWidget);
},
);
testWidgets(
'LessonFlow on complete step: back button finalizes segment and advances lesson',
(tester) async {
await tester.runAsync(
() => CourseRepository.instance.load(levels: {'A0'}),
);
final state = AppState()..finishOnboarding();
expect(state.activeLessonId, 'a0-01');
await tester.pumpWidget(MaterialApp(home: LearningShell(state: state)));
await tester.pumpAndSettle();
// Tap '开始今天的学习' to enter LessonFlow
await tester.tap(find.text('开始今天的学习'));
await tester.pumpAndSettle();
// Complete all steps of a0-01
state.completePreview();
state.completeListening(recognized: true);
state.completeSpeaking();
state.completeReading();
state.completeWriting(assisted: false, rawAnswer: 'hello');
state.completeLessonDialogue();
state.completeIndependentAttempt(assisted: false, rawAnswer: 'hello');
expect(state.lessonStep, LessonStep.complete);
await tester.pumpAndSettle();
// We should be in lesson flow complete screen
expect(find.text('本段已保存'), findsOneWidget);
expect(find.text('回到首页'), findsOneWidget);
expect(find.byIcon(Icons.arrow_back), findsOneWidget);
// Tap the top-left AppBar back button instead of '回到首页'
await tester.tap(find.byIcon(Icons.arrow_back));
await tester.pumpAndSettle();
// Should be back on the 学习 tab, and lesson must have advanced to a0-02
expect(state.completedLessonIds, contains('a0-01'));
expect(state.activeLessonId, 'a0-02');
expect(find.textContaining('第 2 课 · '), findsWidgets);
},
);
testWidgets(
'TodayTaskCard reflects multi-segment progress and completed fallback',
(tester) async {
await tester.runAsync(
() => CourseRepository.instance.load(levels: {'A0'}),
);
final state = AppState()..finishOnboarding();
state.activeLessonId = 'a0-04'; // a0-04 has 3 segments
await tester.pumpWidget(MaterialApp(home: LearningShell(state: state)));
await tester.pumpAndSettle();
// Segment 1 (initial)
expect(find.text('今日新课'), findsOneWidget);
expect(find.text('开始今天的学习'), findsOneWidget);
// Now complete segment 1
state.completeSegment('a0-04', 0);
await tester.pumpAndSettle();
// Segment 2 (resuming)
expect(find.text('继续课程'), findsOneWidget);
expect(find.text('继续学习第 2 段'), findsOneWidget);
expect(find.textContaining('第 2/3 段'), findsWidgets);
// If activeLessonId is marked as completed, card targets next incomplete
state.completedLessonIds.add('a0-04');
state.notifyListeners();
await tester.pumpAndSettle();
// Should target a0-01 or next incomplete, not show completed lesson
expect(find.text('继续课程'), findsNothing);
expect(find.textContaining('第 1 课 · '), findsWidgets);
},
);
}