Skip to content

Commit 089bba3

Browse files
lightbox test: Add video player regression tests
Introduce tests for each of the cases mentioned here: #587 (comment)
1 parent a22c6c9 commit 089bba3

File tree

1 file changed

+244
-27
lines changed

1 file changed

+244
-27
lines changed

test/widgets/lightbox_test.dart

Lines changed: 244 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,30 @@ 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+
// URL for testing the behavior of an unsupported video format on the device.
24+
static const String kTestUnsupportedVideoUrl = "https://a/unsupported.mp4";
25+
static const Duration kTestVideoDuration = Duration(seconds: 10);
2026

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

2547
static void registerWith() {
2648
VideoPlayerPlatform.instance = FakeVideoPlayerPlatform();
@@ -29,49 +51,90 @@ class FakeVideoPlayerPlatform extends Fake
2951
static void reset() {
3052
_streamController.close();
3153
_streamController = StreamController<VideoEvent>();
32-
initialized = false;
33-
isPlaying = false;
54+
_callLogs.clear();
55+
_initialized = false;
56+
_position = Duration.zero;
57+
_lastSetPosition = Duration.zero;
58+
_stopwatch?.stop();
59+
_stopwatch?.reset();
60+
}
61+
62+
static void _pause() {
63+
_stopwatch?.stop();
64+
_streamController.add(VideoEvent(
65+
eventType: VideoEventType.isPlayingStateUpdate,
66+
isPlaying: false,
67+
));
68+
}
69+
70+
static void _updatePosition() {
71+
assert(_stopwatch != null);
72+
_position = _stopwatch!.elapsed + _lastSetPosition;
73+
if (kTestVideoDuration.compareTo(_position) <= 0) {
74+
// Video player reached the end of the video
75+
_position = kTestVideoDuration;
76+
_pause();
77+
}
3478
}
3579

3680
@override
37-
Future<void> init() async {}
81+
Future<void> init() async {
82+
_callLogs.add('init');
83+
}
3884

3985
@override
4086
Future<void> dispose(int textureId) async {
87+
_callLogs.add('dispose');
88+
if (hasError) {
89+
assert(!initialized);
90+
assert(textureId == VideoPlayerController.kUninitializedTextureId);
91+
return;
92+
}
93+
4194
assert(initialized);
42-
assert(textureId == _textureId);
43-
initialized = false;
95+
assert(textureId == _kTextureId);
4496
}
4597

4698
@override
4799
Future<int?> create(DataSource dataSource) async {
100+
_callLogs.add('create');
48101
assert(!initialized);
49-
initialized = true;
102+
if (dataSource.uri == kTestUnsupportedVideoUrl) {
103+
_hasError = true;
104+
_streamController.addError(PlatformException(code: "VideoError", message: "Failed to load video: Cannot Open"));
105+
return null;
106+
}
107+
108+
_stopwatch = clock.stopwatch();
109+
_initialized = true;
50110
_streamController.add(VideoEvent(
51111
eventType: VideoEventType.initialized,
52-
duration: const Duration(seconds: 1),
112+
duration: kTestVideoDuration,
53113
size: const Size(0, 0),
54114
rotationCorrection: 0,
55115
));
56-
return _textureId;
116+
return _kTextureId;
57117
}
58118

59119
@override
60120
Stream<VideoEvent> videoEventsFor(int textureId) {
61-
assert(textureId == _textureId);
121+
_callLogs.add('videoEventsFor');
122+
assert(textureId == _kTextureId);
62123
return _streamController.stream;
63124
}
64125

65126
@override
66127
Future<void> setLooping(int textureId, bool looping) async {
67-
assert(textureId == _textureId);
128+
_callLogs.add('setLooping');
129+
assert(textureId == _kTextureId);
68130
assert(!looping);
69131
}
70132

71133
@override
72134
Future<void> play(int textureId) async {
73-
assert(textureId == _textureId);
74-
isPlaying = true;
135+
_callLogs.add('play');
136+
assert(textureId == _kTextureId);
137+
_stopwatch?.start();
75138
_streamController.add(VideoEvent(
76139
eventType: VideoEventType.isPlayingStateUpdate,
77140
isPlaying: true,
@@ -80,27 +143,44 @@ class FakeVideoPlayerPlatform extends Fake
80143

81144
@override
82145
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-
));
146+
_callLogs.add('pause');
147+
assert(textureId == _kTextureId);
148+
_pause();
89149
}
90150

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

96167
@override
97168
Future<void> setPlaybackSpeed(int textureId, double speed) async {
98-
assert(textureId == _textureId);
169+
_callLogs.add('setPlaybackSpeed');
170+
assert(textureId == _kTextureId);
171+
}
172+
173+
@override
174+
Future<Duration> getPosition(int textureId) async {
175+
_callLogs.add('getPosition');
176+
assert(textureId == _kTextureId);
177+
return position;
99178
}
100179

101180
@override
102181
Widget buildView(int textureId) {
103-
assert(textureId == _textureId);
182+
_callLogs.add('buildView');
183+
assert(textureId == _kTextureId);
104184
return const SizedBox(width: 100, height: 100);
105185
}
106186
}
@@ -109,6 +189,16 @@ void main() {
109189
TestZulipBinding.ensureInitialized();
110190

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

114204
Future<void> setupPage(WidgetTester tester, {
@@ -127,11 +217,13 @@ void main() {
127217
routeEntranceAnimation: kAlwaysCompleteAnimation,
128218
message: eg.streamMessage(),
129219
src: videoSrc)))));
130-
await tester.pumpAndSettle();
220+
await tester.pump(); // global store
221+
await tester.pump(); // per-account store
222+
await tester.pump(); // video controller initialization
131223
}
132224

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

136228
check(FakeVideoPlayerPlatform.initialized).isTrue();
137229
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
@@ -140,7 +232,7 @@ void main() {
140232
});
141233

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

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

0 commit comments

Comments
 (0)