1
1
import 'dart:async' ;
2
+ import 'dart:math' as math;
2
3
3
4
import 'package:checks/checks.dart' ;
5
+ import 'package:flutter/services.dart' ;
4
6
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart' ;
5
7
import 'package:flutter_test/flutter_test.dart' ;
6
8
import 'package:flutter/material.dart' ;
@@ -16,91 +18,180 @@ import '../model/binding.dart';
16
18
class FakeVideoPlayerPlatform extends Fake
17
19
with MockPlatformInterfaceMixin
18
20
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 ;
20
26
21
27
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;
24
40
25
41
static void registerWith () {
26
42
VideoPlayerPlatform .instance = FakeVideoPlayerPlatform ();
27
43
}
28
44
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
+
29
55
static void reset () {
56
+ cancelTimer ();
30
57
_streamController.close ();
31
58
_streamController = StreamController <VideoEvent >();
32
- initialized = false ;
33
- isPlaying = false ;
59
+ _initialized = false ;
60
+ _isPlaying = false ;
61
+ _position = Duration .zero;
62
+ callLogs = [];
34
63
}
35
64
36
65
@override
37
- Future <void > init () async {}
66
+ Future <void > init () async {
67
+ callLogs.add ('init' );
68
+ }
38
69
39
70
@override
40
71
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
+
41
83
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 ;
44
89
}
45
90
46
91
@override
47
92
Future <int ?> create (DataSource dataSource) async {
93
+ callLogs.add ('create' );
48
94
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 ;
50
102
_streamController.add (VideoEvent (
51
103
eventType: VideoEventType .initialized,
52
- duration: const Duration (seconds : 1 ) ,
104
+ duration: kTestVideoDuration ,
53
105
size: const Size (0 , 0 ),
54
106
rotationCorrection: 0 ,
55
107
));
56
- return _textureId ;
108
+ return _kTextureId ;
57
109
}
58
110
59
111
@override
60
112
Stream <VideoEvent > videoEventsFor (int textureId) {
61
- assert (textureId == _textureId);
113
+ callLogs.add ('videoEventsFor' );
114
+ assert (textureId == _kTextureId);
62
115
return _streamController.stream;
63
116
}
64
117
65
118
@override
66
119
Future <void > setLooping (int textureId, bool looping) async {
67
- assert (textureId == _textureId);
120
+ callLogs.add ('setLooping' );
121
+ assert (textureId == _kTextureId);
68
122
assert (! looping);
69
123
}
70
124
71
125
@override
72
126
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 ;
75
140
_streamController.add (VideoEvent (
76
141
eventType: VideoEventType .isPlayingStateUpdate,
77
142
isPlaying: true ,
78
143
));
79
144
}
80
145
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 ;
85
151
_streamController.add (VideoEvent (
86
152
eventType: VideoEventType .isPlayingStateUpdate,
87
153
isPlaying: false ,
88
154
));
89
155
}
90
156
157
+ @override
158
+ Future <void > pause (int textureId) async {
159
+ callLogs.add ('pause' );
160
+ assert (textureId == _kTextureId);
161
+ _pause ();
162
+ }
163
+
91
164
@override
92
165
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));
94
176
}
95
177
96
178
@override
97
179
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;
99
189
}
100
190
101
191
@override
102
192
Widget buildView (int textureId) {
103
- assert (textureId == _textureId);
193
+ callLogs.add ('buildView' );
194
+ assert (textureId == _kTextureId);
104
195
return const SizedBox (width: 100 , height: 100 );
105
196
}
106
197
}
@@ -109,6 +200,16 @@ void main() {
109
200
TestZulipBinding .ensureInitialized ();
110
201
111
202
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
+
112
213
FakeVideoPlayerPlatform .registerWith ();
113
214
114
215
Future <void > setupPage (WidgetTester tester, {
@@ -127,20 +228,24 @@ void main() {
127
228
routeEntranceAnimation: kAlwaysCompleteAnimation,
128
229
message: eg.streamMessage (),
129
230
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
131
234
}
132
235
133
236
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 ));
135
238
136
239
check (FakeVideoPlayerPlatform .initialized).isTrue ();
137
240
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
138
241
139
242
await tester.ensureVisible (find.byType (VideoPlayer ));
243
+
244
+ FakeVideoPlayerPlatform .cancelTimer ();
140
245
});
141
246
142
247
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 ));
144
249
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
145
250
146
251
await tester.tap (find.byIcon (Icons .pause_circle_rounded));
@@ -151,6 +256,141 @@ void main() {
151
256
152
257
await tester.tap (find.byIcon (Icons .play_circle_rounded));
153
258
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 ();
154
394
});
155
395
});
156
396
}
0 commit comments