import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/pages/player/widgets/player_render_stack.dart'; import 'package:fruitshorts/pages/player/widgets/player_video_surface.dart'; import 'package:video_player/video_player.dart'; void main() { testWidgets('should_keep_slots_alive_and_only_translate_during_page_change', ( tester, ) async { final pages = PageController(initialPage: 1); final previousController = _controller('previous'); final currentController = _controller('current'); final nextController = _controller('next'); addTearDown(pages.dispose); addTearDown(previousController.dispose); addTearDown(currentController.dispose); addTearDown(nextController.dispose); var currentIndex = 1; late StateSetter rebuild; await tester.pumpWidget( MaterialApp( home: Scaffold( body: StatefulBuilder( builder: (context, setState) { rebuild = setState; return Stack( fit: StackFit.expand, children: [ PlayerRenderStack( pageController: pages, slots: _slots( currentIndex: currentIndex, previousController: previousController, currentController: currentController, nextController: nextController, ), ), PageView( controller: pages, children: const [ SizedBox.expand(key: Key('page-0')), SizedBox.expand(key: Key('page-1')), SizedBox.expand(key: Key('page-2')), ], ), ], ); }, ), ), ), ); await tester.pump(); final currentSlot = find.byKey( const ValueKey('player-render-slot-current'), ); final currentState = tester.state(currentSlot); final currentElement = tester.element(currentSlot); final currentSurface = tester.widget(currentSlot); expect(currentSurface.controller, same(currentController)); expect(_translationY(tester, 'current'), 0); expect(_translationY(tester, 'next'), greaterThan(0)); pages.jumpToPage(2); await tester.pump(); await tester.pump(); expect(tester.state(currentSlot), same(currentState)); expect(tester.element(currentSlot), same(currentElement)); expect( tester.widget(currentSlot).controller, same(currentController), ); expect(_translationY(tester, 'current'), lessThan(0)); expect(_translationY(tester, 'next'), 0); rebuild(() => currentIndex = 2); await tester.pump(); expect(tester.state(currentSlot), same(currentState)); expect(tester.element(currentSlot), same(currentElement)); expect(_translationY(tester, 'current'), 0); expect(_translationY(tester, 'previous'), lessThan(0)); expect( find.descendant( of: find.byType(PlayerRenderStack), matching: find.byType(IgnorePointer), ), findsOneWidget, ); }); } Map _slots({ required int currentIndex, required VideoPlayerController previousController, required VideoPlayerController currentController, required VideoPlayerController nextController, }) { return { PlayerRenderSlotRole.previous: PlayerRenderSlotData( role: PlayerRenderSlotRole.previous, index: currentIndex - 1, pagePosition: (currentIndex - 1).toDouble(), controller: previousController, placeholder: const ColoredBox( key: Key('previous-placeholder'), color: Colors.red, ), ), PlayerRenderSlotRole.current: PlayerRenderSlotData( role: PlayerRenderSlotRole.current, index: currentIndex, pagePosition: currentIndex.toDouble(), controller: currentController, placeholder: const ColoredBox( key: Key('current-placeholder'), color: Colors.green, ), ), PlayerRenderSlotRole.next: PlayerRenderSlotData( role: PlayerRenderSlotRole.next, index: currentIndex + 1, pagePosition: (currentIndex + 1).toDouble(), controller: nextController, placeholder: const ColoredBox( key: Key('next-placeholder'), color: Colors.blue, ), ), }; } VideoPlayerController _controller(String name) { return VideoPlayerController.networkUrl( Uri.parse('https://example.test/$name.mp4'), ); } double _translationY(WidgetTester tester, String role) { final transform = tester.widget( find.byKey(Key('player-render-transform-$role')), ); return transform.transform.getTranslation().y; }