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,118 @@
|
||||
// Author: fruit.com.cn
|
||||
// 职责:验证共用简介的短文案、展开声明、内容更新与减少动画行为。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/theme/app_theme.dart';
|
||||
import 'package:fruitshorts/l10n/app_localizations.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/expandable_synopsis.dart';
|
||||
|
||||
const _width = 300.0;
|
||||
const _shortCopy = 'A short story.';
|
||||
const _longCopy =
|
||||
'A hidden promise brings two families together after years apart. '
|
||||
'Every secret leads them closer to an unexpected truth. '
|
||||
'Their journey through the city changes everything they once believed.';
|
||||
const _toggle = Key('player-synopsis-more');
|
||||
const _expanded = Key('player-synopsis-expanded');
|
||||
const _collapsed = Key('player-synopsis');
|
||||
const _footer = Text('Author declaration', key: Key('test-author-note'));
|
||||
const _motionSample = Duration(milliseconds: 80);
|
||||
|
||||
Future<void> _pump(
|
||||
WidgetTester tester, {
|
||||
String synopsis = _longCopy,
|
||||
bool disableAnimations = false,
|
||||
}) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.dark(),
|
||||
locale: const Locale('en'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Builder(
|
||||
builder: (context) => MediaQuery(
|
||||
data: MediaQuery.of(
|
||||
context,
|
||||
).copyWith(disableAnimations: disableAnimations),
|
||||
child: Scaffold(
|
||||
body: Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: SizedBox(
|
||||
width: _width,
|
||||
child: ExpandableSynopsis(
|
||||
synopsis: synopsis,
|
||||
keyPrefix: 'player',
|
||||
expandedFooter: _footer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('should_hide_toggle_when_synopsis_fits_two_lines', (
|
||||
tester,
|
||||
) async {
|
||||
await _pump(tester, synopsis: _shortCopy);
|
||||
expect(find.byKey(_toggle), findsNothing);
|
||||
expect(tester.widget<Text>(find.byKey(_collapsed)).maxLines, 2);
|
||||
expect(find.byKey(_footer.key!), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_render_no_copy_when_synopsis_is_empty', (tester) async {
|
||||
await _pump(tester, synopsis: '');
|
||||
expect(find.byKey(_collapsed), findsNothing);
|
||||
expect(find.byKey(_toggle), findsNothing);
|
||||
expect(find.byKey(_footer.key!), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_only_show_author_note_when_synopsis_is_expanded', (
|
||||
tester,
|
||||
) async {
|
||||
await _pump(tester);
|
||||
final toggle = find.byKey(_toggle);
|
||||
final toggleRect = tester.getRect(toggle);
|
||||
expect(find.byKey(_footer.key!), findsNothing);
|
||||
await tester.tap(toggle);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(_expanded), findsOneWidget);
|
||||
expect(find.byKey(_footer.key!), findsOneWidget);
|
||||
expect(tester.getRect(toggle), toggleRect);
|
||||
await tester.tap(toggle);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(_collapsed), findsOneWidget);
|
||||
expect(find.byKey(_footer.key!), findsNothing);
|
||||
expect(tester.getRect(toggle), toggleRect);
|
||||
});
|
||||
|
||||
testWidgets('should_collapse_again_when_synopsis_changes', (tester) async {
|
||||
await _pump(tester);
|
||||
await tester.tap(find.byKey(_toggle));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(_expanded), findsOneWidget);
|
||||
await _pump(tester, synopsis: '$_longCopy $_shortCopy');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(_expanded), findsNothing);
|
||||
expect(find.byKey(_collapsed), findsOneWidget);
|
||||
expect(find.byKey(_footer.key!), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_toggle_immediately_when_animations_are_disabled', (
|
||||
tester,
|
||||
) async {
|
||||
await _pump(tester, disableAnimations: true);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(const Key('player-synopsis-size')), findsNothing);
|
||||
await tester.tap(find.byKey(_toggle));
|
||||
await tester.pump();
|
||||
expect(find.byKey(_expanded), findsOneWidget);
|
||||
expect(find.byKey(_footer.key!), findsOneWidget);
|
||||
final expandedRect = tester.getRect(find.byKey(_expanded));
|
||||
await tester.pump(_motionSample);
|
||||
expect(tester.getRect(find.byKey(_expanded)), expandedRect);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Author: fruit.com.cn
|
||||
// 职责:验证真实主题及素材在同步状态刷新和保留状态重组后的点击绘制。
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||
import 'package:fruitshorts/core/theme/app_theme.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/interaction_rail.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/interaction_rail_burst.dart';
|
||||
|
||||
const _itemKey = Key('asset-like');
|
||||
const _burstKey = Key('rail-tap-burst');
|
||||
const _buttonSize = 48.0;
|
||||
const _iconSize = 36.0;
|
||||
const _motionFrame = Duration(milliseconds: 130);
|
||||
const _asset = 'assets/icon/rail/rail_like.png';
|
||||
const _epsilon = 0.01;
|
||||
const _rayLimit = 26.4;
|
||||
const _rayLengthLimit = 3.5;
|
||||
const _burstPhases = [0.2, 0.5, 0.8];
|
||||
|
||||
Finder get _button => find.descendant(
|
||||
of: find.byKey(_itemKey),
|
||||
matching: find.byType(IconButton),
|
||||
);
|
||||
|
||||
Finder get _image =>
|
||||
find.descendant(of: find.byKey(_itemKey), matching: find.byType(Image));
|
||||
|
||||
double _paintedWidth(WidgetTester tester) {
|
||||
final icon = tester.renderObject<RenderBox>(_image);
|
||||
final rail = tester.renderObject<RenderBox>(find.byKey(_itemKey));
|
||||
return MatrixUtils.transformRect(
|
||||
icon.getTransformTo(rail),
|
||||
Offset.zero & icon.size,
|
||||
).width;
|
||||
}
|
||||
|
||||
Future<void> _pumpAssetRail(
|
||||
WidgetTester tester,
|
||||
ValueNotifier<bool> selected,
|
||||
) => tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.dark(),
|
||||
home: Scaffold(
|
||||
body: Center(
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: selected,
|
||||
builder: (context, value, _) => InteractionRail(
|
||||
buttonSize: _buttonSize,
|
||||
iconSize: _iconSize,
|
||||
items: [
|
||||
InteractionRailItem(
|
||||
key: _itemKey,
|
||||
label: 'Like',
|
||||
asset: _asset,
|
||||
selected: value,
|
||||
selectedColor: AppColors.brandRed,
|
||||
animateTap: true,
|
||||
onTap: () => selected.value = !selected.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
void main() {
|
||||
testWidgets('should_not_show_ink_or_shadow_when_button_is_pressed', (
|
||||
tester,
|
||||
) async {
|
||||
final selected = ValueNotifier(false);
|
||||
addTearDown(selected.dispose);
|
||||
await _pumpAssetRail(tester, selected);
|
||||
final button = tester.widget<IconButton>(_button);
|
||||
const pressed = {WidgetState.pressed};
|
||||
expect(button.style?.overlayColor?.resolve(pressed), AppColors.transparent);
|
||||
expect(button.style?.shadowColor?.resolve(pressed), AppColors.transparent);
|
||||
expect(button.style?.splashFactory, NoSplash.splashFactory);
|
||||
expect(button.style?.elevation?.resolve(pressed), 0);
|
||||
});
|
||||
|
||||
for (final contracting in [false, true]) {
|
||||
testWidgets('should_keep_rays_compact_when_contracting_$contracting', (
|
||||
tester,
|
||||
) async {
|
||||
for (final progress in _burstPhases) {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: SizedBox.square(
|
||||
dimension: _buttonSize,
|
||||
child: RailTapBurst(
|
||||
progress: progress,
|
||||
color: AppColors.brandRed,
|
||||
contracting: contracting,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final paint = tester.renderObject(find.byType(CustomPaint));
|
||||
expect(paint, paintsExactlyCountTimes(#drawCircle, 0));
|
||||
expect(paint, paintsExactlyCountTimes(#drawLine, 8));
|
||||
expect(paint, paints..everything(_isCompactRay));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('should_paint_feedback_at_button_size_when_asset_is_tapped', (
|
||||
tester,
|
||||
) async {
|
||||
final selected = ValueNotifier(false);
|
||||
addTearDown(selected.dispose);
|
||||
await _pumpAssetRail(tester, selected);
|
||||
final touchRect = tester.getRect(_button);
|
||||
await tester.tap(_button);
|
||||
await tester.pump();
|
||||
await tester.pump(_motionFrame);
|
||||
expect(selected.value, isTrue);
|
||||
expect(_paintedWidth(tester), greaterThan(_iconSize));
|
||||
expect(
|
||||
tester.getSize(find.byKey(_burstKey)),
|
||||
const Size.square(_buttonSize),
|
||||
);
|
||||
expect(tester.getRect(_button), touchRect);
|
||||
await tester.pumpAndSettle();
|
||||
expect(_paintedWidth(tester), closeTo(_iconSize, _epsilon));
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'should_paint_opposite_directions_after_preserved_state_reassembly',
|
||||
(tester) async {
|
||||
final selected = ValueNotifier(false);
|
||||
addTearDown(selected.dispose);
|
||||
await _pumpAssetRail(tester, selected);
|
||||
unawaited(tester.binding.reassembleApplication());
|
||||
await tester.pump();
|
||||
for (final cancelling in [false, true]) {
|
||||
await tester.tap(_button);
|
||||
await tester.pump();
|
||||
await tester.pump(_motionFrame);
|
||||
expect(
|
||||
_paintedWidth(tester),
|
||||
cancelling ? lessThan(_iconSize) : greaterThan(_iconSize),
|
||||
);
|
||||
final feedback = tester.widget<RailTapBurst>(find.byKey(_burstKey));
|
||||
expect(feedback.contracting, cancelling);
|
||||
expect(selected.value, !cancelling);
|
||||
await tester.pumpAndSettle();
|
||||
expect(_paintedWidth(tester), closeTo(_iconSize, _epsilon));
|
||||
}
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _isCompactRay(Symbol method, List<dynamic> arguments) {
|
||||
if (method != #drawLine) return true;
|
||||
const center = Offset(_buttonSize / 2, _buttonSize / 2);
|
||||
final start = arguments[0] as Offset;
|
||||
final end = arguments[1] as Offset;
|
||||
return (start - center).distance <= _rayLimit &&
|
||||
(end - center).distance <= _rayLimit &&
|
||||
(end - start).distance <= _rayLengthLimit;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// Author: fruit.com.cn
|
||||
// 职责:验证收藏点赞图标点击回弹、固定触控区与减少动画行为。
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/interaction_rail.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/interaction_rail_burst.dart';
|
||||
|
||||
const _saveKey = Key('save');
|
||||
const _likeKey = Key('like');
|
||||
const _commentsKey = Key('comments');
|
||||
const _pressFrame = Duration(milliseconds: 30);
|
||||
const _popFrame = Duration(milliseconds: 100);
|
||||
const _buttonSize = 48.0;
|
||||
const _compactGap = 6.0;
|
||||
const _epsilon = 0.01;
|
||||
const _cancelledMaximum = 0.8;
|
||||
const _popMinimum = 1.22;
|
||||
const _burstKey = Key('rail-tap-burst');
|
||||
const _directionFrame = Duration(milliseconds: 60);
|
||||
const _directionFrames = 4;
|
||||
|
||||
Future<void> _pumpRail(
|
||||
WidgetTester tester, {
|
||||
required VoidCallback onTap,
|
||||
bool selected = false,
|
||||
bool reduceMotion = false,
|
||||
}) => tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: MediaQueryData(disableAnimations: reduceMotion),
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
child: InteractionRail(
|
||||
itemGap: interactionRailCompactGap,
|
||||
buttonSize: _buttonSize,
|
||||
items: [
|
||||
InteractionRailItem(
|
||||
key: _saveKey,
|
||||
label: 'Save',
|
||||
icon: Icons.bookmark_outline,
|
||||
selected: selected,
|
||||
animateTap: true,
|
||||
onTap: onTap,
|
||||
),
|
||||
InteractionRailItem(
|
||||
key: _likeKey,
|
||||
label: 'Like',
|
||||
icon: Icons.favorite_outline,
|
||||
selected: selected,
|
||||
animateTap: true,
|
||||
onTap: onTap,
|
||||
),
|
||||
InteractionRailItem(
|
||||
key: _commentsKey,
|
||||
label: 'Comments',
|
||||
icon: Icons.chat_bubble_outline,
|
||||
onTap: onTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Finder _button(Key key) =>
|
||||
find.descendant(of: find.byKey(key), matching: find.byType(IconButton));
|
||||
|
||||
double _scale(WidgetTester tester, Key key) => tester
|
||||
.widget<Transform>(
|
||||
find
|
||||
.descendant(of: find.byKey(key), matching: find.byType(Transform))
|
||||
.first,
|
||||
)
|
||||
.transform
|
||||
.entry(0, 0);
|
||||
|
||||
void _expectBurst(WidgetTester tester, Key key, bool cancelling) {
|
||||
final burst = find.descendant(
|
||||
of: find.byKey(key),
|
||||
matching: find.byKey(_burstKey),
|
||||
);
|
||||
expect(burst, findsOneWidget);
|
||||
final feedback = tester.widget<RailTapBurst>(burst);
|
||||
expect(feedback.contracting, cancelling);
|
||||
expect(feedback.color, cancelling ? AppColors.onPrimary : AppColors.brandRed);
|
||||
final paint = find.descendant(of: burst, matching: find.byType(CustomPaint));
|
||||
expect(tester.renderObject(paint), paints..line());
|
||||
expect(tester.renderObject(paint), paintsExactlyCountTimes(#drawCircle, 0));
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('should_keep_compact_spacing_when_rail_is_idle', (tester) async {
|
||||
await _pumpRail(tester, onTap: () {});
|
||||
final save = tester.getRect(find.byKey(_saveKey));
|
||||
final like = tester.getRect(find.byKey(_likeKey));
|
||||
expect(like.top - save.bottom, closeTo(_compactGap, _epsilon));
|
||||
});
|
||||
|
||||
for (final key in [_saveKey, _likeKey]) {
|
||||
for (final selected in [false, true]) {
|
||||
testWidgets(
|
||||
'should_pop_icon_once_when_tapped_${key}_selected_$selected',
|
||||
(tester) async {
|
||||
var calls = 0;
|
||||
await _pumpRail(tester, selected: selected, onTap: () => calls++);
|
||||
final rect = tester.getRect(_button(key));
|
||||
final itemRect = tester.getRect(find.byKey(key));
|
||||
await tester.tap(_button(key));
|
||||
await tester.pump();
|
||||
await tester.pump(_pressFrame);
|
||||
expect(_scale(tester, key), selected ? lessThan(1) : greaterThan(1));
|
||||
await tester.pump(_popFrame);
|
||||
expect(
|
||||
_scale(tester, key),
|
||||
selected ? lessThan(_cancelledMaximum) : greaterThan(_popMinimum),
|
||||
);
|
||||
_expectBurst(tester, key, selected);
|
||||
expect(tester.getRect(_button(key)), rect);
|
||||
expect(tester.getRect(find.byKey(key)), itemRect);
|
||||
expect(rect.size, const Size.square(_buttonSize));
|
||||
expect(calls, 1);
|
||||
for (var frame = 0; frame < _directionFrames; frame++) {
|
||||
await tester.pump(_directionFrame);
|
||||
expect(
|
||||
_scale(tester, key),
|
||||
selected ? lessThanOrEqualTo(1) : greaterThanOrEqualTo(1),
|
||||
);
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(_burstKey), findsNothing);
|
||||
expect(_scale(tester, key), closeTo(1, _epsilon));
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'should_restart_feedback_without_duplicate_callbacks_when_rapid',
|
||||
(tester) async {
|
||||
var calls = 0;
|
||||
await _pumpRail(tester, onTap: () => calls++);
|
||||
for (final _ in [false, true]) {
|
||||
await tester.tap(_button(_likeKey));
|
||||
await tester.pump();
|
||||
await tester.pump(_popFrame);
|
||||
}
|
||||
expect(calls, 2);
|
||||
await tester.pumpAndSettle();
|
||||
expect(_scale(tester, _likeKey), closeTo(1, _epsilon));
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('should_keep_callbacks_without_motion_when_reduced', (
|
||||
tester,
|
||||
) async {
|
||||
var calls = 0;
|
||||
await _pumpRail(tester, reduceMotion: true, onTap: () => calls++);
|
||||
await tester.tap(_button(_saveKey));
|
||||
await tester.pump(_pressFrame);
|
||||
expect(_scale(tester, _saveKey), 1);
|
||||
expect(find.byKey(_burstKey), findsNothing);
|
||||
expect(calls, 1);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('should_keep_burst_intent_when_selection_updates_during_motion', (
|
||||
tester,
|
||||
) async {
|
||||
var calls = 0;
|
||||
void onTap() => calls++;
|
||||
await _pumpRail(tester, onTap: onTap);
|
||||
await tester.tap(_button(_likeKey));
|
||||
await tester.pump();
|
||||
await tester.pump(_popFrame);
|
||||
await _pumpRail(tester, selected: true, onTap: onTap);
|
||||
expect(
|
||||
tester.widget<RailTapBurst>(find.byKey(_burstKey)).contracting,
|
||||
isFalse,
|
||||
);
|
||||
await tester.tap(_button(_likeKey));
|
||||
await tester.pump();
|
||||
await tester.pump(_popFrame);
|
||||
final feedback = tester.widget<RailTapBurst>(find.byKey(_burstKey));
|
||||
expect(feedback.contracting, isTrue);
|
||||
expect(feedback.color, AppColors.onPrimary);
|
||||
expect(calls, 2);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byKey(_burstKey), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('should_stop_burst_when_reduced_motion_changes_during_feedback', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpRail(tester, onTap: () {});
|
||||
await tester.tap(_button(_saveKey));
|
||||
await tester.pump();
|
||||
await tester.pump(_popFrame);
|
||||
expect(find.byKey(_burstKey), findsOneWidget);
|
||||
await _pumpRail(tester, reduceMotion: true, onTap: () {});
|
||||
expect(find.byKey(_burstKey), findsNothing);
|
||||
expect(_scale(tester, _saveKey), 1);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('should_not_pop_comments_when_motion_is_not_enabled', (
|
||||
tester,
|
||||
) async {
|
||||
var calls = 0;
|
||||
await _pumpRail(tester, onTap: () => calls++);
|
||||
await tester.tap(_button(_commentsKey));
|
||||
await tester.pump(_pressFrame);
|
||||
expect(_scale(tester, _commentsKey), 1);
|
||||
expect(find.byKey(_burstKey), findsNothing);
|
||||
expect(calls, 1);
|
||||
});
|
||||
|
||||
testWidgets('should_dispose_running_motion_when_rail_is_removed', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpRail(tester, onTap: () {});
|
||||
await tester.tap(_button(_likeKey));
|
||||
await tester.pump(_pressFrame);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
@@ -1271,6 +1271,22 @@ void main() {
|
||||
expect(find.byKey(const Key('player-interaction-rail')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_keep_episode_entry_regular_when_player_is_open', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpApp(tester);
|
||||
await _openSeriesPlayer(tester);
|
||||
final entry = tester.widget<Text>(
|
||||
find.descendant(
|
||||
of: find.byKey(const Key('player-episodes-pill')),
|
||||
matching: find.byType(Text),
|
||||
),
|
||||
);
|
||||
expect(entry.style?.fontWeight, FontWeight.w400);
|
||||
expect(entry.style?.fontSize, 13.5);
|
||||
expect(entry.data, contains(' · '));
|
||||
});
|
||||
|
||||
testWidgets('should_push_episodes_arrow_to_pill_right', (tester) async {
|
||||
// 宽屏下文案不会撑满胶囊,才能验证箭头是否被推到最右侧。
|
||||
await _pumpApp(tester, size: const Size(900, 844));
|
||||
@@ -1371,9 +1387,9 @@ void main() {
|
||||
final shareRect = tester.getRect(
|
||||
find.byKey(const Key('player-rail-share')),
|
||||
);
|
||||
expect(likeRect.top - saveRect.bottom, closeTo(12, 0.1));
|
||||
expect(commentsRect.top - likeRect.bottom, closeTo(12, 0.1));
|
||||
expect(shareRect.top - commentsRect.bottom, closeTo(12, 0.1));
|
||||
expect(likeRect.top - saveRect.bottom, closeTo(6, 0.1));
|
||||
expect(commentsRect.top - likeRect.bottom, closeTo(6, 0.1));
|
||||
expect(shareRect.top - commentsRect.bottom, closeTo(6, 0.1));
|
||||
|
||||
final saveButton = tester.widget<IconButton>(
|
||||
find.descendant(
|
||||
@@ -1405,7 +1421,9 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('should_toggle_synopsis_with_inline_text_button', (tester) async {
|
||||
testWidgets('should_toggle_synopsis_at_fixed_corner_with_feed_motion', (
|
||||
tester,
|
||||
) async {
|
||||
final longCopy = Drama(
|
||||
id: 'long-copy-demo',
|
||||
title: 'Long Title',
|
||||
@@ -1428,7 +1446,7 @@ void main() {
|
||||
await _pumpApp(tester, catalog: FakeCatalogRepository(dramas: [longCopy]));
|
||||
await _openSeriesPlayer(tester, feedIndex: 0);
|
||||
|
||||
final toggle = find.byKey(const Key('player-synopsis-toggle'));
|
||||
final toggle = find.byKey(const Key('player-synopsis-more'));
|
||||
final synopsis = find.byKey(const Key('player-synopsis'));
|
||||
expect(
|
||||
find.descendant(of: toggle, matching: find.text('More')),
|
||||
@@ -1443,17 +1461,15 @@ void main() {
|
||||
?.color,
|
||||
AppColors.textPrimary,
|
||||
);
|
||||
expect(tester.widget<Text>(synopsis).maxLines, 1);
|
||||
expect(tester.widget<Text>(synopsis).maxLines, 2);
|
||||
expect(tester.widget<Text>(synopsis).overflow, TextOverflow.ellipsis);
|
||||
// 行尾「展开」与正文同一行,只留 6 的左边距。
|
||||
expect(
|
||||
tester
|
||||
.widget<Padding>(
|
||||
find.descendant(of: toggle, matching: find.byType(Padding)),
|
||||
)
|
||||
.padding,
|
||||
const EdgeInsets.only(left: 6),
|
||||
final motion = tester.widget<AnimatedSize>(
|
||||
find.byKey(const Key('player-synopsis-size')),
|
||||
);
|
||||
expect(motion.duration, const Duration(milliseconds: 240));
|
||||
expect(motion.curve, Curves.easeInOutCubic);
|
||||
final blockFinder = find.byKey(const Key('player-episode-synopsis'));
|
||||
final collapsedHeight = tester.getSize(blockFinder).height;
|
||||
final synopsisRect = tester.getRect(synopsis);
|
||||
final toggleRect = tester.getRect(toggle);
|
||||
expect(toggleRect.left, greaterThan(synopsisRect.left));
|
||||
@@ -1463,24 +1479,24 @@ void main() {
|
||||
);
|
||||
|
||||
await tester.tap(toggle);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 80));
|
||||
final growingHeight = tester.getSize(blockFinder).height;
|
||||
expect(growingHeight, greaterThan(collapsedHeight));
|
||||
expect(tester.getRect(toggle), toggleRect);
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(of: toggle, matching: find.text('Less')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
tester.widget<Text>(find.byKey(const Key('player-synopsis'))).maxLines,
|
||||
tester
|
||||
.widget<Text>(find.byKey(const Key('player-synopsis-expanded')))
|
||||
.maxLines,
|
||||
isNull,
|
||||
);
|
||||
final lessRect = tester.getRect(toggle);
|
||||
final textRect = tester.getRect(find.byKey(const Key('player-synopsis')));
|
||||
final block = tester.getRect(
|
||||
find.byKey(const Key('player-episode-synopsis')),
|
||||
);
|
||||
// 展开后「收起」内联在正文最后一行的行尾。
|
||||
expect(lessRect.top, greaterThanOrEqualTo(textRect.top));
|
||||
expect(lessRect.bottom, lessThanOrEqualTo(textRect.bottom + 1));
|
||||
expect(lessRect.right - block.right, lessThanOrEqualTo(2));
|
||||
expect(tester.getRect(toggle), toggleRect);
|
||||
expect(tester.getSize(blockFinder).height, greaterThan(growingHeight));
|
||||
|
||||
await tester.tap(toggle);
|
||||
await tester.pumpAndSettle();
|
||||
@@ -1488,29 +1504,24 @@ void main() {
|
||||
find.descendant(of: toggle, matching: find.text('More')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(tester.widget<Text>(synopsis).maxLines, 1);
|
||||
expect(tester.widget<Text>(synopsis).maxLines, 2);
|
||||
});
|
||||
|
||||
testWidgets('should_share_one_text_style_across_label_and_synopsis', (
|
||||
testWidgets('should_match_feed_synopsis_without_episode_prefix', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpApp(tester);
|
||||
await _openSeriesPlayer(tester);
|
||||
|
||||
// 新 UI 把「第 n 集」与简介合并进同一个富文本,整行共用一套白字规格。
|
||||
final line = tester.widget<Text>(find.byKey(const Key('player-synopsis')));
|
||||
final style = line.style ?? const TextStyle();
|
||||
expect(style.fontSize, 12);
|
||||
expect(style.fontWeight, FontWeight.w500);
|
||||
expect(style.color, AppColors.onPrimary);
|
||||
|
||||
final span = line.textSpan! as TextSpan;
|
||||
final separator = span.children!.whereType<TextSpan>().firstWhere(
|
||||
(child) => (child.text ?? '').contains('|'),
|
||||
);
|
||||
expect(separator.style?.fontSize, 12);
|
||||
expect(separator.style?.fontWeight, FontWeight.w400);
|
||||
expect(separator.style?.color, AppColors.onPrimary.withValues(alpha: 0.45));
|
||||
expect(style.fontSize, 15);
|
||||
expect(style.fontWeight, FontWeight.w400);
|
||||
expect(style.color, AppColors.overlaySecondary);
|
||||
expect(style.shadows ?? <Shadow>[], isEmpty);
|
||||
expect(line.textSpan!.toPlainText(), isNot(startsWith('Episode ')));
|
||||
expect(line.textSpan!.toPlainText(), isNot(contains(' | ')));
|
||||
expect(find.byKey(const Key('player-episode-heading')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should_fit_long_title_and_synopsis_at_320_width', (
|
||||
@@ -1563,7 +1574,7 @@ void main() {
|
||||
tester.getRect(find.byKey(const Key('player-synopsis'))).right,
|
||||
lessThanOrEqualTo(320 - 14 - 54),
|
||||
);
|
||||
expect(synopsis.maxLines, 1);
|
||||
expect(synopsis.maxLines, 2);
|
||||
expect(synopsis.overflow, TextOverflow.ellipsis);
|
||||
expect(
|
||||
tester.getRect(find.byKey(const Key('player-episodes-pill'))).right,
|
||||
|
||||
Reference in new issue
Block a user