// Author: fruit.com.cn import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fruitshorts/core/data/models/drama.dart'; import 'package:fruitshorts/l10n/app_localizations.dart'; import 'package:fruitshorts/pages/player/widgets/home_feed_overlay.dart'; import 'package:fruitshorts/pages/player/widgets/player_overlay.dart'; import 'package:fruitshorts/pages/player/widgets/player_overlay_models.dart'; import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.dart'; import 'package:video_player/video_player.dart'; void main() { testWidgets('tapping the progress bar does not hide the overlay', ( tester, ) async { final harness = await _pumpHarness(tester); await tester.tap(find.byType(Slider)); await tester.pumpAndSettle(); expect(find.byKey(const Key('scrub-overlay')), findsOneWidget); expect(harness.scrubStarts, 0); expect(harness.seeks, 0); expect(_sliderTheme(tester).trackHeight, 2); }); testWidgets('horizontal drag hides the overlay and thickens the track', ( tester, ) async { final harness = await _pumpHarness(tester); final slider = find.byType(Slider); final gesture = await tester.startGesture(tester.getCenter(slider)); await gesture.moveBy(const Offset(24, 0)); await tester.pump(); expect(find.byKey(const Key('scrub-overlay')), findsNothing); expect(harness.scrubStarts, 1); expect(_sliderTheme(tester).trackHeight, 6); // 不画滑块圆点,进度只靠已播段表达。 expect(_sliderTheme(tester).thumbShape, SliderComponentShape.noThumb); await gesture.moveBy(const Offset(72, 0)); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets('releasing restores the overlay and idle track height', ( tester, ) async { final harness = await _pumpHarness(tester); final gesture = await tester.startGesture( tester.getCenter(find.byType(Slider)), ); await gesture.moveBy(const Offset(32, 0)); await tester.pump(); await gesture.moveBy(const Offset(64, 0)); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); expect(find.byKey(const Key('scrub-overlay')), findsOneWidget); expect(harness.scrubEnds, 1); expect(harness.seeks, 1); expect(_sliderTheme(tester).trackHeight, 2); expect(_sliderTheme(tester).thumbShape, SliderComponentShape.noThumb); }); testWidgets('feed progress tap does not clear overlay content', ( tester, ) async { await _pumpFeedOverlay(tester); await tester.tap(find.byKey(const Key('home-feed-progress'))); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(find.byKey(const Key('home-feed-progress')), findsOneWidget); expect( tester .widget( find.byKey(const Key('home-feed-content-opacity')), ) .opacity, 1, ); }); testWidgets('feed progress drag then immediate release restores content', ( tester, ) async { await _pumpFeedOverlay(tester); final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('home-feed-progress'))), ); await gesture.moveBy(const Offset(80, 0)); await tester.pump(); await gesture.up(); await tester.pumpAndSettle(); expect(tester.takeException(), isNull); expect(find.byKey(const Key('home-feed-progress')), findsOneWidget); expect( tester .widget( find.byKey(const Key('home-feed-content-opacity')), ) .opacity, 1, ); expect( tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, 2, ); }); testWidgets('feed progress remains mounted and thick while dragging', ( tester, ) async { await _pumpFeedOverlay(tester); final progress = find.byKey(const Key('home-feed-progress')); final gesture = await tester.startGesture(tester.getCenter(progress)); await gesture.moveBy(const Offset(80, 0)); await tester.pump(); await tester.pump(const Duration(milliseconds: 130)); expect(tester.takeException(), isNull); expect(progress, findsOneWidget); expect(find.byKey(const Key('scrub-preview-time-bubble')), findsOneWidget); expect( tester .widget( find.byKey(const Key('home-feed-content-opacity')), ) .opacity, 0, ); expect( tester.getSize(find.byKey(const Key('home-feed-progress-track'))).height, 6, ); await gesture.up(); await tester.pumpAndSettle(); }); testWidgets( 'feed progress played segment follows the finger when dragged right', (tester) async { await _pumpFeedOverlay(tester); final bar = find.byKey(const Key('home-feed-progress')); final gesture = await tester.startGesture(tester.getCenter(bar)); await gesture.moveBy(const Offset(800, 0)); await tester.pump(); expect( tester .getRect(find.byKey(const Key('home-feed-progress-played'))) .right, closeTo(tester.getRect(bar).right, 0.5), ); await gesture.up(); await tester.pumpAndSettle(); }, ); testWidgets('full player drag clears chrome and release restores it', ( tester, ) async { await _pumpFullPlayerOverlay(tester); expect(find.byKey(const Key('player-controls-top')), findsOneWidget); expect(find.byKey(const Key('player-bottom-actions')), findsOneWidget); final progressTop = tester.getTopLeft(find.byType(Slider)).dy; final gesture = await tester.startGesture( tester.getCenter(find.byKey(const Key('player-progress'))), ); await gesture.moveBy(const Offset(80, 0)); await tester.pump(); expect(_opacity(tester, const Key('player-controls-top')), 0); expect(_opacity(tester, const Key('player-interaction-rail')), 0); expect(_offstage(tester, const Key('player-controls-bottom')), isTrue); expect(_opacity(tester, const Key('player-bottom-actions')), 0); expect(find.byKey(const Key('player-progress')), findsOneWidget); expect(tester.getTopLeft(find.byType(Slider)).dy, progressTop); await gesture.up(); await tester.pumpAndSettle(); expect(_opacity(tester, const Key('player-controls-top')), 1); expect(_opacity(tester, const Key('player-interaction-rail')), 1); expect(_offstage(tester, const Key('player-bottom-actions')), isFalse); expect(tester.getTopLeft(find.byType(Slider)).dy, progressTop); }); } double _opacity(WidgetTester tester, Key key) { return tester .widget( find .ancestor(of: find.byKey(key), matching: find.byType(Opacity)) .first, ) .opacity; } bool _offstage(WidgetTester tester, Key key) { return tester .widget( find .ancestor( of: find.byKey(key, skipOffstage: false), matching: find.byType(Offstage), ) .first, ) .offstage; } Future<_HarnessState> _pumpHarness(WidgetTester tester) async { final controller = ValueNotifier( const VideoPlayerValue( duration: Duration(minutes: 2), position: Duration(seconds: 20), isInitialized: true, ), ); addTearDown(controller.dispose); final key = GlobalKey<_HarnessState>(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: _Harness(key: key, controller: controller), ), ), ); await tester.pumpAndSettle(); return key.currentState!; } SliderThemeData _sliderTheme(WidgetTester tester) { return tester .widget( find .ancestor( of: find.byType(Slider), matching: find.byType(SliderTheme), ) .first, ) .data; } Future> _pumpFeedOverlay( WidgetTester tester, ) async { final controller = ValueNotifier( const VideoPlayerValue( duration: Duration(minutes: 2), position: Duration(seconds: 20), isInitialized: true, ), ); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Builder( builder: (context) => Scaffold( body: HomeFeedOverlay( l10n: AppLocalizations.of(context)!, drama: const Drama( id: 'scrub-drama', title: 'Scrub Drama', synopsis: 'A short synopsis.', posterUrl: '', totalEpisodes: 8, ), episode: const Episode( id: 'scrub-episode', number: 1, title: 'Scrub Episode', isFree: true, ), controller: controller, locked: false, liked: false, saved: false, visible: true, boosting: false, onLike: () {}, onSave: () {}, onComments: () {}, onShare: () {}, onDetail: () {}, onOpenFullPlayer: () {}, onTogglePlay: () {}, onBoostStart: () {}, onBoostEnd: () {}, onMore: () {}, ), ), ), ), ); await tester.pumpAndSettle(); return controller; } class _FullPlayerHarness extends StatefulWidget { const _FullPlayerHarness({required this.controller}); final ValueNotifier controller; @override State<_FullPlayerHarness> createState() => _FullPlayerHarnessState(); } class _FullPlayerHarnessState extends State<_FullPlayerHarness> { bool _scrubbing = false; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; return PlayerPlaybackOverlay( scrubbing: _scrubbing, controlsVisible: true, data: PlayerOverlayData( l10n: l10n, drama: const Drama( id: 'full-drama', title: 'Full Drama', synopsis: 'A short synopsis for the full player.', posterUrl: '', totalEpisodes: 8, detailLoaded: true, ), episode: const Episode( id: 'full-episode', number: 2, title: 'Episode 2', isFree: true, ), controller: widget.controller, locked: false, liked: false, likeCount: 1, commentCount: 1, favoriteCount: 1, saved: false, playing: true, playbackSpeed: 1, totalEpisodes: 8, boosting: false, ), actions: PlayerOverlayActions( onBack: () {}, onTogglePlay: () {}, onClear: () {}, onExitClear: () {}, onDetail: () {}, onSpeed: () {}, onEpisodes: () {}, onComments: () {}, onLike: () {}, onSave: () {}, onShare: () {}, onMore: () {}, onScrubStart: () => setState(() => _scrubbing = true), onScrubEnd: () => setState(() => _scrubbing = false), onSeek: (_) async {}, onLongPressCenter: () {}, onBoostStart: () {}, onBoostEnd: () {}, ), ); } } Future _pumpFullPlayerOverlay(WidgetTester tester) async { final controller = ValueNotifier( const VideoPlayerValue( duration: Duration(minutes: 2), position: Duration(seconds: 20), isInitialized: true, ), ); addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( locale: const Locale('en'), supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, home: Scaffold(body: _FullPlayerHarness(controller: controller)), ), ); await tester.pumpAndSettle(); } class _Harness extends StatefulWidget { const _Harness({super.key, required this.controller}); final ValueNotifier controller; @override State<_Harness> createState() => _HarnessState(); } class _HarnessState extends State<_Harness> { bool visible = true; int scrubStarts = 0; int scrubEnds = 0; int seeks = 0; @override Widget build(BuildContext context) { return Column( children: [ if (visible) const Text('Overlay', key: Key('scrub-overlay')), SizedBox( width: 320, height: 80, child: SeekProgressBar( valueListenable: widget.controller, semanticsLabel: 'Progress', onScrubStart: () => setState(() { visible = false; scrubStarts++; }), onScrubEnd: () => setState(() { visible = true; scrubEnds++; }), onSeek: (_) async { seeks++; }, ), ), ], ); } }