Files
ReelShort/test/player/interaction_rail_motion_test.dart

247 lines
8.2 KiB
Dart

// 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 _cancelledMinimum = 0.8;
const _popMinimum = 1.03;
const _popMaximum = 1.18;
const _burstKey = Key('rail-tap-burst');
const _directionFrame = Duration(milliseconds: 60);
const _directionFrames = 2;
const _motionCompletion = Duration(milliseconds: 230);
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
? allOf(greaterThan(_cancelledMinimum), lessThan(1))
: allOf(greaterThan(_popMinimum), lessThan(_popMaximum)),
);
_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_finish_feedback_within_230ms', (tester) async {
await _pumpRail(tester, onTap: () {});
await tester.tap(_button(_likeKey));
await tester.pump();
await tester.pump(_motionCompletion);
expect(find.byKey(_burstKey), findsNothing);
expect(_scale(tester, _likeKey), closeTo(1, _epsilon));
});
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);
});
}