diff --git a/test/widgets/lightbox_test.dart b/test/widgets/lightbox_test.dart index e314c2fd71..e6536562c0 100644 --- a/test/widgets/lightbox_test.dart +++ b/test/widgets/lightbox_test.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:checks/checks.dart'; +import 'package:clock/clock.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/zulip_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; @@ -16,11 +19,29 @@ import '../model/binding.dart'; class FakeVideoPlayerPlatform extends Fake with MockPlatformInterfaceMixin implements VideoPlayerPlatform { - static const int _textureId = 0xffffffff; + static const String kTestVideoUrl = "https://a/video.mp4"; + static const String kTestUnsupportedVideoUrl = "https://a/unsupported.mp4"; + static const Duration kTestVideoDuration = Duration(seconds: 10); + static const int _kTextureId = 0xffffffff; + + static final List _callLogs = []; static StreamController _streamController = StreamController(); - static bool initialized = false; - static bool isPlaying = false; + static bool _initialized = false; + static bool _hasError = false; + static Duration _position = Duration.zero; + static Duration _lastSetPosition = Duration.zero; + static Stopwatch? _stopwatch; + + static List get callLogs => _callLogs; + static bool get initialized => _initialized; + static bool get hasError => _hasError; + static bool get isPlaying => _stopwatch?.isRunning ?? false; + + static Duration get position { + _updatePosition(); + return _position; + } static void registerWith() { VideoPlayerPlatform.instance = FakeVideoPlayerPlatform(); @@ -29,49 +50,89 @@ class FakeVideoPlayerPlatform extends Fake static void reset() { _streamController.close(); _streamController = StreamController(); - initialized = false; - isPlaying = false; + _callLogs.clear(); + _initialized = false; + _position = Duration.zero; + _lastSetPosition = Duration.zero; + _stopwatch?.stop(); + _stopwatch?.reset(); + } + + static void _pause() { + _stopwatch?.stop(); + _streamController.add(VideoEvent( + eventType: VideoEventType.isPlayingStateUpdate, + isPlaying: false, + )); + } + + static void _updatePosition() { + assert(_stopwatch != null); + _position = _stopwatch!.elapsed + _lastSetPosition; + if (kTestVideoDuration.compareTo(_position) <= 0) { + _position = kTestVideoDuration; + _pause(); + } } @override - Future init() async {} + Future init() async { + _callLogs.add('init'); + } @override Future dispose(int textureId) async { + _callLogs.add('dispose'); + if (hasError) { + assert(!initialized); + assert(textureId == VideoPlayerController.kUninitializedTextureId); + return; + } + assert(initialized); - assert(textureId == _textureId); - initialized = false; + assert(textureId == _kTextureId); } @override Future create(DataSource dataSource) async { + _callLogs.add('create'); assert(!initialized); - initialized = true; + if (dataSource.uri == kTestUnsupportedVideoUrl) { + _hasError = true; + _streamController.addError(PlatformException(code: "VideoError", message: "Failed to load video: Cannot Open")); + return null; + } + + _stopwatch = clock.stopwatch(); + _initialized = true; _streamController.add(VideoEvent( eventType: VideoEventType.initialized, - duration: const Duration(seconds: 1), + duration: kTestVideoDuration, size: const Size(0, 0), rotationCorrection: 0, )); - return _textureId; + return _kTextureId; } @override Stream videoEventsFor(int textureId) { - assert(textureId == _textureId); + _callLogs.add('videoEventsFor'); + assert(textureId == _kTextureId); return _streamController.stream; } @override Future setLooping(int textureId, bool looping) async { - assert(textureId == _textureId); + _callLogs.add('setLooping'); + assert(textureId == _kTextureId); assert(!looping); } @override Future play(int textureId) async { - assert(textureId == _textureId); - isPlaying = true; + _callLogs.add('play'); + assert(textureId == _kTextureId); + _stopwatch?.start(); _streamController.add(VideoEvent( eventType: VideoEventType.isPlayingStateUpdate, isPlaying: true, @@ -80,27 +141,44 @@ class FakeVideoPlayerPlatform extends Fake @override Future pause(int textureId) async { - assert(textureId == _textureId); - isPlaying = false; - _streamController.add(VideoEvent( - eventType: VideoEventType.isPlayingStateUpdate, - isPlaying: false, - )); + _callLogs.add('pause'); + assert(textureId == _kTextureId); + _pause(); } @override Future setVolume(int textureId, double volume) async { - assert(textureId == _textureId); + _callLogs.add('setVolume'); + assert(textureId == _kTextureId); + } + + @override + Future seekTo(int textureId, Duration pos) async { + _callLogs.add('seekTo'); + assert(textureId == _kTextureId); + + _lastSetPosition = Duration( + microseconds: math.min(pos.inMicroseconds, kTestVideoDuration.inMicroseconds)); + _stopwatch?.reset(); } @override Future setPlaybackSpeed(int textureId, double speed) async { - assert(textureId == _textureId); + _callLogs.add('setPlaybackSpeed'); + assert(textureId == _kTextureId); + } + + @override + Future getPosition(int textureId) async { + _callLogs.add('getPosition'); + assert(textureId == _kTextureId); + return position; } @override Widget buildView(int textureId) { - assert(textureId == _textureId); + _callLogs.add('buildView'); + assert(textureId == _kTextureId); return const SizedBox(width: 100, height: 100); } } @@ -109,6 +187,16 @@ void main() { TestZulipBinding.ensureInitialized(); group("VideoLightboxPage", () { + void verifySliderPosition(WidgetTester tester, Duration duration) { + final slider = tester.widget(find.byType(Slider)) as Slider; + check(slider.value) + .equals(duration.inMilliseconds.toDouble()); + final positionLabel = tester.widget( + find.byKey(VideoPositionSliderControl.kCurrentPositionLabelKey)) as VideoDurationLabel; + check(positionLabel.duration) + .equals(duration); + } + FakeVideoPlayerPlatform.registerWith(); Future setupPage(WidgetTester tester, { @@ -127,11 +215,13 @@ void main() { routeEntranceAnimation: kAlwaysCompleteAnimation, message: eg.streamMessage(), src: videoSrc))))); - await tester.pumpAndSettle(); + await tester.pump(); // global store + await tester.pump(); // per-account store + await tester.pump(); // video controller initialization } testWidgets('shows a VideoPlayer, and video is playing', (tester) async { - await setupPage(tester, videoSrc: Uri.parse('https://a/b.mp4')); + await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestVideoUrl)); check(FakeVideoPlayerPlatform.initialized).isTrue(); check(FakeVideoPlayerPlatform.isPlaying).isTrue(); @@ -140,7 +230,7 @@ void main() { }); testWidgets('toggles between play and pause', (tester) async { - await setupPage(tester, videoSrc: Uri.parse('https://a/b.mp4')); + await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestVideoUrl)); check(FakeVideoPlayerPlatform.isPlaying).isTrue(); await tester.tap(find.byIcon(Icons.pause_circle_rounded)); @@ -152,5 +242,130 @@ void main() { await tester.tap(find.byIcon(Icons.play_circle_rounded)); check(FakeVideoPlayerPlatform.isPlaying).isTrue(); }); + + testWidgets('unsupported video shows an error dialog', (tester) async { + await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestUnsupportedVideoUrl)); + await tester.ensureVisible(find.text("Unable to play the video")); + }); + + testWidgets('video advances overtime & stops playing when it ends', (tester) async { + await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestVideoUrl)); + check(FakeVideoPlayerPlatform.isPlaying).isTrue(); + check(FakeVideoPlayerPlatform.position).equals(Duration.zero); + + final halfTime = FakeVideoPlayerPlatform.kTestVideoDuration * 0.5; + + await tester.pump(halfTime); + verifySliderPosition(tester, halfTime); + check(FakeVideoPlayerPlatform.position).equals(halfTime); + check(FakeVideoPlayerPlatform.isPlaying).isTrue(); + + // Near the end of the video. + await tester.pump(halfTime - const Duration(milliseconds: 500)); + verifySliderPosition( + tester, FakeVideoPlayerPlatform.kTestVideoDuration - const Duration(milliseconds: 500)); + check(FakeVideoPlayerPlatform.position) + .equals(FakeVideoPlayerPlatform.kTestVideoDuration - const Duration(milliseconds: 500)); + check(FakeVideoPlayerPlatform.isPlaying).isTrue(); + + // At exactly the end of the video. + await tester.pump(const Duration(milliseconds: 500)); + verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration); + check(FakeVideoPlayerPlatform.position).equals(FakeVideoPlayerPlatform.kTestVideoDuration); + check(FakeVideoPlayerPlatform.isPlaying).isFalse(); + + // After the video ended. + await tester.pump(const Duration(milliseconds: 500)); + verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration); + check(FakeVideoPlayerPlatform.position).equals(FakeVideoPlayerPlatform.kTestVideoDuration); + check(FakeVideoPlayerPlatform.isPlaying).isFalse(); + }); + + testWidgets('ensure \'seekTo\' is called only once', (tester) async { + await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestVideoUrl)); + check(FakeVideoPlayerPlatform.isPlaying).isTrue(); + check(FakeVideoPlayerPlatform.position).equals(Duration.zero); + + const padding = 24.0; + final rect = tester.getRect(find.byType(Slider)); + final trackWidth = rect.width - padding - padding; + final trackStartPos = rect.centerLeft + const Offset(padding, 0); + final twentyPercent = trackWidth * 0.2; // 20% increments + + // Verify the actually displayed current position at each + // gesture increments. + final gesture = await tester.startGesture(trackStartPos); + await tester.pump(); + verifySliderPosition(tester, Duration.zero); + check(FakeVideoPlayerPlatform.position).equals(Duration.zero); + + await gesture.moveBy(Offset(twentyPercent, 0.0)); + await tester.pump(); + verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration * 0.2); + check(FakeVideoPlayerPlatform.position).equals(Duration.zero); + + await gesture.moveBy(Offset(twentyPercent, 0.0)); + await tester.pump(); + verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration * 0.4); + check(FakeVideoPlayerPlatform.position).equals(Duration.zero); + + await gesture.moveBy(Offset(twentyPercent, 0.0)); + await tester.pump(); + verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration * 0.6); + check(FakeVideoPlayerPlatform.position).equals(Duration.zero); + + await gesture.up(); + await tester.pump(); + verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration * 0.6); + check(FakeVideoPlayerPlatform.position).equals(FakeVideoPlayerPlatform.kTestVideoDuration * 0.6); + + // Verify seekTo is called only once. + check(FakeVideoPlayerPlatform.callLogs.where((v) => v == 'seekTo').length).equals(1); + }); + + testWidgets('video advances overtime after dragging the slider', (tester) async { + await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestVideoUrl)); + check(FakeVideoPlayerPlatform.isPlaying).isTrue(); + check(FakeVideoPlayerPlatform.position).equals(Duration.zero); + + const padding = 24.0; + final rect = tester.getRect(find.byType(Slider)); + final trackWidth = rect.width - padding - padding; + final trackStartPos = rect.centerLeft + const Offset(padding, 0); + final fiftyPercent = trackWidth * 0.5; + final halfTime = FakeVideoPlayerPlatform.kTestVideoDuration * 0.5; + + final gesture = await tester.startGesture(trackStartPos); + await tester.pump(); + verifySliderPosition(tester, Duration.zero); + check(FakeVideoPlayerPlatform.position).equals(Duration.zero); + + await gesture.moveBy(Offset(fiftyPercent, 0)); + await tester.pump(); + verifySliderPosition(tester, halfTime); + check(FakeVideoPlayerPlatform.position).equals(Duration.zero); + + await gesture.up(); + await tester.pump(); + verifySliderPosition(tester, halfTime); + + // Verify that after dragging ends, video position is at the + // halfway point, and after that it starts advancing as the time + // passes. + check(FakeVideoPlayerPlatform.position).equals(halfTime); + + const waitTime = Duration(seconds: 1); + await tester.pump(waitTime); + verifySliderPosition(tester, halfTime + (waitTime * 1)); + check(FakeVideoPlayerPlatform.position).equals(halfTime + (waitTime * 1)); + + await tester.pump(waitTime); + verifySliderPosition(tester, halfTime + (waitTime * 2)); + check(FakeVideoPlayerPlatform.position).equals(halfTime + (waitTime * 2)); + + await tester.pump(waitTime); + verifySliderPosition(tester, halfTime + (waitTime * 3)); + check(FakeVideoPlayerPlatform.position).equals(halfTime + (waitTime * 3)); + }); }); }