feat(home): 顶部导航随翻页联动并把推荐更名为精选

This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-30 22:53:15 +08:00
1 parent 2f4593a46e
commit 335e5dc286
27 files changed
+1095 -214

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
// Author: fruit.com.cn
// 职责:验证顶部标签双向过渡、固定布局和减少动画行为。
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/animated_nav_label.dart';
const _halfDuration = Duration(milliseconds: 90);
const _duration = Duration(milliseconds: 180);
const _style = TextStyle(fontSize: 20, color: AppColors.navSelected);
const _firstKey = Key('first');
const _secondKey = Key('second');
Widget _labels(bool firstSelected, {bool reduceMotion = false}) {
return MaterialApp(
home: MediaQuery(
data: MediaQueryData(
devicePixelRatio: 3,
disableAnimations: reduceMotion,
),
child: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedNavLabel(
key: _firstKey,
label: '精选 Featured',
selected: firstSelected,
style: _style,
),
AnimatedNavLabel(
key: _secondKey,
label: '推荐 Rekomendasi',
selected: !firstSelected,
style: _style,
),
],
),
),
),
);
}
double _scale(WidgetTester tester, Key key) {
final transform = tester.widget<Transform>(
find.descendant(of: find.byKey(key), matching: find.byType(Transform)),
);
return transform.transform.entry(0, 0);
}
Color _color(WidgetTester tester, Key key) {
return tester
.widget<Text>(
find.descendant(of: find.byKey(key), matching: find.byType(Text)),
)
.style!
.color!;
}
void main() {
testWidgets('should_cross_animate_without_relayout_when_selection_changes', (
tester,
) async {
await tester.pumpWidget(_labels(true));
final firstRect = tester.getRect(find.byKey(_firstKey));
final secondRect = tester.getRect(find.byKey(_secondKey));
await tester.pumpWidget(_labels(false));
await tester.pump(_halfDuration);
expect(_scale(tester, _firstKey), inExclusiveRange(0.9, 1.0));
expect(_scale(tester, _secondKey), inExclusiveRange(0.9, 1.0));
expect(_color(tester, _firstKey), isNot(AppColors.navSelected));
expect(_color(tester, _firstKey), isNot(AppColors.textTabInactive));
expect(_color(tester, _secondKey), isNot(AppColors.navSelected));
expect(_color(tester, _secondKey), isNot(AppColors.textTabInactive));
expect(tester.getRect(find.byKey(_firstKey)), firstRect);
expect(tester.getRect(find.byKey(_secondKey)), secondRect);
await tester.pump(_duration);
expect(_scale(tester, _firstKey), closeTo(0.9, 0.0001));
expect(_scale(tester, _secondKey), 1);
expect(_color(tester, _secondKey), AppColors.navSelected);
expect(tester.getRect(find.byKey(_secondKey)), secondRect);
});
testWidgets('should_continue_from_current_value_when_selection_reverses', (
tester,
) async {
await tester.pumpWidget(_labels(true));
await tester.pumpWidget(_labels(false));
await tester.pump(_halfDuration);
final scale = _scale(tester, _firstKey);
final color = _color(tester, _firstKey);
await tester.pumpWidget(_labels(true));
expect(_scale(tester, _firstKey), scale);
expect(_color(tester, _firstKey), color);
await tester.pump(_duration);
expect(_scale(tester, _firstKey), 1);
});
testWidgets('should_skip_transition_when_reduce_motion_is_enabled', (
tester,
) async {
await tester.pumpWidget(_labels(true, reduceMotion: true));
await tester.pumpWidget(_labels(false, reduceMotion: true));
await tester.pump();
expect(_scale(tester, _firstKey), closeTo(0.9, 0.0001));
expect(_color(tester, _secondKey), AppColors.navSelected);
expect(tester.binding.hasScheduledFrame, isFalse);
});
}
+66
View File
@@ -0,0 +1,66 @@
// Author: fruit.com.cn
// 职责:首页顶部精选与底部推荐使用不同文案,覆盖支持的三种语言。
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/widgets/app_root_tab.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/recommend/recommend_types.dart';
import 'package:fruitshorts/pages/recommend/widgets/recommend_top_nav.dart';
const _labels = [
('zh', '精选', '推荐', '剧单', '全部短剧'),
('en', 'Featured', 'Recommend', 'Playlists', 'All Dramas'),
('id', 'Unggulan', 'Rekomendasi', 'Daftar Putar', 'Semua'),
];
void main() {
for (final (language, featured, recommend, playlists, all) in _labels) {
testWidgets(
'should_distinguish_featured_from_recommend_when_locale_is_$language',
(tester) async {
await tester.pumpWidget(_host(language));
await tester.pumpAndSettle();
final top = find.byKey(const Key('recommend-top-nav'));
final bottom = find.byKey(const Key('tab-recommend'));
expect(
find.descendant(of: top, matching: find.text(featured)),
findsOneWidget,
);
expect(
find.descendant(of: top, matching: find.text(recommend)),
findsNothing,
);
expect(
find.descendant(of: bottom, matching: find.text(recommend)),
findsOneWidget,
);
expect(
find.descendant(of: top, matching: find.text(playlists)),
findsOneWidget,
);
expect(
find.descendant(of: top, matching: find.text(all)),
findsOneWidget,
);
expect(tester.takeException(), isNull);
},
);
}
}
Widget _host(String language) => MaterialApp(
locale: Locale(language),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: RecommendTopNav(
selected: RecommendInnerTab.feed,
onSelected: (_) {},
onSearch: () {},
),
bottomNavigationBar: AppRootBottomNav(
selected: AppRootTab.home,
onSelected: (_) {},
),
),
);
+44 -2
View File
@@ -13,6 +13,8 @@ import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/pages/player/player_view.dart';
import 'package:fruitshorts/pages/recommend/recommend_types.dart';
import 'package:fruitshorts/pages/recommend/widgets/recommend_top_nav.dart';
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
@@ -50,7 +52,7 @@ void main() {
.toList();
expect(centers, orderedEquals(centers.toList()..sort()));
expect(find.byKey(const Key('recommend-section-hits')), findsNothing);
expect(find.text('Featured'), findsNothing);
expect(find.text('Featured'), findsOneWidget);
expect(find.text('Categories'), findsNothing);
expect(find.text('Charts'), findsNothing);
expect(find.byKey(const Key('mine-mini-player')), findsNothing);
@@ -173,7 +175,7 @@ void main() {
matching: find.text('Playlists'),
),
);
expect(selected.style?.fontWeight, FontWeight.w700);
expect(selected.style?.fontWeight, FontWeight.w400);
expect(
tester.getRect(find.byKey(const Key('recommend-top-nav'))),
topNavBefore,
@@ -191,6 +193,46 @@ void main() {
expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget);
});
testWidgets('should_follow_partial_swipe_before_selected_page_changes', (
tester,
) async {
await _pumpApp(tester, _PagedCatalogRepository());
final pages = find.byKey(const Key('recommend-tab-page-view'));
final gesture = await tester.startGesture(tester.getCenter(pages));
await gesture.moveBy(const Offset(-30, 0));
await tester.pump();
await gesture.moveBy(const Offset(-80, 0));
await tester.pump();
final nav = tester.widget<RecommendTopNav>(find.byType(RecommendTopNav));
expect(nav.pagePosition, inExclusiveRange(0.0, 0.5));
final underline = tester.widget<Container>(
find.byKey(const Key('recommend-tab-playlist-underline')),
);
final color = (underline.decoration! as BoxDecoration).color!;
expect(color.a, inExclusiveRange(0.0, 0.5));
await gesture.up();
await tester.pumpAndSettle();
_drainLayoutExceptions(tester);
});
testWidgets(
'should_keep_all_dramas_selected_during_direct_cross_page_click',
(tester) async {
await _pumpApp(tester, _PagedCatalogRepository());
await tester.ensureVisible(find.byKey(const Key('recommend-tab-all')));
await tester.pumpAndSettle();
await tester.tap(find.byKey(const Key('recommend-tab-all')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 70));
final nav = tester.widget<RecommendTopNav>(find.byType(RecommendTopNav));
expect(nav.pagePosition, inExclusiveRange(0.5, 1.5));
expect(nav.selected, RecommendInnerTab.allDramas);
await tester.pumpAndSettle();
_drainLayoutExceptions(tester);
expect(find.byKey(const Key('all-dramas-grid')), findsOneWidget);
},
);
testWidgets('should_load_next_page_without_expanding_page_size', (
tester,
) async {
+3 -6
View File
@@ -67,10 +67,7 @@ void main() {
tester.getCenter(searchEntry).dy,
closeTo(tester.getCenter(walletEntry).dy, 0.1),
);
expect(
tester.getTopRight(walletEntry).dx,
closeTo(390 - 8, 0.1),
);
expect(tester.getTopRight(walletEntry).dx, closeTo(390 - 8, 0.1));
expect(
tester.getTopRight(searchEntry).dx,
closeTo(tester.getTopLeft(walletEntry).dx, 0.1),
@@ -160,7 +157,7 @@ void main() {
expect(
find.descendant(
of: find.byKey(const Key('tab-recommend')),
matching: find.byIcon(Icons.celebration_outlined),
matching: find.byIcon(Icons.smart_display_outlined),
),
findsOneWidget,
);
@@ -191,7 +188,7 @@ void main() {
expect(
find.descendant(
of: find.byKey(const Key('tab-recommend')),
matching: find.byIcon(Icons.celebration_rounded),
matching: find.byIcon(Icons.smart_display_rounded),
),
findsOneWidget,
);
+24 -13
View File
@@ -198,6 +198,11 @@ void main() {
posterUrl: '',
genre: 'Romance',
category: 'Urban',
types: [
{'en': 'Romance'},
{'en': 'Urban'},
{'en': 'Revenge'},
],
totalEpisodes: 8,
freeEpisodes: 8,
viewCount: 419000,
@@ -255,7 +260,7 @@ void main() {
);
expect(seriesTitle.data, 'Feed Drama');
expect(find.text('Episode 2'), findsNothing);
expect(seriesTitle.style?.fontSize, 17);
expect(seriesTitle.style?.fontSize, 18);
expect(seriesTitle.style?.fontWeight, FontWeight.w400);
expect(seriesTitle.style?.color, AppColors.textPrimary);
final seriesChevron = tester.widget<Icon>(
@@ -284,11 +289,13 @@ void main() {
expect(find.byKey(const Key('home-feed-full-series')), findsOneWidget);
expect(find.byKey(const Key('home-feed-tags')), findsOneWidget);
expect(find.byKey(const Key('home-feed-tag-Romance')), findsOneWidget);
expect(find.byKey(const Key('home-feed-hot-badge')), findsNothing);
expect(find.byKey(const Key('home-feed-tag-Urban')), findsOneWidget);
expect(find.byKey(const Key('home-feed-tag-Revenge')), findsNothing);
final collapsedSynopsis = tester.widget<Text>(
find.byKey(const Key('home-feed-synopsis')),
);
expect(collapsedSynopsis.maxLines, 1);
expect(collapsedSynopsis.maxLines, 2);
expect(collapsedSynopsis.overflow, TextOverflow.ellipsis);
expect(find.byKey(const Key('home-feed-synopsis-more')), findsOneWidget);
await tester.tap(find.byKey(const Key('home-feed-synopsis-more')));
@@ -302,8 +309,9 @@ void main() {
.widget<Text>(find.byKey(const Key('home-feed-synopsis-expanded')))
.textSpan!
.toPlainText(),
allOf(contains('EP2'), contains('dangerous city')),
contains('dangerous city'),
);
expect(find.text('Episode 2 / 8'), findsOneWidget);
expect(
tester.widget<Text>(find.byKey(const Key('rail-like-count'))).data,
'1006',
@@ -358,10 +366,10 @@ void main() {
final likeRect = tester.getRect(find.byKey(const Key('rail-like')));
final commentsRect = tester.getRect(find.byKey(const Key('rail-comments')));
final shareRect = tester.getRect(find.byKey(const Key('rail-share')));
// 新版互动栏顺序:收藏 → 评论 → 喜欢 → 分享,间距 12。
expect(commentsRect.top - saveRect.bottom, closeTo(12, 0.1));
expect(likeRect.top - commentsRect.bottom, closeTo(12, 0.1));
expect(shareRect.top - likeRect.bottom, closeTo(12, 0.1));
// 互动栏顺序:收藏 → 评论 → 喜欢 → 分享,紧凑间距 6。
expect(commentsRect.top - saveRect.bottom, closeTo(6, 0.1));
expect(likeRect.top - commentsRect.bottom, closeTo(6, 0.1));
expect(shareRect.top - likeRect.bottom, closeTo(6, 0.1));
expect(
tester.getRect(find.byKey(const Key('home-feed-action-rail'))).right,
closeTo(
@@ -582,9 +590,13 @@ void main() {
final overlayRect = tester.getRect(
find.byKey(const Key('home-feed-gesture')),
);
// 观看条与底部进度条共用同一组左右边距(各 20pt),宽度一致。
// 观看条与左侧信息等宽,进度条仍横跨整个页面,避开右侧互动栏。
expect(bar.left, closeTo(overlayRect.left + 20, 0.1));
expect(bar.width, closeTo(overlayRect.width - 40, 0.1));
final infoRect = tester.getRect(
find.byKey(const Key('home-feed-info-panel')),
);
expect(bar.width, closeTo(infoRect.width, 0.1));
expect(bar.right, closeTo(infoRect.right, 0.1));
expect(
bar.left,
closeTo(
@@ -594,9 +606,8 @@ void main() {
);
expect(
bar.width,
closeTo(
lessThan(
tester.getRect(find.byKey(const Key('home-feed-progress'))).width,
0.1,
),
);
// 进度线贴着浮层底部(线心距底 4pt),且位于观看条下方。
@@ -609,10 +620,10 @@ void main() {
tester
.widget<Material>(find.byKey(const Key('home-feed-full-series-bg')))
.color,
AppColors.textPrimary.withValues(alpha: 0.15),
Colors.transparent,
);
expect(
tester.getRect(find.textContaining('Watch full series')).center.dy,
tester.getRect(find.text('Watch all')).center.dy,
closeTo(bar.center.dy, 2),
);
expect(
+149
View File
@@ -0,0 +1,149 @@
// Author: fruit.com.cn
// 职责:验证三种顶部导航接入稳定文字过渡并保留点击与选中语义。
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/content_category.dart';
import 'package:fruitshorts/core/widgets/animated_nav_label.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/home/widgets/home_grid_header.dart';
import 'package:fruitshorts/pages/recommend/recommend_types.dart';
import 'package:fruitshorts/pages/recommend/widgets/recommend_channel_nav.dart';
import 'package:fruitshorts/pages/recommend/widgets/recommend_top_nav.dart';
const _midpoint = Duration(milliseconds: 90);
const _duration = Duration(milliseconds: 180);
const _categories = [
ContentCategory(id: 'featured', name: '精选'),
ContentCategory(id: 'new', name: '新剧'),
];
const _shortcuts = [
HomeGridCategoryShortcut(
label: '精选',
categoryName: 'featured',
categoryId: 'featured',
tab: HomePrimaryTab.featured,
),
HomeGridCategoryShortcut(
label: '新剧',
categoryName: 'new',
categoryId: 'new',
tab: HomePrimaryTab.newReleases,
),
];
Widget _host(Widget child) => MaterialApp(
locale: const Locale('zh'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(body: child),
);
double _scale(WidgetTester tester, Finder label) {
return tester
.widget<Transform>(
find.descendant(of: label, matching: find.byType(Transform)),
)
.transform
.entry(0, 0);
}
Future<void> _verifyTransition(
WidgetTester tester,
Widget Function(bool) build,
) async {
await tester.pumpWidget(_host(build(true)));
await tester.pumpAndSettle();
final labels = find.byType(AnimatedNavLabel);
final rects = [tester.getRect(labels.at(0)), tester.getRect(labels.at(1))];
await tester.pumpWidget(_host(build(false)));
await tester.pump(_midpoint);
expect(_scale(tester, labels.at(0)), inExclusiveRange(0.9, 1.0));
expect(_scale(tester, labels.at(1)), inExclusiveRange(0.9, 1.0));
expect(tester.getRect(labels.at(0)), rects[0]);
expect(tester.getRect(labels.at(1)), rects[1]);
await tester.pump(_duration);
expect(tester.getRect(labels.at(1)), rects[1]);
final semantics = tester.widget<Semantics>(
find.descendant(of: labels.at(1), matching: find.byType(Semantics)).first,
);
expect(semantics.properties.selected, isTrue);
}
void main() {
testWidgets('should_keep_channel_labels_regular_when_selection_changes', (
tester,
) async {
const categories = [
ContentCategory(id: 'featured', name: '精选'),
ContentCategory(id: 'weekly', name: '本周必看'),
];
for (final selected in categories) {
await tester.pumpWidget(
_host(
RecommendChannelNav(
categories: categories,
selectedId: selected.id,
onSelected: (_) {},
onSearch: () {},
),
),
);
await tester.pumpAndSettle();
for (final category in categories) {
final text = tester.widget<Text>(find.text(category.name));
expect(text.style?.fontWeight, FontWeight.w400);
expect(text.style?.fontSize, 17);
}
expect(tester.takeException(), isNull);
}
});
testWidgets('should_keep_home_labels_stable_when_category_changes', (
tester,
) async {
int? tapped;
HomeGridHeader header(bool first) => HomeGridHeader(
callbacks: HomeGridCallbacks(
onSearch: () {},
onOpenVip: () {},
onOpenWallet: () {},
onOpenFilter: () {},
),
categories: _shortcuts,
selectedIndex: first ? 0 : 1,
onSectionSelected: (index) => tapped = index,
);
await _verifyTransition(tester, header);
await tester.tap(find.byKey(const Key('home-primary-label-new')));
expect(tapped, 1);
});
testWidgets('should_keep_inner_nav_labels_stable_when_tab_changes', (
tester,
) async {
RecommendInnerTab? tapped;
RecommendTopNav nav(bool first) => RecommendTopNav(
selected: first ? RecommendInnerTab.feed : RecommendInnerTab.playlists,
onSelected: (tab) => tapped = tab,
onSearch: () {},
);
await _verifyTransition(tester, nav);
await tester.tap(find.byKey(const Key('recommend-tab-playlist')));
expect(tapped, RecommendInnerTab.playlists);
});
testWidgets('should_keep_channel_labels_stable_when_selection_changes', (
tester,
) async {
String? tapped;
RecommendChannelNav nav(bool first) => RecommendChannelNav(
categories: _categories,
selectedId: first ? 'featured' : 'new',
onSelected: (id) => tapped = id,
onSearch: () {},
);
await _verifyTransition(tester, nav);
await tester.tap(find.byKey(const Key('recommend-tab-new')));
expect(tapped, 'new');
});
}
@@ -0,0 +1,73 @@
// Author: fruit.com.cn
// 职责:验证推荐页封面标签使用常规字重,保留原有样式与点击。
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/recommend/widgets/home_recommend_content.dart';
const _tag = '热播';
const _fallback = '上新';
const _posterWidth = 110.0;
const _posterHeight = 230.0;
void main() {
for (final tag in [_tag, '']) {
testWidgets(
'should_keep_badge_regular_when_using_${tag.isEmpty ? 'fallback' : 'tag'}',
(tester) async {
var taps = 0;
await tester.pumpWidget(_host(tag, () => taps++));
await tester.pumpAndSettle();
final label = find.text(tag.isEmpty ? _fallback : tag);
final text = tester.widget<Text>(label);
expect(text.style?.fontWeight, FontWeight.w400);
expect(text.style?.fontSize, 11);
expect(text.style?.color, AppColors.textPrimary);
final badge = tester.widget<Container>(
find.ancestor(of: label, matching: find.byType(Container)).first,
);
final decoration = badge.decoration! as BoxDecoration;
expect(decoration.color, AppColors.brandRed);
expect(decoration.borderRadius, BorderRadius.circular(6));
expect(decoration.boxShadow, isNull);
expect(
badge.padding,
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
);
final position = tester.widget<Positioned>(
find.ancestor(of: label, matching: find.byType(Positioned)).first,
);
expect(position.left, 8);
expect(position.top, 8);
await tester.tap(label);
expect(taps, 1);
expect(tester.takeException(), isNull);
},
);
}
}
Widget _host(String tag, VoidCallback onTap) => MaterialApp(
locale: const Locale('zh'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: SizedBox(
width: _posterWidth,
height: _posterHeight,
child: HomePosterCard(
drama: Drama(
id: 'poster',
title: '测试短剧',
synopsis: '',
posterUrl: '',
tag: tag,
),
badge: _fallback,
onTap: onTap,
),
),
),
);
@@ -0,0 +1,105 @@
// Author: fruit.com.cn
// 职责:验证首页跨页动画的目标保留、连点打断和减少动画行为。
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/pages/recommend/home_tab_motion_controller.dart';
const _midpoint = Duration(milliseconds: 90);
const _finish = Duration(milliseconds: 300);
Widget _pager(HomeTabMotionController controller, List<int> accepted) =>
MaterialApp(
home: PageView(
controller: controller,
onPageChanged: (index) {
if (controller.acceptsSelection(index)) accepted.add(index);
},
children: const [
ColoredBox(color: Colors.black),
ColoredBox(color: Colors.red),
ColoredBox(color: Colors.grey),
],
),
);
void main() {
testWidgets(
'should_ignore_intermediate_page_when_clicking_across_two_pages',
(tester) async {
int? settled;
final controller = HomeTabMotionController(
onSettled: (index) => settled = index,
);
addTearDown(controller.dispose);
final accepted = <int>[];
await tester.pumpWidget(_pager(controller, accepted));
final transition = controller.selectPage(2, reduceMotion: false);
await tester.pump();
await tester.pump(_midpoint);
expect(controller.pagePosition, inExclusiveRange(0.0, 2.0));
expect(controller.acceptsSelection(1), isFalse);
expect(accepted, isNot(contains(1)));
await tester.pump(_finish);
await transition;
expect(controller.pagePosition, 2);
expect(settled, 2);
expect(controller.acceptsSelection(1), isTrue);
},
);
testWidgets('should_keep_last_target_when_clicks_interrupt_each_other', (
tester,
) async {
int? settled;
final controller = HomeTabMotionController(
onSettled: (index) => settled = index,
);
addTearDown(controller.dispose);
await tester.pumpWidget(_pager(controller, []));
final first = controller.selectPage(2, reduceMotion: false);
await tester.pump();
await tester.pump(_midpoint);
final last = controller.selectPage(0, reduceMotion: false);
await tester.pump();
expect(controller.acceptsSelection(2), isFalse);
await tester.pump(_finish);
await Future.wait([first, last]);
expect(controller.pagePosition, 0);
expect(settled, 0);
});
testWidgets('should_jump_to_target_when_reduced_motion_is_enabled', (
tester,
) async {
int? settled;
final controller = HomeTabMotionController(
onSettled: (index) => settled = index,
);
addTearDown(controller.dispose);
await tester.pumpWidget(_pager(controller, []));
await controller.selectPage(2, reduceMotion: true);
expect(controller.pagePosition, 2);
expect(settled, 2);
});
testWidgets('should_release_target_when_user_drag_interrupts_animation', (
tester,
) async {
final controller = HomeTabMotionController(onSettled: (_) {});
addTearDown(controller.dispose);
await tester.pumpWidget(_pager(controller, []));
final transition = controller.selectPage(2, reduceMotion: false);
await tester.pump();
await tester.pump(_midpoint);
final gesture = await tester.startGesture(
tester.getCenter(find.byType(PageView)),
);
await gesture.moveBy(const Offset(60, 0));
await tester.pump();
await transition;
expect(controller.acceptsSelection(0), isTrue);
expect(controller.acceptsSelection(1), isTrue);
await gesture.up();
await tester.pumpAndSettle();
});
}
+13 -6
View File
@@ -44,11 +44,11 @@ void main() {
final recommendText = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('recommend-tab-recommend')),
matching: find.text('Recommend'),
matching: find.text('Featured'),
),
);
expect(recommendText.style?.fontSize, 17);
expect(recommendText.style?.fontWeight, FontWeight.w700);
expect(recommendText.style?.fontSize, 18);
expect(recommendText.style?.fontWeight, FontWeight.w400);
expect(recommendText.style?.color, AppColors.textPrimary);
final playlistText = tester.widget<Text>(
find.descendant(
@@ -56,10 +56,17 @@ void main() {
matching: find.text('Playlists'),
),
);
expect(playlistText.style?.fontSize, 17);
expect(playlistText.style?.fontSize, 18);
// 字形度量固定,未选中态靠绘制缩放和灰色区分,切换时不重新排版。
expect(playlistText.style?.fontWeight, FontWeight.w400);
// 新版顶部 tab 未选中态也用主文字色,选中态只靠字重和下划线区分。
expect(playlistText.style?.color, AppColors.textPrimary);
expect(playlistText.style?.color, AppColors.homeTabInactive);
final playlistScale = tester.widget<Transform>(
find.descendant(
of: find.byKey(const Key('recommend-tab-playlist')),
matching: find.byType(Transform),
),
);
expect(playlistScale.transform.entry(0, 0), closeTo(0.9, 0.0001));
expect(
tester
.widget<IconButton>(find.byKey(const Key('recommend-search')))
@@ -0,0 +1,146 @@
// Author: fruit.com.cn
// 职责:验证首页三标签文字和下划线随页面进度连续变化。
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/recommend/recommend_types.dart';
import 'package:fruitshorts/pages/recommend/widgets/recommend_top_nav.dart';
const _pagePosition = 0.35;
const _duration = Duration(milliseconds: 90);
const _epsilon = 0.0001;
Widget _host({
double? position,
RecommendInnerTab tab = RecommendInnerTab.feed,
}) {
return MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(
body: RecommendTopNav(
selected: tab,
pagePosition: position,
onSelected: (_) {},
onSearch: () {},
),
),
);
}
double _scale(WidgetTester tester, String tab) => tester
.widget<Transform>(
find.descendant(
of: find.byKey(Key('recommend-tab-$tab')),
matching: find.byType(Transform),
),
)
.transform
.entry(0, 0);
Color _underline(WidgetTester tester, String tab) {
final line = tester.widget<Container>(
find.byKey(Key('recommend-tab-$tab-underline')),
);
return (line.decoration! as BoxDecoration).color!;
}
void main() {
testWidgets(
'should_brighten_only_inactive_home_tabs_when_selection_changes',
(tester) async {
const inactive = Color(0xFFC7C7CC);
const codes = ['recommend', 'playlist', 'all'];
for (final tab in RecommendInnerTab.values) {
await tester.pumpWidget(
_host(position: tab.index.toDouble(), tab: tab),
);
for (final (index, code) in codes.indexed) {
final text = tester.widget<Text>(
find.descendant(
of: find.byKey(Key('recommend-tab-$code')),
matching: find.byType(Text),
),
);
expect(
text.style?.color,
index == tab.index ? AppColors.textPrimary : inactive,
);
}
}
await tester.pumpWidget(_host(position: _pagePosition));
final text = tester.widget<Text>(
find.descendant(
of: find.byKey(const Key('recommend-tab-playlist')),
matching: find.byType(Text),
),
);
expect(
text.style?.color,
Color.lerp(inactive, AppColors.textPrimary, _pagePosition),
);
expect(AppColors.textTabInactive, const Color(0xFFB0B0B0));
},
);
testWidgets('should_keep_regular_enlarged_labels_for_every_selection', (
tester,
) async {
for (final tab in RecommendInnerTab.values) {
await tester.pumpWidget(_host(position: tab.index.toDouble(), tab: tab));
for (final code in ['recommend', 'playlist', 'all']) {
final text = tester.widget<Text>(
find.descendant(
of: find.byKey(Key('recommend-tab-$code')),
matching: find.byType(Text),
),
);
expect(text.style?.fontSize, 18);
expect(text.style?.fontWeight, FontWeight.w400);
}
expect(tester.takeException(), isNull);
}
});
testWidgets('should_follow_page_position_without_delayed_label_animation', (
tester,
) async {
await tester.pumpWidget(_host(position: 0));
final first = tester.getRect(
find.byKey(const Key('recommend-tab-recommend')),
);
final second = tester.getRect(
find.byKey(const Key('recommend-tab-playlist')),
);
await tester.pumpWidget(
_host(position: _pagePosition, tab: RecommendInnerTab.playlists),
);
expect(_scale(tester, 'recommend'), closeTo(0.965, _epsilon));
expect(_scale(tester, 'playlist'), closeTo(0.935, _epsilon));
expect(_underline(tester, 'recommend').a, closeTo(0.65, _epsilon));
expect(_underline(tester, 'playlist').a, closeTo(0.35, _epsilon));
expect(_underline(tester, 'all').a, 0);
expect(
tester.getRect(find.byKey(const Key('recommend-tab-recommend'))),
first,
);
expect(
tester.getRect(find.byKey(const Key('recommend-tab-playlist'))),
second,
);
await tester.pumpWidget(_host(position: 0));
expect(_scale(tester, 'recommend'), 1);
expect(_underline(tester, 'recommend'), AppColors.brandRed);
});
testWidgets('should_fade_both_underlines_when_used_without_page_position', (
tester,
) async {
await tester.pumpWidget(_host());
await tester.pumpWidget(_host(tab: RecommendInnerTab.playlists));
await tester.pump(_duration);
expect(_underline(tester, 'recommend').a, inExclusiveRange(0.0, 1.0));
expect(_underline(tester, 'playlist').a, inExclusiveRange(0.0, 1.0));
});
}