1
1
import 'dart:async' ;
2
+ import 'dart:math' as math;
2
3
3
4
import 'package:checks/checks.dart' ;
5
+ import 'package:clock/clock.dart' ;
6
+ import 'package:flutter/services.dart' ;
4
7
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart' ;
5
8
import 'package:flutter_test/flutter_test.dart' ;
6
9
import 'package:flutter/material.dart' ;
@@ -16,11 +19,30 @@ import '../model/binding.dart';
16
19
class FakeVideoPlayerPlatform extends Fake
17
20
with MockPlatformInterfaceMixin
18
21
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 );
20
25
26
+ static const int _kTextureId = 0xffffffff ;
27
+
28
+ static final List <String > _callLogs = [];
21
29
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
+ }
24
46
25
47
static void registerWith () {
26
48
VideoPlayerPlatform .instance = FakeVideoPlayerPlatform ();
@@ -29,49 +51,92 @@ class FakeVideoPlayerPlatform extends Fake
29
51
static void reset () {
30
52
_streamController.close ();
31
53
_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
+ }
34
79
}
35
80
36
81
@override
37
- Future <void > init () async {}
82
+ Future <void > init () async {
83
+ _callLogs.add ('init' );
84
+ }
38
85
39
86
@override
40
87
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
+
41
95
assert (initialized);
42
- assert (textureId == _textureId);
43
- initialized = false ;
96
+ assert (textureId == _kTextureId);
44
97
}
45
98
46
99
@override
47
100
Future <int ?> create (DataSource dataSource) async {
101
+ _callLogs.add ('create' );
48
102
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 ;
50
111
_streamController.add (VideoEvent (
51
112
eventType: VideoEventType .initialized,
52
- duration: const Duration (seconds : 1 ) ,
113
+ duration: kTestVideoDuration ,
53
114
size: const Size (0 , 0 ),
54
115
rotationCorrection: 0 ,
55
116
));
56
- return _textureId ;
117
+ return _kTextureId ;
57
118
}
58
119
59
120
@override
60
121
Stream <VideoEvent > videoEventsFor (int textureId) {
61
- assert (textureId == _textureId);
122
+ _callLogs.add ('videoEventsFor' );
123
+ assert (textureId == _kTextureId);
62
124
return _streamController.stream;
63
125
}
64
126
65
127
@override
66
128
Future <void > setLooping (int textureId, bool looping) async {
67
- assert (textureId == _textureId);
129
+ _callLogs.add ('setLooping' );
130
+ assert (textureId == _kTextureId);
68
131
assert (! looping);
69
132
}
70
133
71
134
@override
72
135
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 ;
75
140
_streamController.add (VideoEvent (
76
141
eventType: VideoEventType .isPlayingStateUpdate,
77
142
isPlaying: true ,
@@ -80,27 +145,44 @@ class FakeVideoPlayerPlatform extends Fake
80
145
81
146
@override
82
147
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 ();
89
151
}
90
152
91
153
@override
92
154
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 ();
94
167
}
95
168
96
169
@override
97
170
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;
99
180
}
100
181
101
182
@override
102
183
Widget buildView (int textureId) {
103
- assert (textureId == _textureId);
184
+ _callLogs.add ('buildView' );
185
+ assert (textureId == _kTextureId);
104
186
return const SizedBox (width: 100 , height: 100 );
105
187
}
106
188
}
@@ -109,6 +191,16 @@ void main() {
109
191
TestZulipBinding .ensureInitialized ();
110
192
111
193
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
+
112
204
FakeVideoPlayerPlatform .registerWith ();
113
205
114
206
Future <void > setupPage (WidgetTester tester, {
@@ -127,11 +219,13 @@ void main() {
127
219
routeEntranceAnimation: kAlwaysCompleteAnimation,
128
220
message: eg.streamMessage (),
129
221
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
131
225
}
132
226
133
227
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 ));
135
229
136
230
check (FakeVideoPlayerPlatform .initialized).isTrue ();
137
231
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
@@ -140,7 +234,7 @@ void main() {
140
234
});
141
235
142
236
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 ));
144
238
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
145
239
146
240
await tester.tap (find.byIcon (Icons .pause_circle_rounded));
@@ -152,5 +246,130 @@ void main() {
152
246
await tester.tap (find.byIcon (Icons .play_circle_rounded));
153
247
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
154
248
});
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
+ });
155
374
});
156
375
}
0 commit comments