diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index 01c81bfd7..dc5ffe177 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -4,9 +4,14 @@ This version requires version 2.2.0-dev.1 of `patrol_cli` package. +## 2.2.5 + +- Fix `grantPermissionOnlyThisTime()` crashing on Android <11 (#1698) + ## 2.2.4 -- Remove deprecation of `nativeAutomation` and add message about migration to `patrol_finders` (#1670) +- Remove deprecation of `nativeAutomation` and add message about migration to + `patrol_finders` (#1670) ## 2.2.3 diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt index fc7438a5a..a1098bf6d 100644 --- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt +++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/Automator.kt @@ -392,9 +392,9 @@ class Automator private constructor() { fun allowPermissionWhileUsingApp() { val identifiers = arrayOf( - "com.android.packageinstaller:id/permission_allow_button", - "com.android.permissioncontroller:id/permission_allow_button", - "com.android.permissioncontroller:id/permission_allow_foreground_only_button" + "com.android.packageinstaller:id/permission_allow_button", // API <= 28 + "com.android.permissioncontroller:id/permission_allow_button", // API 29 + "com.android.permissioncontroller:id/permission_allow_foreground_only_button" // API >= 30 + API 29 (only for location permission) ) val uiObject = waitForUiObjectByResourceId(*identifiers, timeout = timeoutMillis) @@ -404,10 +404,15 @@ class Automator private constructor() { } fun allowPermissionOnce() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + // One-time permissions are available only on API 30 (R) and above. + // See: https://developer.android.com/training/permissions/requesting#one-time + allowPermissionWhileUsingApp() + return + } + val identifiers = arrayOf( - "com.android.packageinstaller:id/permission_allow_button", - "com.android.permissioncontroller:id/permission_allow_button", - "com.android.permissioncontroller:id/permission_allow_one_time_button" + "com.android.permissioncontroller:id/permission_allow_one_time_button" // API >= 30 ) val uiObject = waitForUiObjectByResourceId(*identifiers, timeout = timeoutMillis) @@ -418,8 +423,8 @@ class Automator private constructor() { fun denyPermission() { val identifiers = arrayOf( - "com.android.packageinstaller:id/permission_deny_button", - "com.android.permissioncontroller:id/permission_deny_button" + "com.android.packageinstaller:id/permission_deny_button", // API <= 28 + "com.android.permissioncontroller:id/permission_deny_button" // API >= 29 ) val uiObject = waitForUiObjectByResourceId(*identifiers, timeout = timeoutMillis) diff --git a/packages/patrol/example/integration_test/permissions/permissions_location_test.dart b/packages/patrol/example/integration_test/permissions/permissions_location_test.dart index cc312b023..d47f1b917 100644 --- a/packages/patrol/example/integration_test/permissions/permissions_location_test.dart +++ b/packages/patrol/example/integration_test/permissions/permissions_location_test.dart @@ -40,7 +40,7 @@ void main() { await $.native.selectFineLocation(); await $.native.selectCoarseLocation(); await $.native.selectFineLocation(); - await $.native.grantPermissionWhenInUse(); + await $.native.grantPermissionOnlyThisTime(); } await $.pump(); diff --git a/packages/patrol/lib/src/native/native_automator.dart b/packages/patrol/lib/src/native/native_automator.dart index e4d014564..cde6bfaf5 100644 --- a/packages/patrol/lib/src/native/native_automator.dart +++ b/packages/patrol/lib/src/native/native_automator.dart @@ -694,6 +694,10 @@ class NativeAutomator { /// On iOS, this is the same as [grantPermissionWhenInUse] except for the /// location permission. /// + /// On Android versions older than 11 (R, API level 30), the concept of + /// "one-time permissions" doesn't exist. In this case, this method is the + /// same as [grantPermissionWhenInUse]. + /// /// See also: /// /// * [grantPermissionWhenInUse] and [denyPermission] diff --git a/packages/patrol_cli/CHANGELOG.md b/packages/patrol_cli/CHANGELOG.md index 59cbbced0..14a1309d0 100644 --- a/packages/patrol_cli/CHANGELOG.md +++ b/packages/patrol_cli/CHANGELOG.md @@ -4,6 +4,10 @@ This version requires version 2.3.0-dev.1 of `patrol` package. +## 2.1.4 + +- Uninstall RunnerUITests app on iOS when flavor is present (#1694) + ## 2.1.3 - Add migration message due to release of `patrol_finders` diff --git a/packages/patrol_cli/lib/src/commands/develop.dart b/packages/patrol_cli/lib/src/commands/develop.dart index d9a376002..03ff86857 100644 --- a/packages/patrol_cli/lib/src/commands/develop.dart +++ b/packages/patrol_cli/lib/src/commands/develop.dart @@ -223,7 +223,11 @@ class DevelopCommand extends PatrolCommand { case TargetPlatform.iOS: final bundleId = iosOpts.bundleId; if (bundleId != null) { - action = () => _iosTestBackend.uninstall(bundleId, device); + action = () => _iosTestBackend.uninstall( + appId: bundleId, + flavor: iosOpts.flutter.flavor, + device: device, + ); } break; } @@ -238,7 +242,7 @@ class DevelopCommand extends PatrolCommand { Future _execute( FlutterAppOptions flutterOpts, AndroidAppOptions android, - IOSAppOptions ios, { + IOSAppOptions iosOpts, { required bool uninstall, required Device device, }) async { @@ -257,12 +261,16 @@ class DevelopCommand extends PatrolCommand { } break; case TargetPlatform.iOS: - appId = ios.bundleId; + appId = iosOpts.bundleId; action = () async => - _iosTestBackend.execute(ios, device, interruptible: true); - final bundle = ios.bundleId; - if (bundle != null && uninstall) { - finalizer = () => _iosTestBackend.uninstall(bundle, device); + _iosTestBackend.execute(iosOpts, device, interruptible: true); + final bundleId = iosOpts.bundleId; + if (bundleId != null && uninstall) { + finalizer = () => _iosTestBackend.uninstall( + appId: bundleId, + flavor: iosOpts.flutter.flavor, + device: device, + ); } } diff --git a/packages/patrol_cli/lib/src/commands/test.dart b/packages/patrol_cli/lib/src/commands/test.dart index 3fef35591..bfe02ccfe 100644 --- a/packages/patrol_cli/lib/src/commands/test.dart +++ b/packages/patrol_cli/lib/src/commands/test.dart @@ -200,7 +200,11 @@ See https://github.com/leancodepl/patrol/issues/1316 to learn more. case TargetPlatform.iOS: final bundleId = iosOpts.bundleId; if (bundleId != null) { - action = () => _iosTestBackend.uninstall(bundleId, device); + action = () => _iosTestBackend.uninstall( + appId: bundleId, + flavor: iosOpts.flutter.flavor, + device: device, + ); } break; } @@ -240,7 +244,7 @@ See https://github.com/leancodepl/patrol/issues/1316 to learn more. Future _execute( FlutterAppOptions flutterOpts, AndroidAppOptions android, - IOSAppOptions ios, { + IOSAppOptions iosOpts, { required bool uninstall, required Device device, }) async { @@ -256,10 +260,14 @@ See https://github.com/leancodepl/patrol/issues/1316 to learn more. } break; case TargetPlatform.iOS: - action = () async => _iosTestBackend.execute(ios, device); - final bundle = ios.bundleId; - if (bundle != null && uninstall) { - finalizer = () => _iosTestBackend.uninstall(bundle, device); + action = () async => _iosTestBackend.execute(iosOpts, device); + final bundleId = iosOpts.bundleId; + if (bundleId != null && uninstall) { + finalizer = () => _iosTestBackend.uninstall( + appId: bundleId, + flavor: iosOpts.flutter.flavor, + device: device, + ); } break; } diff --git a/packages/patrol_cli/lib/src/ios/ios_test_backend.dart b/packages/patrol_cli/lib/src/ios/ios_test_backend.dart index 19fb2ea18..9e1edec3c 100644 --- a/packages/patrol_cli/lib/src/ios/ios_test_backend.dart +++ b/packages/patrol_cli/lib/src/ios/ios_test_backend.dart @@ -4,6 +4,7 @@ import 'dart:io' show Process; import 'package:dispose_scope/dispose_scope.dart'; import 'package:file/file.dart'; import 'package:glob/glob.dart'; +import 'package:meta/meta.dart'; import 'package:path/path.dart' show join; import 'package:patrol_cli/src/base/exceptions.dart'; import 'package:patrol_cli/src/base/logger.dart'; @@ -189,7 +190,14 @@ class IOSTestBackend { }); } - Future uninstall(String appId, Device device) async { + /// Uninstalls the app under test and the test runner from [device]. + /// + /// [flavor] is required to infer the test runner bundle ID. + Future uninstall({ + required String appId, + required String? flavor, + required Device device, + }) async { if (device.real) { // uninstall from iOS device await _processManager.run( @@ -208,8 +216,10 @@ class IOSTestBackend { ); } - // TODO: Not being removed https://github.com/leancodepl/patrol/issues/1094 - final testApp = '$appId.RunnerUITests.xctrunner'; + // See rationale: https://github.com/leancodepl/patrol/issues/1094 + final appIdWithoutFlavor = stripFlavorFromAppId(appId, flavor); + final testApp = '$appIdWithoutFlavor.RunnerUITests.xctrunner'; + if (device.real) { // uninstall from iOS device await _processManager.run( @@ -229,6 +239,24 @@ class IOSTestBackend { } } + /// Removes [flavor] from the end of [appId]. + /// + /// Assumes that [appId] and [flavor] are separated by a dot. + @visibleForTesting + String stripFlavorFromAppId(String appId, String? flavor) { + if (flavor == null) { + return appId; + } + + final idx = appId.indexOf('.$flavor'); + + if (idx == -1) { + return appId; + } + + return appId.substring(0, idx); + } + Future xcTestRunPath({ required bool real, required String scheme, diff --git a/packages/patrol_cli/test/ios/ios_test_backend_test.dart b/packages/patrol_cli/test/ios/ios_test_backend_test.dart index c1bced1cc..1bbbda658 100644 --- a/packages/patrol_cli/test/ios/ios_test_backend_test.dart +++ b/packages/patrol_cli/test/ios/ios_test_backend_test.dart @@ -178,6 +178,38 @@ void main() { testPlan: 'SomeTestPlan', ); }); + + group('stripFlavorFromAppId', () { + test('simply returns appId when flavor is null', () { + const appId = 'com.company.app'; + const String? flavor = null; + + expect( + iosTestBackend.stripFlavorFromAppId(appId, flavor), + 'com.company.app', + ); + }); + + test('works when appId contains flavor', () { + const appId = 'com.company.app.dev'; + const flavor = 'dev'; + + expect( + iosTestBackend.stripFlavorFromAppId(appId, flavor), + 'com.company.app', + ); + }); + + test('ignores when appId contains flavor not preceded by a dot', () { + const appId = 'com.company.app_dev'; + const flavor = 'dev'; + + expect( + iosTestBackend.stripFlavorFromAppId(appId, flavor), + 'com.company.app_dev', + ); + }); + }); }); }