[video_player] Improve DartDocs and test coverage (#2286)

Also adds a lint to prevent undocumented APIs going forward.
diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index 1bac34f..d4824c4 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.10.3+3
+
+* Add DartDocs and unit tests.
+
 ## 0.10.3+2
 
 * Update the homepage to point to the new plugin location
diff --git a/packages/video_player/video_player/analysis_options.yaml b/packages/video_player/video_player/analysis_options.yaml
new file mode 100644
index 0000000..6c7fd57
--- /dev/null
+++ b/packages/video_player/video_player/analysis_options.yaml
@@ -0,0 +1,11 @@
+# This exists to add a lint for missing API docs just on this specific package,
+# since not all packages have coverage for all their public members yet and
+# adding it in would be non-trivial. `public_member_api_docs` should be applied
+# to new packages going forward, and ideally the main `analysis_options.yaml`
+# file as soon as possible.
+
+include: ../../../analysis_options.yaml
+
+linter:
+  rules:
+    - public_member_api_docs
diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart
index 43d9355..f55d3f7 100644
--- a/packages/video_player/video_player/example/lib/main.dart
+++ b/packages/video_player/video_player/example/lib/main.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// ignore_for_file: public_member_api_docs
+
 /// An example of using the plugin, controlling lifecycle and playback of the
 /// video.
 
diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart
index f1b0e7c..f92693e 100644
--- a/packages/video_player/video_player/lib/video_player.dart
+++ b/packages/video_player/video_player/lib/video_player.dart
@@ -14,16 +14,46 @@
   // performed.
   ..invokeMethod<void>('init');
 
+/// Describes a discrete segment of time within a video using a [start] and
+/// [end] [Duration].
 class DurationRange {
+  /// Trusts that the given [start] and [end] are actually in order. They should
+  /// both be non-null.
   DurationRange(this.start, this.end);
 
+  /// The beginning of the segment described relative to the beginning of the
+  /// entire video. Should be shorter than or equal to [end].
+  ///
+  /// For example, if the entire video is 4 minutes long and the range is from
+  /// 1:00-2:00, this should be a `Duration` of one minute.
   final Duration start;
+
+  /// The end of the segment described as a duration relative to the beginning of
+  /// the entire video. This is expected to be non-null and longer than or equal
+  /// to [start].
+  ///
+  /// For example, if the entire video is 4 minutes long and the range is from
+  /// 1:00-2:00, this should be a `Duration` of two minutes.
   final Duration end;
 
+  /// Assumes that [duration] is the total length of the video that this
+  /// DurationRange is a segment form. It returns the percentage that [start] is
+  /// through the entire video.
+  ///
+  /// For example, assume that the entire video is 4 minutes long. If [start] has
+  /// a duration of one minute, this will return `0.25` since the DurationRange
+  /// starts 25% of the way through the video's total length.
   double startFraction(Duration duration) {
     return start.inMilliseconds / duration.inMilliseconds;
   }
 
+  /// Assumes that [duration] is the total length of the video that this
+  /// DurationRange is a segment form. It returns the percentage that [start] is
+  /// through the entire video.
+  ///
+  /// For example, assume that the entire video is 4 minutes long. If [end] has a
+  /// duration of two minutes, this will return `0.5` since the DurationRange
+  /// ends 50% of the way through the video's total length.
   double endFraction(Duration duration) {
     return end.inMilliseconds / duration.inMilliseconds;
   }
@@ -32,11 +62,26 @@
   String toString() => '$runtimeType(start: $start, end: $end)';
 }
 
-enum VideoFormat { dash, hls, ss, other }
+/// The file format of the given video.
+enum VideoFormat {
+  /// Dynamic Adaptive Streaming over HTTP, also known as MPEG-DASH.
+  dash,
+
+  /// HTTP Live Streaming.
+  hls,
+
+  /// Smooth Streaming.
+  ss,
+
+  /// Any format other than the other ones defined in this enum.
+  other
+}
 
 /// The duration, current position, buffering state, error state and settings
 /// of a [VideoPlayerController].
 class VideoPlayerValue {
+  /// Constructs a video with the given values. Only [duration] is required. The
+  /// rest will initialize with default values when unset.
   VideoPlayerValue({
     @required this.duration,
     this.size,
@@ -49,8 +94,11 @@
     this.errorDescription,
   });
 
+  /// Returns an instance with a `null` [Duration].
   VideoPlayerValue.uninitialized() : this(duration: null);
 
+  /// Returns an instance with a `null` [Duration] and the given
+  /// [errorDescription].
   VideoPlayerValue.erroneous(String errorDescription)
       : this(duration: null, errorDescription: errorDescription);
 
@@ -87,10 +135,19 @@
   /// Is null when [initialized] is false.
   final Size size;
 
+  /// Indicates whether or not the video has been loaded and is ready to play.
   bool get initialized => duration != null;
+
+  /// Indicates whether or not the video is in an error state. If this is true
+  /// [errorDescription] should have information about the problem.
   bool get hasError => errorDescription != null;
+
+  /// Returns [size.width] / [size.height] when size is non-null, or `1.0.` when
+  /// it is.
   double get aspectRatio => size != null ? size.width / size.height : 1.0;
 
+  /// Returns a new instance that has the same values as this current instance,
+  /// except for any overrides passed in as arguments to [copyWidth].
   VideoPlayerValue copyWith({
     Duration duration,
     Size size,
@@ -130,7 +187,19 @@
   }
 }
 
-enum DataSourceType { asset, network, file }
+/// The way in which the video was originally loaded. This has nothing to do
+/// with the video's file type. It's just the place from which the video is
+/// fetched from.
+enum DataSourceType {
+  /// The video was included in the app's asset files.
+  asset,
+
+  /// The video was downloaded from the internet.
+  network,
+
+  /// The video was loaded off of the local filesystem.
+  file
+}
 
 /// Controls a platform video player, and provides updates when the state is
 /// changing.
@@ -177,13 +246,20 @@
         super(VideoPlayerValue(duration: null));
 
   int _textureId;
+
+  /// The URI to the video file. This will be in different formats depending on
+  /// the [DataSourceType] of the original video.
   final String dataSource;
+
+  /// **Android only**. Will override the platform's generic file format
+  /// detection with whatever is set here.
   final VideoFormat formatHint;
 
   /// Describes the type of data source this [VideoPlayerController]
   /// is constructed with.
   final DataSourceType dataSourceType;
 
+  /// Only set for [asset] videos. The package that the asset was loaded from.
   final String package;
   Timer _timer;
   bool _isDisposed = false;
@@ -191,9 +267,12 @@
   StreamSubscription<dynamic> _eventSubscription;
   _VideoAppLifeCycleObserver _lifeCycleObserver;
 
+  /// This is just exposed for testing. It shouldn't be used by anyone depending
+  /// on the plugin.
   @visibleForTesting
   int get textureId => _textureId;
 
+  /// Attempts to open the given [dataSource] and load metadata about the video.
   Future<void> initialize() async {
     _lifeCycleObserver = _VideoAppLifeCycleObserver(this);
     _lifeCycleObserver.initialize();
@@ -305,16 +384,24 @@
     super.dispose();
   }
 
+  /// Starts playing the video.
+  ///
+  /// This method returns a future that completes as soon as the "play" command
+  /// has been sent to the platform, not when playback itself is totally
+  /// finished.
   Future<void> play() async {
     value = value.copyWith(isPlaying: true);
     await _applyPlayPause();
   }
 
+  /// Sets whether or not the video should loop after playing once. See also
+  /// [VideoPlayerValue.isLooping].
   Future<void> setLooping(bool looping) async {
     value = value.copyWith(isLooping: looping);
     await _applyLooping();
   }
 
+  /// Pauses the video.
   Future<void> pause() async {
     value = value.copyWith(isPlaying: false);
     await _applyPlayPause();
@@ -384,6 +471,11 @@
     );
   }
 
+  /// Sets the video's current timestamp to be at [moment]. The next
+  /// time the video is played it will resume from the given [moment].
+  ///
+  /// If [moment] is outside of the video's full range it will be automatically
+  /// and silently clamped.
   Future<void> seekTo(Duration moment) async {
     if (_isDisposed) {
       return;
@@ -449,10 +541,13 @@
   }
 }
 
-/// Displays the video controlled by [controller].
+/// Widget that displays the video controlled by [controller].
 class VideoPlayer extends StatefulWidget {
+  /// Uses the given [controller] for all video rendered in this widget.
   VideoPlayer(this.controller);
 
+  /// The [VideoPlayerController] responsible for the video being rendered in
+  /// this widget.
   final VideoPlayerController controller;
 
   @override
@@ -503,15 +598,43 @@
   }
 }
 
+/// Used to configure the [VideoProgressIndicator] widget's colors for how it
+/// describes the video's status.
+///
+/// The widget uses default colors that are customizeable through this class.
 class VideoProgressColors {
+  /// Any property can be set to any color. They each have defaults.
+  ///
+  /// [playedColor] defaults to red at 70% opacity. This fills up a portion of
+  /// the [VideoProgressIndicator] to represent how much of the video has played
+  /// so far.
+  ///
+  /// [bufferedColor] defaults to blue at 20% opacity. This fills up a portion
+  /// of [VideoProgressIndicator] to represent how much of the video has
+  /// buffered so far.
+  ///
+  /// [backgroundColor] defaults to gray at 50% opacity. This is the background
+  /// color behind both [playedColor] and [bufferedColor] to denote the total
+  /// size of the video compared to either of those values.
   VideoProgressColors({
     this.playedColor = const Color.fromRGBO(255, 0, 0, 0.7),
     this.bufferedColor = const Color.fromRGBO(50, 50, 200, 0.2),
     this.backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5),
   });
 
+  /// [playedColor] defaults to red at 70% opacity. This fills up a portion of
+  /// the [VideoProgressIndicator] to represent how much of the video has played
+  /// so far.
   final Color playedColor;
+
+  /// [bufferedColor] defaults to blue at 20% opacity. This fills up a portion
+  /// of [VideoProgressIndicator] to represent how much of the video has
+  /// buffered so far.
   final Color bufferedColor;
+
+  /// [backgroundColor] defaults to gray at 50% opacity. This is the background
+  /// color behind both [playedColor] and [bufferedColor] to denote the total
+  /// size of the video compared to either of those values.
   final Color backgroundColor;
 }
 
@@ -584,6 +707,12 @@
 /// [padding] allows to specify some extra padding around the progress indicator
 /// that will also detect the gestures.
 class VideoProgressIndicator extends StatefulWidget {
+  /// Construct an instance that displays the play/buffering status of the video
+  /// controlled by [controller].
+  ///
+  /// Defaults will be used for everything except [controller] if they're not
+  /// provided. [allowScrubbing] defaults to false, and [padding] will default
+  /// to `top: 5.0`.
   VideoProgressIndicator(
     this.controller, {
     VideoProgressColors colors,
@@ -591,9 +720,25 @@
     this.padding = const EdgeInsets.only(top: 5.0),
   }) : colors = colors ?? VideoProgressColors();
 
+  /// The [VideoPlayerController] that actually associates a video with this
+  /// widget.
   final VideoPlayerController controller;
+
+  /// The default colors used throughout the indicator.
+  ///
+  /// See [VideoProgressColors] for default values.
   final VideoProgressColors colors;
+
+  /// When true, the widget will detect touch input and try to seek the video
+  /// accordingly. The widget ignores such input when false.
+  ///
+  /// Defaults to false.
   final bool allowScrubbing;
+
+  /// This allows for visual padding around the progress indicator that can
+  /// still detect gestures via [allowScrubbing].
+  ///
+  /// Defaults to `top: 5.0`.
   final EdgeInsets padding;
 
   @override
diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml
index 32d1e98..cf85568 100644
--- a/packages/video_player/video_player/pubspec.yaml
+++ b/packages/video_player/video_player/pubspec.yaml
@@ -2,8 +2,8 @@
 description: Flutter plugin for displaying inline video with other Flutter
   widgets on Android and iOS.
 author: Flutter Team <flutter-dev@googlegroups.com>
-version: 0.10.3+2
-homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
+version: 0.10.3+3
+homepage: https://github.com/flutter/plugins/tree/master/packages/video_player
 
 flutter:
   plugin:
diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart
index 9211f0c..10b5754 100644
--- a/packages/video_player/video_player/test/video_player_test.dart
+++ b/packages/video_player/video_player/test/video_player_test.dart
@@ -90,55 +90,338 @@
       fakeVideoPlayerPlatform = FakeVideoPlayerPlatform();
     });
 
-    test('initialize asset', () async {
-      final VideoPlayerController controller = VideoPlayerController.asset(
-        'a.avi',
-      );
-      await controller.initialize();
+    group('initialize', () {
+      test('asset', () async {
+        final VideoPlayerController controller = VideoPlayerController.asset(
+          'a.avi',
+        );
+        await controller.initialize();
 
-      expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0], <String, dynamic>{
-        'asset': 'a.avi',
-        'package': null,
+        expect(
+            fakeVideoPlayerPlatform.dataSourceDescriptions[0],
+            <String, dynamic>{
+              'asset': 'a.avi',
+              'package': null,
+            });
+      });
+
+      test('network', () async {
+        final VideoPlayerController controller = VideoPlayerController.network(
+          'https://127.0.0.1',
+        );
+        await controller.initialize();
+
+        expect(
+            fakeVideoPlayerPlatform.dataSourceDescriptions[0],
+            <String, dynamic>{
+              'uri': 'https://127.0.0.1',
+              'formatHint': null,
+            });
+      });
+
+      test('network with hint', () async {
+        final VideoPlayerController controller = VideoPlayerController.network(
+            'https://127.0.0.1',
+            formatHint: VideoFormat.dash);
+        await controller.initialize();
+
+        expect(
+            fakeVideoPlayerPlatform.dataSourceDescriptions[0],
+            <String, dynamic>{
+              'uri': 'https://127.0.0.1',
+              'formatHint': 'dash',
+            });
+      });
+
+      test('file', () async {
+        final VideoPlayerController controller =
+            VideoPlayerController.file(File('a.avi'));
+        await controller.initialize();
+
+        expect(
+            fakeVideoPlayerPlatform.dataSourceDescriptions[0],
+            <String, dynamic>{
+              'uri': 'file://a.avi',
+            });
       });
     });
 
-    test('initialize network', () async {
+    test('dispose', () async {
+      final VideoPlayerController controller = VideoPlayerController.network(
+        'https://127.0.0.1',
+      );
+      expect(controller.textureId, isNull);
+      expect(await controller.position, const Duration(seconds: 0));
+      controller.initialize();
+
+      await controller.dispose();
+
+      expect(controller.textureId, isNotNull);
+      expect(await controller.position, isNull);
+    });
+
+    test('play', () async {
       final VideoPlayerController controller = VideoPlayerController.network(
         'https://127.0.0.1',
       );
       await controller.initialize();
+      expect(controller.value.isPlaying, isFalse);
+      await controller.play();
 
-      expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0], <String, dynamic>{
-        'uri': 'https://127.0.0.1',
-        'formatHint': null,
-      });
+      expect(controller.value.isPlaying, isTrue);
+      expect(fakeVideoPlayerPlatform.calls.last.method, 'play');
     });
 
-    test('initialize network with hint', () async {
+    test('setLooping', () async {
       final VideoPlayerController controller = VideoPlayerController.network(
+        'https://127.0.0.1',
+      );
+      await controller.initialize();
+      expect(controller.value.isLooping, isFalse);
+      await controller.setLooping(true);
+
+      expect(controller.value.isLooping, isTrue);
+    });
+
+    test('pause', () async {
+      final VideoPlayerController controller = VideoPlayerController.network(
+        'https://127.0.0.1',
+      );
+      await controller.initialize();
+      await controller.play();
+      expect(controller.value.isPlaying, isTrue);
+
+      await controller.pause();
+
+      expect(controller.value.isPlaying, isFalse);
+      expect(fakeVideoPlayerPlatform.calls.last.method, 'pause');
+    });
+
+    group('seekTo', () {
+      test('works', () async {
+        final VideoPlayerController controller = VideoPlayerController.network(
           'https://127.0.0.1',
-          formatHint: VideoFormat.dash);
-      await controller.initialize();
+        );
+        await controller.initialize();
+        expect(await controller.position, const Duration(seconds: 0));
 
-      expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0], <String, dynamic>{
-        'uri': 'https://127.0.0.1',
-        'formatHint': 'dash',
+        await controller.seekTo(const Duration(milliseconds: 500));
+
+        expect(await controller.position, const Duration(milliseconds: 500));
+      });
+
+      test('clamps values that are too high or low', () async {
+        final VideoPlayerController controller = VideoPlayerController.network(
+          'https://127.0.0.1',
+        );
+        await controller.initialize();
+        expect(await controller.position, const Duration(seconds: 0));
+
+        await controller.seekTo(const Duration(seconds: 100));
+        expect(await controller.position, const Duration(seconds: 1));
+
+        await controller.seekTo(const Duration(seconds: -100));
+        expect(await controller.position, const Duration(seconds: 0));
       });
     });
 
-    test('initialize file', () async {
-      final VideoPlayerController controller =
-          VideoPlayerController.file(File('a.avi'));
-      await controller.initialize();
+    group('setVolume', () {
+      test('works', () async {
+        final VideoPlayerController controller = VideoPlayerController.network(
+          'https://127.0.0.1',
+        );
+        await controller.initialize();
+        expect(controller.value.volume, 1.0);
 
-      expect(
-          fakeVideoPlayerPlatform.dataSourceDescriptions[0], <String, dynamic>{
-        'uri': 'file://a.avi',
+        const double volume = 0.5;
+        await controller.setVolume(volume);
+
+        expect(controller.value.volume, volume);
+      });
+
+      test('clamps values that are too high or low', () async {
+        final VideoPlayerController controller = VideoPlayerController.network(
+          'https://127.0.0.1',
+        );
+        await controller.initialize();
+        expect(controller.value.volume, 1.0);
+
+        await controller.setVolume(-1);
+        expect(controller.value.volume, 0.0);
+
+        await controller.setVolume(11);
+        expect(controller.value.volume, 1.0);
       });
     });
+
+    group('Platform callbacks', () {
+      testWidgets('playing completed', (WidgetTester tester) async {
+        final VideoPlayerController controller = VideoPlayerController.network(
+          'https://127.0.0.1',
+        );
+        await controller.initialize();
+        expect(controller.value.isPlaying, isFalse);
+        await controller.play();
+        expect(controller.value.isPlaying, isTrue);
+        final FakeVideoEventStream fakeVideoEventStream =
+            fakeVideoPlayerPlatform.streams[controller.textureId];
+        assert(fakeVideoEventStream != null);
+
+        fakeVideoEventStream.eventsChannel
+            .sendEvent(<String, dynamic>{'event': 'completed'});
+        await tester.pumpAndSettle();
+
+        expect(controller.value.isPlaying, isFalse);
+        expect(controller.value.position, controller.value.duration);
+      });
+
+      testWidgets('buffering status', (WidgetTester tester) async {
+        final VideoPlayerController controller = VideoPlayerController.network(
+          'https://127.0.0.1',
+        );
+        await controller.initialize();
+        expect(controller.value.isBuffering, false);
+        expect(controller.value.buffered, isEmpty);
+        final FakeVideoEventStream fakeVideoEventStream =
+            fakeVideoPlayerPlatform.streams[controller.textureId];
+        assert(fakeVideoEventStream != null);
+
+        fakeVideoEventStream.eventsChannel
+            .sendEvent(<String, dynamic>{'event': 'bufferingStart'});
+        await tester.pumpAndSettle();
+        expect(controller.value.isBuffering, isTrue);
+
+        const Duration bufferStart = Duration(seconds: 0);
+        const Duration bufferEnd = Duration(milliseconds: 500);
+        fakeVideoEventStream.eventsChannel.sendEvent(<String, dynamic>{
+          'event': 'bufferingUpdate',
+          'values': <List<int>>[
+            <int>[bufferStart.inMilliseconds, bufferEnd.inMilliseconds]
+          ],
+        });
+        await tester.pumpAndSettle();
+        expect(controller.value.isBuffering, isTrue);
+        expect(controller.value.buffered.length, 1);
+        expect(controller.value.buffered[0].toString(),
+            DurationRange(bufferStart, bufferEnd).toString());
+
+        fakeVideoEventStream.eventsChannel
+            .sendEvent(<String, dynamic>{'event': 'bufferingEnd'});
+        await tester.pumpAndSettle();
+        expect(controller.value.isBuffering, isFalse);
+      });
+    });
+  });
+
+  group('DurationRange', () {
+    test('uses given values', () {
+      const Duration start = Duration(seconds: 2);
+      const Duration end = Duration(seconds: 8);
+
+      final DurationRange range = DurationRange(start, end);
+
+      expect(range.start, start);
+      expect(range.end, end);
+      expect(range.toString(), contains('start: $start, end: $end'));
+    });
+
+    test('calculates fractions', () {
+      const Duration start = Duration(seconds: 2);
+      const Duration end = Duration(seconds: 8);
+      const Duration total = Duration(seconds: 10);
+
+      final DurationRange range = DurationRange(start, end);
+
+      expect(range.startFraction(total), .2);
+      expect(range.endFraction(total), .8);
+    });
+  });
+
+  group('VideoPlayerValue', () {
+    test('uninitialized()', () {
+      final VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized();
+
+      expect(uninitialized.duration, isNull);
+      expect(uninitialized.position, equals(const Duration(seconds: 0)));
+      expect(uninitialized.buffered, isEmpty);
+      expect(uninitialized.isPlaying, isFalse);
+      expect(uninitialized.isLooping, isFalse);
+      expect(uninitialized.isBuffering, isFalse);
+      expect(uninitialized.volume, 1.0);
+      expect(uninitialized.errorDescription, isNull);
+      expect(uninitialized.size, isNull);
+      expect(uninitialized.size, isNull);
+      expect(uninitialized.initialized, isFalse);
+      expect(uninitialized.hasError, isFalse);
+      expect(uninitialized.aspectRatio, 1.0);
+    });
+
+    test('erroneous()', () {
+      const String errorMessage = 'foo';
+      final VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage);
+
+      expect(error.duration, isNull);
+      expect(error.position, equals(const Duration(seconds: 0)));
+      expect(error.buffered, isEmpty);
+      expect(error.isPlaying, isFalse);
+      expect(error.isLooping, isFalse);
+      expect(error.isBuffering, isFalse);
+      expect(error.volume, 1.0);
+      expect(error.errorDescription, errorMessage);
+      expect(error.size, isNull);
+      expect(error.size, isNull);
+      expect(error.initialized, isFalse);
+      expect(error.hasError, isTrue);
+      expect(error.aspectRatio, 1.0);
+    });
+
+    test('toString()', () {
+      const Duration duration = Duration(seconds: 5);
+      const Size size = Size(400, 300);
+      const Duration position = Duration(seconds: 1);
+      final List<DurationRange> buffered = <DurationRange>[
+        DurationRange(const Duration(seconds: 0), const Duration(seconds: 4))
+      ];
+      const bool isPlaying = true;
+      const bool isLooping = true;
+      const bool isBuffering = true;
+      const double volume = 0.5;
+
+      final VideoPlayerValue value = VideoPlayerValue(
+          duration: duration,
+          size: size,
+          position: position,
+          buffered: buffered,
+          isPlaying: isPlaying,
+          isLooping: isLooping,
+          isBuffering: isBuffering,
+          volume: volume);
+
+      expect(value.toString(),
+          'VideoPlayerValue(duration: 0:00:05.000000, size: Size(400.0, 300.0), position: 0:00:01.000000, buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], isPlaying: true, isLooping: true, isBuffering: truevolume: 0.5, errorDescription: null)');
+    });
+
+    test('copyWith()', () {
+      final VideoPlayerValue original = VideoPlayerValue.uninitialized();
+      final VideoPlayerValue exactCopy = original.copyWith();
+
+      expect(exactCopy.toString(), original.toString());
+    });
+  });
+
+  test('VideoProgressColors', () {
+    const Color playedColor = Color.fromRGBO(0, 0, 255, 0.75);
+    const Color bufferedColor = Color.fromRGBO(0, 255, 0, 0.5);
+    const Color backgroundColor = Color.fromRGBO(255, 255, 0, 0.25);
+
+    final VideoProgressColors colors = VideoProgressColors(
+        playedColor: playedColor,
+        bufferedColor: bufferedColor,
+        backgroundColor: backgroundColor);
+
+    expect(colors.playedColor, playedColor);
+    expect(colors.bufferedColor, bufferedColor);
+    expect(colors.backgroundColor, backgroundColor);
   });
 }
 
@@ -150,16 +433,20 @@
   final MethodChannel _channel = const MethodChannel('flutter.io/videoPlayer');
 
   Completer<bool> initialized = Completer<bool>();
+  List<MethodCall> calls = <MethodCall>[];
   List<Map<String, dynamic>> dataSourceDescriptions = <Map<String, dynamic>>[];
+  final Map<int, FakeVideoEventStream> streams = <int, FakeVideoEventStream>{};
   int nextTextureId = 0;
+  final Map<int, Duration> _positions = <int, Duration>{};
 
   Future<dynamic> onMethodCall(MethodCall call) {
+    calls.add(call);
     switch (call.method) {
       case 'init':
         initialized.complete(true);
         break;
       case 'create':
-        FakeVideoEventStream(
+        streams[nextTextureId] = FakeVideoEventStream(
             nextTextureId, 100, 100, const Duration(seconds: 1));
         final Map<dynamic, dynamic> dataSource = call.arguments;
         dataSourceDescriptions.add(dataSource.cast<String, dynamic>());
@@ -169,11 +456,20 @@
           };
         });
         break;
-      case 'setLooping':
+      case 'position':
+        final Duration position = _positions[call.arguments['textureId']] ??
+            const Duration(seconds: 0);
+        return Future<int>.value(position.inMilliseconds);
         break;
-      case 'setVolume':
+      case 'seekTo':
+        _positions[call.arguments['textureId']] =
+            Duration(milliseconds: call.arguments['location']);
         break;
+      case 'dispose':
       case 'pause':
+      case 'play':
+      case 'setLooping':
+      case 'setVolume':
         break;
       default:
         throw UnimplementedError(