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
+ // 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 );
20
26
27
+ static const int _kTextureId = 0xffffffff ;
28
+
29
+ static final List <String > _callLogs = [];
21
30
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
+ }
24
46
25
47
static void registerWith () {
26
48
VideoPlayerPlatform .instance = FakeVideoPlayerPlatform ();
@@ -29,49 +51,90 @@ 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
+ _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
+ }
34
78
}
35
79
36
80
@override
37
- Future <void > init () async {}
81
+ Future <void > init () async {
82
+ _callLogs.add ('init' );
83
+ }
38
84
39
85
@override
40
86
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
+
41
94
assert (initialized);
42
- assert (textureId == _textureId);
43
- initialized = false ;
95
+ assert (textureId == _kTextureId);
44
96
}
45
97
46
98
@override
47
99
Future <int ?> create (DataSource dataSource) async {
100
+ _callLogs.add ('create' );
48
101
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 ;
50
110
_streamController.add (VideoEvent (
51
111
eventType: VideoEventType .initialized,
52
- duration: const Duration (seconds : 1 ) ,
112
+ duration: kTestVideoDuration ,
53
113
size: const Size (0 , 0 ),
54
114
rotationCorrection: 0 ,
55
115
));
56
- return _textureId ;
116
+ return _kTextureId ;
57
117
}
58
118
59
119
@override
60
120
Stream <VideoEvent > videoEventsFor (int textureId) {
61
- assert (textureId == _textureId);
121
+ _callLogs.add ('videoEventsFor' );
122
+ assert (textureId == _kTextureId);
62
123
return _streamController.stream;
63
124
}
64
125
65
126
@override
66
127
Future <void > setLooping (int textureId, bool looping) async {
67
- assert (textureId == _textureId);
128
+ _callLogs.add ('setLooping' );
129
+ assert (textureId == _kTextureId);
68
130
assert (! looping);
69
131
}
70
132
71
133
@override
72
134
Future <void > play (int textureId) async {
73
- assert (textureId == _textureId);
74
- isPlaying = true ;
135
+ _callLogs.add ('play' );
136
+ assert (textureId == _kTextureId);
137
+ _stopwatch? .start ();
75
138
_streamController.add (VideoEvent (
76
139
eventType: VideoEventType .isPlayingStateUpdate,
77
140
isPlaying: true ,
@@ -80,27 +143,44 @@ class FakeVideoPlayerPlatform extends Fake
80
143
81
144
@override
82
145
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 ();
89
149
}
90
150
91
151
@override
92
152
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 ();
94
165
}
95
166
96
167
@override
97
168
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;
99
178
}
100
179
101
180
@override
102
181
Widget buildView (int textureId) {
103
- assert (textureId == _textureId);
182
+ _callLogs.add ('buildView' );
183
+ assert (textureId == _kTextureId);
104
184
return const SizedBox (width: 100 , height: 100 );
105
185
}
106
186
}
@@ -109,6 +189,16 @@ void main() {
109
189
TestZulipBinding .ensureInitialized ();
110
190
111
191
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
+
112
202
FakeVideoPlayerPlatform .registerWith ();
113
203
114
204
Future <void > setupPage (WidgetTester tester, {
@@ -127,11 +217,13 @@ void main() {
127
217
routeEntranceAnimation: kAlwaysCompleteAnimation,
128
218
message: eg.streamMessage (),
129
219
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
131
223
}
132
224
133
225
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 ));
135
227
136
228
check (FakeVideoPlayerPlatform .initialized).isTrue ();
137
229
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
@@ -140,7 +232,7 @@ void main() {
140
232
});
141
233
142
234
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 ));
144
236
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
145
237
146
238
await tester.tap (find.byIcon (Icons .pause_circle_rounded));
@@ -152,5 +244,130 @@ void main() {
152
244
await tester.tap (find.byIcon (Icons .play_circle_rounded));
153
245
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
154
246
});
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
+ });
155
372
});
156
373
}
0 commit comments