[image_picker] Add native tests for pick results, camera access, and presentation - #12539
[image_picker] Add native tests for pick results, camera access, and presentation#12539victogomez-cs wants to merge 2 commits into
Conversation
…and presentation.
There was a problem hiding this comment.
Code Review
This pull request adds comprehensive unit tests to ImagePickerPluginTests.m covering image and video picking results, camera access permissions, image quality clamping, and view controller presentation. The review feedback suggests refactoring several tests to avoid relying on UIApplication.sharedApplication.connectedScenes and real UIWindow instances, recommending instead the use of mock view controllers or initializing UIWindow with a simple frame to ensure tests are robust and run reliably in headless environments.
| UIWindowScene *scene = | ||
| (UIWindowScene *)UIApplication.sharedApplication.connectedScenes.allObjects.firstObject; | ||
| UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene]; | ||
| window.frame = scene.coordinateSpace.bounds; | ||
| UIViewController *rootViewController = [[UIViewController alloc] init]; | ||
| window.rootViewController = rootViewController; | ||
| [rootViewController loadViewIfNeeded]; | ||
| [window makeKeyAndVisible]; | ||
|
|
||
| FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] | ||
| initWithViewProvider:[[StubViewProvider alloc] initWithViewController:rootViewController]]; | ||
| [plugin setImagePickerControllerOverrides:@[ [[UIImagePickerController alloc] init] ]]; | ||
|
|
||
| XCTestExpectation *resultExpectation = [self expectationWithDescription:@"unavailable"]; | ||
| [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera | ||
| camera:FLTSourceCameraRear] | ||
| maxSize:[[FLTMaxSize alloc] init] | ||
| quality:nil | ||
| fullMetadata:YES | ||
| completion:^(NSString *result, FlutterError *error) { | ||
| XCTAssertNil(result); | ||
| [resultExpectation fulfill]; | ||
| }]; | ||
| [self waitForExpectationsWithTimeout:30 handler:nil]; | ||
|
|
||
| UIAlertController *alert = (UIAlertController *)rootViewController.presentedViewController; | ||
| XCTAssertTrue([alert isKindOfClass:[UIAlertController class]]); | ||
| void (^handler)(UIAlertAction *) = [alert.actions.firstObject valueForKey:@"handler"]; | ||
| if (handler) { | ||
| handler(alert.actions.firstObject); | ||
| } |
There was a problem hiding this comment.
Relying on UIApplication.sharedApplication.connectedScenes and creating/making key a real UIWindow is fragile, slow, and will fail in headless environments or logic tests where no active scene or host application window exists. Instead, you can use a mock UIViewController to capture and verify the presented UIAlertController and its action handler directly. This is faster, more robust, and completely independent of the window hierarchy.
id mockViewController = OCMClassMock([UIViewController class]);
__block UIAlertController *alert = nil;
OCMStub([mockViewController presentViewController:[OCMArg any] animated:YES completion:nil])
.andDo(^(NSInvocation *invocation) {
[invocation getArgument:&alert atIndex:2];
});
FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc]
initWithViewProvider:[[StubViewProvider alloc] initWithViewController:mockViewController]];
[plugin setImagePickerControllerOverrides:@[ [[UIImagePickerController alloc] init] ]];
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"unavailable"];
[plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera
camera:FLTSourceCameraRear]
maxSize:[[FLTMaxSize alloc] init]
quality:nil
fullMetadata:YES
completion:^(NSString *result, FlutterError *error) {
XCTAssertNil(result);
[resultExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:30 handler:nil];
XCTAssertTrue([alert isKindOfClass:[UIAlertController class]]);
void (^handler)(UIAlertAction *) = [alert.actions.firstObject valueForKey:@"handler"];
if (handler) {
handler(alert.actions.firstObject);
}There was a problem hiding this comment.
Agreed and fixed. This test now uses an OCMock UIViewController to capture the presented UIAlertController instead of creating a real key window from UIApplication.sharedApplication.connectedScenes.
| UIWindowScene *scene = | ||
| (UIWindowScene *)UIApplication.sharedApplication.connectedScenes.allObjects.firstObject; | ||
| UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene]; | ||
| window.frame = scene.coordinateSpace.bounds; | ||
| UIViewController *rootViewController = [[UIViewController alloc] init]; | ||
| window.rootViewController = rootViewController; | ||
| [rootViewController loadViewIfNeeded]; | ||
| [window makeKeyAndVisible]; |
There was a problem hiding this comment.
Using UIApplication.sharedApplication.connectedScenes to initialize the UIWindow is fragile and will fail in headless environments or logic tests where connectedScenes is empty. You can initialize the UIWindow with a simple frame instead, which is fully supported and doesn't rely on active scenes or host application state.
| UIWindowScene *scene = | |
| (UIWindowScene *)UIApplication.sharedApplication.connectedScenes.allObjects.firstObject; | |
| UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene]; | |
| window.frame = scene.coordinateSpace.bounds; | |
| UIViewController *rootViewController = [[UIViewController alloc] init]; | |
| window.rootViewController = rootViewController; | |
| [rootViewController loadViewIfNeeded]; | |
| [window makeKeyAndVisible]; | |
| UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; | |
| UIViewController *rootViewController = [[UIViewController alloc] init]; | |
| window.rootViewController = rootViewController; | |
| [rootViewController loadViewIfNeeded]; |
There was a problem hiding this comment.
Agreed and fixed. The window is now created with initWithFrame: so the test doesn’t depend on an active UIWindowScene.
| UIWindowScene *scene = | ||
| (UIWindowScene *)UIApplication.sharedApplication.connectedScenes.allObjects.firstObject; | ||
| UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene]; | ||
| window.frame = scene.coordinateSpace.bounds; | ||
| UIViewController *rootViewController = [[UIViewController alloc] init]; | ||
| window.rootViewController = rootViewController; | ||
| [rootViewController loadViewIfNeeded]; | ||
| [window makeKeyAndVisible]; | ||
|
|
||
| id mockWindow = OCMPartialMock(window); | ||
| OCMStub([mockWindow windowScene]).andReturn(nil); | ||
|
|
||
| FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] | ||
| initWithViewProvider:[[StubViewProvider alloc] initWithViewController:rootViewController]]; | ||
| XCTAssertNotNil([plugin presentingViewControllerForImagePickerInNewWindow]); | ||
| [plugin removeInteractionBlocker]; | ||
| [mockWindow stopMocking]; |
There was a problem hiding this comment.
By initializing the UIWindow with initWithFrame:, the window's windowScene is nil by default on iOS 13+. This allows you to test the fallback frame path directly without needing to query connectedScenes, instantiate a scene, or use OCMPartialMock to stub windowScene to return nil.
| UIWindowScene *scene = | |
| (UIWindowScene *)UIApplication.sharedApplication.connectedScenes.allObjects.firstObject; | |
| UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene]; | |
| window.frame = scene.coordinateSpace.bounds; | |
| UIViewController *rootViewController = [[UIViewController alloc] init]; | |
| window.rootViewController = rootViewController; | |
| [rootViewController loadViewIfNeeded]; | |
| [window makeKeyAndVisible]; | |
| id mockWindow = OCMPartialMock(window); | |
| OCMStub([mockWindow windowScene]).andReturn(nil); | |
| FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] | |
| initWithViewProvider:[[StubViewProvider alloc] initWithViewController:rootViewController]]; | |
| XCTAssertNotNil([plugin presentingViewControllerForImagePickerInNewWindow]); | |
| [plugin removeInteractionBlocker]; | |
| [mockWindow stopMocking]; | |
| UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; | |
| UIViewController *rootViewController = [[UIViewController alloc] init]; | |
| window.rootViewController = rootViewController; | |
| [rootViewController loadViewIfNeeded]; | |
| FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] | |
| initWithViewProvider:[[StubViewProvider alloc] initWithViewController:rootViewController]]; | |
| XCTAssertNotNil([plugin presentingViewControllerForImagePickerInNewWindow]); | |
| [plugin removeInteractionBlocker]; |
There was a problem hiding this comment.
Agreed and fixed. initWithFrame: already leaves windowScene nil, so this covers the frame fallback without a partial mock.
…ontroller for alert presentation
Adds native unit tests for previously untested paths in
FLTImagePickerPlugin.m(pick-result handling, camera authorization, and presentation) before the Objective-C → Swift migration. Production code is unchanged. New cases follow the existingStubViewProvider/ OCMock conventions inImagePickerPluginTests.m.This is a tests-only change, so it does not bump the package version or CHANGELOG.
ImagePickerPluginTestsgo from 33 tests to 52 tests (+19).New test cases
Pick results / dismiss:
testPickImageInvalidResultWhenMultiplePathsReturned— image pick with multiple paths uses the existinginvalid_resultdouble-completion behaviortestPickVideoInvalidResultWhenMultiplePathsReturned— same for video picktestPHPickerCancelSendsEmptyPathList— PHPicker cancel completes with an empty path listtestPresentationControllerDidDismissSendsEmptyPathList— dismiss completes with an empty path listImage quality:
testDesiredImageQualityClampsOutOfRangeValues— quality below 0 is clampedtestDesiredImageQualityScalesValidPercent— a valid percent is scaledtestDesiredImageQualityOver100IsClamped— quality above 100 is clampedCamera access:
testCameraAccessDeniedReturnsError— denied authorization returns an errortestCameraAccessRestrictedReturnsError— restricted authorization returns an errortestCameraAccessNotDeterminedDenied— prompt denied returns an errortestCameraAccessNotDeterminedGrantedPresentsCamera— prompt granted presents the cameratestCameraAccessUnknownStatusTreatedAsDenied— unknown status is treated as deniedtestShowCameraWhenUnavailableSendsNilPathList— unavailable camera completes with a nil path listtestShowCameraReturnsEarlyWhenAlreadyBeingPresented— a second present is a no-optestShowCameraUnavailableAlertOKHandler— the unavailable-camera alert OK handler runsPresentation / blocker window:
testPresentingViewControllerWithoutWindowReturnsHostController— no window returns the host view controllertestPresentingViewControllerReusesExistingBlockerWindow— an existing interaction-blocker window is reusedtestPresentingViewControllerWithoutWindowSceneUsesFrame— missing window scene falls back to a frametestDefaultViewProviderReturnsRegistrarViewController— the default view provider returns the registrar’s view controllerSecond part of
image_picker_ioscoverage backfill before the Obj-C → Swift migration for flutter/flutter#119107Pre-Review Checklist
[shared_preferences]///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.Footnotes
Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2