From 353e9a2f951d10a2c2443a3ce0c62904ed62edc8 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth <15619084+vashworth@users.noreply.github.com> Date: Thu, 19 Oct 2023 15:20:11 -0500 Subject: [PATCH] [image_picker_ios] Update UITests for Xcode 15/iOS 17 (#5176) With Xcode 15, XCTest's `addUIInterruptionMonitorWithDescription` sometimes doesn't work. To fix, I added a fallback to query for the buttons in the permissions dialog. Also, the Allow text in the permissions dialog is different in iOS 17 than previous versions. Fixes https://github.com/flutter/flutter/issues/136747 Example passing on Xcode 15 with iOS 17 simulator: https://ci.chromium.org/ui/p/flutter/builders/try/Mac_arm64%20ios_platform_tests_shard_3%20master/7604/overview --- .../ImagePickerFromGalleryUITests.m | 59 ++++++++++++++++--- .../ImagePickerFromLimitedGalleryUITests.m | 44 ++++++++++++-- 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m index dc5693b286..ddad2f03de 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m @@ -11,24 +11,30 @@ const int kElementWaitingTime = 30; @property(nonatomic, strong) XCUIApplication *app; +@property(nonatomic, assign) BOOL interceptedPermissionInterruption; + @end @implementation ImagePickerFromGalleryUITests - (void)setUp { [super setUp]; - // Delete the app if already exists, to test permission popups self.continueAfterFailure = NO; self.app = [[XCUIApplication alloc] init]; + if (@available(iOS 13.4, *)) { + // Reset the authorization status for Photos to test permission popups + [self.app resetAuthorizationStatusForResource:XCUIProtectedResourcePhotos]; + } [self.app launch]; + self.interceptedPermissionInterruption = NO; __weak typeof(self) weakSelf = self; [self addUIInterruptionMonitorWithDescription:@"Permission popups" handler:^BOOL(XCUIElement *_Nonnull interruptingElement) { if (@available(iOS 14, *)) { XCUIElement *allPhotoPermission = interruptingElement - .buttons[@"Allow Access to All Photos"]; + .buttons[weakSelf.allowAccessPermissionText]; if (![allPhotoPermission waitForExistenceWithTimeout: kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", @@ -50,6 +56,7 @@ const int kElementWaitingTime = 30; } [ok tap]; } + weakSelf.interceptedPermissionInterruption = YES; return YES; }]; } @@ -59,6 +66,46 @@ const int kElementWaitingTime = 30; [self.app terminate]; } +- (NSString *)allowAccessPermissionText { + NSString *fullAccessButtonText = @"Allow Access to All Photos"; + if (@available(iOS 17, *)) { + fullAccessButtonText = @"Allow Full Access"; + } + return fullAccessButtonText; +} + +- (void)handlePermissionInterruption { + // addUIInterruptionMonitorWithDescription is only invoked when trying to interact with an element + // (the app in this case) the alert is blocking. We expect a permission popup here so do a swipe + // up action (which should be harmless). + [self.app swipeUp]; + + if (@available(iOS 17, *)) { + // addUIInterruptionMonitorWithDescription does not work consistently on Xcode 15 simulators, so + // use a backup method of accepting permissions popup. + + if (self.interceptedPermissionInterruption == YES) { + return; + } + + // If cancel button exists, permission has already been given. + XCUIElement *cancelButton = self.app.buttons[@"Cancel"].firstMatch; + if ([cancelButton waitForExistenceWithTimeout:kElementWaitingTime]) { + return; + } + + XCUIApplication *springboardApp = + [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"]; + XCUIElement *allowButton = springboardApp.buttons[self.allowAccessPermissionText]; + if (![allowButton waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find Allow Access button with %@ seconds", + @(kElementWaitingTime)); + } + [allowButton tap]; + } +} + - (void)testCancel { // Find and tap on the pick from gallery button. XCUIElement *imageFromGalleryButton = @@ -80,9 +127,7 @@ const int kElementWaitingTime = 30; [pickButton tap]; - // There is a known bug where the permission popups interruption won't get fired until a tap - // happened in the app. We expect a permission popup so we do a tap here. - [self.app tap]; + [self handlePermissionInterruption]; // Find and tap on the `Cancel` button. XCUIElement *cancelButton = self.app.buttons[@"Cancel"].firstMatch; @@ -151,9 +196,7 @@ const int kElementWaitingTime = 30; } [pickButton tap]; - // There is a known bug where the permission popups interruption won't get fired until a tap - // happened in the app. We expect a permission popup so we do a tap here. - [self.app tap]; + [self handlePermissionInterruption]; // Find an image and tap on it. (IOS 14 UI, images are showing directly) XCUIElement *aImage; diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m index 7cce052021..bae11d5c01 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m @@ -11,16 +11,21 @@ const int kLimitedElementWaitingTime = 30; @property(nonatomic, strong) XCUIApplication *app; +@property(nonatomic, assign) BOOL interceptedPermissionInterruption; + @end @implementation ImagePickerFromLimitedGalleryUITests - (void)setUp { [super setUp]; - // Delete the app if already exists, to test permission popups self.continueAfterFailure = NO; self.app = [[XCUIApplication alloc] init]; + if (@available(iOS 13.4, *)) { + // Reset the authorization status for Photos to test permission popups + [self.app resetAuthorizationStatusForResource:XCUIProtectedResourcePhotos]; + } [self.app launch]; __weak typeof(self) weakSelf = self; [self addUIInterruptionMonitorWithDescription:@"Permission popups" @@ -37,6 +42,7 @@ const int kLimitedElementWaitingTime = 30; @(kLimitedElementWaitingTime)); } [limitedPhotoPermission tap]; + weakSelf.interceptedPermissionInterruption = YES; return YES; }]; } @@ -46,6 +52,38 @@ const int kLimitedElementWaitingTime = 30; [self.app terminate]; } +- (void)handlePermissionInterruption { + // addUIInterruptionMonitorWithDescription is only invoked when trying to interact with an element + // (the app in this case) the alert is blocking. We expect a permission popup here so do a swipe + // up action (which should be harmless). + [self.app swipeUp]; + + if (@available(iOS 17, *)) { + // addUIInterruptionMonitorWithDescription does not work consistently on Xcode 15 simulators, so + // use a backup method of accepting permissions popup. + + if (self.interceptedPermissionInterruption == YES) { + return; + } + + // If cancel button exists, permission has already been given. + XCUIElement *cancelButton = self.app.buttons[@"Cancel"].firstMatch; + if ([cancelButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + return; + } + + XCUIApplication *springboardApp = + [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"]; + XCUIElement *allowButton = springboardApp.buttons[@"Limit Access…"]; + if (![allowButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find Limit Access button with %@ seconds", + @(kLimitedElementWaitingTime)); + } + [allowButton tap]; + } +} + // Test the `Select Photos` button which is available after iOS 14. - (void)testSelectingFromGallery API_AVAILABLE(ios(14)) { // Find and tap on the pick from gallery button. @@ -66,9 +104,7 @@ const int kLimitedElementWaitingTime = 30; } [pickButton tap]; - // There is a known bug where the permission popups interruption won't get fired until a tap - // happened in the app. We expect a permission popup so we do a tap here. - [self.app tap]; + [self handlePermissionInterruption]; // Find an image and tap on it. XCUIElement *aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1];