Files
ReelShort/test/playback/seek_progress_bar_test.dart

471 lines
14 KiB
Dart

// Author: fruit.com.cn
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/services/playback_thumbnail_service.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/pages/player/widgets/scrub_preview_card.dart';
import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.dart';
import 'package:video_player/video_player.dart';
void main() {
test('should_keep_seek_target_inside_playable_duration_when_dragging', () {
const duration = Duration(seconds: 12);
expect(seekPositionForFraction(0, duration), Duration.zero);
expect(
seekPositionForFraction(0.5, duration),
greaterThan(const Duration(seconds: 5)),
);
expect(seekPositionForFraction(1, duration), lessThan(duration));
});
test('should_clamp_preview_card_inside_track_safe_margins', () {
expect(
scrubPreviewLeft(trackWidth: 300, fraction: 0),
scrubPreviewSafeMargin,
);
expect(
scrubPreviewLeft(trackWidth: 300, fraction: 1),
300 - scrubPreviewWidth - scrubPreviewSafeMargin,
);
expect(scrubPreviewLeft(trackWidth: 100, fraction: 0.5), 2);
});
testWidgets('should_show_inactive_track_when_video_is_not_initialized', (
tester,
) async {
final video = ValueNotifier<VideoPlayerValue>(
const VideoPlayerValue.uninitialized(),
);
addTearDown(video.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SeekProgressBar(
valueListenable: video,
semanticsLabel: 'Seek video',
),
),
),
);
expect(find.byKey(const Key('progress-loading')), findsNothing);
expect(find.byKey(const Key('progress-inactive')), findsOneWidget);
expect(find.byType(Slider), findsNothing);
expect(
find.byKey(const Key('player-progress-uninitialized-time')),
findsNothing,
);
expect(find.textContaining('00:00'), findsNothing);
});
testWidgets('should_show_inactive_track_and_disable_seek_when_locked', (
tester,
) async {
final video = ValueNotifier<VideoPlayerValue>(
const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 3),
isInitialized: true,
),
);
addTearDown(video.dispose);
var seeks = 0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SeekProgressBar(
valueListenable: video,
semanticsLabel: 'Seek video',
locked: true,
onSeek: (_) async => seeks++,
),
),
),
);
expect(find.byKey(const Key('progress-loading')), findsNothing);
expect(find.byKey(const Key('progress-loading-highlight')), findsNothing);
expect(find.byType(Slider), findsNothing);
final inactive = tester.widget<ColoredBox>(
find.byKey(const Key('progress-inactive')),
);
expect(inactive.color, AppColors.border);
await tester.drag(
find.byKey(const Key('progress-inactive')),
const Offset(120, 0),
);
await tester.pumpAndSettle();
expect(seeks, 0);
});
testWidgets('should_debounce_buffering_before_showing_loading_track', (
tester,
) async {
final video = ValueNotifier<VideoPlayerValue>(
const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 3),
isInitialized: true,
isBuffering: true,
),
);
addTearDown(video.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SeekProgressBar(
valueListenable: video,
semanticsLabel: 'Seek video',
onSeek: (_) async {},
),
),
),
);
expect(find.byKey(const Key('progress-loading')), findsNothing);
expect(find.byType(Slider), findsOneWidget);
expect(find.textContaining('00:12'), findsNothing);
await tester.pump(const Duration(milliseconds: 200));
expect(find.byKey(const Key('progress-loading')), findsNothing);
expect(find.byType(Slider), findsOneWidget);
await tester.pump(const Duration(milliseconds: 200));
expect(find.byKey(const Key('progress-loading')), findsOneWidget);
expect(find.byType(Slider), findsNothing);
video.value = const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 3),
isInitialized: true,
);
await tester.pump();
expect(find.byKey(const Key('progress-loading')), findsNothing);
expect(find.byType(Slider), findsOneWidget);
});
testWidgets(
'should_hide_buffering_loading_immediately_when_position_advances',
(tester) async {
final video = ValueNotifier<VideoPlayerValue>(
const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 3),
isInitialized: true,
isPlaying: true,
isBuffering: true,
),
);
addTearDown(video.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SeekProgressBar(
valueListenable: video,
semanticsLabel: 'Seek video',
onSeek: (_) async {},
),
),
),
);
expect(find.byKey(const Key('progress-loading')), findsNothing);
await tester.pump(const Duration(milliseconds: 200));
video.value = const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 4),
isInitialized: true,
isBuffering: true,
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
expect(find.byKey(const Key('progress-loading')), findsNothing);
expect(find.byType(Slider), findsOneWidget);
video.value = const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 4),
isInitialized: true,
isPlaying: true,
isBuffering: true,
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
expect(find.byKey(const Key('progress-loading')), findsOneWidget);
expect(find.byType(Slider), findsNothing);
},
);
testWidgets('should_not_show_loading_for_prewarmed_episode', (tester) async {
final video = ValueNotifier<VideoPlayerValue>(
const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 5),
isInitialized: true,
isPlaying: true,
isBuffering: true,
),
);
addTearDown(video.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SeekProgressBar(
valueListenable: video,
semanticsLabel: 'Seek video',
onSeek: (_) async {},
),
),
),
);
await tester.pump(const Duration(milliseconds: 400));
expect(find.byKey(const Key('progress-loading')), findsNothing);
expect(find.byType(Slider), findsOneWidget);
});
testWidgets(
'should_suppress_progress_loading_while_entry_loading_is_visible',
(tester) async {
final video = ValueNotifier<VideoPlayerValue>(
const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration.zero,
isInitialized: true,
isBuffering: true,
),
);
addTearDown(video.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SeekProgressBar(
valueListenable: video,
semanticsLabel: 'Seek video',
suppressBufferLoading: true,
onSeek: (_) async {},
),
),
),
);
await tester.pump(const Duration(milliseconds: 400));
expect(find.byKey(const Key('progress-loading')), findsNothing);
expect(find.byType(Slider), findsOneWidget);
},
);
testWidgets('should_keep_slider_while_scrubbing_during_buffering', (
tester,
) async {
final video = ValueNotifier<VideoPlayerValue>(
const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 3),
isInitialized: true,
),
);
addTearDown(video.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 300,
child: SeekProgressBar(
valueListenable: video,
semanticsLabel: 'Seek video',
onSeek: (_) async {},
),
),
),
),
),
);
final gesture = await tester.startGesture(
tester.getCenter(find.byType(Slider)),
);
await gesture.moveBy(const Offset(60, 0));
await tester.pump();
video.value = const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 5),
isInitialized: true,
isBuffering: true,
);
await tester.pump(const Duration(milliseconds: 400));
expect(find.byKey(const Key('progress-loading')), findsNothing);
expect(find.byType(Slider), findsOneWidget);
video.value = const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 5),
isInitialized: true,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
});
testWidgets(
'should_preview_while_dragging_and_seek_on_release_when_slider_is_horizontal',
(tester) async {
final video = ValueNotifier<VideoPlayerValue>(
const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 3),
isInitialized: true,
),
);
addTearDown(video.dispose);
var started = 0;
Duration? target;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 300,
child: SeekProgressBar(
valueListenable: video,
semanticsLabel: 'Seek video',
onScrubStart: () => started++,
onSeek: (position) async {
target = position;
},
),
),
),
),
),
);
final gesture = await tester.startGesture(
tester.getCenter(find.byType(Slider)),
);
await gesture.moveBy(const Offset(120, 0));
await tester.pump();
expect(started, 1);
expect(target, isNull);
expect(find.textContaining('/ 00:12'), findsOneWidget);
await gesture.up();
await tester.pumpAndSettle();
expect(target, isNotNull);
expect(target!, greaterThan(const Duration(seconds: 3)));
expect(target!, lessThan(const Duration(seconds: 12)));
expect(find.textContaining('/ 00:12'), findsNothing);
},
);
testWidgets('should_show_frame_card_then_hide_on_release', (tester) async {
final video = ValueNotifier<VideoPlayerValue>(
const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 3),
isInitialized: true,
),
);
final service = _thumbnailService();
addTearDown(video.dispose);
addTearDown(service.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: SizedBox(
width: 300,
child: SeekProgressBar(
valueListenable: video,
semanticsLabel: 'Seek video',
episodeId: 'episode-1',
thumbnailService: service,
onSeek: (_) async {},
),
),
),
),
),
);
final gesture = await tester.startGesture(
tester.getCenter(find.byType(Slider)),
);
await gesture.moveBy(const Offset(80, 0));
await tester.pump();
expect(find.byKey(const Key('scrub-preview-card')), findsOneWidget);
expect(find.byKey(const Key('scrub-preview-background')), findsOneWidget);
expect(find.byKey(const Key('scrub-preview-frame')), findsNothing);
expect(find.textContaining('/ 00:12'), findsOneWidget);
expect(
tester.getSize(find.byKey(const Key('scrub-preview-card'))).width,
96,
);
await gesture.up();
await tester.pump();
expect(find.byKey(const Key('scrub-preview-card')), findsNothing);
});
testWidgets('should_show_time_only_while_dragging_when_seek_bar_is_compact', (
tester,
) async {
final video = ValueNotifier<VideoPlayerValue>(
const VideoPlayerValue(
duration: Duration(seconds: 12),
position: Duration(seconds: 3),
isInitialized: true,
),
);
addTearDown(video.dispose);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: SeekProgressBar(
valueListenable: video,
semanticsLabel: 'Seek video',
onSeek: (_) async {},
),
),
),
);
expect(find.textContaining('00:12'), findsNothing);
final gesture = await tester.startGesture(
tester.getCenter(find.byType(Slider)),
);
await gesture.moveBy(const Offset(40, 0));
await tester.pump();
expect(find.textContaining('/ 00:12'), findsOneWidget);
await gesture.up();
await tester.pumpAndSettle();
expect(find.textContaining('00:12'), findsNothing);
});
}
PlaybackThumbnailService _thumbnailService() {
return PlaybackThumbnailService(
frameLoader: _NullThumbnailLoader(),
sourceResolver: (_) =>
const PlaybackThumbnailSource(remoteUrl: 'https://example/video.mp4'),
prefetchDelay: const Duration(hours: 1),
);
}
class _NullThumbnailLoader implements PlaybackThumbnailFrameLoader {
@override
Future<ByteData?> load(PlaybackThumbnailFrameRequest request) async => null;
@override
void dispose() {}
}