diff --git a/.cirrus.yml b/.cirrus.yml index 95bd6092674b..50b6cca0362a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -128,14 +128,6 @@ task: # Restore the tree to a clean state, to avoid accidental issues if # other script steps are added to this task. - git checkout . - ### Android tasks ### - - name: android-build_all_plugins - env: - BUILD_ALL_ARGS: "apk" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Web tasks ### - name: web-build_all_plugins env: @@ -195,7 +187,7 @@ task: CHANNEL: "master" CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] - GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[!c9446a7b11d5520c2ebce3c64ccc82fe6d146272cb06a4a4590e22c389f33153f951347a25422522df1a81fe2f085e9a!] + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[de02374f8d2d14d50792c6b521af2dfb86cbb522efed104f905002e4332546104d387d2bb8710956b729b4bd6533bba0] build_script: # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. @@ -238,6 +230,13 @@ task: path: "**/reports/lint-results-debug.xml" type: text/xml format: android-lint + - name: android-build_all_plugins + env: + BUILD_ALL_ARGS: "apk" + matrix: + CHANNEL: "master" + CHANNEL: "stable" + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE ### Web tasks ### - name: web-platform_tests env: diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index f9031685b583..b87ac4bbeded 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,7 +1,16 @@ -## NEXT +## 2.2.9 +* Adds compatibility with `video_player_platform_interface` 5.0, which does not + include non-dev test dependencies. + +## 2.2.8 + +* Changes the way the `VideoPlayerPlatform` instance is cached in the + controller, so that it's no longer impossible to change after the first use. +* Updates unit tests to be self-contained. * Fixes integration tests. * Updates Android compileSdkVersion to 31. +* Fixes a flaky integration test. ## 2.2.7 diff --git a/packages/video_player/video_player/CONTRIBUTING.md b/packages/video_player/video_player/CONTRIBUTING.md index 15c48038f6fc..387551bda2f6 100644 --- a/packages/video_player/video_player/CONTRIBUTING.md +++ b/packages/video_player/video_player/CONTRIBUTING.md @@ -31,52 +31,3 @@ pigeon, not your version or the version on master. In either case, the configuration will be obtained automatically from the `pigeons/messages.dart` file (see `configurePigeon` at the bottom of that file). - -While contributing, you may also want to set the following dependency -overrides: - -```yaml -dependency_overrides: - video_player_platform_interface: - path: - ../video_player_platform_interface - video_player_web: - path: - ../video_player_web -``` - -## Publishing plugin updates that span multiple plugin packages - -If your change affects both the interface package and the -implementation packages, then you will need to publish a version of -the plugin in between landing the interface changes and the -implementation changes, since the implementations depend on the -interface via pub. - -To do this, follow these steps: - -1. Create a PR that has all the changes, and update the -`pubspec.yaml`s to have path-based dependency overrides as described -in the "Updating pigeon-generated files" section above. - -2. Upload that PR and get it reviewed and into a state where the only -test failure is the one complaining that you can't publish a package -that has dependency overrides. - -3. Create a PR that's a subset of the one in the previous step that -only includes the interface changes, with no dependency overrides, and -submit that. - -4. Once you have had that reviewed and landed, publish the interface -parts of the plugin to pub. - -5. Now, update the original full PR to not use dependency overrides -but to instead refer to the new version of the plugin, and sync it to -master (so that the interface changes are gone from the PR). Submit -that PR. - -6. Once you have had _that_ PR reviewed and landed, publish the -implementation parts of the plugin to pub. - -You may need to publish each implementation package independently of -the main package also, depending on exactly what your change entails. diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index 866b5bce0a8d..63a38290a613 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -48,17 +48,13 @@ void main() { await networkController.setVolume(0); final Completer started = Completer(); final Completer ended = Completer(); - bool startedBuffering = false; - bool endedBuffering = false; networkController.addListener(() { - if (networkController.value.isBuffering && !startedBuffering) { - startedBuffering = true; + if (!started.isCompleted && networkController.value.isBuffering) { started.complete(); } - if (startedBuffering && + if (started.isCompleted && !networkController.value.isBuffering && - !endedBuffering) { - endedBuffering = true; + !ended.isCompleted) { ended.complete(); } }); @@ -72,11 +68,8 @@ void main() { expect(networkController.value.position, (Duration position) => position > const Duration(seconds: 0)); - await started; - expect(startedBuffering, true); - - await ended; - expect(endedBuffering, true); + await expectLater(started.future, completes); + await expectLater(ended.future, completes); }, skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), ); diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index ff727f1432c8..523a1adc5425 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -17,10 +17,18 @@ export 'package:video_player_platform_interface/video_player_platform_interface. import 'src/closed_caption_file.dart'; export 'src/closed_caption_file.dart'; -final VideoPlayerPlatform _videoPlayerPlatform = VideoPlayerPlatform.instance - // This will clear all open videos on the platform when a full restart is - // performed. - ..init(); +VideoPlayerPlatform? _lastVideoPlayerPlatform; + +VideoPlayerPlatform get _videoPlayerPlatform { + VideoPlayerPlatform currentInstance = VideoPlayerPlatform.instance; + if (_lastVideoPlayerPlatform != currentInstance) { + // This will clear all open videos on the platform when a full restart is + // performed. + currentInstance.init(); + _lastVideoPlayerPlatform = currentInstance; + } + return currentInstance; +} /// The duration, current position, buffering state, error state and settings /// of a [VideoPlayerController]. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index f0d5951b1403..3ae9c0dfd301 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.2.7 +version: 2.2.9 environment: sdk: ">=2.14.0 <3.0.0" @@ -24,13 +24,7 @@ dependencies: flutter: sdk: flutter meta: ^1.3.0 - video_player_platform_interface: ^4.2.0 - # The design on https://flutter.dev/go/federated-plugins was to leave - # this constraint as "any". We cannot do it right now as it fails pub publish - # validation, so we set a ^ constraint. The exact value doesn't matter since - # the constraints on the interface pins it. - # TODO(amirh): Revisit this (either update this part in the design or the pub tool). - # https://github.com/flutter/flutter/issues/46264 + video_player_platform_interface: ">=4.2.0 <6.0.0" video_player_web: ^2.0.0 html: ^0.15.0 diff --git a/packages/video_player/video_player/test/video_player_initialization_test.dart b/packages/video_player/video_player/test/video_player_initialization_test.dart index 13bfd7be7889..1870934a931e 100644 --- a/packages/video_player/video_player/test/video_player_initialization_test.dart +++ b/packages/video_player/video_player/test/video_player_initialization_test.dart @@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; +import 'package:video_player_platform_interface/video_player_platform_interface.dart'; import 'video_player_test.dart' show FakeVideoPlayerPlatform; @@ -13,6 +14,7 @@ void main() { test('plugin initialized', () async { TestWidgetsFlutterBinding.ensureInitialized(); FakeVideoPlayerPlatform fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); + VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', 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 08fd9dc604f1..959f98f25e28 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -11,8 +11,6 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; -import 'package:video_player_platform_interface/messages.dart'; -import 'package:video_player_platform_interface/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; class FakeController extends ValueNotifier @@ -187,6 +185,7 @@ void main() { setUp(() { fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); + VideoPlayerPlatform.instance = fakeVideoPlayerPlatform; }); group('initialize', () { @@ -196,10 +195,8 @@ void main() { ); await controller.initialize(); - expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].asset, 'a.avi'); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].packageName, - null); + expect(fakeVideoPlayerPlatform.dataSources[0].asset, 'a.avi'); + expect(fakeVideoPlayerPlatform.dataSources[0].package, null); }); test('network', () async { @@ -209,15 +206,15 @@ void main() { await controller.initialize(); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1', ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, + fakeVideoPlayerPlatform.dataSources[0].formatHint, null, ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {}, ); }); @@ -230,16 +227,16 @@ void main() { await controller.initialize(); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1', ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, - 'dash', + fakeVideoPlayerPlatform.dataSources[0].formatHint, + VideoFormat.dash, ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, - {}, + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, + {}, ); }); @@ -251,15 +248,15 @@ void main() { await controller.initialize(); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + fakeVideoPlayerPlatform.dataSources[0].uri, 'https://127.0.0.1', ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, + fakeVideoPlayerPlatform.dataSources[0].formatHint, null, ); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, + fakeVideoPlayerPlatform.dataSources[0].httpHeaders, {'Authorization': 'Bearer token'}, ); }); @@ -268,15 +265,12 @@ void main() { final VideoPlayerController controller = VideoPlayerController.network( 'http://testing.com/invalid_url', ); - try { - late dynamic error; - fakeVideoPlayerPlatform.forceInitError = true; - await controller.initialize().catchError((dynamic e) => error = e); - final PlatformException platformEx = error; - expect(platformEx.code, equals('VideoError')); - } finally { - fakeVideoPlayerPlatform.forceInitError = false; - } + + late dynamic error; + fakeVideoPlayerPlatform.forceInitError = true; + await controller.initialize().catchError((dynamic e) => error = e); + final PlatformException platformEx = error; + expect(platformEx.code, equals('VideoError')); }); test('file', () async { @@ -284,8 +278,7 @@ void main() { VideoPlayerController.file(File('a.avi')); await controller.initialize(); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, - 'file://a.avi'); + expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'file://a.avi'); }); }); @@ -294,8 +287,7 @@ void main() { VideoPlayerController.contentUri(Uri.parse('content://video')); await controller.initialize(); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, - 'content://video'); + expect(fakeVideoPlayerPlatform.dataSources[0].uri, 'content://video'); }); test('dispose', () async { @@ -571,11 +563,11 @@ void main() { expect(controller.value.isPlaying, isFalse); await controller.play(); expect(controller.value.isPlaying, isTrue); - final FakeVideoEventStream fakeVideoEventStream = + final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.textureId]!; - fakeVideoEventStream.eventsChannel - .sendEvent({'event': 'completed'}); + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.completed)); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); @@ -589,30 +581,30 @@ void main() { await controller.initialize(); expect(controller.value.isBuffering, false); expect(controller.value.buffered, isEmpty); - final FakeVideoEventStream fakeVideoEventStream = + final StreamController fakeVideoEventStream = fakeVideoPlayerPlatform.streams[controller.textureId]!; - fakeVideoEventStream.eventsChannel - .sendEvent({'event': 'bufferingStart'}); + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.bufferingStart)); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isTrue); const Duration bufferStart = Duration(seconds: 0); const Duration bufferEnd = Duration(milliseconds: 500); - fakeVideoEventStream.eventsChannel.sendEvent({ - 'event': 'bufferingUpdate', - 'values': >[ - [bufferStart.inMilliseconds, bufferEnd.inMilliseconds] - ], - }); + fakeVideoEventStream + ..add(VideoEvent( + eventType: VideoEventType.bufferingUpdate, + buffered: [ + DurationRange(bufferStart, bufferEnd), + ])); 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({'event': 'bufferingEnd'}); + fakeVideoEventStream + .add(VideoEvent(eventType: VideoEventType.bufferingEnd)); await tester.pumpAndSettle(); expect(controller.value.isBuffering, isFalse); }); @@ -807,155 +799,88 @@ void main() { }); } -class FakeVideoPlayerPlatform extends TestHostVideoPlayerApi { - FakeVideoPlayerPlatform() { - TestHostVideoPlayerApi.setup(this); - } - +class FakeVideoPlayerPlatform extends VideoPlayerPlatform { Completer initialized = Completer(); List calls = []; - List dataSourceDescriptions = []; - final Map streams = {}; + List dataSources = []; + final Map> streams = + >{}; bool forceInitError = false; int nextTextureId = 0; final Map _positions = {}; @override - TextureMessage create(CreateMessage arg) { + Future create(DataSource dataSource) async { calls.add('create'); - streams[nextTextureId] = FakeVideoEventStream( - nextTextureId, 100, 100, const Duration(seconds: 1), forceInitError); - TextureMessage result = TextureMessage(); - result.textureId = nextTextureId++; - dataSourceDescriptions.add(arg); - return result; + StreamController stream = StreamController(); + streams[nextTextureId] = stream; + if (forceInitError) { + stream.addError(PlatformException( + code: 'VideoError', message: 'Video player had error XYZ')); + } else { + stream.add(VideoEvent( + eventType: VideoEventType.initialized, + size: Size(100, 100), + duration: Duration(seconds: 1))); + } + dataSources.add(dataSource); + return nextTextureId++; } @override - void dispose(TextureMessage arg) { + Future dispose(int textureId) async { calls.add('dispose'); } @override - void initialize() { + Future init() async { calls.add('init'); initialized.complete(true); } + Stream videoEventsFor(int textureId) { + return streams[textureId]!.stream; + } + @override - void pause(TextureMessage arg) { + Future pause(int textureId) async { calls.add('pause'); } @override - void play(TextureMessage arg) { + Future play(int textureId) async { calls.add('play'); } @override - PositionMessage position(TextureMessage arg) { + Future getPosition(int textureId) async { calls.add('position'); - final Duration position = - _positions[arg.textureId] ?? const Duration(seconds: 0); - return PositionMessage()..position = position.inMilliseconds; + return _positions[textureId] ?? const Duration(seconds: 0); } @override - void seekTo(PositionMessage arg) { + Future seekTo(int textureId, Duration position) async { calls.add('seekTo'); - _positions[arg.textureId!] = Duration(milliseconds: arg.position!); + _positions[textureId] = position; } @override - void setLooping(LoopingMessage arg) { + Future setLooping(int textureId, bool looping) async { calls.add('setLooping'); } @override - void setVolume(VolumeMessage arg) { + Future setVolume(int textureId, double volume) async { calls.add('setVolume'); } @override - void setPlaybackSpeed(PlaybackSpeedMessage arg) { + Future setPlaybackSpeed(int textureId, double speed) async { calls.add('setPlaybackSpeed'); } @override - void setMixWithOthers(MixWithOthersMessage arg) { + Future setMixWithOthers(bool mixWithOthers) async { calls.add('setMixWithOthers'); } } - -class FakeVideoEventStream { - FakeVideoEventStream(this.textureId, this.width, this.height, this.duration, - this.initWithError) { - eventsChannel = FakeEventsChannel( - 'flutter.io/videoPlayer/videoEvents$textureId', onListen); - } - - int textureId; - int width; - int height; - Duration duration; - bool initWithError; - late FakeEventsChannel eventsChannel; - - void onListen() { - if (!initWithError) { - eventsChannel.sendEvent({ - 'event': 'initialized', - 'duration': duration.inMilliseconds, - 'width': width, - 'height': height, - }); - } else { - eventsChannel.sendError('VideoError', 'Video player had error XYZ'); - } - } -} - -class FakeEventsChannel { - FakeEventsChannel(String name, this.onListen) { - eventsMethodChannel = MethodChannel(name); - eventsMethodChannel.setMockMethodCallHandler(onMethodCall); - } - - late MethodChannel eventsMethodChannel; - VoidCallback onListen; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'listen': - onListen(); - break; - } - return Future.sync(() {}); - } - - void sendEvent(dynamic event) { - _sendMessage(const StandardMethodCodec().encodeSuccessEnvelope(event)); - } - - void sendError(String code, [String? message, dynamic details]) { - _sendMessage(const StandardMethodCodec().encodeErrorEnvelope( - code: code, - message: message, - details: details, - )); - } - - void _sendMessage(ByteData data) { - _ambiguate(ServicesBinding.instance)! - .defaultBinaryMessenger - .handlePlatformMessage( - eventsMethodChannel.name, data, (ByteData? data) {}); - } -} - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. -T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index b3da9c8924ef..e0e6a11065ee 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 5.0.0 + +* **BREAKING CHANGES**: + * Updates to extending `PlatformInterface`. Removes `isMock`, in favor of the + now-standard `MockPlatformInterfaceMixin`. + * Removes test.dart from the public interface. Tests in other packages should + mock `VideoPlatformInterface` rather than the method channel. + ## 4.2.0 * Add `contentUri` to `DataSourceType`. diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 21ad972d8e06..66b6d709e9fe 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -2,12 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:ui'; - import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart' show visibleForTesting; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'method_channel_video_player.dart'; @@ -18,37 +15,24 @@ import 'method_channel_video_player.dart'; /// (using `extends`) ensures that the subclass will get the default implementation, while /// platform implementations that `implements` this interface will be broken by newly added /// [VideoPlayerPlatform] methods. -abstract class VideoPlayerPlatform { - /// Only mock implementations should set this to true. - /// - /// Mockito mocks are implementing this class with `implements` which is forbidden for anything - /// other than mocks (see class docs). This property provides a backdoor for mockito mocks to - /// skip the verification that the class isn't implemented with `implements`. - @visibleForTesting - bool get isMock => false; +abstract class VideoPlayerPlatform extends PlatformInterface { + /// Constructs a VideoPlayerPlatform. + VideoPlayerPlatform() : super(token: _token); + + static final Object _token = Object(); static VideoPlayerPlatform _instance = MethodChannelVideoPlayer(); /// The default instance of [VideoPlayerPlatform] to use. /// - /// Platform-specific plugins should override this with their own - /// platform-specific class that extends [VideoPlayerPlatform] when they - /// register themselves. - /// /// Defaults to [MethodChannelVideoPlayer]. static VideoPlayerPlatform get instance => _instance; - // TODO(amirh): Extract common platform interface logic. - // https://github.com/flutter/flutter/issues/43368 + /// Platform-specific plugins should override this with their own + /// platform-specific class that extends [VideoPlayerPlatform] when they + /// register themselves. static set instance(VideoPlayerPlatform instance) { - if (!instance.isMock) { - try { - instance._verifyProvidesDefaultImplementations(); - } on NoSuchMethodError catch (_) { - throw AssertionError( - 'Platform interfaces must not be implemented with `implements`'); - } - } + PlatformInterface.verifyToken(instance, _token); _instance = instance; } @@ -119,14 +103,6 @@ abstract class VideoPlayerPlatform { Future setMixWithOthers(bool mixWithOthers) { throw UnimplementedError('setMixWithOthers() has not been implemented.'); } - - // This method makes sure that VideoPlayer isn't implemented with `implements`. - // - // See class doc for more details on why implementing this class is forbidden. - // - // This private method is called by the instance setter, which fails if the class is - // implemented with `implements`. - void _verifyProvidesDefaultImplementations() {} } /// Description of the data source used to create an instance of diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 35b30793a20f..b8404772bffa 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 4.2.0 +version: 5.0.0 environment: sdk: ">=2.12.0 <3.0.0" @@ -13,9 +13,9 @@ environment: dependencies: flutter: sdk: flutter - flutter_test: - sdk: flutter - meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 dev_dependencies: + flutter_test: + sdk: flutter pedantic: ^1.10.0 diff --git a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart index f5439b844045..4d1c9b78fc34 100644 --- a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart +++ b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart @@ -8,9 +8,10 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player_platform_interface/messages.dart'; import 'package:video_player_platform_interface/method_channel_video_player.dart'; -import 'package:video_player_platform_interface/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; +import 'test.dart'; + class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; TextureMessage? textureMessage; diff --git a/packages/video_player/video_player_platform_interface/lib/test.dart b/packages/video_player/video_player_platform_interface/test/test.dart similarity index 99% rename from packages/video_player/video_player_platform_interface/lib/test.dart rename to packages/video_player/video_player_platform_interface/test/test.dart index b4fd81f44f41..a12ae45e59db 100644 --- a/packages/video_player/video_player_platform_interface/lib/test.dart +++ b/packages/video_player/video_player_platform_interface/test/test.dart @@ -10,8 +10,7 @@ import 'dart:async'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'messages.dart'; +import 'package:video_player_platform_interface/messages.dart'; abstract class TestHostVideoPlayerApi { void initialize(); diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 4eb7c9d610b5..13cbf2e24dae 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,7 +1,12 @@ +## 2.0.5 + +* Adds compatibility with `video_player_platform_interface` 5.0, which does not + include non-dev test dependencies. + ## 2.0.4 * Adopt `video_player_platform_interface` 4.2 and opt out of `contentUri` data source. - + ## 2.0.3 * Add `implements` to pubspec. diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index ac0754b1a5d0..ec2377ea1fb1 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_web description: Web platform implementation of video_player. repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.0.4 +version: 2.0.5 environment: sdk: ">=2.12.0 <3.0.0" @@ -23,7 +23,7 @@ dependencies: sdk: flutter meta: ^1.3.0 pedantic: ^1.10.0 - video_player_platform_interface: ^4.2.0 + video_player_platform_interface: ">=4.2.0 <6.0.0" dev_dependencies: flutter_test: diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 7bd33879cd7d..818a13439c95 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.8.2 + +* Adds the `WebSettings.setAllowFileAccess()` method and ensure that file access is allowed when the `WebViewAndroidWidget.loadFile()` method is executed. + +## 2.8.1 + +* Fixes bug where the default user agent string was being set for every rebuild. See + https://github.com/flutter/flutter/issues/94847. + ## 2.8.0 * Implements new cookie manager for setting cookies and providing initial cookies. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index 8ef0b8d11f96..15b78b718115 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -1152,6 +1152,8 @@ public interface WebSettingsHostApi { void setBuiltInZoomControls(Long instanceId, Boolean enabled); + void setAllowFileAccess(Long instanceId, Boolean enabled); + /** The codec used by WebSettingsHostApi. */ static MessageCodec getCodec() { return WebSettingsHostApiCodec.INSTANCE; @@ -1556,6 +1558,37 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean enabledArg = (Boolean) args.get(1); + if (enabledArg == null) { + throw new NullPointerException("enabledArg unexpectedly null."); + } + api.setAllowFileAccess(instanceIdArg.longValue(), enabledArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java index 239ef473b546..b168e206214f 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebSettingsHostApiImpl.java @@ -118,4 +118,10 @@ public void setBuiltInZoomControls(Long instanceId, Boolean enabled) { final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); webSettings.setBuiltInZoomControls(enabled); } + + @Override + public void setAllowFileAccess(Long instanceId, Boolean enabled) { + final WebSettings webSettings = (WebSettings) instanceManager.getInstance(instanceId); + webSettings.setAllowFileAccess(enabled); + } } diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart index dfa05cd92ee6..10989321a9bb 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -546,6 +546,16 @@ class WebSettings { Future setBuiltInZoomControls(bool enabled) { return api.setBuiltInZoomControlsFromInstance(this, enabled); } + + /// Enables or disables file access within WebView. + /// + /// This enables or disables file system access only. Assets and resources are + /// still accessible using file:///android_asset and file:///android_res. The + /// default value is true for apps targeting Build.VERSION_CODES.Q and below, + /// and false when targeting Build.VERSION_CODES.R and above. + Future setAllowFileAccess(bool enabled) { + return api.setAllowFileAccessFromInstance(this, enabled); + } } /// Exposes a channel to receive calls from javaScript. diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart index 810a71732e7b..20391c43d966 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart @@ -1179,6 +1179,31 @@ class WebSettingsHostApi { return; } } + + Future setAllowFileAccess(int arg_instanceId, bool arg_enabled) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_instanceId, arg_enabled]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } } class _JavaScriptChannelHostApiCodec extends StandardMessageCodec { diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart index 1db5ed449c56..ead60f6a2b35 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -437,6 +437,17 @@ class WebSettingsHostApiImpl extends WebSettingsHostApi { enabled, ); } + + /// Helper method to convert instances ids to objects. + Future setAllowFileAccessFromInstance( + WebSettings instance, + bool enabled, + ) { + return setAllowFileAccess( + instanceManager.getInstanceId(instance)!, + enabled, + ); + } } /// Host api implementation for [JavaScriptChannel]. diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart index 1dec9c105741..bf85ac97687e 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart @@ -179,6 +179,7 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { ? absoluteFilePath : 'file://$absoluteFilePath'; + webView.settings.setAllowFileAccess(true); return webView.loadUrl(url, {}); } @@ -418,11 +419,12 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { } Future _setUserAgent(WebSetting userAgent) { - if (userAgent.isPresent && userAgent.value != null) { - return webView.settings.setUserAgentString(userAgent.value!); + if (userAgent.isPresent) { + // If the string is empty, the system default value will be used. + return webView.settings.setUserAgentString(userAgent.value ?? ''); } - return webView.settings.setUserAgentString(''); + return Future.value(); } Future _setZoomEnabled(bool zoomEnabled) { diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart index 36862f7cbacc..b29835266717 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -132,6 +132,8 @@ abstract class WebSettingsHostApi { void setDisplayZoomControls(int instanceId, bool enabled); void setBuiltInZoomControls(int instanceId, bool enabled); + + void setAllowFileAccess(int instanceId, bool enabled); } @HostApi(dartHostTestHandler: 'TestJavaScriptChannelHostApi') diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 34bea570ae43..8905d7fb66e2 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.8.0 +version: 2.8.2 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart index 1e47c79d32b7..720fe408d96c 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview.pigeon.dart @@ -649,6 +649,7 @@ abstract class TestWebSettingsHostApi { void setUseWideViewPort(int instanceId, bool use); void setDisplayZoomControls(int instanceId, bool enabled); void setBuiltInZoomControls(int instanceId, bool enabled); + void setAllowFileAccess(int instanceId, bool enabled); static void setup(TestWebSettingsHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -940,6 +941,28 @@ abstract class TestWebSettingsHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess was null, expected non-null int.'); + final bool? arg_enabled = (args[1] as bool?); + assert(arg_enabled != null, + 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess was null, expected non-null bool.'); + api.setAllowFileAccess(arg_instanceId!, arg_enabled!); + return {}; + }); + } + } } } diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart index cc29fc755067..8688a1977d83 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -430,6 +430,14 @@ void main() { true, )); }); + + test('setAllowFileAccess', () { + webSettings.setAllowFileAccess(true); + verify(mockPlatformHostApi.setAllowFileAccess( + webSettingsInstanceId, + true, + )); + }); }); group('$JavaScriptChannel', () { diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart index d25d2338886b..2134de54a415 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -1,7 +1,3 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - // Mocks generated by Mockito 5.0.16 from annotations // in webview_flutter_android/test/android_webview_test.dart. // Do not manually edit this file. @@ -211,6 +207,10 @@ class MockTestWebSettingsHostApi extends _i1.Mock Invocation.method(#setBuiltInZoomControls, [instanceId, enabled]), returnValueForMissingStub: null); @override + void setAllowFileAccess(int? instanceId, bool? enabled) => super.noSuchMethod( + Invocation.method(#setAllowFileAccess, [instanceId, enabled]), + returnValueForMissingStub: null); + @override String toString() => super.toString(); } diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart index c203ef04a2ce..fed1c1113e55 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.dart @@ -304,6 +304,15 @@ void main() { )); }); + testWidgets('loadFile should setAllowFileAccess to true', + (WidgetTester tester) async { + await buildWidget(tester); + + await testController.loadFile('file:///path/to/file.html'); + + verify(mockWebSettings.setAllowFileAccess(true)); + }); + testWidgets('loadFlutterAsset', (WidgetTester tester) async { await buildWidget(tester); const String assetKey = 'test_assets/index.html'; @@ -483,6 +492,32 @@ void main() { }); }); + testWidgets('no update to userAgentString when there is no change', + (WidgetTester tester) async { + await buildWidget(tester); + + reset(mockWebSettings); + + await testController.updateSettings(WebSettings( + userAgent: const WebSetting.absent(), + )); + + verifyNever(mockWebSettings.setUserAgentString(any)); + }); + + testWidgets('update null userAgentString with empty string', + (WidgetTester tester) async { + await buildWidget(tester); + + reset(mockWebSettings); + + await testController.updateSettings(WebSettings( + userAgent: const WebSetting.of(null), + )); + + verify(mockWebSettings.setUserAgentString('')); + }); + testWidgets('currentUrl', (WidgetTester tester) async { await buildWidget(tester); diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart index ece17ad61cb8..12e993bafa31 100644 --- a/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_widget_test.mocks.dart @@ -1,7 +1,3 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - // Mocks generated by Mockito 5.0.16 from annotations // in webview_flutter_android/test/webview_android_widget_test.dart. // Do not manually edit this file. @@ -120,6 +116,11 @@ class MockWebSettings extends _i1.Mock implements _i2.WebSettings { returnValue: Future.value(), returnValueForMissingStub: Future.value()) as _i4.Future); @override + _i4.Future setAllowFileAccess(bool? enabled) => + (super.noSuchMethod(Invocation.method(#setAllowFileAccess, [enabled]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override String toString() => super.toString(); } diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 72539c24c5fc..12ccf17d4d0b 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -13,6 +13,8 @@ - Fix `federation-safety-check` handling of plugin deletion, and of top-level files in unfederated plugins whose names match federated plugin heuristics (e.g., `packages/foo/foo_android.iml`). +- Add an auto-retry for failed Firebase Test Lab tests as a short-term patch + for flake issues. ## 0.7.3 diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 4e53ee8fbace..e824d8ad1a90 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -176,30 +176,22 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { final String testRunId = getStringArg('test-run-id'); final String resultsDir = 'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/'; - final List args = [ - 'firebase', - 'test', - 'android', - 'run', - '--type', - 'instrumentation', - '--app', - 'build/app/outputs/apk/debug/app-debug.apk', - '--test', - 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', - '--timeout', - '7m', - '--results-bucket=${getStringArg('results-bucket')}', - '--results-dir=$resultsDir', - ]; - for (final String device in getStringListArg('device')) { - args.addAll(['--device', device]); - } - final int exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: example.directory); - if (exitCode != 0) { - printError('Test failure for $testName'); + // Automatically retry failures; there is significant flake with these + // tests whose cause isn't yet understood, and having to re-run the + // entire shard for a flake in any one test is extremely slow. This should + // be removed once the root cause of the flake is understood. + // See https://github.com/flutter/flutter/issues/95063 + const int maxRetries = 2; + bool passing = false; + for (int i = 1; i <= maxRetries && !passing; ++i) { + if (i > 1) { + logWarning('$testName failed on attempt ${i - 1}. Retrying...'); + } + passing = await _runFirebaseTest(example, test, resultsDir: resultsDir); + } + if (!passing) { + printError('Test failure for $testName after $maxRetries attempts'); errors.add('$testName failed tests'); } } @@ -238,6 +230,42 @@ class FirebaseTestLabCommand extends PackageLoopingCommand { return true; } + /// Runs [test] from [example] as a Firebase Test Lab test, returning true if + /// the test passed. + /// + /// [resultsDir] should be a unique-to-the-test-run directory to store the + /// results on the server. + Future _runFirebaseTest( + RepositoryPackage example, + File test, { + required String resultsDir, + }) async { + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '7m', + '--results-bucket=${getStringArg('results-bucket')}', + '--results-dir=$resultsDir', + for (final String device in getStringListArg('device')) ...[ + '--device', + device + ], + ]; + final int exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: example.directory); + + return exitCode == 0; + } + /// Builds [target] using Gradle in the given [project]. Assumes Gradle is /// already configured. /// diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index 65f398b32ca8..1dfd8ba66b58 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -271,7 +271,7 @@ public class MainActivityTest { ); }); - test('fails if a test fails', () async { + test('fails if a test fails twice', () async { const String javaTestFileRelativePath = 'example/android/app/src/androidTest/MainActivityTest.java'; final Directory pluginDir = @@ -287,6 +287,7 @@ public class MainActivityTest { MockProcess(), // auth MockProcess(), // config MockProcess(exitCode: 1), // integration test #1 + MockProcess(exitCode: 1), // integration test #1 retry MockProcess(), // integration test #2 ]; @@ -315,6 +316,44 @@ public class MainActivityTest { ); }); + test('passes with warning if a test fails once, then passes on retry', + () async { + const String javaTestFileRelativePath = + 'example/android/app/src/androidTest/MainActivityTest.java'; + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + javaTestFileRelativePath, + ]); + _writeJavaTestFile(pluginDir, javaTestFileRelativePath); + + processRunner.mockProcessesForExecutable['gcloud'] = [ + MockProcess(), // auth + MockProcess(), // config + MockProcess(exitCode: 1), // integration test #1 + MockProcess(), // integration test #1 retry + MockProcess(), // integration test #2 + ]; + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=redfin,version=30', + ]); + + expect( + output, + containsAllInOrder([ + contains('Testing example/integration_test/bar_test.dart...'), + contains('bar_test.dart failed on attempt 1. Retrying...'), + contains('Testing example/integration_test/foo_test.dart...'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + }); + test('fails for packages with no androidTest directory', () async { createFakePlugin('plugin', packagesDir, extraFiles: [ 'example/integration_test/foo_test.dart',