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,29 @@ 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 Duration _position = Duration .zero;
33
+ static Duration _lastSetPosition = Duration .zero;
34
+ static Stopwatch ? _stopwatch;
35
+
36
+ static List <String > get callLogs => _callLogs;
37
+ static bool get initialized => _initialized;
38
+ static bool get hasError => _hasError;
39
+ static bool get isPlaying => _stopwatch? .isRunning ?? false ;
40
+
41
+ static Duration get position {
42
+ _updatePosition ();
43
+ return _position;
44
+ }
24
45
25
46
static void registerWith () {
26
47
VideoPlayerPlatform .instance = FakeVideoPlayerPlatform ();
@@ -29,49 +50,89 @@ class FakeVideoPlayerPlatform extends Fake
29
50
static void reset () {
30
51
_streamController.close ();
31
52
_streamController = StreamController <VideoEvent >();
32
- initialized = false ;
33
- isPlaying = false ;
53
+ _callLogs.clear ();
54
+ _initialized = false ;
55
+ _position = Duration .zero;
56
+ _lastSetPosition = Duration .zero;
57
+ _stopwatch? .stop ();
58
+ _stopwatch? .reset ();
59
+ }
60
+
61
+ static void _pause () {
62
+ _stopwatch? .stop ();
63
+ _streamController.add (VideoEvent (
64
+ eventType: VideoEventType .isPlayingStateUpdate,
65
+ isPlaying: false ,
66
+ ));
67
+ }
68
+
69
+ static void _updatePosition () {
70
+ assert (_stopwatch != null );
71
+ _position = _stopwatch! .elapsed + _lastSetPosition;
72
+ if (kTestVideoDuration.compareTo (_position) <= 0 ) {
73
+ _position = kTestVideoDuration;
74
+ _pause ();
75
+ }
34
76
}
35
77
36
78
@override
37
- Future <void > init () async {}
79
+ Future <void > init () async {
80
+ _callLogs.add ('init' );
81
+ }
38
82
39
83
@override
40
84
Future <void > dispose (int textureId) async {
85
+ _callLogs.add ('dispose' );
86
+ if (hasError) {
87
+ assert (! initialized);
88
+ assert (textureId == VideoPlayerController .kUninitializedTextureId);
89
+ return ;
90
+ }
91
+
41
92
assert (initialized);
42
- assert (textureId == _textureId);
43
- initialized = false ;
93
+ assert (textureId == _kTextureId);
44
94
}
45
95
46
96
@override
47
97
Future <int ?> create (DataSource dataSource) async {
98
+ _callLogs.add ('create' );
48
99
assert (! initialized);
49
- initialized = true ;
100
+ if (dataSource.uri == kTestUnsupportedVideoUrl) {
101
+ _hasError = true ;
102
+ _streamController.addError (PlatformException (code: "VideoError" , message: "Failed to load video: Cannot Open" ));
103
+ return null ;
104
+ }
105
+
106
+ _stopwatch = clock.stopwatch ();
107
+ _initialized = true ;
50
108
_streamController.add (VideoEvent (
51
109
eventType: VideoEventType .initialized,
52
- duration: const Duration (seconds : 1 ) ,
110
+ duration: kTestVideoDuration ,
53
111
size: const Size (0 , 0 ),
54
112
rotationCorrection: 0 ,
55
113
));
56
- return _textureId ;
114
+ return _kTextureId ;
57
115
}
58
116
59
117
@override
60
118
Stream <VideoEvent > videoEventsFor (int textureId) {
61
- assert (textureId == _textureId);
119
+ _callLogs.add ('videoEventsFor' );
120
+ assert (textureId == _kTextureId);
62
121
return _streamController.stream;
63
122
}
64
123
65
124
@override
66
125
Future <void > setLooping (int textureId, bool looping) async {
67
- assert (textureId == _textureId);
126
+ _callLogs.add ('setLooping' );
127
+ assert (textureId == _kTextureId);
68
128
assert (! looping);
69
129
}
70
130
71
131
@override
72
132
Future <void > play (int textureId) async {
73
- assert (textureId == _textureId);
74
- isPlaying = true ;
133
+ _callLogs.add ('play' );
134
+ assert (textureId == _kTextureId);
135
+ _stopwatch? .start ();
75
136
_streamController.add (VideoEvent (
76
137
eventType: VideoEventType .isPlayingStateUpdate,
77
138
isPlaying: true ,
@@ -80,27 +141,44 @@ class FakeVideoPlayerPlatform extends Fake
80
141
81
142
@override
82
143
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
- ));
144
+ _callLogs.add ('pause' );
145
+ assert (textureId == _kTextureId);
146
+ _pause ();
89
147
}
90
148
91
149
@override
92
150
Future <void > setVolume (int textureId, double volume) async {
93
- assert (textureId == _textureId);
151
+ _callLogs.add ('setVolume' );
152
+ assert (textureId == _kTextureId);
153
+ }
154
+
155
+ @override
156
+ Future <void > seekTo (int textureId, Duration pos) async {
157
+ _callLogs.add ('seekTo' );
158
+ assert (textureId == _kTextureId);
159
+
160
+ _lastSetPosition = Duration (
161
+ microseconds: math.min (pos.inMicroseconds, kTestVideoDuration.inMicroseconds));
162
+ _stopwatch? .reset ();
94
163
}
95
164
96
165
@override
97
166
Future <void > setPlaybackSpeed (int textureId, double speed) async {
98
- assert (textureId == _textureId);
167
+ _callLogs.add ('setPlaybackSpeed' );
168
+ assert (textureId == _kTextureId);
169
+ }
170
+
171
+ @override
172
+ Future <Duration > getPosition (int textureId) async {
173
+ _callLogs.add ('getPosition' );
174
+ assert (textureId == _kTextureId);
175
+ return position;
99
176
}
100
177
101
178
@override
102
179
Widget buildView (int textureId) {
103
- assert (textureId == _textureId);
180
+ _callLogs.add ('buildView' );
181
+ assert (textureId == _kTextureId);
104
182
return const SizedBox (width: 100 , height: 100 );
105
183
}
106
184
}
@@ -109,6 +187,16 @@ void main() {
109
187
TestZulipBinding .ensureInitialized ();
110
188
111
189
group ("VideoLightboxPage" , () {
190
+ void verifySliderPosition (WidgetTester tester, Duration duration) {
191
+ final slider = tester.widget (find.byType (Slider )) as Slider ;
192
+ check (slider.value)
193
+ .equals (duration.inMilliseconds.toDouble ());
194
+ final positionLabel = tester.widget (
195
+ find.byKey (VideoPositionSliderControl .kCurrentPositionLabelKey)) as VideoDurationLabel ;
196
+ check (positionLabel.duration)
197
+ .equals (duration);
198
+ }
199
+
112
200
FakeVideoPlayerPlatform .registerWith ();
113
201
114
202
Future <void > setupPage (WidgetTester tester, {
@@ -127,11 +215,13 @@ void main() {
127
215
routeEntranceAnimation: kAlwaysCompleteAnimation,
128
216
message: eg.streamMessage (),
129
217
src: videoSrc)))));
130
- await tester.pumpAndSettle ();
218
+ await tester.pump (); // global store
219
+ await tester.pump (); // per-account store
220
+ await tester.pump (); // video controller initialization
131
221
}
132
222
133
223
testWidgets ('shows a VideoPlayer, and video is playing' , (tester) async {
134
- await setupPage (tester, videoSrc: Uri .parse ('https://a/b.mp4' ));
224
+ await setupPage (tester, videoSrc: Uri .parse (FakeVideoPlayerPlatform .kTestVideoUrl ));
135
225
136
226
check (FakeVideoPlayerPlatform .initialized).isTrue ();
137
227
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
@@ -140,7 +230,7 @@ void main() {
140
230
});
141
231
142
232
testWidgets ('toggles between play and pause' , (tester) async {
143
- await setupPage (tester, videoSrc: Uri .parse ('https://a/b.mp4' ));
233
+ await setupPage (tester, videoSrc: Uri .parse (FakeVideoPlayerPlatform .kTestVideoUrl ));
144
234
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
145
235
146
236
await tester.tap (find.byIcon (Icons .pause_circle_rounded));
@@ -152,5 +242,130 @@ void main() {
152
242
await tester.tap (find.byIcon (Icons .play_circle_rounded));
153
243
check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
154
244
});
245
+
246
+ testWidgets ('unsupported video shows an error dialog' , (tester) async {
247
+ await setupPage (tester, videoSrc: Uri .parse (FakeVideoPlayerPlatform .kTestUnsupportedVideoUrl));
248
+ await tester.ensureVisible (find.text ("Unable to play the video" ));
249
+ });
250
+
251
+ testWidgets ('video advances overtime & stops playing when it ends' , (tester) async {
252
+ await setupPage (tester, videoSrc: Uri .parse (FakeVideoPlayerPlatform .kTestVideoUrl));
253
+ check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
254
+ check (FakeVideoPlayerPlatform .position).equals (Duration .zero);
255
+
256
+ final halfTime = FakeVideoPlayerPlatform .kTestVideoDuration * 0.5 ;
257
+
258
+ await tester.pump (halfTime);
259
+ verifySliderPosition (tester, halfTime);
260
+ check (FakeVideoPlayerPlatform .position).equals (halfTime);
261
+ check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
262
+
263
+ // Near the end of the video.
264
+ await tester.pump (halfTime - const Duration (milliseconds: 500 ));
265
+ verifySliderPosition (
266
+ tester, FakeVideoPlayerPlatform .kTestVideoDuration - const Duration (milliseconds: 500 ));
267
+ check (FakeVideoPlayerPlatform .position)
268
+ .equals (FakeVideoPlayerPlatform .kTestVideoDuration - const Duration (milliseconds: 500 ));
269
+ check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
270
+
271
+ // At exactly the end of the video.
272
+ await tester.pump (const Duration (milliseconds: 500 ));
273
+ verifySliderPosition (tester, FakeVideoPlayerPlatform .kTestVideoDuration);
274
+ check (FakeVideoPlayerPlatform .position).equals (FakeVideoPlayerPlatform .kTestVideoDuration);
275
+ check (FakeVideoPlayerPlatform .isPlaying).isFalse ();
276
+
277
+ // After the video ended.
278
+ await tester.pump (const Duration (milliseconds: 500 ));
279
+ verifySliderPosition (tester, FakeVideoPlayerPlatform .kTestVideoDuration);
280
+ check (FakeVideoPlayerPlatform .position).equals (FakeVideoPlayerPlatform .kTestVideoDuration);
281
+ check (FakeVideoPlayerPlatform .isPlaying).isFalse ();
282
+ });
283
+
284
+ testWidgets ('ensure \' seekTo\' is called only once' , (tester) async {
285
+ await setupPage (tester, videoSrc: Uri .parse (FakeVideoPlayerPlatform .kTestVideoUrl));
286
+ check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
287
+ check (FakeVideoPlayerPlatform .position).equals (Duration .zero);
288
+
289
+ const padding = 24.0 ;
290
+ final rect = tester.getRect (find.byType (Slider ));
291
+ final trackWidth = rect.width - padding - padding;
292
+ final trackStartPos = rect.centerLeft + const Offset (padding, 0 );
293
+ final twentyPercent = trackWidth * 0.2 ; // 20% increments
294
+
295
+ // Verify the actually displayed current position at each
296
+ // gesture increments.
297
+ final gesture = await tester.startGesture (trackStartPos);
298
+ await tester.pump ();
299
+ verifySliderPosition (tester, Duration .zero);
300
+ check (FakeVideoPlayerPlatform .position).equals (Duration .zero);
301
+
302
+ await gesture.moveBy (Offset (twentyPercent, 0.0 ));
303
+ await tester.pump ();
304
+ verifySliderPosition (tester, FakeVideoPlayerPlatform .kTestVideoDuration * 0.2 );
305
+ check (FakeVideoPlayerPlatform .position).equals (Duration .zero);
306
+
307
+ await gesture.moveBy (Offset (twentyPercent, 0.0 ));
308
+ await tester.pump ();
309
+ verifySliderPosition (tester, FakeVideoPlayerPlatform .kTestVideoDuration * 0.4 );
310
+ check (FakeVideoPlayerPlatform .position).equals (Duration .zero);
311
+
312
+ await gesture.moveBy (Offset (twentyPercent, 0.0 ));
313
+ await tester.pump ();
314
+ verifySliderPosition (tester, FakeVideoPlayerPlatform .kTestVideoDuration * 0.6 );
315
+ check (FakeVideoPlayerPlatform .position).equals (Duration .zero);
316
+
317
+ await gesture.up ();
318
+ await tester.pump ();
319
+ verifySliderPosition (tester, FakeVideoPlayerPlatform .kTestVideoDuration * 0.6 );
320
+ check (FakeVideoPlayerPlatform .position).equals (FakeVideoPlayerPlatform .kTestVideoDuration * 0.6 );
321
+
322
+ // Verify seekTo is called only once.
323
+ check (FakeVideoPlayerPlatform .callLogs.where ((v) => v == 'seekTo' ).length).equals (1 );
324
+ });
325
+
326
+ testWidgets ('video advances overtime after dragging the slider' , (tester) async {
327
+ await setupPage (tester, videoSrc: Uri .parse (FakeVideoPlayerPlatform .kTestVideoUrl));
328
+ check (FakeVideoPlayerPlatform .isPlaying).isTrue ();
329
+ check (FakeVideoPlayerPlatform .position).equals (Duration .zero);
330
+
331
+ const padding = 24.0 ;
332
+ final rect = tester.getRect (find.byType (Slider ));
333
+ final trackWidth = rect.width - padding - padding;
334
+ final trackStartPos = rect.centerLeft + const Offset (padding, 0 );
335
+ final fiftyPercent = trackWidth * 0.5 ;
336
+ final halfTime = FakeVideoPlayerPlatform .kTestVideoDuration * 0.5 ;
337
+
338
+ final gesture = await tester.startGesture (trackStartPos);
339
+ await tester.pump ();
340
+ verifySliderPosition (tester, Duration .zero);
341
+ check (FakeVideoPlayerPlatform .position).equals (Duration .zero);
342
+
343
+ await gesture.moveBy (Offset (fiftyPercent, 0 ));
344
+ await tester.pump ();
345
+ verifySliderPosition (tester, halfTime);
346
+ check (FakeVideoPlayerPlatform .position).equals (Duration .zero);
347
+
348
+ await gesture.up ();
349
+ await tester.pump ();
350
+ verifySliderPosition (tester, halfTime);
351
+
352
+ // Verify that after dragging ends, video position is at the
353
+ // halfway point, and after that it starts advancing as the time
354
+ // passes.
355
+ check (FakeVideoPlayerPlatform .position).equals (halfTime);
356
+
357
+ const waitTime = Duration (seconds: 1 );
358
+ await tester.pump (waitTime);
359
+ verifySliderPosition (tester, halfTime + (waitTime * 1 ));
360
+ check (FakeVideoPlayerPlatform .position).equals (halfTime + (waitTime * 1 ));
361
+
362
+ await tester.pump (waitTime);
363
+ verifySliderPosition (tester, halfTime + (waitTime * 2 ));
364
+ check (FakeVideoPlayerPlatform .position).equals (halfTime + (waitTime * 2 ));
365
+
366
+ await tester.pump (waitTime);
367
+ verifySliderPosition (tester, halfTime + (waitTime * 3 ));
368
+ check (FakeVideoPlayerPlatform .position).equals (halfTime + (waitTime * 3 ));
369
+ });
155
370
});
156
371
}
0 commit comments