feat(recommend): 上新板块改按后端语义码判断,顶部轮播改为无缝循环
flutter-ci / test (push) Failing after 30m39s

Drama 新增 tagCode,推荐页只看 NEW_ARRIVAL 这个码决定上新横滑,
不再依赖可变的标签文案;分类网格高度改为复用卡片的统一常量。
头部轮播改用虚拟无限页码,首尾相接且能反向滑,自动播放不再倒滑回第一张。
This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-29 15:44:33 +08:00
1 parent c311985601
commit 3590cd0c55
5 files changed
+235 -73

No files matched your search

+18 -3
View File
@@ -102,6 +102,7 @@ class Drama {
this.synopsisLocales = const {}, this.synopsisLocales = const {},
this.tag = '', this.tag = '',
this.tagLocales = const {}, this.tagLocales = const {},
this.tagCode = '',
this.tagColor = '', this.tagColor = '',
this.category = '', this.category = '',
this.categoryLocales = const {}, this.categoryLocales = const {},
@@ -127,6 +128,9 @@ class Drama {
final Map<String, String> synopsisLocales; final Map<String, String> synopsisLocales;
final String tag; final String tag;
final Map<String, String> tagLocales; final Map<String, String> tagLocales;
/// 标签语义码(如 `NEW_ARRIVAL`)。文案会随语言变,语义判断只认这个码。
final String tagCode;
final String tagColor; final String tagColor;
final String posterUrl; final String posterUrl;
final List<Episode> episodes; final List<Episode> episodes;
@@ -172,6 +176,7 @@ class Drama {
synopsisLocales: text.synopsisLocales, synopsisLocales: text.synopsisLocales,
tag: text.tag, tag: text.tag,
tagLocales: text.tagLocales, tagLocales: text.tagLocales,
tagCode: (json['tagCode'] as String? ?? '').trim(),
tagColor: json['tagColor'] as String? ?? '', tagColor: json['tagColor'] as String? ?? '',
posterUrl: json['cover'] as String? ?? '', posterUrl: json['cover'] as String? ?? '',
episodes: preview is Map<String, dynamic> episodes: preview is Map<String, dynamic>
@@ -204,6 +209,7 @@ class Drama {
synopsisLocales: text.synopsisLocales, synopsisLocales: text.synopsisLocales,
tag: text.tag, tag: text.tag,
tagLocales: text.tagLocales, tagLocales: text.tagLocales,
tagCode: (json['tagCode'] as String? ?? '').trim(),
tagColor: json['tagColor'] as String? ?? '', tagColor: json['tagColor'] as String? ?? '',
posterUrl: json['cover'] as String? ?? '', posterUrl: json['cover'] as String? ?? '',
category: text.category, category: text.category,
@@ -315,7 +321,10 @@ class _DramaText {
final category = _firstText(categoryLocales, json['category'], types); final category = _firstText(categoryLocales, json['category'], types);
return _DramaText( return _DramaText(
title: _firstText(titleLocales, json['title']), title: _firstText(titleLocales, json['title']),
synopsis: _firstText(synopsisLocales, json['synopsis'] ?? json['description']), synopsis: _firstText(
synopsisLocales,
json['synopsis'] ?? json['description'],
),
titleLocales: titleLocales, titleLocales: titleLocales,
synopsisLocales: synopsisLocales, synopsisLocales: synopsisLocales,
tag: _firstText(tagLocales, json['tag']), tag: _firstText(tagLocales, json['tag']),
@@ -328,7 +337,11 @@ class _DramaText {
} }
} }
String _firstText(Map<String, String> locales, Object? fallback, [List<Map<String, String>>? types]) { String _firstText(
Map<String, String> locales,
Object? fallback, [
List<Map<String, String>>? types,
]) {
final chinese = locales['zh'] ?? ''; final chinese = locales['zh'] ?? '';
if (chinese.isNotEmpty) return chinese; if (chinese.isNotEmpty) return chinese;
if (types != null && types.isNotEmpty) { if (types != null && types.isNotEmpty) {
@@ -339,7 +352,9 @@ String _firstText(Map<String, String> locales, Object? fallback, [List<Map<Strin
} }
String _genreText(List<Map<String, String>> types, Object? fallback) { String _genreText(List<Map<String, String>> types, Object? fallback) {
final names = types.map((item) => item['zh'] ?? '').where((value) => value.isNotEmpty); final names = types
.map((item) => item['zh'] ?? '')
.where((value) => value.isNotEmpty);
final joined = names.join(' / '); final joined = names.join(' / ');
if (joined.isNotEmpty) return joined; if (joined.isNotEmpty) return joined;
return fallback is String ? fallback : ''; return fallback is String ? fallback : '';
@@ -138,8 +138,7 @@ class _RecommendDiscoverPageState extends State<RecommendDiscoverPage>
} }
} }
Future<void> _openDrama(Drama drama) => Future<void> _openDrama(Drama drama) => playDramaFromCatalog(context, drama);
playDramaFromCatalog(context, drama);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -163,9 +162,7 @@ class _RecommendDiscoverPageState extends State<RecommendDiscoverPage>
Positioned.fill( Positioned.fill(
child: categoryList.isEmpty child: categoryList.isEmpty
? const Center( ? const Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(color: AppColors.brandRed),
color: AppColors.brandRed,
),
) )
: PageView.builder( : PageView.builder(
key: const Key('recommend-channel-pager'), key: const Key('recommend-channel-pager'),
@@ -184,7 +181,8 @@ class _RecommendDiscoverPageState extends State<RecommendDiscoverPage>
carouselActive: widget.active && index == selectedIndex, carouselActive: widget.active && index == selectedIndex,
mediaTop: mediaTop, mediaTop: mediaTop,
onRequest: _ensureCategory, onRequest: _ensureCategory,
onRefresh: () => _refreshCategory(category.id, index == 0), onRefresh: () =>
_refreshCategory(category.id, index == 0),
onOpenDrama: _openDrama, onOpenDrama: _openDrama,
); );
}, },
@@ -294,11 +292,7 @@ class _ChannelFeedState extends State<_ChannelFeed>
List<Drama> newArrivals = const []; List<Drama> newArrivals = const [];
List<Drama> rest = dramas; List<Drama> rest = dramas;
if (widget.isFeatured && dramas.isNotEmpty) { if (widget.isFeatured && dramas.isNotEmpty) {
final lang = Localizations.localeOf(context).languageCode; newArrivals = dramas.where(isNewArrivalTag).toList(growable: false);
final badge = AppLocalizations.of(context)!.newArrivalBadge;
newArrivals = dramas
.where((drama) => _isNewArrivalTag(drama, lang, badge))
.toList(growable: false);
if (newArrivals.isNotEmpty) { if (newArrivals.isNotEmpty) {
final newIds = newArrivals.map((drama) => drama.id).toSet(); final newIds = newArrivals.map((drama) => drama.id).toSet();
rest = dramas rest = dramas
@@ -326,7 +320,9 @@ class _ChannelFeedState extends State<_ChannelFeed>
hasScrollBody: false, hasScrollBody: false,
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: showCarousel ? 0 : widget.mediaTop + RecommendChannelNav.height, top: showCarousel
? 0
: widget.mediaTop + RecommendChannelNav.height,
), ),
child: Center(child: _placeholder(slot.loading)), child: Center(child: _placeholder(slot.loading)),
), ),
@@ -334,12 +330,11 @@ class _ChannelFeedState extends State<_ChannelFeed>
); );
} else if (widget.isFeatured) { } else if (widget.isFeatured) {
// 无轮播图时内容需避开顶部悬浮导航。 // 无轮播图时内容需避开顶部悬浮导航。
final contentTop = final contentTop = showCarousel
showCarousel ? 24.0 : widget.mediaTop + RecommendChannelNav.height + 16; ? 24.0
: widget.mediaTop + RecommendChannelNav.height + 16;
if (newArrivals.isNotEmpty) { if (newArrivals.isNotEmpty) {
sliverList.add( sliverList.add(SliverToBoxAdapter(child: SizedBox(height: contentTop)));
SliverToBoxAdapter(child: SizedBox(height: contentTop)),
);
sliverList.add( sliverList.add(
SliverToBoxAdapter( SliverToBoxAdapter(
child: HomeNewArrivalRow( child: HomeNewArrivalRow(
@@ -348,9 +343,7 @@ class _ChannelFeedState extends State<_ChannelFeed>
), ),
), ),
); );
sliverList.add( sliverList.add(const SliverToBoxAdapter(child: SizedBox(height: 28)));
const SliverToBoxAdapter(child: SizedBox(height: 28)),
);
} }
sliverList.add( sliverList.add(
_gridSliver( _gridSliver(
@@ -363,7 +356,9 @@ class _ChannelFeedState extends State<_ChannelFeed>
sliverList.add( sliverList.add(
_gridSliver( _gridSliver(
dramas, dramas,
topPadding: showCarousel ? 20 : widget.mediaTop + RecommendChannelNav.height + 16, topPadding: showCarousel
? 20
: widget.mediaTop + RecommendChannelNav.height + 16,
crossAxisCount: 3, crossAxisCount: 3,
), ),
); );
@@ -427,7 +422,6 @@ class _ChannelFeedState extends State<_ChannelFeed>
const pageSide = 14.0; const pageSide = 14.0;
const cardGap = 9.0; const cardGap = 9.0;
const rowGap = 16.0; const rowGap = 16.0;
const cardBodyExtent = 44.0; // 封面下间距 + 剧名行 + 集数行
// 两列时卡更大,卡间距略放宽。 // 两列时卡更大,卡间距略放宽。
final spacing = crossAxisCount == 2 ? 12.0 : cardGap; final spacing = crossAxisCount == 2 ? 12.0 : cardGap;
return SliverPadding( return SliverPadding(
@@ -437,7 +431,7 @@ class _ChannelFeedState extends State<_ChannelFeed>
final itemWidth = final itemWidth =
(constraints.crossAxisExtent - spacing * (crossAxisCount - 1)) / (constraints.crossAxisExtent - spacing * (crossAxisCount - 1)) /
crossAxisCount; crossAxisCount;
final itemExtent = itemWidth * 157 / 110 + cardBodyExtent; final itemExtent = itemWidth * 157 / 110 + homePosterCardBodyExtent;
return SliverGrid( return SliverGrid(
key: Key('recommend-category-grid-${widget.categoryId}'), key: Key('recommend-category-grid-${widget.categoryId}'),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
@@ -446,17 +440,14 @@ class _ChannelFeedState extends State<_ChannelFeed>
mainAxisSpacing: rowGap, mainAxisSpacing: rowGap,
mainAxisExtent: itemExtent, mainAxisExtent: itemExtent,
), ),
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate((context, index) {
(context, index) { final drama = dramas[index];
final drama = dramas[index]; return HomePosterCard(
return HomePosterCard( key: Key('recommend-category-card-${drama.id}'),
key: Key('recommend-category-card-${drama.id}'), drama: drama,
drama: drama, onTap: () => widget.onOpenDrama(drama),
onTap: () => widget.onOpenDrama(drama), );
); }, childCount: dramas.length),
},
childCount: dramas.length,
),
); );
}, },
), ),
@@ -464,14 +455,11 @@ class _ChannelFeedState extends State<_ChannelFeed>
} }
} }
/// 判定接口下发的短剧标签是否为「上新」。 /// 「上新」标签语义码,对应后台 content_tag.code。
const String newArrivalTagCode = 'NEW_ARRIVAL';
/// 判定短剧是否属于「上新」板块。
/// ///
/// 完全以后端返回的 `tag` / `tagLocales`(经 [Drama.localizedTag] 本地化) /// 只看语义码:码由后台标签管理配置,文案怎么改、切什么语言都不影响。
/// 为准;兼容「上新 / NEW / BARU」三种文案,并兼容「本周上新」这类带前后缀的写法。 bool isNewArrivalTag(Drama drama) =>
bool _isNewArrivalTag(Drama drama, String lang, String localizedBadge) { drama.tagCode.trim().toUpperCase() == newArrivalTagCode;
final tag = drama.localizedTag(lang).trim();
if (tag.isEmpty) return false;
if (tag == localizedBadge) return true;
final lower = tag.toLowerCase();
return tag.contains('上新') || lower == 'new' || lower == 'baru';
}
@@ -46,15 +46,25 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
static const double _dotGap = 9; static const double _dotGap = 9;
static const double _ctaHeight = 38; static const double _ctaHeight = 38;
final PageController _controller = PageController(); /// 无限轮播:PageView 用虚拟大页码,取模映射到真实 banner,首尾天然相接。
static const int _basePage = 1000;
late final PageController _controller;
Timer? _timer; Timer? _timer;
int _page = 0;
int _index = 0; int _index = 0;
bool _pressed = false; bool _pressed = false;
bool _tickerEnabled = true; bool _tickerEnabled = true;
/// 起始页码取 banner 数量的大倍数,向左右滑都不会到头。
static int _initialPageOf(int bannerCount) =>
bannerCount > 1 ? bannerCount * _basePage : 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_page = _initialPageOf(widget.banners.length);
_controller = PageController(initialPage: _page);
WidgetsBinding.instance.addPostFrameCallback((_) => _syncTimer()); WidgetsBinding.instance.addPostFrameCallback((_) => _syncTimer());
} }
@@ -72,8 +82,11 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
void didUpdateWidget(covariant HomeHeroCarousel oldWidget) { void didUpdateWidget(covariant HomeHeroCarousel oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.banners.length != widget.banners.length) { if (oldWidget.banners.length != widget.banners.length) {
if (_index >= widget.banners.length) _index = 0; // 首屏 banner 是异步到的:列表从空变有之后要把页码挪到虚拟起点,
_syncTimer(); // 否则左滑第一下就到头了。
_page = _initialPageOf(widget.banners.length);
_index = 0;
_jumpToBasePage();
} }
if (oldWidget.banners.length != widget.banners.length || if (oldWidget.banners.length != widget.banners.length ||
oldWidget.active != widget.active) { oldWidget.active != widget.active) {
@@ -91,7 +104,8 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
void _syncTimer() { void _syncTimer() {
_timer?.cancel(); _timer?.cancel();
_timer = null; _timer = null;
final canAdvance = mounted && final canAdvance =
mounted &&
widget.active && widget.active &&
_tickerEnabled && _tickerEnabled &&
!_pressed && !_pressed &&
@@ -102,12 +116,16 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
void _advance() { void _advance() {
if (!_controller.hasClients || widget.banners.length < 2) return; if (!_controller.hasClients || widget.banners.length < 2) return;
final next = (_index + 1) % widget.banners.length; // 虚拟页码无限,直接翻下一页即可,不用在末尾跳回第 0 页。
_controller.animateToPage( _controller.nextPage(duration: _pageDuration, curve: Curves.easeOutCubic);
next, }
duration: _pageDuration,
curve: Curves.easeOutCubic, /// 把 PageView 静默挪到虚拟起点:视觉上是同一张图,不会闪。
); void _jumpToBasePage() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !_controller.hasClients) return;
if (_controller.page?.round() != _page) _controller.jumpToPage(_page);
});
} }
@override @override
@@ -134,9 +152,17 @@ class _HomeHeroCarouselState extends State<HomeHeroCarousel> {
children: [ children: [
PageView.builder( PageView.builder(
controller: _controller, controller: _controller,
itemCount: widget.banners.length, // 数量大于 1 时用无限页码做无缝循环,取模映射到真实 banner。
onPageChanged: (value) => setState(() => _index = value), itemCount: widget.banners.length > 1
itemBuilder: (context, index) => _bannerPage(context, index), ? null
: widget.banners.length,
onPageChanged: (value) => setState(() {
_page = value;
final count = widget.banners.length;
_index = count == 0 ? 0 : value % count;
}),
itemBuilder: (context, page) =>
_bannerPage(context, page % widget.banners.length),
), ),
Positioned(left: 0, right: 0, bottom: 14, child: _dots(context)), Positioned(left: 0, right: 0, bottom: 14, child: _dots(context)),
], ],
@@ -288,16 +314,18 @@ class _HeroCopy extends StatelessWidget {
color: AppColors.textPrimary.withValues(alpha: 0.95), color: AppColors.textPrimary.withValues(alpha: 0.95),
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
shadows: const [ shadows: const [Shadow(color: Color(0x80000000), blurRadius: 8)],
Shadow(color: Color(0x80000000), blurRadius: 8),
],
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
_WatchButton(height: ctaHeight, label: l10n.watchNowAction, onTap: onWatch), _WatchButton(
height: ctaHeight,
label: l10n.watchNowAction,
onTap: onWatch,
),
const SizedBox(width: 12), const SizedBox(width: 12),
_FavoriteCircle(size: ctaHeight, drama: drama), _FavoriteCircle(size: ctaHeight, drama: drama),
], ],
@@ -0,0 +1,83 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/home_feed.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
import 'package:fruitshorts/pages/recommend/widgets/home_hero_carousel.dart';
const List<FeedBanner> _banners = [
FeedBanner(id: '1', imageUrl: 'https://cdn.test/a.png'),
FeedBanner(id: '2', imageUrl: 'https://cdn.test/b.png'),
];
Widget _harness(List<FeedBanner> banners) {
return MaterialApp(
locale: const Locale('en'),
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
home: Scaffold(
body: HomeHeroCarousel(
banners: banners,
dramaFor: (_) => null,
onWatch: (_) {},
),
),
);
}
Color? _dotColor(WidgetTester tester, int index) {
final decoration =
tester
.widget<Container>(find.byKey(Key('home-hero-dot-$index')))
.decoration
as BoxDecoration?;
return decoration?.color;
}
void main() {
testWidgets('should_keep_looping_backwards_from_the_first_banner', (
tester,
) async {
await tester.pumpWidget(_harness(_banners));
await tester.pumpAndSettle();
expect(_dotColor(tester, 0), AppColors.brandRed);
// 从第一张往右滑:无缝轮播应该接到最后一张,而不是被边界挡住。
await tester.fling(find.byType(PageView), const Offset(700, 0), 1200);
await tester.pumpAndSettle();
expect(_dotColor(tester, 1), AppColors.brandRed);
expect(_dotColor(tester, 0), isNot(AppColors.brandRed));
});
testWidgets('should_wrap_to_first_banner_after_last_auto_advance', (
tester,
) async {
await tester.pumpWidget(_harness(_banners));
await tester.pumpAndSettle();
// 自动播两次:第 2 张之后应回到第 1 张,且页码继续往前走不回头。
await tester.pump(const Duration(milliseconds: 4600));
await tester.pumpAndSettle();
expect(_dotColor(tester, 1), AppColors.brandRed);
await tester.pump(const Duration(milliseconds: 4600));
await tester.pumpAndSettle();
expect(_dotColor(tester, 0), AppColors.brandRed);
});
testWidgets('should_not_loop_when_only_one_banner', (tester) async {
await tester.pumpWidget(
_harness(const [FeedBanner(id: '1', imageUrl: 'https://cdn.test/a.png')]),
);
await tester.pumpAndSettle();
expect(find.byKey(const Key('home-hero-dot-0')), findsNothing);
await tester.pump(const Duration(milliseconds: 6000));
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
});
}
+57 -9
View File
@@ -3,6 +3,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/app.dart'; import 'package:fruitshorts/app.dart';
import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart'; import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
import 'package:fruitshorts/core/data/repositories/playback_repository.dart'; import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart'; import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
@@ -10,6 +11,7 @@ import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/core/theme/app_dimens.dart'; import 'package:fruitshorts/core/theme/app_dimens.dart';
import 'package:fruitshorts/core/widgets/app_bottom_nav.dart'; import 'package:fruitshorts/core/widgets/app_bottom_nav.dart';
import 'package:fruitshorts/pages/player/player_view.dart'; import 'package:fruitshorts/pages/player/player_view.dart';
import 'package:fruitshorts/pages/recommend/recommend_discover_page.dart';
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.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'; import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
@@ -34,7 +36,10 @@ void main() {
expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget);
expect(find.byKey(const Key('recommend-tab-playlist')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-playlist')), findsOneWidget);
expect(find.byKey(const Key('recommend-tab-all')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-all')), findsOneWidget);
expect(find.byKey(const Key('recommend-feed-player'), skipOffstage: false), findsOneWidget); expect(
find.byKey(const Key('recommend-feed-player'), skipOffstage: false),
findsOneWidget,
);
expect(find.byKey(const Key('recommend-top-gradient')), findsOneWidget); expect(find.byKey(const Key('recommend-top-gradient')), findsOneWidget);
final recommendText = tester.widget<Text>( final recommendText = tester.widget<Text>(
find.descendant( find.descendant(
@@ -65,7 +70,9 @@ void main() {
find.byKey(const Key('recommend-top-nav')), find.byKey(const Key('recommend-top-nav')),
); );
// 右侧依次是搜索和金色礼物盒(钱包入口),礼物盒贴右边距 8。 // 右侧依次是搜索和金色礼物盒(钱包入口),礼物盒贴右边距 8。
final searchRect = tester.getRect(find.byKey(const Key('recommend-search'))); final searchRect = tester.getRect(
find.byKey(const Key('recommend-search')),
);
final giftRect = tester.getRect(find.byKey(const Key('recommend-gift'))); final giftRect = tester.getRect(find.byKey(const Key('recommend-gift')));
expect(giftRect.right, closeTo(topNavRect.right - 8, 0.1)); expect(giftRect.right, closeTo(topNavRect.right - 8, 0.1));
expect(searchRect.right, lessThanOrEqualTo(giftRect.left)); expect(searchRect.right, lessThanOrEqualTo(giftRect.left));
@@ -78,7 +85,12 @@ void main() {
tester tester
.getRect(find.byKey(const Key('recommend-top-nav'))) .getRect(find.byKey(const Key('recommend-top-nav')))
.overlaps( .overlaps(
tester.getRect(find.byKey(const Key('recommend-feed-player'), skipOffstage: false)), tester.getRect(
find.byKey(
const Key('recommend-feed-player'),
skipOffstage: false,
),
),
), ),
isTrue, isTrue,
); );
@@ -387,7 +399,10 @@ void main() {
tester.getRect(find.byKey(const Key('all-dramas-filters'))).top, tester.getRect(find.byKey(const Key('all-dramas-filters'))).top,
closeTo(fixedFilterTop, 0.1), closeTo(fixedFilterTop, 0.1),
); );
expect(find.byKey(const Key('all-filter-classification-all')), findsOneWidget); expect(
find.byKey(const Key('all-filter-classification-all')),
findsOneWidget,
);
expect(find.byKey(const Key('all-filter-category-all')), findsOneWidget); expect(find.byKey(const Key('all-filter-category-all')), findsOneWidget);
expect(find.byKey(const Key('all-filter-tag-all')), findsOneWidget); expect(find.byKey(const Key('all-filter-tag-all')), findsOneWidget);
expect( expect(
@@ -407,7 +422,10 @@ void main() {
await tester.tap(find.byKey(const Key('recommend-tab-playlist'))); await tester.tap(find.byKey(const Key('recommend-tab-playlist')));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget);
expect(find.byKey(const Key('recommend-feed-player'), skipOffstage: false), findsOneWidget); expect(
find.byKey(const Key('recommend-feed-player'), skipOffstage: false),
findsOneWidget,
);
expect(find.byType(AppBottomNav), findsOneWidget); expect(find.byType(AppBottomNav), findsOneWidget);
await tester.ensureVisible(find.byKey(const Key('recommend-tab-all'))); await tester.ensureVisible(find.byKey(const Key('recommend-tab-all')));
@@ -430,7 +448,10 @@ void main() {
await tester.drag(pages, const Offset(-320, 0)); await tester.drag(pages, const Offset(-320, 0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget); expect(find.byKey(const Key('recommend-section-hits')), findsOneWidget);
expect(find.byKey(const Key('recommend-feed-player'), skipOffstage: false), findsOneWidget); expect(
find.byKey(const Key('recommend-feed-player'), skipOffstage: false),
findsOneWidget,
);
expect( expect(
tester tester
.widget<Text>( .widget<Text>(
@@ -454,7 +475,10 @@ void main() {
await tester.drag(pages, const Offset(640, 0)); await tester.drag(pages, const Offset(640, 0));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byKey(const Key('recommend-feed-player'), skipOffstage: false), findsOneWidget); expect(
find.byKey(const Key('recommend-feed-player'), skipOffstage: false),
findsOneWidget,
);
expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget); expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget);
_drainOverflows(tester); _drainOverflows(tester);
}); });
@@ -497,7 +521,9 @@ void main() {
await _pumpRecommend(tester); await _pumpRecommend(tester);
final gesture = await tester.startGesture( final gesture = await tester.startGesture(
tester.getCenter(find.byKey(const Key('recommend-feed-player'), skipOffstage: false)), tester.getCenter(
find.byKey(const Key('recommend-feed-player'), skipOffstage: false),
),
); );
await gesture.moveBy(const Offset(0, 80)); await gesture.moveBy(const Offset(0, 80));
await tester.pump(); await tester.pump();
@@ -505,7 +531,10 @@ void main() {
final feedScroll = tester.state<ScrollableState>( final feedScroll = tester.state<ScrollableState>(
find find
.descendant( .descendant(
of: find.byKey(const Key('recommend-feed-player'), skipOffstage: false), of: find.byKey(
const Key('recommend-feed-player'),
skipOffstage: false,
),
matching: find.byType(Scrollable), matching: find.byType(Scrollable),
) )
.first, .first,
@@ -544,6 +573,25 @@ void main() {
expect(search.focusNode?.hasFocus, isTrue); expect(search.focusNode?.hasFocus, isTrue);
_drainOverflows(tester); _drainOverflows(tester);
}); });
test('should_judge_new_arrival_only_by_tag_code', () {
Drama tagged({String tagCode = '', String tag = ''}) => Drama(
id: 'tagged',
title: 'Tagged',
synopsis: 'synopsis',
posterUrl: '',
tag: tag,
tagCode: tagCode,
);
expect(isNewArrivalTag(tagged(tagCode: 'NEW_ARRIVAL')), isTrue);
expect(isNewArrivalTag(tagged(tagCode: ' new_arrival ')), isTrue);
// 其它语义码(以及没有码的标签)都不算上新,文案不参与判断。
expect(isNewArrivalTag(tagged(tagCode: 'HOT')), isFalse);
expect(isNewArrivalTag(tagged(tagCode: 'NEW_RELEASE')), isFalse);
expect(isNewArrivalTag(tagged(tag: '上新')), isFalse);
expect(isNewArrivalTag(tagged()), isFalse);
});
} }
/// 测试字体(等宽方块字形)比真实字体宽/高,卡片固定高度会报 2~4px /// 测试字体(等宽方块字形)比真实字体宽/高,卡片固定高度会报 2~4px