// 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/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! as RoundSliderThumbShape) .enabledThumbRadius, 8, ); 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! as RoundSliderThumbShape) .enabledThumbRadius, 6, ); }); 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(); }); } 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 _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++; }, ), ), ], ); } }