// 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/core/theme/app_theme.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/pages/player/widgets/home_feed_overlay.dart'; const _drama = Drama( id: 'info-hierarchy', title: 'The Last Promise', synopsis: 'A hidden promise brings two families together after years apart. ' 'Every secret leads them closer to an unexpected truth.', titleLocales: {'id': 'Janji Terakhir'}, synopsisLocales: { 'id': 'Sebuah janji tersembunyi mempertemukan kembali dua keluarga yang ' 'telah lama terpisah. Setiap rahasia membawa mereka semakin dekat ' 'pada kebenaran yang tidak pernah mereka bayangkan sebelumnya.', }, posterUrl: '', genre: 'Romance', category: 'Urban', types: [ {'zh': '甜宠', 'en': 'Romance', 'id': 'Romantis'}, {'zh': '都市', 'en': 'Urban', 'id': 'Perkotaan'}, ], viewCount: 120000, totalEpisodes: 24, ); const _episode = Episode(id: 'info-2', number: 2, title: '', isFree: true); const _gap = 2.0; const _metadataGap = 8.0; const _epsilon = 0.1; const _wideWidth = 500.0; const _maximumTags = 2; const _shortTag = 'A'; const _secondTag = 'B'; const _extraTag = 'C'; const _motionSample = Duration(milliseconds: 80); const _watchRadius = 6.0; Future _pump( WidgetTester tester, { String language = 'en', double width = 390, double scale = 1, Drama drama = _drama, }) async { tester.view.physicalSize = Size(width, 844); tester.view.devicePixelRatio = 1; addTearDown(tester.view.resetPhysicalSize); addTearDown(tester.view.resetDevicePixelRatio); await tester.pumpWidget( MaterialApp( theme: AppTheme.dark(), locale: Locale(language), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Builder( builder: (context) => MediaQuery( data: MediaQuery.of( context, ).copyWith(textScaler: TextScaler.linear(scale)), child: Scaffold( body: HomeFeedOverlay( l10n: AppLocalizations.of(context)!, drama: drama, episode: _episode, controller: null, locked: false, liked: false, saved: false, visible: true, boosting: false, onLike: () {}, onSave: () {}, onComments: () {}, onShare: () {}, onDetail: () {}, onOpenFullPlayer: () {}, onOpenSearch: () {}, onTogglePlay: () {}, onBoostStart: () {}, onBoostEnd: () {}, onMore: () {}, ), ), ), ), ), ); } Iterable _spanStyles(InlineSpan? span) sync* { if (span is! TextSpan) return; if (span.style != null) yield span.style!; for (final child in span.children ?? const []) { yield* _spanStyles(child); } } void _expectNoShadows(WidgetTester tester) { final panel = find.byKey(const Key('home-feed-info-panel')); for (final text in tester.widgetList( find.descendant(of: panel, matching: find.byType(Text)), )) { final styles = [ if (text.style != null) text.style!, ..._spanStyles(text.textSpan), ]; for (final style in styles) { expect(style.shadows ?? const [], isEmpty); } } } void _expectWatchGap(WidgetTester tester) { final panel = tester.getRect(find.byKey(const Key('home-feed-info-panel'))); final watch = tester.getRect(find.byKey(const Key('home-feed-full-series'))); expect(watch.top - panel.bottom, closeTo(_gap, _epsilon)); } void _expectSynopsisTopGap(WidgetTester tester, Finder synopsis) { final metadata = tester.getRect(find.byKey(const Key('home-feed-metadata'))); expect( tester.getRect(synopsis).top - metadata.bottom, closeTo(_gap, _epsilon), ); } void _expectExpandedSynopsis(WidgetTester tester) { final expanded = find.byKey(const Key('home-feed-synopsis-expanded')); expect( tester.widget(expanded).textSpan?.style?.color, AppColors.homeFeedVideoCopy, ); _expectNoShadows(tester); _expectWatchGap(tester); _expectSynopsisTopGap(tester, expanded); } void main() { testWidgets('should_render_complete_chinese_toggle_when_theme_font_applies', ( tester, ) async { await _pump(tester, language: 'zh', width: 320, scale: 1.3); final toggle = find.byKey(const Key('home-feed-synopsis-more')); final rect = tester.getRect(toggle); for (final label in ['展开', '收起']) { final rich = find.descendant(of: toggle, matching: find.byType(RichText)); final paragraph = tester.widget(rich); expect(paragraph.text.toPlainText(), label); final painter = TextPainter( text: paragraph.text, textDirection: paragraph.textDirection ?? TextDirection.ltr, textScaler: paragraph.textScaler, locale: paragraph.locale, maxLines: 1, )..layout(); expect( tester.getSize(rich).width + _epsilon, greaterThanOrEqualTo(painter.width), ); expect( tester.getSize(rich).height + _epsilon, greaterThanOrEqualTo(painter.height), ); painter.dispose(); expect(tester.getRect(toggle), rect); await tester.tap(toggle); await tester.pumpAndSettle(); } expect(tester.takeException(), isNull); }); testWidgets('should_use_neutral_text_when_rendering_type_tag', ( tester, ) async { await _pump(tester); final tag = tester.widget( find.descendant( of: find.byKey(const Key('home-feed-tag-Romance')), matching: find.byType(Text), ), ); expect(tag.style?.color, AppColors.homeFeedVideoCopy); }); testWidgets('should_skip_motion_when_system_disables_animations', ( tester, ) async { tester.platformDispatcher.accessibilityFeaturesTestValue = const FakeAccessibilityFeatures(disableAnimations: true); addTearDown(tester.platformDispatcher.clearAccessibilityFeaturesTestValue); await _pump(tester); expect(find.byKey(const Key('home-feed-synopsis-size')), findsNothing); await tester.tap(find.byKey(const Key('home-feed-synopsis-more'))); await tester.pumpAndSettle(); _expectExpandedSynopsis(tester); expect(tester.takeException(), isNull); }); testWidgets('should_reverse_cleanly_when_toggling_during_motion', ( tester, ) async { await _pump(tester); final panel = find.byKey(const Key('home-feed-info-panel')); final height = tester.getSize(panel).height; final toggle = find.byKey(const Key('home-feed-synopsis-more')); final position = tester.getRect(toggle); for (final _ in [false, true]) { await tester.tap(toggle); await tester.pump(); await tester.pump(_motionSample); expect(tester.getRect(toggle), position); } await tester.pumpAndSettle(); expect(find.byKey(const Key('home-feed-synopsis-expanded')), findsNothing); expect(tester.getSize(panel).height, closeTo(height, _epsilon)); _expectWatchGap(tester); expect(tester.takeException(), isNull); }); testWidgets('should_omit_toggle_when_synopsis_fits_two_lines', ( tester, ) async { await _pump( tester, drama: const Drama( id: 'short-synopsis', title: 'Short', synopsis: 'A short story.', posterUrl: '', ), ); expect(find.byKey(const Key('home-feed-synopsis')), findsOneWidget); expect(find.byKey(const Key('home-feed-synopsis-more')), findsNothing); _expectWatchGap(tester); expect(tester.takeException(), isNull); }); testWidgets('should_remove_tag_surfaces_when_rendering_types', ( tester, ) async { await _pump(tester); for (final key in ['home-feed-tag-Urban', 'home-feed-tag-Romance']) { expect(find.byKey(Key(key)), findsOneWidget); expect( find.descendant( of: find.byKey(Key(key)), matching: find.byType(DecoratedBox), ), findsNothing, ); } _expectNoShadows(tester); }); testWidgets('should_use_reference_surface_when_watching_all', (tester) async { await _pump(tester); final background = find.byKey(const Key('home-feed-full-series-bg')); final decorated = tester.widget( find.byKey(const Key('home-feed-full-series-surface')), ); final decoration = decorated.decoration as BoxDecoration; expect(decoration.color, AppColors.watchAllSurface); expect(decoration.border?.top.color, AppColors.watchAllBorder); expect(decoration.gradient, isNull); expect(tester.widget(find.text('Watch all')).style?.fontSize, 16); expect( tester.widget(find.text('Watch all')).style?.fontWeight, FontWeight.w400, ); final clip = tester.widget( find.ancestor(of: background, matching: find.byType(ClipRRect)).first, ); expect(clip.borderRadius, BorderRadius.circular(_watchRadius)); }); for (final hot in [false, true]) { testWidgets( 'should_limit_tags_and_follow_them_with_episode_when_hot_is_$hot', (tester) async { await _pump( tester, width: _wideWidth, drama: Drama( id: 'compact-tags', title: 'Compact', synopsis: '', posterUrl: '', types: const [ {'en': _shortTag}, {'en': _secondTag}, {'en': _extraTag}, ], isHot: hot, totalEpisodes: _drama.totalEpisodes, ), ); final strip = find.byKey(const Key('home-feed-tags')); final row = tester.widget( find.descendant(of: strip, matching: find.byType(Row)), ); expect(row.children.length, _maximumTags * 2 - 1); final lastTag = find.byKey(const Key('home-feed-tag-$_secondTag')); expect(lastTag, findsOneWidget); expect(find.byKey(const Key('home-feed-tag-$_extraTag')), findsNothing); expect(find.byKey(const Key('home-feed-hot-badge')), findsNothing); final episode = find.byKey(const Key('home-feed-episode-meta')); expect( tester.getRect(episode).left - tester.getRect(lastTag).right, closeTo(_metadataGap, _epsilon), ); _expectNoShadows(tester); expect(tester.takeException(), isNull); }, ); } testWidgets( 'should_remove_text_shadows_and_quieten_metadata_when_collapsed', (tester) async { await _pump(tester); _expectNoShadows(tester); final title = tester.widget( find.byKey(const Key('home-feed-series-title')), ); expect(title.style?.fontWeight, FontWeight.w400); expect(title.style?.fontSize, 18); expect(title.style?.color, AppColors.textPrimary); final synopsis = tester.widget( find.byKey(const Key('home-feed-synopsis')), ); expect(synopsis.style?.color, AppColors.homeFeedVideoCopy); expect(synopsis.style?.fontSize, 15); expect(synopsis.maxLines, 2); final episode = tester.widget( find.byKey(const Key('home-feed-episode-meta')), ); expect(episode.style?.color, AppColors.textPrimary); expect(episode.style?.fontSize, 14); expect(episode.data, 'Episode 2 / 24'); final tag = tester.widget(find.text('Romance')); expect(tag.style?.color, AppColors.homeFeedVideoCopy); expect(tag.style?.color, synopsis.style?.color); expect(tag.style?.fontSize, 13); final hotSearch = tester.widget( find.descendant( of: find.byKey(const Key('home-feed-hot-search-pill')), matching: find.byType(Text), ), ); expect(hotSearch.style?.fontSize, 13); _expectWatchGap(tester); final titleRect = tester.getRect( find.byKey(const Key('home-feed-series-name')), ); final metadata = tester.getRect( find.byKey(const Key('home-feed-metadata')), ); final tags = tester.getRect(find.byKey(const Key('home-feed-tags'))); final episodeRect = tester.getRect( find.byKey(const Key('home-feed-episode-meta')), ); final description = tester.getRect( find.byKey(const Key('home-feed-synopsis')), ); expect(metadata.top - titleRect.bottom, closeTo(_gap, _epsilon)); expect(tags.center.dy, closeTo(episodeRect.center.dy, _epsilon)); expect(description.top - metadata.bottom, closeTo(_gap, _epsilon)); final panel = tester.getRect( find.byKey(const Key('home-feed-info-panel')), ); final watch = tester.getRect( find.byKey(const Key('home-feed-full-series')), ); final rail = tester.getRect( find.byKey(const Key('home-feed-action-rail')), ); expect(watch.left, panel.left); expect(watch.right, panel.right); expect(watch.right, lessThan(rail.left)); expect( tester.getCenter(find.text('Watch all')).dx, closeTo(watch.center.dx, _epsilon), ); }, ); for (final language in ['zh', 'en', 'id']) { testWidgets('should_keep_clear_gap_when_expanded_in_$language', ( tester, ) async { await _pump(tester, language: language, width: 320, scale: 1.3); final toggle = find.byKey(const Key('home-feed-synopsis-more')); final toggleRect = tester.getRect(toggle); final panel = find.byKey(const Key('home-feed-info-panel')); final collapsedHeight = tester.getSize(panel).height; final label = tester.widget( find.descendant(of: toggle, matching: find.byType(Text)), ); expect(label.style?.fontWeight, FontWeight.w400); expect(label.style?.fontSize, 15); _expectWatchGap(tester); _expectSynopsisTopGap( tester, find.byKey(const Key('home-feed-synopsis-layout')), ); await tester.tap(toggle); await tester.pump(); await tester.pump(_motionSample); final growingHeight = tester.getSize(panel).height; expect(tester.getRect(toggle), toggleRect); await tester.pumpAndSettle(); final expandedHeight = tester.getSize(panel).height; expect(growingHeight, greaterThan(collapsedHeight)); expect(growingHeight, lessThan(expandedHeight)); _expectExpandedSynopsis(tester); expect(tester.getRect(toggle), toggleRect); await tester.tap(toggle); await tester.pump(); await tester.pump(_motionSample); final shrinkingHeight = tester.getSize(panel).height; expect(shrinkingHeight, greaterThan(collapsedHeight)); expect(shrinkingHeight, lessThan(expandedHeight)); expect(tester.getRect(toggle), toggleRect); await tester.pumpAndSettle(); expect(tester.getSize(panel).height, closeTo(collapsedHeight, _epsilon)); _expectWatchGap(tester); expect(tester.takeException(), isNull); }); } testWidgets('should_keep_episode_without_empty_tags_or_unknown_total', ( tester, ) async { await _pump( tester, drama: const Drama( id: 'minimal', title: 'Minimal', synopsis: '', posterUrl: '', ), ); final episode = tester.widget( find.byKey(const Key('home-feed-episode-meta')), ); expect(episode.data, 'EP2'); expect(find.byKey(const Key('home-feed-tags')), findsNothing); expect(find.byKey(const Key('home-feed-synopsis')), findsNothing); _expectWatchGap(tester); expect(tester.takeException(), isNull); }); }