// Author: fruit.com.cn import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/pages/player/widgets/player_double_tap_hearts.dart'; void main() { testWidgets('should_show_three_develop_hearts_when_burst_starts', ( tester, ) async { final clock = _HeartClock(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: PlayerDoubleTapHeartBurst( burstId: 1, now: clock.now, onCompleted: _noop, ), ), ), ); await _pumpHeartTime(tester, clock, const Duration(milliseconds: 200)); expect(find.byIcon(Icons.favorite_rounded), findsNWidgets(3)); expect(_size(tester, 0), 48); expect(_size(tester, 1), 36); expect(_size(tester, 2), 40); expect( tester .widgetList( find.descendant( of: find.byType(PlayerDoubleTapHeartBurst), matching: find.byType(Stack), ), ) .every((stack) => stack.clipBehavior == Clip.none), isTrue, ); }); testWidgets('heart burst keeps opacity then fades after 55 percent', ( tester, ) async { final clock = _HeartClock(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: PlayerDoubleTapHeartBurst( burstId: 2, now: clock.now, onCompleted: _noop, ), ), ), ); await _pumpHeartTime(tester, clock, const Duration(milliseconds: 450)); expect(_opacity(tester, 0), closeTo(1, 1e-9)); await _pumpHeartTime(tester, clock, const Duration(milliseconds: 750)); expect(_opacity(tester, 0), lessThan(0.5)); }); testWidgets('heart position is anchored to the overlay origin', ( tester, ) async { const origin = Offset(300, 546); final clock = _HeartClock(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Stack( fit: StackFit.expand, children: [ Positioned.fill( child: PlayerDoubleTapHeartBurst( burstId: 4, origin: origin, now: clock.now, onCompleted: _noop, ), ), ], ), ), ), ); final firstHeart = find.byKey(const Key('double-tap-heart-0')); expect( (tester.getCenter(firstHeart) - origin).distance, lessThanOrEqualTo(1), ); await _pumpHeartTime(tester, clock, const Duration(milliseconds: 750)); final midpoint = tester.getCenter(firstHeart); expect(midpoint.dy, lessThan(origin.dy)); expect((midpoint.dx - origin.dx).abs(), lessThanOrEqualTo(1)); }); testWidgets('develop heart burst completes after about 1590ms', ( tester, ) async { var completed = false; final clock = _HeartClock(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: PlayerDoubleTapHeartBurst( burstId: 3, now: clock.now, onCompleted: () => completed = true, ), ), ), ); await _pumpHeartTime(tester, clock, const Duration(milliseconds: 1600)); expect(completed, isTrue); expect(clock.elapsedMs, greaterThanOrEqualTo(1590)); expect(clock.elapsedMs, lessThanOrEqualTo(1600)); }); } double _size(WidgetTester tester, int index) { return tester.widget(find.byKey(Key('double-tap-heart-$index'))).size!; } double _opacity(WidgetTester tester, int index) { final opacity = find .ancestor( of: find.byKey(Key('double-tap-heart-$index')), matching: find.byType(Opacity), ) .first; return tester.widget(opacity).opacity; } void _noop() {} Future _pumpHeartTime( WidgetTester tester, _HeartClock clock, Duration duration, ) async { const frameMs = 16; var remainingMs = duration.inMilliseconds; while (remainingMs > 0) { final stepMs = remainingMs >= frameMs ? frameMs : remainingMs; clock.advance(Duration(milliseconds: stepMs)); await tester.pump(const Duration(milliseconds: frameMs)); remainingMs -= stepMs; } } class _HeartClock { DateTime _now = DateTime(2026, 10, 26, 12); DateTime now() => _now; int get elapsedMs => _now.difference(DateTime(2026, 10, 26, 12)).inMilliseconds; void advance(Duration duration) { _now = _now.add(duration); } }