Files
ReelShort/test/home/feed_info_hierarchy_test.dart
2026-11-04 15:01:36 +08:00

461 lines
16 KiB
Dart

// 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<void> _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<TextStyle> _spanStyles(InlineSpan? span) sync* {
if (span is! TextSpan) return;
if (span.style != null) yield span.style!;
for (final child in span.children ?? const <InlineSpan>[]) {
yield* _spanStyles(child);
}
}
void _expectNoShadows(WidgetTester tester) {
final panel = find.byKey(const Key('home-feed-info-panel'));
for (final text in tester.widgetList<Text>(
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 <Shadow>[], 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<Text>(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<RichText>(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<Text>(
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<DecoratedBox>(
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<Text>(find.text('Watch all')).style?.fontSize, 16);
expect(
tester.widget<Text>(find.text('Watch all')).style?.fontWeight,
FontWeight.w400,
);
final clip = tester.widget<ClipRRect>(
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<Row>(
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<Text>(
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<Text>(
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<Text>(
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<Text>(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<Text>(
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<Text>(
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<Text>(
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);
});
}