Skip to content

Commit 75f365e

Browse files
lightbox test: Add video player regression tests
1 parent e453648 commit 75f365e

File tree

1 file changed

+242
-27
lines changed

1 file changed

+242
-27
lines changed

test/widgets/lightbox_test.dart

Lines changed: 242 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import 'dart:async';
2+
import 'dart:math' as math;
23

34
import 'package:checks/checks.dart';
5+
import 'package:clock/clock.dart';
6+
import 'package:flutter/services.dart';
47
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
58
import 'package:flutter_test/flutter_test.dart';
69
import 'package:flutter/material.dart';
@@ -16,11 +19,29 @@ import '../model/binding.dart';
1619
class FakeVideoPlayerPlatform extends Fake
1720
with MockPlatformInterfaceMixin
1821
implements VideoPlayerPlatform {
19-
static const int _textureId = 0xffffffff;
22+
static const String kTestVideoUrl = "https://a/video.mp4";
23+
static const String kTestUnsupportedVideoUrl = "https://a/unsupported.mp4";
24+
static const Duration kTestVideoDuration = Duration(seconds: 10);
2025

26+
static const int _kTextureId = 0xffffffff;
27+
28+
static final List<String> _callLogs = [];
2129
static StreamController<VideoEvent> _streamController = StreamController<VideoEvent>();
22-
static bool initialized = false;
23-
static bool isPlaying = false;
30+
static bool _initialized = false;
31+
static bool _hasError = false;
32+
static Duration _position = Duration.zero;
33+
static Duration _lastSetPosition = Duration.zero;
34+
static Stopwatch? _stopwatch;
35+
36+
static List<String> get callLogs => _callLogs;
37+
static bool get initialized => _initialized;
38+
static bool get hasError => _hasError;
39+
static bool get isPlaying => _stopwatch?.isRunning ?? false;
40+
41+
static Duration get position {
42+
_updatePosition();
43+
return _position;
44+
}
2445

2546
static void registerWith() {
2647
VideoPlayerPlatform.instance = FakeVideoPlayerPlatform();
@@ -29,49 +50,89 @@ class FakeVideoPlayerPlatform extends Fake
2950
static void reset() {
3051
_streamController.close();
3152
_streamController = StreamController<VideoEvent>();
32-
initialized = false;
33-
isPlaying = false;
53+
_callLogs.clear();
54+
_initialized = false;
55+
_position = Duration.zero;
56+
_lastSetPosition = Duration.zero;
57+
_stopwatch?.stop();
58+
_stopwatch?.reset();
59+
}
60+
61+
static void _pause() {
62+
_stopwatch?.stop();
63+
_streamController.add(VideoEvent(
64+
eventType: VideoEventType.isPlayingStateUpdate,
65+
isPlaying: false,
66+
));
67+
}
68+
69+
static void _updatePosition() {
70+
assert(_stopwatch != null);
71+
_position = _stopwatch!.elapsed + _lastSetPosition;
72+
if (kTestVideoDuration.compareTo(_position) <= 0) {
73+
_position = kTestVideoDuration;
74+
_pause();
75+
}
3476
}
3577

3678
@override
37-
Future<void> init() async {}
79+
Future<void> init() async {
80+
_callLogs.add('init');
81+
}
3882

3983
@override
4084
Future<void> dispose(int textureId) async {
85+
_callLogs.add('dispose');
86+
if (hasError) {
87+
assert(!initialized);
88+
assert(textureId == VideoPlayerController.kUninitializedTextureId);
89+
return;
90+
}
91+
4192
assert(initialized);
42-
assert(textureId == _textureId);
43-
initialized = false;
93+
assert(textureId == _kTextureId);
4494
}
4595

4696
@override
4797
Future<int?> create(DataSource dataSource) async {
98+
_callLogs.add('create');
4899
assert(!initialized);
49-
initialized = true;
100+
if (dataSource.uri == kTestUnsupportedVideoUrl) {
101+
_hasError = true;
102+
_streamController.addError(PlatformException(code: "VideoError", message: "Failed to load video: Cannot Open"));
103+
return null;
104+
}
105+
106+
_stopwatch = clock.stopwatch();
107+
_initialized = true;
50108
_streamController.add(VideoEvent(
51109
eventType: VideoEventType.initialized,
52-
duration: const Duration(seconds: 1),
110+
duration: kTestVideoDuration,
53111
size: const Size(0, 0),
54112
rotationCorrection: 0,
55113
));
56-
return _textureId;
114+
return _kTextureId;
57115
}
58116

59117
@override
60118
Stream<VideoEvent> videoEventsFor(int textureId) {
61-
assert(textureId == _textureId);
119+
_callLogs.add('videoEventsFor');
120+
assert(textureId == _kTextureId);
62121
return _streamController.stream;
63122
}
64123

65124
@override
66125
Future<void> setLooping(int textureId, bool looping) async {
67-
assert(textureId == _textureId);
126+
_callLogs.add('setLooping');
127+
assert(textureId == _kTextureId);
68128
assert(!looping);
69129
}
70130

71131
@override
72132
Future<void> play(int textureId) async {
73-
assert(textureId == _textureId);
74-
isPlaying = true;
133+
_callLogs.add('play');
134+
assert(textureId == _kTextureId);
135+
_stopwatch?.start();
75136
_streamController.add(VideoEvent(
76137
eventType: VideoEventType.isPlayingStateUpdate,
77138
isPlaying: true,
@@ -80,27 +141,44 @@ class FakeVideoPlayerPlatform extends Fake
80141

81142
@override
82143
Future<void> pause(int textureId) async {
83-
assert(textureId == _textureId);
84-
isPlaying = false;
85-
_streamController.add(VideoEvent(
86-
eventType: VideoEventType.isPlayingStateUpdate,
87-
isPlaying: false,
88-
));
144+
_callLogs.add('pause');
145+
assert(textureId == _kTextureId);
146+
_pause();
89147
}
90148

91149
@override
92150
Future<void> setVolume(int textureId, double volume) async {
93-
assert(textureId == _textureId);
151+
_callLogs.add('setVolume');
152+
assert(textureId == _kTextureId);
153+
}
154+
155+
@override
156+
Future<void> seekTo(int textureId, Duration pos) async {
157+
_callLogs.add('seekTo');
158+
assert(textureId == _kTextureId);
159+
160+
_lastSetPosition = Duration(
161+
microseconds: math.min(pos.inMicroseconds, kTestVideoDuration.inMicroseconds));
162+
_stopwatch?.reset();
94163
}
95164

96165
@override
97166
Future<void> setPlaybackSpeed(int textureId, double speed) async {
98-
assert(textureId == _textureId);
167+
_callLogs.add('setPlaybackSpeed');
168+
assert(textureId == _kTextureId);
169+
}
170+
171+
@override
172+
Future<Duration> getPosition(int textureId) async {
173+
_callLogs.add('getPosition');
174+
assert(textureId == _kTextureId);
175+
return position;
99176
}
100177

101178
@override
102179
Widget buildView(int textureId) {
103-
assert(textureId == _textureId);
180+
_callLogs.add('buildView');
181+
assert(textureId == _kTextureId);
104182
return const SizedBox(width: 100, height: 100);
105183
}
106184
}
@@ -109,6 +187,16 @@ void main() {
109187
TestZulipBinding.ensureInitialized();
110188

111189
group("VideoLightboxPage", () {
190+
void verifySliderPosition(WidgetTester tester, Duration duration) {
191+
final slider = tester.widget(find.byType(Slider)) as Slider;
192+
check(slider.value)
193+
.equals(duration.inMilliseconds.toDouble());
194+
final positionLabel = tester.widget(
195+
find.byKey(VideoPositionSliderControl.kCurrentPositionLabelKey)) as VideoDurationLabel;
196+
check(positionLabel.duration)
197+
.equals(duration);
198+
}
199+
112200
FakeVideoPlayerPlatform.registerWith();
113201

114202
Future<void> setupPage(WidgetTester tester, {
@@ -127,11 +215,13 @@ void main() {
127215
routeEntranceAnimation: kAlwaysCompleteAnimation,
128216
message: eg.streamMessage(),
129217
src: videoSrc)))));
130-
await tester.pumpAndSettle();
218+
await tester.pump(); // global store
219+
await tester.pump(); // per-account store
220+
await tester.pump(); // video controller initialization
131221
}
132222

133223
testWidgets('shows a VideoPlayer, and video is playing', (tester) async {
134-
await setupPage(tester, videoSrc: Uri.parse('https://a/b.mp4'));
224+
await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestVideoUrl));
135225

136226
check(FakeVideoPlayerPlatform.initialized).isTrue();
137227
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
@@ -140,7 +230,7 @@ void main() {
140230
});
141231

142232
testWidgets('toggles between play and pause', (tester) async {
143-
await setupPage(tester, videoSrc: Uri.parse('https://a/b.mp4'));
233+
await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestVideoUrl));
144234
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
145235

146236
await tester.tap(find.byIcon(Icons.pause_circle_rounded));
@@ -152,5 +242,130 @@ void main() {
152242
await tester.tap(find.byIcon(Icons.play_circle_rounded));
153243
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
154244
});
245+
246+
testWidgets('unsupported video shows an error dialog', (tester) async {
247+
await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestUnsupportedVideoUrl));
248+
await tester.ensureVisible(find.text("Unable to play the video"));
249+
});
250+
251+
testWidgets('video advances overtime & stops playing when it ends', (tester) async {
252+
await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestVideoUrl));
253+
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
254+
check(FakeVideoPlayerPlatform.position).equals(Duration.zero);
255+
256+
final halfTime = FakeVideoPlayerPlatform.kTestVideoDuration * 0.5;
257+
258+
await tester.pump(halfTime);
259+
verifySliderPosition(tester, halfTime);
260+
check(FakeVideoPlayerPlatform.position).equals(halfTime);
261+
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
262+
263+
// Near the end of the video.
264+
await tester.pump(halfTime - const Duration(milliseconds: 500));
265+
verifySliderPosition(
266+
tester, FakeVideoPlayerPlatform.kTestVideoDuration - const Duration(milliseconds: 500));
267+
check(FakeVideoPlayerPlatform.position)
268+
.equals(FakeVideoPlayerPlatform.kTestVideoDuration - const Duration(milliseconds: 500));
269+
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
270+
271+
// At exactly the end of the video.
272+
await tester.pump(const Duration(milliseconds: 500));
273+
verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration);
274+
check(FakeVideoPlayerPlatform.position).equals(FakeVideoPlayerPlatform.kTestVideoDuration);
275+
check(FakeVideoPlayerPlatform.isPlaying).isFalse();
276+
277+
// After the video ended.
278+
await tester.pump(const Duration(milliseconds: 500));
279+
verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration);
280+
check(FakeVideoPlayerPlatform.position).equals(FakeVideoPlayerPlatform.kTestVideoDuration);
281+
check(FakeVideoPlayerPlatform.isPlaying).isFalse();
282+
});
283+
284+
testWidgets('ensure \'seekTo\' is called only once', (tester) async {
285+
await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestVideoUrl));
286+
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
287+
check(FakeVideoPlayerPlatform.position).equals(Duration.zero);
288+
289+
const padding = 24.0;
290+
final rect = tester.getRect(find.byType(Slider));
291+
final trackWidth = rect.width - padding - padding;
292+
final trackStartPos = rect.centerLeft + const Offset(padding, 0);
293+
final twentyPercent = trackWidth * 0.2; // 20% increments
294+
295+
// Verify the actually displayed current position at each
296+
// gesture increments.
297+
final gesture = await tester.startGesture(trackStartPos);
298+
await tester.pump();
299+
verifySliderPosition(tester, Duration.zero);
300+
check(FakeVideoPlayerPlatform.position).equals(Duration.zero);
301+
302+
await gesture.moveBy(Offset(twentyPercent, 0.0));
303+
await tester.pump();
304+
verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration * 0.2);
305+
check(FakeVideoPlayerPlatform.position).equals(Duration.zero);
306+
307+
await gesture.moveBy(Offset(twentyPercent, 0.0));
308+
await tester.pump();
309+
verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration * 0.4);
310+
check(FakeVideoPlayerPlatform.position).equals(Duration.zero);
311+
312+
await gesture.moveBy(Offset(twentyPercent, 0.0));
313+
await tester.pump();
314+
verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration * 0.6);
315+
check(FakeVideoPlayerPlatform.position).equals(Duration.zero);
316+
317+
await gesture.up();
318+
await tester.pump();
319+
verifySliderPosition(tester, FakeVideoPlayerPlatform.kTestVideoDuration * 0.6);
320+
check(FakeVideoPlayerPlatform.position).equals(FakeVideoPlayerPlatform.kTestVideoDuration * 0.6);
321+
322+
// Verify seekTo is called only once.
323+
check(FakeVideoPlayerPlatform.callLogs.where((v) => v == 'seekTo').length).equals(1);
324+
});
325+
326+
testWidgets('video advances overtime after dragging the slider', (tester) async {
327+
await setupPage(tester, videoSrc: Uri.parse(FakeVideoPlayerPlatform.kTestVideoUrl));
328+
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
329+
check(FakeVideoPlayerPlatform.position).equals(Duration.zero);
330+
331+
const padding = 24.0;
332+
final rect = tester.getRect(find.byType(Slider));
333+
final trackWidth = rect.width - padding - padding;
334+
final trackStartPos = rect.centerLeft + const Offset(padding, 0);
335+
final fiftyPercent = trackWidth * 0.5;
336+
final halfTime = FakeVideoPlayerPlatform.kTestVideoDuration * 0.5;
337+
338+
final gesture = await tester.startGesture(trackStartPos);
339+
await tester.pump();
340+
verifySliderPosition(tester, Duration.zero);
341+
check(FakeVideoPlayerPlatform.position).equals(Duration.zero);
342+
343+
await gesture.moveBy(Offset(fiftyPercent, 0));
344+
await tester.pump();
345+
verifySliderPosition(tester, halfTime);
346+
check(FakeVideoPlayerPlatform.position).equals(Duration.zero);
347+
348+
await gesture.up();
349+
await tester.pump();
350+
verifySliderPosition(tester, halfTime);
351+
352+
// Verify that after dragging ends, video position is at the
353+
// halfway point, and after that it starts advancing as the time
354+
// passes.
355+
check(FakeVideoPlayerPlatform.position).equals(halfTime);
356+
357+
const waitTime = Duration(seconds: 1);
358+
await tester.pump(waitTime);
359+
verifySliderPosition(tester, halfTime + (waitTime * 1));
360+
check(FakeVideoPlayerPlatform.position).equals(halfTime + (waitTime * 1));
361+
362+
await tester.pump(waitTime);
363+
verifySliderPosition(tester, halfTime + (waitTime * 2));
364+
check(FakeVideoPlayerPlatform.position).equals(halfTime + (waitTime * 2));
365+
366+
await tester.pump(waitTime);
367+
verifySliderPosition(tester, halfTime + (waitTime * 3));
368+
check(FakeVideoPlayerPlatform.position).equals(halfTime + (waitTime * 3));
369+
});
155370
});
156371
}

0 commit comments

Comments
 (0)