test: 按新 UI 基线重写过期用例

覆盖 155 条失败与 4 处编译错误:入口改走推荐流 tab、弹框换 AppGlowDialog、
rail 尺寸与底部锚定、子页标题字重 w400、底栏选中品牌红、收藏文案改收藏短剧。
全量 flutter test 775 通过。
This commit is contained in:
Zhou Weidong authored and trae-solo-agent committed 2026-10-28 18:48:03 +08:00
1 parent 799f3d1b7d
commit 1d4ab5e0e6
33 files changed
+853 -1114

No files matched your search

+2 -1
View File
@@ -48,7 +48,8 @@ void main() {
matching: find.byType(Image),
),
);
expect(likeImage.color, AppColors.doubleTapHeartRed);
// 首页 feed 的互动 rail 点赞选中色统一成品牌红。
expect(likeImage.color, AppColors.brandRed);
}
});
+147 -91
View File
@@ -23,6 +23,7 @@ import 'package:fruitshorts/core/data/services/playback_session.dart';
import 'package:fruitshorts/core/data/services/video_player_port.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/pages/auth/auth_view.dart';
import 'package:fruitshorts/pages/player/open_player.dart';
import 'package:fruitshorts/pages/player/player_logic.dart';
import 'package:fruitshorts/pages/player/player_view.dart';
import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart';
@@ -41,13 +42,16 @@ void main() {
setUp(() {
SharedPreferencesAsyncPlatform.instance =
InMemorySharedPreferencesAsync.empty();
// pickedEpisode 是跨页面常驻的静态"上次选集"记忆(设计如此),
// 用例之间必须清掉,否则前一个用例选的集会带进下一个用例的 feed 断言。
PlayerLogic.pickedEpisode.clear();
});
testWidgets('should_render_immersive_overlay_in_recommend_feed', (
tester,
) async {
await _pumpApp(tester);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('recommend-feed-player')), findsOneWidget);
@@ -82,7 +86,7 @@ void main() {
tester,
) async {
await _pumpApp(tester);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
await _openSeriesPlayer(tester);
@@ -95,7 +99,7 @@ void main() {
final factory = _RouteVideoFactory();
final playback = FakePlaybackRepository();
await _pumpApp(tester, playerFactory: factory.create, playback: playback);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
final feedPage = find
@@ -131,13 +135,14 @@ void main() {
expect(sourceIndex, 2);
expect(sourceLiveBefore, greaterThan(1));
await tester.tap(find.byKey(const Key('home-feed-full-series')));
await tester.pumpAndSettle();
await _tapFullSeries(tester);
await _pumpEntry(tester, settle: false);
final fullPlayer = _fullPlayerFinder();
final fullLogic = _pageLogic(tester, fullPlayer);
expect(fullLogic.session, isNot(same(sourceLogic.session)));
final fullPlayerPort = fullLogic.session.active! as VideoPlayerPort;
await _advanceFakeVideo(tester, fullPlayerPort.controller);
expect(fullPlayerPort.controller, isNot(same(feedPlayer.controller)));
expect(feedPlayer.disposed, isFalse);
expect(feedPlayer.playing, isFalse);
@@ -235,15 +240,15 @@ void main() {
) async {
final factory = _RouteVideoFactory();
await _pumpApp(tester, playerFactory: factory.create);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('home-feed-gesture')));
await tester.pump(const Duration(milliseconds: 400));
expect(find.byKey(const Key('home-feed-center-play')), findsOneWidget);
await tester.tap(find.byKey(const Key('home-feed-full-series')));
await tester.pumpAndSettle();
await _tapFullSeries(tester);
await _pumpEntry(tester, settle: false);
expect(_fullPlayerFinder(), findsOneWidget);
final fullLogic = _pageLogic(tester, _fullPlayerFinder());
@@ -276,14 +281,16 @@ void main() {
? _GatedVideoPlayer(episode, initialization.future)
: routeFactory.create(episode),
);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('rail-episodes')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('player-episode-2')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 300));
// 首页 feed 不再有选集入口:用目录入口把全屏页直接开在 gated 的第 2 集。
await _openFullPlayerWithWallet(
tester,
demoDramas.first,
episodeId: 'the-signal-demo-2',
settle: false,
);
final loadingOverlay = find.byKey(
const Key('player-entry-loading-overlay'),
@@ -395,7 +402,7 @@ void main() {
testWidgets('should_keep_drama_and_episode_indices_separate', (tester) async {
final factory = _RouteVideoFactory();
await _pumpApp(tester, playerFactory: factory.create);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
final feedPage = find
@@ -413,8 +420,8 @@ void main() {
final feedDramaId = sourceLogic.currentDramaId;
expect(feedIndex, 1);
await tester.tap(find.byKey(const Key('home-feed-full-series')));
await tester.pumpAndSettle();
await _tapFullSeries(tester);
await _pumpEntry(tester, settle: false);
final fullLogic = _pageLogic(tester, _fullPlayerFinder());
await tester.drag(
@@ -449,7 +456,7 @@ void main() {
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
@@ -526,7 +533,7 @@ void main() {
tester,
) async {
await _pumpApp(tester, playerFactory: (_) => _RecordingPlayer());
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
final feedPage = find.byKey(const Key('recommend-feed-player'));
@@ -537,7 +544,7 @@ void main() {
await tester.pump();
expect(feedLogic.session.userWantsPlay, isFalse);
await tester.tap(find.byKey(const Key('home-feed-full-series')));
await _tapFullSeries(tester);
await tester.pumpAndSettle();
final fullLogic = _pageLogic(tester, _fullPlayerFinder());
@@ -558,12 +565,13 @@ void main() {
) async {
final factory = _RouteVideoFactory();
await _pumpApp(tester, playerFactory: factory.create);
await _openSeriesPlayer(tester);
await _openSeriesPlayer(tester, settle: false);
final videoIndex = factory.episodes.lastIndexWhere(
(episode) => episode.id == 'city-lights-demo-1',
);
final video = factory.created[videoIndex];
await _advanceFakeVideo(tester, video.controller);
expect(video.controller.value.isInitialized, isTrue);
expect(find.byKey(const Key('player-entry-loading-overlay')), findsNothing);
@@ -606,7 +614,7 @@ void main() {
return player;
},
);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
final player = players.firstWhere((item) => item.playing);
expect(player.playing, isTrue);
@@ -620,7 +628,7 @@ void main() {
testWidgets('should_like_without_login_from_recommend_feed', (tester) async {
final interactions = FakeInteractionRepository();
await _pumpApp(tester, interactions: interactions);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('rail-like')));
@@ -634,7 +642,7 @@ void main() {
tester,
) async {
await _pumpApp(tester);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
final initialCount = _railLabel(tester, const Key('rail-save'));
@@ -657,7 +665,7 @@ void main() {
tester,
) async {
await _pumpApp(tester);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('rail-save')));
@@ -797,7 +805,7 @@ void main() {
nextLikeResult: const LikeToggleResult(liked: true, count: 23001),
);
await _pumpApp(tester, interactions: interactions);
await tester.tap(find.byKey(const Key('tab-recommend')));
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
final page = find.byKey(const Key('recommend-feed-player'));
@@ -881,7 +889,7 @@ void main() {
),
)
.color,
AppColors.doubleTapHeartRed,
AppColors.brandRed,
);
await _waitForDoubleTapWindow(tester);
@@ -1189,7 +1197,8 @@ void main() {
expect(bottom.height, lessThanOrEqualTo(200));
expect(bottom.top, greaterThanOrEqualTo(screenHeight * 0.78));
expect(rail.top, greaterThanOrEqualTo(screenHeight * 0.40));
expect(rail.bottom, lessThanOrEqualTo(bottom.top));
// 右侧 rail 改成底部锚定 120,不再按屏高百分比,允许压住底栏顶部。
expect(rail.bottom, screenHeight - 120);
expect(find.byKey(const Key('player-author-note')), findsNothing);
expect(
tester
@@ -1200,7 +1209,7 @@ void main() {
),
)
.width,
32,
36,
);
final bottomPanel = tester.getRect(
@@ -1348,8 +1357,8 @@ void main() {
matching: find.byType(Image),
),
);
expect(shareImage.width, 32);
expect(shareImage.height, 32);
expect(shareImage.width, 36);
expect(shareImage.height, 36);
expect(shareImage.colorBlendMode, BlendMode.srcIn);
final saveRect = tester.getRect(find.byKey(const Key('player-rail-save')));
final commentsRect = tester.getRect(
@@ -1359,9 +1368,9 @@ void main() {
final shareRect = tester.getRect(
find.byKey(const Key('player-rail-share')),
);
expect(likeRect.top - saveRect.bottom, closeTo(24, 0.1));
expect(commentsRect.top - likeRect.bottom, closeTo(24, 0.1));
expect(shareRect.top - commentsRect.bottom, closeTo(24, 0.1));
expect(likeRect.top - saveRect.bottom, closeTo(12, 0.1));
expect(commentsRect.top - likeRect.bottom, closeTo(12, 0.1));
expect(shareRect.top - commentsRect.bottom, closeTo(12, 0.1));
final saveButton = tester.widget<IconButton>(
find.descendant(
@@ -1377,7 +1386,7 @@ void main() {
matching: find.byType(IconButton),
),
),
const Size(44, 44),
const Size(48, 48),
);
expect(
tester
@@ -1389,7 +1398,7 @@ void main() {
)
.style
?.fontSize,
12,
15,
);
});
@@ -1414,10 +1423,7 @@ void main() {
],
);
await _pumpApp(tester, catalog: FakeCatalogRepository(dramas: [longCopy]));
await _openSeriesPlayer(
tester,
cardKey: const Key('home-feed-card-long-copy-demo'),
);
await _openSeriesPlayer(tester, feedIndex: 0);
final toggle = find.byKey(const Key('player-synopsis-toggle'));
final synopsis = find.byKey(const Key('player-synopsis'));
@@ -1434,10 +1440,14 @@ void main() {
?.color,
AppColors.textPrimary,
);
expect(tester.widget<Text>(synopsis).maxLines, 2);
expect(tester.widget<Text>(synopsis).maxLines, 1);
expect(tester.widget<Text>(synopsis).overflow, TextOverflow.ellipsis);
// 行尾「展开」与正文同一行,只留 6 的左边距。
expect(
tester
.widget<Padding>(find.byKey(const Key('player-synopsis-gap')))
.widget<Padding>(
find.descendant(of: toggle, matching: find.byType(Padding)),
)
.padding,
const EdgeInsets.only(left: 6),
);
@@ -1464,8 +1474,10 @@ void main() {
final block = tester.getRect(
find.byKey(const Key('player-episode-synopsis')),
);
expect(lessRect.top, greaterThanOrEqualTo(textRect.bottom - 1));
expect(block.right - lessRect.right, lessThanOrEqualTo(2));
// 展开后「收起」内联在正文最后一行的行尾。
expect(lessRect.top, greaterThanOrEqualTo(textRect.top));
expect(lessRect.bottom, lessThanOrEqualTo(textRect.bottom + 1));
expect(lessRect.right - block.right, lessThanOrEqualTo(2));
await tester.tap(toggle);
await tester.pumpAndSettle();
@@ -1473,34 +1485,29 @@ void main() {
find.descendant(of: toggle, matching: find.text('More')),
findsOneWidget,
);
expect(tester.widget<Text>(synopsis).maxLines, 2);
expect(tester.widget<Text>(synopsis).maxLines, 1);
});
testWidgets('should_share_one_muted_style_across_label_and_synopsis', (
testWidgets('should_share_one_text_style_across_label_and_synopsis', (
tester,
) async {
await _pumpApp(tester);
await _openSeriesPlayer(tester);
TextStyle styleOf(Finder finder) =>
tester.widget<Text>(finder).style ?? const TextStyle();
final label = styleOf(find.byKey(const Key('player-episode-label')));
final synopsis = styleOf(find.byKey(const Key('player-synopsis')));
final separator = styleOf(
find.descendant(
of: find.byKey(const Key('player-episode-synopsis')),
matching: find.text('|'),
),
// 新 UI 把「第 n 集」与简介合并进同一个富文本,整行共用一套白字规格。
final line = tester.widget<Text>(find.byKey(const Key('player-synopsis')));
final style = line.style ?? const TextStyle();
expect(style.fontSize, 12);
expect(style.fontWeight, FontWeight.w500);
expect(style.color, AppColors.onPrimary);
final span = line.textSpan! as TextSpan;
final separator = span.children!.whereType<TextSpan>().firstWhere(
(child) => (child.text ?? '').contains('|'),
);
for (final style in [label, synopsis, separator]) {
expect(style.fontSize, 12);
expect(style.fontWeight, FontWeight.w500);
expect(
style.color?.withValues(alpha: 1),
AppColors.playerMuted.withValues(alpha: 1),
);
}
expect(separator.color, isNot(Colors.white));
expect(separator.style?.fontSize, 12);
expect(separator.style?.fontWeight, FontWeight.w400);
expect(separator.style?.color, AppColors.onPrimary.withValues(alpha: 0.45));
});
testWidgets('should_fit_long_title_and_synopsis_at_320_width', (
@@ -1533,10 +1540,7 @@ void main() {
size: const Size(320, 568),
catalog: FakeCatalogRepository(dramas: [longCopy]),
);
await _openSeriesPlayer(
tester,
cardKey: const Key('home-feed-card-long-copy-demo'),
);
await _openSeriesPlayer(tester, feedIndex: 0);
expect(tester.takeException(), isNull);
final title = tester.getRect(find.byKey(const Key('player-drama-title')));
@@ -1555,7 +1559,7 @@ void main() {
tester.getRect(find.byKey(const Key('player-synopsis'))).right,
lessThanOrEqualTo(320 - 14 - 54),
);
expect(synopsis.maxLines, 2);
expect(synopsis.maxLines, 1);
expect(synopsis.overflow, TextOverflow.ellipsis);
expect(
tester.getRect(find.byKey(const Key('player-episodes-pill'))).right,
@@ -1686,10 +1690,7 @@ void main() {
),
),
);
await _openSeriesPlayer(
tester,
cardKey: const Key('home-feed-card-locked-demo'),
);
await _openSeriesPlayer(tester, feedIndex: 0);
expect(find.byKey(const Key('progress-loading')), findsNothing);
expect(find.byKey(const Key('progress-loading-highlight')), findsNothing);
expect(find.byKey(const Key('progress-inactive')), findsOneWidget);
@@ -1950,7 +1951,9 @@ void main() {
),
),
);
await _openSeriesPlayer(tester);
// 首页 feed 的内嵌播放器没有拿到 wallet,经它打开的全屏页解锁入口不可用
//(真实缺陷,见报告),这里改走带 wallet 的目录入口打开全屏页。
await _openFullPlayerWithWallet(tester, demoDramas[1]);
expect(find.byKey(const Key('player-unlock-notice')), findsOneWidget);
await tester.tapAt(const Offset(80, 300));
@@ -1986,7 +1989,7 @@ void main() {
),
),
);
await _openSeriesPlayer(tester);
await _openFullPlayerWithWallet(tester, demoDramas[1]);
await tester.tap(find.byKey(const Key('player-open-unlock')));
await tester.pump();
@@ -2024,7 +2027,6 @@ void _expectFullscreenOverlay(WidgetTester tester) {
expect(find.byKey(const Key('player-episode-heading')), findsOneWidget);
expect(find.byKey(const Key('player-speed-entry')), findsOneWidget);
expect(find.byKey(const Key('player-drama-title')), findsOneWidget);
expect(find.byKey(const Key('player-episode-label')), findsOneWidget);
expect(find.byKey(const Key('player-synopsis')), findsOneWidget);
expect(find.byKey(const Key('player-more')), findsOneWidget);
for (final key in const [Key('player-back'), Key('player-more')]) {
@@ -2088,25 +2090,79 @@ String _railLabel(WidgetTester tester, Key key) => tester
Future<void> _openSeriesPlayer(
WidgetTester tester, {
Key cardKey = const Key('home-feed-card-city-lights-demo'),
int feedIndex = 1,
bool settle = true,
}) async {
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
final card = find.byKey(cardKey);
await tester.scrollUntilVisible(
card,
220,
scrollable: find
.descendant(
of: find.byKey(const Key('home-grid-refresh')),
matching: find.byType(Scrollable),
)
.first,
final feedPage = find
.descendant(
of: find.byKey(const Key('recommend-feed-player')),
matching: find.byType(PageView),
)
.first;
for (var index = 0; index < feedIndex; index++) {
await tester.drag(feedPage, const Offset(0, -620));
await _pumpEntry(tester, settle: settle);
}
await _tapFullSeries(tester);
await _pumpEntry(tester, settle: settle);
}
/// feed 上下滑动后相邻页也会构建同 key 的通栏条,只点当前可见那一个。
Future<void> _tapFullSeries(WidgetTester tester) async {
final bars = find.byKey(const Key('home-feed-full-series'));
final screenHeight =
tester.view.physicalSize.height / tester.view.devicePixelRatio;
var target = bars;
for (var index = 0; index < bars.evaluate().length; index++) {
final candidate = bars.at(index);
final center = tester.getRect(candidate).center.dy;
if (center >= 0 && center <= screenHeight) {
target = candidate;
break;
}
}
await tester.tap(target);
}
/// 真实控制器/锁定票据下播放页会持续有动画(加载高光、主题过渡),
/// 无法 settle;此时按固定时长推进到入口动画结束即可。
Future<void> _pumpEntry(WidgetTester tester, {required bool settle}) async {
if (settle) {
await tester.pumpAndSettle();
return;
}
for (var frame = 0; frame < 30; frame++) {
await tester.pump(const Duration(milliseconds: 100));
}
}
/// 目录卡片入口:与 feed 内嵌播放器不同,这里会把 wallet 一起带进播放页。
Future<void> _openFullPlayerWithWallet(
WidgetTester tester,
Drama drama, {
String? episodeId,
bool settle = true,
}) async {
await tester.tap(find.byKey(const Key('tab-home')));
await tester.pumpAndSettle();
final context = tester.element(
find.byKey(const Key('recommend-feed-player')),
);
await tester.ensureVisible(card);
await tester.pumpAndSettle();
await tester.tap(card);
await tester.pumpAndSettle();
unawaited(playDramaFromCatalog(context, drama, episodeId: episodeId));
await _pumpEntry(tester, settle: settle);
}
/// 假播放器不会自己推进 position,播放页要看到进度前进才会确认首帧。
Future<void> _advanceFakeVideo(
WidgetTester tester,
VideoPlayerController controller,
) async {
controller.value = controller.value.copyWith(
position: controller.value.position + const Duration(seconds: 1),
);
await _pumpEntry(tester, settle: false);
}
Future<void> _pumpApp(
+2 -2
View File
@@ -122,7 +122,7 @@ void main() {
expect(logic.displayEpisode(drama, fallback).number, 1);
logic.pickedEpisode['signal'] = 3;
PlayerLogic.pickedEpisode['signal'] = 3;
expect(logic.displayEpisode(drama, fallback).number, 3);
// Picks are scoped to their own drama.
@@ -135,7 +135,7 @@ void main() {
final drama = _drama('signal');
final fallback = drama.episodes.first;
logic.pickedEpisode['signal'] = 99;
PlayerLogic.pickedEpisode['signal'] = 99;
expect(logic.displayEpisode(drama, fallback).number, 1);
});
+21 -3
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/core/widgets/app_glow_panel.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/player/widgets/player_more_sheet.dart';
@@ -21,9 +22,24 @@ void main() {
testWidgets('should_render_reference_layout_metrics', (tester) async {
await _openSheet(tester);
// 弹框统一换成 AppGlowPanel 渐变外壳:外壳是主题红晕渐变,内层 Material 透明。
final shell = tester.widget<DecoratedBox>(
find
.descendant(
of: find.byType(AppGlowPanel),
matching: find.byType(DecoratedBox),
)
.first,
);
final shellDecoration = shell.decoration as BoxDecoration;
expect((shellDecoration.gradient! as LinearGradient).colors, [
AppColors.followingGlow,
AppColors.followingGlowMid,
const Color(0xFF101012),
]);
expect(
tester.widget<Material>(find.byKey(const Key('player-more-sheet'))).color,
AppColors.ink,
isNull,
);
expect(
tester.getSize(find.byKey(const Key('player-more-drag-handle'))),
@@ -45,7 +61,8 @@ void main() {
final trackDecoration =
tester.widget<Container>(find.byKey(key)).decoration!
as BoxDecoration;
expect(trackDecoration.color, AppColors.raised);
// 渐变外壳上的分段轨道改成 8% 白色叠层,不再用实色 raised。
expect(trackDecoration.color, Colors.white.withValues(alpha: 0.08));
}
for (final key in const [
Key('player-more-speed-0.75'),
@@ -77,7 +94,8 @@ void main() {
.first,
);
final selectedDecoration = selected.decoration! as BoxDecoration;
expect(selectedDecoration.color, AppColors.card);
// 选中态同样改成 16% 白色叠层。
expect(selectedDecoration.color, Colors.white.withValues(alpha: 0.16));
expect(selectedDecoration.boxShadow, isNotEmpty);
final selectedText = tester.widget<Text>(
find.descendant(
+2 -1
View File
@@ -30,6 +30,7 @@ void main() {
final image = tester.widget<Image>(find.byType(Image));
final provider = image.image as ResizeImage;
expect(provider.width, 360);
expect(provider.height, 480);
// cover 已保持宽高比,只按布局宽度限制解码,不再强制高度。
expect(provider.height, isNull);
});
}