feat(home): 顶部导航随翻页联动并把推荐更名为精选
This commit is contained in:
1 parent
2f4593a46e
commit
335e5dc286
27 files changed
+1095
-214
No files matched your search
@@ -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: (_) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user