// 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 _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( 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(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(find.byKey(_burstKey)).contracting, isFalse, ); await tester.tap(_button(_likeKey)); await tester.pump(); await tester.pump(_popFrame); final feedback = tester.widget(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); }); }