Skip to content

Commit 63a2d20

Browse files
committed
TEST: 修复 CI 测试并清理低价值样式断言
1 parent ed4e3f1 commit 63a2d20

7 files changed

Lines changed: 95 additions & 80 deletions

‎TASKS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
55
## 本次完成
66

7+
- [x] 删除低价值的意群 badge 样式单测(2026-10-17):移除只断言普通态/播放态填充及精确边框色值和透明度的两个用例与专用辅助函数,保留意群高亮、收藏标记等行为测试。意群组件测试 8 项通过,测试文件 `flutter analyze --no-pub` 无问题,`dart format` 与 `git diff --check` 通过;未运行 `scripts/check.sh`:本次仅删除样式常量断言,定向检查已覆盖。**完成时间**: 2026-10-17 15:21
8+
- [x] 修复 GitHub CI 中七个测试失败(2026-10-17):完整 App 路由回归测试改用确定性的启动、收藏列表和学习推荐 provider 替身,避免真实启动流程阻塞路由挂载及 Drift 查询流留下 pending timer;收藏词汇页面测试固定地区配置结果,日志保留测试滚动到懒加载列表中的最新记录再断言。CI 失败涉及的四个测试文件共 30 项通过;随后全仓 `flutter test --no-pub --reporter=compact` 共 5536 项通过、11 项跳过,五个修改 Dart 文件定向 `flutter analyze --no-pub` 无问题,`dart format` 与 `git diff --check` 通过。未运行 `scripts/check.sh`:改动只涉及测试 harness 和断言,全量测试与定向分析已覆盖;未运行 Maestro:未修改应用 UI 或路由行为。**完成时间**: 2026-10-17 15:26
79
- [x] 提高夜间意群边框可见度(2026-10-17):暗色主题下普通态与已播放态边框不透明度从 0.3 提高到 0.6;亮色主题、播放中状态和收藏态保持原样。新增暗色主题边框回归测试;意群组件定向测试 10 项通过,相关 Dart 文件 `flutter analyze --no-pub` 无问题,`git diff --check` 通过。未运行 Maestro:现有 Flow 会 `clearState` 且不覆盖该组件;未运行 `scripts/check.sh`:改动局限于意群边框样式,定向验证已覆盖。**完成时间**: 2026-10-17 12:48
810
- [x] 移除未收藏意群普通态与播放态的背景填充(2026-10-17):普通态与播放态改为透明底,继续由边框颜色区分;已收藏意群保留橙色背景。新增组件回归测试,普通态和播放态无填充且边框不同;定向 widget 测试 9 项通过,相关 Dart 文件 `flutter analyze --no-pub` 无问题,`git diff --check` 通过。未运行 Maestro:现有 Flow 均会 `clearState` 且不覆盖意群组件,为避免清除设备应用数据而跳过;未运行 `scripts/check.sh`:改动局限于意群 badge 样式,定向验证已覆盖。**完成时间**: 2026-10-17 12:37
911
- [x] 精听音频与视频统一到 MediaEngine(2026-10-17):音频和视频精听共用 `MediaSentencePlaybackDriver`、媒体加载/重试/取消入口及页面加载状态;仅视频显示画面,保留音频普通加载反馈。进入时暂停并移交 Free Player 监听,失败、取消和退出时按会话生命周期恢复;保留断点、收藏、意群试听和自动补做语义。补充结构化日志,覆盖入口、材料类型、断点、媒体 session、逐句索引/区间/速度/结果/耗时、停顿、暂停/续播/切句、取消和退出,不记录句子正文;修正冷启动日志将空 backend 显示为 `uninitialized`。Provider、状态机、页面和 MediaEngine 定向测试分别通过(38 项通过/4 项跳过、54 项、50 项、22 项),相关 Dart 文件 `flutter analyze --no-pub` 无问题,`git diff --check` 通过。Android dev 包构建安装成功;SM A065F 上无清理数据的 Maestro 启动 Flow 通过。未运行 `scripts/check.sh`:改动范围局限于精听播放链路,定向测试、分析和设备启动验证已覆盖受影响代码。**完成时间**: 2026-10-17 11:29

‎test/helpers/test_app.dart‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import 'package:echo_loop/database/app_database.dart'
1919
hide AudioItem, Collection;
2020
import 'package:echo_loop/database/providers.dart';
2121
import 'package:echo_loop/features/onboarding_survey/providers/onboarding_survey_provider.dart';
22+
import 'package:echo_loop/features/remote_config/remote_config.dart';
23+
import 'package:echo_loop/features/user_region/user_region_bootstrapper.dart';
24+
import 'package:echo_loop/features/user_region/user_region_providers.dart';
2225
import 'package:echo_loop/l10n/app_localizations.dart';
2326
import 'package:echo_loop/main.dart';
2427
import 'package:echo_loop/models/audio_engine_state.dart';
@@ -29,9 +32,13 @@ import 'package:echo_loop/models/sentence.dart';
2932
import 'package:echo_loop/database/enums.dart' show LearningStage;
3033
import 'package:echo_loop/providers/new_user_guide_provider.dart';
3134
import 'package:echo_loop/providers/package_info_provider.dart';
35+
import 'package:echo_loop/providers/startup_bootstrap_provider.dart';
3236
import 'package:echo_loop/providers/settings_provider.dart';
3337
import 'package:echo_loop/providers/audio_library_provider.dart';
3438
import 'package:echo_loop/providers/collection_provider.dart';
39+
import 'package:echo_loop/providers/saved_word_provider.dart';
40+
import 'package:echo_loop/providers/saved_sense_group_provider.dart';
41+
import 'package:echo_loop/providers/study_task_provider.dart';
3542
import 'package:echo_loop/providers/tag_provider.dart';
3643
import 'package:echo_loop/providers/listening_practice/listening_practice_provider.dart';
3744
import 'package:echo_loop/providers/audio_engine/audio_engine_provider.dart';
@@ -45,7 +52,6 @@ import 'package:echo_loop/providers/learning_session/intensive_listen_player_pro
4552
import 'package:echo_loop/providers/learning_session/retell_player_provider.dart';
4653
import 'package:echo_loop/providers/learning_session/review_difficult_practice_provider.dart';
4754
import 'package:echo_loop/providers/offline_asr_settings_provider.dart';
48-
import 'package:echo_loop/providers/saved_word_provider.dart';
4955
import 'package:echo_loop/providers/transcription_task_provider.dart';
5056
import 'package:echo_loop/router/app_router.dart';
5157
import 'package:echo_loop/theme/app_theme.dart';
@@ -297,6 +303,45 @@ class _DisabledGuideEnabledNotifier extends GuideEnabledNotifier {
297303
}
298304
}
299305

306+
/// 完整 App 的路由测试使用已完成的地区决策和失败的本地启动结果。
307+
///
308+
/// 这样启动门禁会挂载路由树,同时避免测试运行真实数据库初始化、远程配置
309+
/// 请求和本地启动成功后的后台副作用。
310+
List<Override> fullAppRouteTestStartupOverrides() => [
311+
localStartupProvider.overrideWith(_FailedLocalStartupController.new),
312+
userRegionBootstrapProvider.overrideWith(
313+
(ref) async => const UserRegionBootstrapResult(
314+
isChinaUser: false,
315+
source: UserRegionDecisionSource.systemFallback,
316+
remoteConfig: RemoteConfig.defaults,
317+
),
318+
),
319+
bookmarkListProvider.overrideWith((ref) => Stream.value(const [])),
320+
savedWordListProvider.overrideWith(_EmptySavedWordList.new),
321+
savedSenseGroupListProvider.overrideWith(_EmptySavedSenseGroupList.new),
322+
savedTextIndexProvider.overrideWithValue(const SavedTextIndex.empty()),
323+
studyRecommendationProvider.overrideWithValue(
324+
const AsyncData<AudioItem?>(null),
325+
),
326+
];
327+
328+
class _FailedLocalStartupController extends LocalStartupController {
329+
@override
330+
Future<StartupReport> build() async => throw StateError(
331+
'Local startup is outside the scope of this full-app route test.',
332+
);
333+
}
334+
335+
class _EmptySavedWordList extends SavedWordList {
336+
@override
337+
Stream<List<SavedWord>> build() => Stream.value(const []);
338+
}
339+
340+
class _EmptySavedSenseGroupList extends SavedSenseGroupList {
341+
@override
342+
Stream<List<SavedSenseGroup>> build() => Stream.value(const []);
343+
}
344+
300345
/// 在 flutter_test 中渲染完整 [EchoLoopApp] 并预置一份音频学习数据。
301346
///
302347
/// 对应 integration_test 中的 `createTestAppWithAudio`:注入 1 个 AudioItem +

‎test/router/free_player_back_gesture_test.dart‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@ void main() {
4343
await pumpFullAppWithAudio(
4444
tester,
4545
audioItem: createTestAudioItem(id: 'audio-1'),
46-
overrides: [mediaPlaybackProvider.overrideWith(() => player)],
46+
overrides: [
47+
...fullAppRouteTestStartupOverrides(),
48+
mediaPlaybackProvider.overrideWith(() => player),
49+
],
4750
);
51+
await tester.pump();
4852

4953
final router = GoRouter.of(tester.element(find.byType(MainShell)));
5054
unawaited(router.push<void>(AppRoutes.audioPlayer('audio-1')));

‎test/router/review_back_gesture_test.dart‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,13 @@ void main() {
9595

9696
await pumpFullApp(
9797
tester,
98-
overrides: [bookmarkReviewProvider.overrideWith(() => review)],
98+
overrides: [
99+
...fullAppRouteTestStartupOverrides(),
100+
bookmarkReviewProvider.overrideWith(() => review),
101+
],
99102
);
103+
await tester.pump();
104+
await tester.pump(const Duration(milliseconds: 200));
100105
final router = GoRouter.of(tester.element(find.byType(MainShell)));
101106
router.go(AppRoutes.favorites);
102107
await tester.pumpAndSettle();
@@ -126,8 +131,13 @@ void main() {
126131

127132
await pumpFullApp(
128133
tester,
129-
overrides: [favoriteVocabularyReviewProvider.overrideWith(() => review)],
134+
overrides: [
135+
...fullAppRouteTestStartupOverrides(),
136+
favoriteVocabularyReviewProvider.overrideWith(() => review),
137+
],
130138
);
139+
await tester.pump();
140+
await tester.pump(const Duration(milliseconds: 200));
131141
final router = GoRouter.of(tester.element(find.byType(MainShell)));
132142
router.go(AppRoutes.favorites);
133143
await tester.pumpAndSettle();
@@ -154,8 +164,13 @@ void main() {
154164

155165
await pumpFullApp(
156166
tester,
157-
overrides: [bookmarkReviewProvider.overrideWith(() => review)],
167+
overrides: [
168+
...fullAppRouteTestStartupOverrides(),
169+
bookmarkReviewProvider.overrideWith(() => review),
170+
],
158171
);
172+
await tester.pump();
173+
await tester.pump(const Duration(milliseconds: 200));
159174
final router = GoRouter.of(tester.element(find.byType(MainShell)));
160175
router.go(AppRoutes.favorites);
161176
await tester.pumpAndSettle();
@@ -178,8 +193,13 @@ void main() {
178193

179194
await pumpFullApp(
180195
tester,
181-
overrides: [favoriteVocabularyReviewProvider.overrideWith(() => review)],
196+
overrides: [
197+
...fullAppRouteTestStartupOverrides(),
198+
favoriteVocabularyReviewProvider.overrideWith(() => review),
199+
],
182200
);
201+
await tester.pump();
202+
await tester.pump(const Duration(milliseconds: 200));
183203
final router = GoRouter.of(tester.element(find.byType(MainShell)));
184204
router.go(AppRoutes.favorites);
185205
await tester.pumpAndSettle();

‎test/screens/favorite_vocabulary_review_screen_test.dart‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import 'package:echo_loop/features/memory_scheduler/domain/memory_model_adapter.
66
import 'package:echo_loop/features/memory_scheduler/domain/memory_scheduler_results.dart';
77
import 'package:echo_loop/features/memory_scheduler/domain/memory_schedule.dart';
88
import 'package:echo_loop/features/scheduled_flashcard/domain/review_session_summary.dart';
9+
import 'package:echo_loop/features/remote_config/remote_config.dart';
10+
import 'package:echo_loop/features/user_region/user_region_bootstrapper.dart';
11+
import 'package:echo_loop/features/user_region/user_region_providers.dart';
912
import 'package:echo_loop/l10n/app_localizations.dart';
1013
import 'package:echo_loop/models/dict_entry.dart';
1114
import 'package:echo_loop/models/dictionary/dictionary_lookup_result.dart';
@@ -246,6 +249,13 @@ Widget _app({
246249
),
247250
),
248251
sharedPreferencesProvider.overrideWithValue(_sharedPreferences),
252+
userRegionBootstrapProvider.overrideWith(
253+
(ref) async => const UserRegionBootstrapResult(
254+
isChinaUser: false,
255+
source: UserRegionDecisionSource.systemFallback,
256+
remoteConfig: RemoteConfig.defaults,
257+
),
258+
),
249259
localDictionarySourceProvider.overrideWithValue(
250260
_TestLocalDictionarySource(),
251261
),

‎test/screens/log_viewer_screen_test.dart‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,14 @@ void main() {
128128
await tester.pump(const Duration(milliseconds: 50));
129129

130130
expect(find.text('日志(最近 500 条)'), findsOneWidget);
131-
expect(find.textContaining('line-0'), findsNothing);
132-
expect(find.textContaining('line-519'), findsOneWidget);
131+
expect(find.textContaining('line-0', findRichText: true), findsNothing);
132+
final latestEntry = find.textContaining('line-519', findRichText: true);
133+
await tester.scrollUntilVisible(
134+
latestEntry,
135+
400,
136+
scrollable: find.byType(Scrollable).last,
137+
);
138+
expect(latestEntry, findsOneWidget);
133139
expect(find.text('仅显示最近 500 条;分享日志可导出完整记录'), findsOneWidget);
134140
});
135141

‎test/widgets/sense_group_text_test.dart‎

Lines changed: 0 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
22
import 'package:flutter_test/flutter_test.dart';
33
import 'package:echo_loop/models/speech_practice_models.dart';
44
import 'package:echo_loop/providers/saved_word_provider.dart';
5-
import 'package:echo_loop/theme/app_theme.dart';
65
import 'package:echo_loop/utils/saved_text_index.dart';
76
import 'package:echo_loop/widgets/practice/sense_group_text.dart';
87

@@ -35,77 +34,6 @@ void main() {
3534
SpeechTranscriptSegment word(String text, {required bool matched}) =>
3635
SpeechTranscriptSegment(text: text, isMatched: matched);
3736

38-
BoxDecoration badgeDecorationFor(WidgetTester tester, String text) {
39-
final badge = tester.widget<Container>(
40-
find
41-
.ancestor(of: find.text(text), matching: find.byType(Container))
42-
.first,
43-
);
44-
return switch (badge.decoration) {
45-
BoxDecoration decoration => decoration,
46-
_ => throw StateError('意群 badge 应使用 BoxDecoration'),
47-
};
48-
}
49-
50-
Color badgeBorderColor(BoxDecoration decoration) {
51-
return switch (decoration.border) {
52-
Border border => border.top.color,
53-
_ => throw StateError('意群 badge 应使用 Border'),
54-
};
55-
}
56-
57-
testWidgets('普通态与播放态意群无背景填充,播放态仍由边框区分', (tester) async {
58-
await tester.pumpWidget(
59-
createTestApp(
60-
SenseGroupText(
61-
chunks: const ['idle group', 'playing group'],
62-
timings: const [],
63-
onTapGroup: (_) {},
64-
playingGroupIndex: 1,
65-
),
66-
),
67-
);
68-
69-
final idleDecoration = badgeDecorationFor(tester, 'idle group');
70-
final playingDecoration = badgeDecorationFor(tester, 'playing group');
71-
72-
expect(idleDecoration.color, isNull);
73-
expect(playingDecoration.color, isNull);
74-
expect(idleDecoration.border, isNot(equals(playingDecoration.border)));
75-
});
76-
77-
testWidgets('暗色主题提高普通态与已播放态边框的可见度', (tester) async {
78-
await tester.pumpWidget(
79-
createTestApp(
80-
Theme(
81-
data: AppTheme.dark(),
82-
child: Scaffold(
83-
backgroundColor: Colors.black,
84-
body: SenseGroupText(
85-
chunks: const ['idle group', 'played group'],
86-
timings: const [],
87-
onTapGroup: (_) {},
88-
playedGroupIndices: const {1},
89-
),
90-
),
91-
),
92-
),
93-
);
94-
95-
final idleDecoration = badgeDecorationFor(tester, 'idle group');
96-
final playedDecoration = badgeDecorationFor(tester, 'played group');
97-
final darkColorScheme = AppTheme.dark().colorScheme;
98-
99-
expect(
100-
badgeBorderColor(idleDecoration),
101-
darkColorScheme.outline.withValues(alpha: 0.6),
102-
);
103-
expect(
104-
badgeBorderColor(playedDecoration),
105-
darkColorScheme.primary.withValues(alpha: 0.6),
106-
);
107-
});
108-
10937
group('SenseGroupText — 意群模式单词高亮', () {
11038
testWidgets('匹配单词染绿,未匹配单词不染绿', (tester) async {
11139
// 原文 "Hello brave world" → 意群拆为 ["Hello brave", "world"]

0 commit comments

Comments
 (0)