Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 15 additions & 25 deletions shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,11 @@ - (void)userNotificationCenter:(UNUserNotificationCenter*)center
}
}

static BOOL IsDeepLinkingEnabled(NSDictionary* infoDictionary) {
NSNumber* isEnabled = [infoDictionary objectForKey:@"FlutterDeepLinkingEnabled"];
if (isEnabled) {
return [isEnabled boolValue];
} else {
return NO;
}
}

- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options
infoPlistGetter:(NSDictionary* (^)())infoPlistGetter {
if ([_lifeCycleDelegate application:application openURL:url options:options]) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is now shared between two app delegate methods. Move the method-specific _lifeCycleDelegate check up one level to -application:openURL:options:.

return YES;
} else if (!IsDeepLinkingEnabled(infoPlistGetter())) {
- (BOOL)openURL:(NSURL*)url {
NSNumber* isDeepLinkingEnabled =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to get rid of the static function here. -[NSBundle objectForInfoDictionaryKey:] is now mocked in the tests.

[[NSBundle mainBundle] objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"];
if (!isDeepLinkingEnabled.boolValue) {
// Not set or NO.
return NO;
} else {
FlutterViewController* flutterViewController = [self rootFlutterViewController];
Expand Down Expand Up @@ -181,12 +170,10 @@ - (BOOL)application:(UIApplication*)application
- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
return [self application:application
openURL:url
options:options
infoPlistGetter:^NSDictionary*() {
return [[NSBundle mainBundle] infoDictionary];
}];
if ([_lifeCycleDelegate application:application openURL:url options:options]) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(it was moved here)

return YES;
}
return [self openURL:url];
}

- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
Expand Down Expand Up @@ -229,9 +216,12 @@ - (BOOL)application:(UIApplication*)application
continueUserActivity:(NSUserActivity*)userActivity
restorationHandler:(void (^)(NSArray* __nullable restorableObjects))restorationHandler {
#endif
return [_lifeCycleDelegate application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
if ([_lifeCycleDelegate application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler]) {
return YES;
}
return [self openURL:userActivity.webpageURL];
}

#pragma mark - FlutterPluginRegistry methods. All delegating to the rootViewController
Expand Down
159 changes: 102 additions & 57 deletions shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -14,84 +14,129 @@
FLUTTER_ASSERT_ARC

@interface FlutterAppDelegateTest : XCTestCase
@property(strong) FlutterAppDelegate* appDelegate;

@property(strong) id mockMainBundle;
@property(strong) id mockNavigationChannel;

// Retain callback until the tests are done.
// https://github.com/flutter/flutter/issues/74267
@property(strong) id mockEngineFirstFrameCallback;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only just landed #27854 but this is probably a better name than blockNoInvoker. Open to suggestions though!

@end

@implementation FlutterAppDelegateTest

- (void)testLaunchUrl {
- (void)setUp {
[super setUp];

id mockMainBundle = OCMClassMock([NSBundle class]);
OCMStub([mockMainBundle mainBundle]).andReturn(mockMainBundle);
self.mockMainBundle = mockMainBundle;

FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init];
self.appDelegate = appDelegate;

FlutterViewController* viewController = OCMClassMock([FlutterViewController class]);
FlutterEngine* engine = OCMClassMock([FlutterEngine class]);
FlutterMethodChannel* navigationChannel = OCMClassMock([FlutterMethodChannel class]);
self.mockNavigationChannel = navigationChannel;

FlutterEngine* engine = OCMClassMock([FlutterEngine class]);
OCMStub([engine navigationChannel]).andReturn(navigationChannel);
OCMStub([viewController engine]).andReturn(engine);
// Set blockNoInvoker to a strong local to retain to end of scope.
id blockNoInvoker = [OCMArg invokeBlockWithArgs:@NO, nil];
OCMStub([engine waitForFirstFrame:3.0 callback:blockNoInvoker]);

id mockEngineFirstFrameCallback = [OCMArg invokeBlockWithArgs:@NO, nil];
self.mockEngineFirstFrameCallback = mockEngineFirstFrameCallback;
OCMStub([engine waitForFirstFrame:3.0 callback:mockEngineFirstFrameCallback]);
appDelegate.rootFlutterViewControllerGetter = ^{
return viewController;
};
NSURL* url = [NSURL URLWithString:@"http://myApp/custom/route?query=test"];
NSDictionary<UIApplicationOpenURLOptionsKey, id>* options = @{};
BOOL result = [appDelegate application:[UIApplication sharedApplication]
openURL:url
options:options
infoPlistGetter:^NSDictionary*() {
return @{@"FlutterDeepLinkingEnabled" : @(YES)};
}];
}

- (void)tearDown {
// Explicitly stop mocking the NSBundle class property.
[self.mockMainBundle stopMocking];
[super tearDown];
}

- (void)testLaunchUrl {
OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"])
.andReturn(@YES);

BOOL result =
[self.appDelegate application:[UIApplication sharedApplication]
openURL:[NSURL URLWithString:@"http://myApp/custom/route?query=test"]
options:@{}];
XCTAssertTrue(result);
OCMVerify([navigationChannel invokeMethod:@"pushRoute" arguments:@"/custom/route?query=test"]);
OCMVerify([self.mockNavigationChannel invokeMethod:@"pushRoute"
arguments:@"/custom/route?query=test"]);
}

- (void)testLaunchUrlWithDeepLinkingNotSet {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New test.

OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"])
.andReturn(nil);

BOOL result =
[self.appDelegate application:[UIApplication sharedApplication]
openURL:[NSURL URLWithString:@"http://myApp/custom/route?query=test"]
options:@{}];
XCTAssertFalse(result);
OCMReject([self.mockNavigationChannel invokeMethod:OCMOCK_ANY arguments:OCMOCK_ANY]);
}

- (void)testLaunchUrlWithDeepLinkingDisabled {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New test.

OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"])
.andReturn(@NO);

BOOL result =
[self.appDelegate application:[UIApplication sharedApplication]
openURL:[NSURL URLWithString:@"http://myApp/custom/route?query=test"]
options:@{}];
XCTAssertFalse(result);
OCMReject([self.mockNavigationChannel invokeMethod:OCMOCK_ANY arguments:OCMOCK_ANY]);
}

- (void)testLaunchUrlWithQueryParameterAndFragment {
FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init];
FlutterViewController* viewController = OCMClassMock([FlutterViewController class]);
FlutterEngine* engine = OCMClassMock([FlutterEngine class]);
FlutterMethodChannel* navigationChannel = OCMClassMock([FlutterMethodChannel class]);
OCMStub([engine navigationChannel]).andReturn(navigationChannel);
OCMStub([viewController engine]).andReturn(engine);
// Set blockNoInvoker to a strong local to retain to end of scope.
id blockNoInvoker = [OCMArg invokeBlockWithArgs:@NO, nil];
OCMStub([engine waitForFirstFrame:3.0 callback:blockNoInvoker]);
appDelegate.rootFlutterViewControllerGetter = ^{
return viewController;
};
NSURL* url = [NSURL URLWithString:@"http://myApp/custom/route?query=test#fragment"];
NSDictionary<UIApplicationOpenURLOptionsKey, id>* options = @{};
BOOL result = [appDelegate application:[UIApplication sharedApplication]
openURL:url
options:options
infoPlistGetter:^NSDictionary*() {
return @{@"FlutterDeepLinkingEnabled" : @(YES)};
}];
OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"])
.andReturn(@YES);

BOOL result = [self.appDelegate
application:[UIApplication sharedApplication]
openURL:[NSURL URLWithString:@"http://myApp/custom/route?query=test#fragment"]
options:@{}];
XCTAssertTrue(result);
OCMVerify([navigationChannel invokeMethod:@"pushRoute"
arguments:@"/custom/route?query=test#fragment"]);
OCMVerify([self.mockNavigationChannel invokeMethod:@"pushRoute"
arguments:@"/custom/route?query=test#fragment"]);
}

- (void)testLaunchUrlWithFragmentNoQueryParameter {
FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init];
FlutterViewController* viewController = OCMClassMock([FlutterViewController class]);
FlutterEngine* engine = OCMClassMock([FlutterEngine class]);
FlutterMethodChannel* navigationChannel = OCMClassMock([FlutterMethodChannel class]);
OCMStub([engine navigationChannel]).andReturn(navigationChannel);
OCMStub([viewController engine]).andReturn(engine);
// Set blockNoInvoker to a strong local to retain to end of scope.
id blockNoInvoker = [OCMArg invokeBlockWithArgs:@NO, nil];
OCMStub([engine waitForFirstFrame:3.0 callback:blockNoInvoker]);
appDelegate.rootFlutterViewControllerGetter = ^{
return viewController;
};
NSURL* url = [NSURL URLWithString:@"http://myApp/custom/route#fragment"];
NSDictionary<UIApplicationOpenURLOptionsKey, id>* options = @{};
BOOL result = [appDelegate application:[UIApplication sharedApplication]
openURL:url
options:options
infoPlistGetter:^NSDictionary*() {
return @{@"FlutterDeepLinkingEnabled" : @(YES)};
}];
OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"])
.andReturn(@YES);

BOOL result =
[self.appDelegate application:[UIApplication sharedApplication]
openURL:[NSURL URLWithString:@"http://myApp/custom/route#fragment"]
options:@{}];
XCTAssertTrue(result);
OCMVerify([self.mockNavigationChannel invokeMethod:@"pushRoute"
arguments:@"/custom/route#fragment"]);
}

#pragma mark - Deep linking

- (void)testUniversalLinkPushRoute {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 tests are very similar, maybe it's worth considering pulling out a helper function to remove some of the boilerplate so the differences are more apparent.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pulled the boilderplate in all these tests up into setUp. Hopefully it's easier to read now.

OCMStub([self.mockMainBundle objectForInfoDictionaryKey:@"FlutterDeepLinkingEnabled"])
.andReturn(@YES);

NSUserActivity* userActivity = [[NSUserActivity alloc] initWithActivityType:@"com.example.test"];
userActivity.webpageURL = [NSURL URLWithString:@"http://myApp/custom/route?query=test"];
BOOL result = [self.appDelegate
application:[UIApplication sharedApplication]
continueUserActivity:userActivity
restorationHandler:^(NSArray<id<UIUserActivityRestoring>>* __nullable restorableObjects){
}];
XCTAssertTrue(result);
OCMVerify([navigationChannel invokeMethod:@"pushRoute" arguments:@"/custom/route#fragment"]);
OCMVerify([self.mockNavigationChannel invokeMethod:@"pushRoute"
arguments:@"/custom/route?query=test"]);
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,4 @@
@interface FlutterAppDelegate (Test)
@property(nonatomic, copy) FlutterViewController* (^rootFlutterViewControllerGetter)(void);

- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options
infoPlistGetter:(NSDictionary* (^)())infoPlistGetter;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wound up getting rid of this test injection helper and mocking out -[NSBundle objectForInfoDictionaryKey:] instead.


@end
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
0D6AB73E22BD8F0200EEE540 /* FlutterEngineConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FlutterEngineConfig.xcconfig; sourceTree = "<group>"; };
F7521D7226BB671E005F15C5 /* libios_test_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libios_test_flutter.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libios_test_flutter.dylib"; sourceTree = "<group>"; };
F7521D7526BB673E005F15C5 /* libocmock_shared.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libocmock_shared.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libocmock_shared.dylib"; sourceTree = "<group>"; };
F7A3FDE026B9E0A300EADD61 /* FlutterAppDelegateTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterAppDelegateTest.mm; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -93,6 +94,7 @@
0AC232E924BA71D300A85907 /* Source */ = {
isa = PBXGroup;
children = (
F7A3FDE026B9E0A300EADD61 /* FlutterAppDelegateTest.mm */,
0AC232F424BA71D300A85907 /* SemanticsObjectTest.mm */,
0AC232F724BA71D300A85907 /* FlutterEngineTest.mm */,
0AC2330324BA71D300A85907 /* accessibility_bridge_test.mm */,
Expand Down