feat(player): 重做播放页信息层级与互动栏点击动效
This commit is contained in:
1 parent
aed9ee2b01
commit
fd2ba0bf43
15 files changed
+2074
-551
No files matched your search
@@ -0,0 +1,467 @@
|
||||
// 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.overlaySecondary,
|
||||
);
|
||||
_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.overlaySecondary);
|
||||
});
|
||||
|
||||
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_small_radius_and_brand_gradient_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-gradient')),
|
||||
);
|
||||
final decoration = decorated.decoration as BoxDecoration;
|
||||
expect((decoration.gradient! as LinearGradient).colors, [
|
||||
AppColors.mineStatShade,
|
||||
AppColors.brandRed,
|
||||
AppColors.vipButtonEnd,
|
||||
]);
|
||||
final gradient = decoration.gradient! as LinearGradient;
|
||||
expect(gradient.begin, Alignment.centerLeft);
|
||||
expect(gradient.end, Alignment.centerRight);
|
||||
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.overlaySecondary);
|
||||
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.overlaySecondary);
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// Author: fruit.com.cn
|
||||
// 职责:验证首页标签仅取 types 前两项,以及多语言/字号下与集数的文字基线对齐。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/core/theme/app_theme.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/home_feed_recommend_panel.dart';
|
||||
|
||||
const _types = [
|
||||
{'zh': '都市', 'en': 'Urban', 'id': 'Perkotaan'},
|
||||
{'zh': '甜宠', 'en': 'Romance', 'id': 'Romantis'},
|
||||
{'zh': '复仇', 'en': 'Revenge', 'id': 'Balas Dendam'},
|
||||
];
|
||||
const _typedDrama = Drama(
|
||||
id: 'types-alignment',
|
||||
title: 'Types',
|
||||
synopsis: '',
|
||||
posterUrl: '',
|
||||
types: _types,
|
||||
genre: 'Legacy genre',
|
||||
category: 'Legacy category',
|
||||
isHot: true,
|
||||
totalEpisodes: 24,
|
||||
);
|
||||
const _episode = Episode(id: 'types-2', number: 2, title: '', isFree: true);
|
||||
const _tolerance = 0.1;
|
||||
const _narrowWidth = 320.0;
|
||||
const _regularWidth = 390.0;
|
||||
const _largeScale = 1.3;
|
||||
const _tagCount = 2;
|
||||
const _longType = 'A very long urban romance type description';
|
||||
const _scrollDelta = Offset(-180, 0);
|
||||
|
||||
Future<void> _pump(
|
||||
WidgetTester tester, {
|
||||
Drama drama = _typedDrama,
|
||||
String language = 'en',
|
||||
double scale = 1,
|
||||
double width = _narrowWidth,
|
||||
}) async {
|
||||
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: Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: HomeFeedInfoPanel(
|
||||
drama: drama,
|
||||
episode: _episode,
|
||||
onDetail: () {},
|
||||
onOpenFullPlayer: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double _baseline(WidgetTester tester, Finder text) {
|
||||
final rich = find.descendant(of: text, matching: find.byType(RichText));
|
||||
final paragraph = tester.renderObject<RenderParagraph>(rich);
|
||||
final widget = tester.widget<RichText>(rich);
|
||||
final painter =
|
||||
TextPainter(
|
||||
text: widget.text,
|
||||
textDirection: widget.textDirection ?? TextDirection.ltr,
|
||||
textScaler: widget.textScaler,
|
||||
strutStyle: widget.strutStyle,
|
||||
textWidthBasis: widget.textWidthBasis,
|
||||
textHeightBehavior: widget.textHeightBehavior,
|
||||
locale: widget.locale,
|
||||
maxLines: widget.maxLines,
|
||||
)..layout(
|
||||
minWidth: paragraph.constraints.minWidth,
|
||||
maxWidth: paragraph.constraints.maxWidth,
|
||||
);
|
||||
final distance = painter.computeLineMetrics().first.baseline;
|
||||
painter.dispose();
|
||||
return tester.getTopLeft(rich).dy + distance;
|
||||
}
|
||||
|
||||
Finder _tag(String label) => find.byKey(Key('home-feed-tag-$label'));
|
||||
|
||||
void main() {
|
||||
testWidgets('should_use_first_two_types_when_drama_is_hot', (tester) async {
|
||||
await _pump(tester);
|
||||
final first = _tag('Urban');
|
||||
final second = _tag('Romance');
|
||||
expect(first, findsOneWidget);
|
||||
expect(second, findsOneWidget);
|
||||
expect(_tag('Revenge'), findsNothing);
|
||||
expect(find.byKey(const Key('home-feed-hot-badge')), findsNothing);
|
||||
expect(_tag(_typedDrama.genre), findsNothing);
|
||||
expect(_tag(_typedDrama.category), findsNothing);
|
||||
expect(tester.getRect(first).left, lessThan(tester.getRect(second).left));
|
||||
});
|
||||
|
||||
testWidgets('should_omit_legacy_fallback_when_types_are_empty', (
|
||||
tester,
|
||||
) async {
|
||||
await _pump(
|
||||
tester,
|
||||
drama: const Drama(
|
||||
id: 'empty-types',
|
||||
title: '',
|
||||
synopsis: '',
|
||||
posterUrl: '',
|
||||
genre: 'Legacy genre',
|
||||
category: 'Legacy category',
|
||||
categoryLocales: {'zh': '旧分类', 'en': 'Old category'},
|
||||
isHot: true,
|
||||
),
|
||||
);
|
||||
expect(find.byKey(const Key('home-feed-tags')), findsNothing);
|
||||
expect(find.byKey(const Key('home-feed-episode-meta')), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-feed-hot-badge')), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('should_keep_only_existing_type_when_types_have_one_item', (
|
||||
tester,
|
||||
) async {
|
||||
await _pump(
|
||||
tester,
|
||||
drama: const Drama(
|
||||
id: 'one-type',
|
||||
title: '',
|
||||
synopsis: '',
|
||||
posterUrl: '',
|
||||
types: [
|
||||
{'zh': '都市', 'en': 'Urban'},
|
||||
],
|
||||
genre: 'Legacy genre',
|
||||
isHot: true,
|
||||
),
|
||||
);
|
||||
expect(_tag('Urban'), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-feed-hot-badge')), findsNothing);
|
||||
expect(_tag('Legacy genre'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_not_fill_from_third_type_when_first_type_is_empty', (
|
||||
tester,
|
||||
) async {
|
||||
await _pump(
|
||||
tester,
|
||||
drama: const Drama(
|
||||
id: 'blank-type',
|
||||
title: '',
|
||||
synopsis: '',
|
||||
posterUrl: '',
|
||||
types: [
|
||||
{'zh': '', 'en': ''},
|
||||
{'zh': '都市', 'en': 'Urban'},
|
||||
{'zh': '甜宠', 'en': 'Romance'},
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(_tag('Urban'), findsOneWidget);
|
||||
expect(_tag('Romance'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_use_chinese_fallback_when_type_translation_is_absent', (
|
||||
tester,
|
||||
) async {
|
||||
await _pump(
|
||||
tester,
|
||||
language: 'id',
|
||||
drama: const Drama(
|
||||
id: 'type-fallback',
|
||||
title: '',
|
||||
synopsis: '',
|
||||
posterUrl: '',
|
||||
types: [
|
||||
{'zh': '都市', 'en': 'Urban'},
|
||||
{'zh': '甜宠', 'id': 'Romantis'},
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(_tag('都市'), findsOneWidget);
|
||||
expect(_tag('Romantis'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_keep_episode_aligned_when_long_types_scroll', (
|
||||
tester,
|
||||
) async {
|
||||
await _pump(
|
||||
tester,
|
||||
scale: _largeScale,
|
||||
drama: const Drama(
|
||||
id: 'long-types',
|
||||
title: '',
|
||||
synopsis: '',
|
||||
posterUrl: '',
|
||||
totalEpisodes: 24,
|
||||
types: [
|
||||
{'en': _longType},
|
||||
{'en': 'Romance'},
|
||||
],
|
||||
),
|
||||
);
|
||||
final strip = find.byKey(const Key('home-feed-tags'));
|
||||
final episode = find.byKey(const Key('home-feed-episode-meta'));
|
||||
final episodeRect = tester.getRect(episode);
|
||||
final tagRect = tester.getRect(_tag(_longType));
|
||||
expect(tagRect.right, greaterThan(tester.getRect(strip).right));
|
||||
await tester.drag(strip, _scrollDelta);
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(_tag(_longType)).left, lessThan(tagRect.left));
|
||||
expect(tester.getRect(episode), episodeRect);
|
||||
expect(
|
||||
_baseline(tester, _tag(_longType)),
|
||||
closeTo(_baseline(tester, episode), _tolerance),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
for (final language in ['zh', 'en', 'id']) {
|
||||
for (final scale in [1.0, _largeScale]) {
|
||||
for (final width in [_narrowWidth, _regularWidth]) {
|
||||
testWidgets(
|
||||
'should_align_type_baselines_when_${language}_${scale}_$width',
|
||||
(tester) async {
|
||||
await _pump(tester, language: language, scale: scale, width: width);
|
||||
final episode = find.byKey(const Key('home-feed-episode-meta'));
|
||||
final expected = _baseline(tester, episode);
|
||||
for (final type in _types.take(_tagCount)) {
|
||||
expect(
|
||||
_baseline(tester, _tag(type[language]!)),
|
||||
closeTo(expected, _tolerance),
|
||||
);
|
||||
}
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Author: fruit.com.cn
|
||||
// 职责:检查观看全集按钮实际绘制像素,防止渐变只配置而不可见。
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/home_feed_recommend_panel.dart';
|
||||
|
||||
const _boundaryKey = Key('watch-gradient-boundary');
|
||||
const _width = 280.0;
|
||||
const _sampleInset = 16;
|
||||
const _sampleY = 4;
|
||||
const _rgbaChannels = 4;
|
||||
const _minimumRedDelta = 50;
|
||||
const _drama = Drama(
|
||||
id: 'gradient',
|
||||
title: 'Gradient',
|
||||
synopsis: '',
|
||||
posterUrl: '',
|
||||
);
|
||||
|
||||
Future<void> _pumpButton(WidgetTester tester) => tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: RepaintBoundary(
|
||||
key: _boundaryKey,
|
||||
child: SizedBox(
|
||||
width: _width,
|
||||
child: HomeFeedWatchAllBar(drama: _drama, onTap: () {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Color _sample(ByteData bytes, int width, int x) {
|
||||
final offset = (_sampleY * width + x) * _rgbaChannels;
|
||||
return Color.fromARGB(
|
||||
bytes.getUint8(offset + 3),
|
||||
bytes.getUint8(offset),
|
||||
bytes.getUint8(offset + 1),
|
||||
bytes.getUint8(offset + 2),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('should_paint_visible_red_gradient_instead_of_flat_fill', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpButton(tester);
|
||||
final boundary = tester.renderObject<RenderRepaintBoundary>(
|
||||
find.byKey(_boundaryKey),
|
||||
);
|
||||
await tester.runAsync(() async {
|
||||
final image = await boundary.toImage();
|
||||
final bytes = (await image.toByteData(
|
||||
format: ui.ImageByteFormat.rawRgba,
|
||||
))!;
|
||||
final left = _sample(bytes, image.width, _sampleInset);
|
||||
final middle = _sample(bytes, image.width, image.width ~/ 2);
|
||||
final right = _sample(bytes, image.width, image.width - _sampleInset);
|
||||
image.dispose();
|
||||
expect(left.a, 1);
|
||||
expect(right.a, 1);
|
||||
expect((right.r - left.r) * 255, greaterThan(_minimumRedDelta));
|
||||
expect(left.g, lessThan(middle.g));
|
||||
expect(middle.g, lessThan(right.g));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user