Skip to content

Commit bdf531f

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

File tree

1 file changed

+246
-27
lines changed

1 file changed

+246
-27
lines changed

test/widgets/lightbox_test.dart

Lines changed: 246 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+
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 bool _isPlaying = 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 => _isPlaying;
41+
42+
static Duration get position {
43+
_updatePosition();
44+
return _position;
45+
}
2446

2547
static void registerWith() {
2648
VideoPlayerPlatform.instance = FakeVideoPlayerPlatform();
@@ -29,49 +51,92 @@ 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+
_isPlaying = false;
57+
_position = Duration.zero;
58+
_lastSetPosition = Duration.zero;
59+
_stopwatch?.stop();
60+
_stopwatch?.reset();
61+
}
62+
63+
static void _pause() {
64+
_stopwatch?.stop();
65+
_isPlaying = false;
66+
_streamController.add(VideoEvent(
67+
eventType: VideoEventType.isPlayingStateUpdate,
68+
isPlaying: false,
69+
));
70+
}
71+
72+
static void _updatePosition() {
73+
assert(_stopwatch != null);
74+
_position = _stopwatch!.elapsed + _lastSetPosition;
75+
if (kTestVideoDuration.compareTo(_position) <= 0) {
76+
_position = kTestVideoDuration;
77+
_pause();
78+
}
3479
}
3580

3681
@override
37-
Future<void> init() async {}
82+
Future<void> init() async {
83+
_callLogs.add('init');
84+
}
3885

3986
@override
4087
Future<void> dispose(int textureId) async {
88+
_callLogs.add('dispose');
89+
if (hasError) {
90+
assert(!initialized);
91+
assert(textureId == VideoPlayerController.kUninitializedTextureId);
92+
return;
93+
}
94+
4195
assert(initialized);
42-
assert(textureId == _textureId);
43-
initialized = false;
96+
assert(textureId == _kTextureId);
4497
}
4598

4699
@override
47100
Future<int?> create(DataSource dataSource) async {
101+
_callLogs.add('create');
48102
assert(!initialized);
49-
initialized = true;
103+
if (dataSource.uri == kTestUnsupportedVideoUrl) {
104+
_hasError = true;
105+
_streamController.addError(PlatformException(code: "VideoError", message: "Failed to load video: Cannot Open"));
106+
return null;
107+
}
108+
109+
_stopwatch = clock.stopwatch();
110+
_initialized = true;
50111
_streamController.add(VideoEvent(
51112
eventType: VideoEventType.initialized,
52-
duration: const Duration(seconds: 1),
113+
duration: kTestVideoDuration,
53114
size: const Size(0, 0),
54115
rotationCorrection: 0,
55116
));
56-
return _textureId;
117+
return _kTextureId;
57118
}
58119

59120
@override
60121
Stream<VideoEvent> videoEventsFor(int textureId) {
61-
assert(textureId == _textureId);
122+
_callLogs.add('videoEventsFor');
123+
assert(textureId == _kTextureId);
62124
return _streamController.stream;
63125
}
64126

65127
@override
66128
Future<void> setLooping(int textureId, bool looping) async {
67-
assert(textureId == _textureId);
129+
_callLogs.add('setLooping');
130+
assert(textureId == _kTextureId);
68131
assert(!looping);
69132
}
70133

71134
@override
72135
Future<void> play(int textureId) async {
73-
assert(textureId == _textureId);
74-
isPlaying = true;
136+
_callLogs.add('play');
137+
assert(textureId == _kTextureId);
138+
_stopwatch?.start();
139+
_isPlaying = true;
75140
_streamController.add(VideoEvent(
76141
eventType: VideoEventType.isPlayingStateUpdate,
77142
isPlaying: true,
@@ -80,27 +145,44 @@ class FakeVideoPlayerPlatform extends Fake
80145

81146
@override
82147
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-
));
148+
_callLogs.add('pause');
149+
assert(textureId == _kTextureId);
150+
_pause();
89151
}
90152

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

96169
@override
97170
Future<void> setPlaybackSpeed(int textureId, double speed) async {
98-
assert(textureId == _textureId);
171+
_callLogs.add('setPlaybackSpeed');
172+
assert(textureId == _kTextureId);
173+
}
174+
175+
@override
176+
Future<Duration> getPosition(int textureId) async {
177+
_callLogs.add('getPosition');
178+
assert(textureId == _kTextureId);
179+
return position;
99180
}
100181

101182
@override
102183
Widget buildView(int textureId) {
103-
assert(textureId == _textureId);
184+
_callLogs.add('buildView');
185+
assert(textureId == _kTextureId);
104186
return const SizedBox(width: 100, height: 100);
105187
}
106188
}
@@ -109,6 +191,16 @@ void main() {
109191
TestZulipBinding.ensureInitialized();
110192

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

114206
Future<void> setupPage(WidgetTester tester, {
@@ -127,11 +219,13 @@ void main() {
127219
routeEntranceAnimation: kAlwaysCompleteAnimation,
128220
message: eg.streamMessage(),
129221
src: videoSrc)))));
130-
await tester.pumpAndSettle();
222+
await tester.pump(); // global store
223+
await tester.pump(); // per-account store
224+
await tester.pump(); // video controller initialization
131225
}
132226

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

136230
check(FakeVideoPlayerPlatform.initialized).isTrue();
137231
check(FakeVideoPlayerPlatform.isPlaying).isTrue();
@@ -140,7 +234,7 @@ void main() {
140234
});
141235

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

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

0 commit comments

Comments
 (0)