Skip to content

Commit f28e841

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

File tree

1 file changed

+265
-25
lines changed

1 file changed

+265
-25
lines changed

test/widgets/lightbox_test.dart

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

34
import 'package:checks/checks.dart';
5+
import 'package:flutter/services.dart';
46
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
57
import 'package:flutter_test/flutter_test.dart';
68
import 'package:flutter/material.dart';
@@ -16,91 +18,180 @@ import '../model/binding.dart';
1618
class FakeVideoPlayerPlatform extends Fake
1719
with MockPlatformInterfaceMixin
1820
implements VideoPlayerPlatform {
19-
static const int _textureId = 0xffffffff;
21+
static const String kTestVideoUrl = "https://a/video.mp4";
22+
static const String kTestUnsupportedVideoUrl = "https://a/unsupported.mp4";
23+
static const Duration kTestVideoDuration = Duration(seconds: 10);
24+
25+
static const int _kTextureId = 0xffffffff;
2026

2127
static StreamController<VideoEvent> _streamController = StreamController<VideoEvent>();
22-
static bool initialized = false;
23-
static bool isPlaying = false;
28+
static bool _initialized = false;
29+
static bool _hasError = false;
30+
static bool _isPlaying = false;
31+
static Duration _position = Duration.zero;
32+
static Timer? _timer;
33+
34+
static List<String> callLogs = [];
35+
36+
static bool get initialized => _initialized;
37+
static bool get hasError => _hasError;
38+
static bool get isPlaying => _isPlaying;
39+
static Duration get position => _position;
2440

2541
static void registerWith() {
2642
VideoPlayerPlatform.instance = FakeVideoPlayerPlatform();
2743
}
2844

45+
// A helper that needs to be called manually, to avoid tests complaining
46+
// about a leaking timer.
47+
//
48+
// TODO: This can be avoided by pushing the page in a navigation and popping
49+
// out before the end, to make sure the dispose method is called.
50+
static void cancelTimer() {
51+
_timer?.cancel();
52+
_timer = null;
53+
}
54+
2955
static void reset() {
56+
cancelTimer();
3057
_streamController.close();
3158
_streamController = StreamController<VideoEvent>();
32-
initialized = false;
33-
isPlaying = false;
59+
_initialized = false;
60+
_isPlaying = false;
61+
_position = Duration.zero;
62+
callLogs = [];
3463
}
3564

3665
@override
37-
Future<void> init() async {}
66+
Future<void> init() async {
67+
callLogs.add('init');
68+
}
3869

3970
@override
4071
Future<void> dispose(int textureId) async {
72+
callLogs.add('dispose');
73+
if (hasError) {
74+
assert(!initialized);
75+
assert(textureId == VideoPlayerController.kUninitializedTextureId);
76+
_initialized = false;
77+
_hasError = false;
78+
_timer?.cancel();
79+
_timer = null;
80+
return;
81+
}
82+
4183
assert(initialized);
42-
assert(textureId == _textureId);
43-
initialized = false;
84+
assert(textureId == _kTextureId);
85+
_initialized = false;
86+
_hasError = false;
87+
_timer?.cancel();
88+
_timer = null;
4489
}
4590

4691
@override
4792
Future<int?> create(DataSource dataSource) async {
93+
callLogs.add('create');
4894
assert(!initialized);
49-
initialized = true;
95+
if (dataSource.uri == kTestUnsupportedVideoUrl) {
96+
_hasError = true;
97+
_streamController.addError(PlatformException(code: "VideoError", message: "Failed to load video: Cannot Open"));
98+
return null;
99+
}
100+
101+
_initialized = true;
50102
_streamController.add(VideoEvent(
51103
eventType: VideoEventType.initialized,
52-
duration: const Duration(seconds: 1),
104+
duration: kTestVideoDuration,
53105
size: const Size(0, 0),
54106
rotationCorrection: 0,
55107
));
56-
return _textureId;
108+
return _kTextureId;
57109
}
58110

59111
@override
60112
Stream<VideoEvent> videoEventsFor(int textureId) {
61-
assert(textureId == _textureId);
113+
callLogs.add('videoEventsFor');
114+
assert(textureId == _kTextureId);
62115
return _streamController.stream;
63116
}
64117

65118
@override
66119
Future<void> setLooping(int textureId, bool looping) async {
67-
assert(textureId == _textureId);
120+
callLogs.add('setLooping');
121+
assert(textureId == _kTextureId);
68122
assert(!looping);
69123
}
70124

71125
@override
72126
Future<void> play(int textureId) async {
73-
assert(textureId == _textureId);
74-
isPlaying = true;
127+
callLogs.add('play');
128+
assert(textureId == _kTextureId);
129+
130+
_timer = Timer.periodic(const Duration(milliseconds: 10), (_) {
131+
if (!initialized) return;
132+
_position += const Duration(milliseconds: 10);
133+
if (kTestVideoDuration.compareTo(position) <= 0) {
134+
_position = kTestVideoDuration;
135+
_pause();
136+
}
137+
});
138+
139+
_isPlaying = true;
75140
_streamController.add(VideoEvent(
76141
eventType: VideoEventType.isPlayingStateUpdate,
77142
isPlaying: true,
78143
));
79144
}
80145

81-
@override
82-
Future<void> pause(int textureId) async {
83-
assert(textureId == _textureId);
84-
isPlaying = false;
146+
void _pause() {
147+
_timer?.cancel();
148+
_timer = null;
149+
150+
_isPlaying = false;
85151
_streamController.add(VideoEvent(
86152
eventType: VideoEventType.isPlayingStateUpdate,
87153
isPlaying: false,
88154
));
89155
}
90156

157+
@override
158+
Future<void> pause(int textureId) async {
159+
callLogs.add('pause');
160+
assert(textureId == _kTextureId);
161+
_pause();
162+
}
163+
91164
@override
92165
Future<void> setVolume(int textureId, double volume) async {
93-
assert(textureId == _textureId);
166+
callLogs.add('setVolume');
167+
assert(textureId == _kTextureId);
168+
}
169+
170+
@override
171+
Future<void> seekTo(int textureId, Duration position) async {
172+
callLogs.add('seekTo');
173+
assert(textureId == _kTextureId);
174+
_position = Duration(
175+
microseconds: math.min(position.inMicroseconds, kTestVideoDuration.inMicroseconds));
94176
}
95177

96178
@override
97179
Future<void> setPlaybackSpeed(int textureId, double speed) async {
98-
assert(textureId == _textureId);
180+
callLogs.add('seekTo');
181+
assert(textureId == _kTextureId);
182+
}
183+
184+
@override
185+
Future<Duration> getPosition(int textureId) async {
186+
callLogs.add('getPosition');
187+
assert(textureId == _kTextureId);
188+
return position;
99189
}
100190

101191
@override
102192
Widget buildView(int textureId) {
103-
assert(textureId == _textureId);
193+
callLogs.add('buildView');
194+
assert(textureId == _kTextureId);
104195
return const SizedBox(width: 100, height: 100);
105196
}
106197
}
@@ -109,6 +200,16 @@ void main() {
109200
TestZulipBinding.ensureInitialized();
110201

111202
group("VideoLightboxPage", () {
203+
void verifySliderPosition(WidgetTester tester, Duration duration) {
204+
final slider = tester.widget(find.byType(Slider)) as Slider;
205+
check(slider.value)
206+
.equals(duration.inMilliseconds.toDouble());
207+
final positionLabel = tester.widget(
208+
find.byKey(VideoPositionSliderControl.kCurrentPositionLabelKey)) as VideoDurationLabel;
209+
check(positionLabel.duration)
210+
.equals(duration);
211+
}
212+
112213
FakeVideoPlayerPlatform.registerWith();
113214

114215
Future<void> setupPage(WidgetTester tester, {
@@ -127,20 +228,24 @@ void main() {
127228
routeEntranceAnimation: kAlwaysCompleteAnimation,
128229
message: eg.streamMessage(),
129230
src: videoSrc)))));
130-
await tester.pumpAndSettle();
231+
await tester.pump(); // global store
232+
await tester.pump(); // per-account store
233+
await tester.pump(); // video controller initialization
131234
}
132235

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

136239
check(FakeVideoPlayerPlatform.initialized).isTrue();
137240
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
138241

139242
await tester.ensureVisible(find.byType(VideoPlayer));
243+
244+
FakeVideoPlayerPlatform.cancelTimer();
140245
});
141246

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

146251
await tester.tap(find.byIcon(Icons.pause_circle_rounded));
@@ -151,6 +256,141 @@ void main() {
151256

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

0 commit comments

Comments
 (0)