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
Original file line number Diff line number Diff line change
Expand Up @@ -537,23 +537,32 @@ - (void)applicationBecameActive:(NSNotification*)notification {
TRACE_EVENT0("flutter", "applicationBecameActive");
if (_viewportMetrics.physical_width)
[self surfaceUpdated:YES];
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"];
[self goToApplicationLifecycle:@"AppLifecycleState.resumed"];
}

- (void)applicationWillResignActive:(NSNotification*)notification {
TRACE_EVENT0("flutter", "applicationWillResignActive");
[self surfaceUpdated:NO];
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
[self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
}

- (void)applicationDidEnterBackground:(NSNotification*)notification {
TRACE_EVENT0("flutter", "applicationDidEnterBackground");
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"];
[self goToApplicationLifecycle:@"AppLifecycleState.paused"];
}

- (void)applicationWillEnterForeground:(NSNotification*)notification {
TRACE_EVENT0("flutter", "applicationWillEnterForeground");
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
[self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
}

// Make this transition only while this current view controller is visible.
- (void)goToApplicationLifecycle:(nonnull NSString*)state {
// Accessing self.view will create the view. Check whether the view is organically loaded
// first before checking whether the view is attached to window.
if (self.isViewLoaded && self.view.window) {
[[_engine.get() lifecycleChannel] sendMessage:state];
}
}

#pragma mark - Touch event handling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

@implementation ScreenBeforeFlutter

FlutterEngine* _engine;
@synthesize engine = _engine;

- (id)initWithEngineRunCompletion:(void (^)(void))engineRunCompletion {
self = [super init];
Expand Down
228 changes: 151 additions & 77 deletions testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@
#import <XCTest/XCTest.h>
#import "ScreenBeforeFlutter.h"

@interface XCAppLifecycleTestExpectation : XCTestExpectation

- (instancetype)initForLifecycle:(NSString*)expectedLifecycle forStep:(NSString*)step;
@property(nonatomic, readonly, copy) NSString* expectedLifecycle;

@end

@implementation XCAppLifecycleTestExpectation

@synthesize expectedLifecycle = _expectedLifecycle;
- (instancetype)initForLifecycle:(NSString*)expectedLifecycle forStep:(NSString*)step {
// The step is here because the callbacks into the handler which checks these expectations isn't
// synchronous with the executions in the test, so it's hard to find the cause in the test
// otherwise.
self = [super initWithDescription:[NSString stringWithFormat:@"Expected state %@ during step %@",
expectedLifecycle, step]];
_expectedLifecycle = [expectedLifecycle copy];
return self;
}

@end

@interface AppLifecycleTests : XCTestCase
@end

Expand All @@ -16,7 +38,7 @@ - (void)setUp {
self.continueAfterFailure = NO;
}

- (void)testLifecycleChannel {
- (void)testDismissedFlutterViewControllerNotRespondingToApplicationLifecycle {
XCTestExpectation* engineStartedExpectation = [self expectationWithDescription:@"Engine started"];

// Let the engine finish booting (at the end of which the channels are properly set-up) before
Expand All @@ -32,121 +54,173 @@ - (void)testLifecycleChannel {
FlutterEngine* engine = rootVC.engine;

NSMutableArray* lifecycleExpectations = [NSMutableArray arrayWithCapacity:10];
NSMutableArray* lifecycleEvents = [NSMutableArray arrayWithCapacity:10];

[lifecycleExpectations addObject:[[XCTestExpectation alloc]
initWithDescription:@"A loading FlutterViewController goes "
@"through AppLifecycleState.inactive"]];
[lifecycleExpectations
addObject:[[XCTestExpectation alloc]
initWithDescription:
@"A loading FlutterViewController goes through AppLifecycleState.resumed"]];

// Expected sequence from showing the FlutterViewController is inactive and resumed.
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"showing a FlutterViewController"],
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.resumed"
forStep:@"showing a FlutterViewController"]
]];

// Holding onto this FlutterViewController is consequential here. Since a released
// FlutterViewController wouldn't keep listening to the application lifecycle events and produce
// false positives for the application lifecycle tests further below.
FlutterViewController* flutterVC = [rootVC showFlutter];
[engine.lifecycleChannel setMessageHandler:^(id message, FlutterReply callback) {
if (lifecycleExpectations.count == 0) {
XCTFail(@"Unexpected lifecycle transition: %@", message);
return;
}
XCAppLifecycleTestExpectation* nextExpectation = [lifecycleExpectations objectAtIndex:0];
if (![[nextExpectation expectedLifecycle] isEqualToString:message]) {
XCTFail(@"Expected lifecycle %@ but instead received %@", [nextExpectation expectedLifecycle],
message);
return;
}
[lifecycleEvents addObject:message];
[[lifecycleExpectations objectAtIndex:0] fulfill];

[nextExpectation fulfill];
[lifecycleExpectations removeObjectAtIndex:0];
}];

[self waitForExpectations:lifecycleExpectations timeout:5];

// Expected sequence from showing the FlutterViewController is inactive and resumed.
NSArray* expectedStates = @[ @"AppLifecycleState.inactive", @"AppLifecycleState.resumed" ];
XCTAssertEqualObjects(lifecycleEvents, expectedStates,
@"AppLifecycleState transitions while presenting not as expected");
// The expectations list isn't dequeued by the message handler yet.
[self waitForExpectations:lifecycleExpectations timeout:5 enforceOrder:YES];

// Now dismiss the FlutterViewController again and expect another inactive and paused.
[lifecycleExpectations
addObject:[[XCTestExpectation alloc]
initWithDescription:@"A dismissed FlutterViewController goes through "
@"AppLifecycleState.inactive"]];
[lifecycleExpectations
addObject:[[XCTestExpectation alloc]
initWithDescription:@"A dismissed FlutterViewController goes through "
@"AppLifecycleState.paused"]];
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"dismissing a FlutterViewController"],
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.paused"
forStep:@"dismissing a FlutterViewController"]
]];
[flutterVC dismissViewControllerAnimated:NO completion:nil];
[self waitForExpectations:lifecycleExpectations timeout:5];
expectedStates = @[
@"AppLifecycleState.inactive", @"AppLifecycleState.resumed", @"AppLifecycleState.inactive",
@"AppLifecycleState.paused"
];
XCTAssertEqualObjects(lifecycleEvents, expectedStates,
@"AppLifecycleState transitions while dismissing not as expected");
[self waitForExpectations:lifecycleExpectations timeout:5 enforceOrder:YES];

// Now put the app in the background (while the engine is still running) and bring it back to
// the foreground. Granted, we're not winning any awards for hyper-realism but at least we're
// checking that we aren't observing the UIApplication notifications and double registering
// for AppLifecycleState events.

// However the production is temporarily wrong. https://github.com/flutter/flutter/issues/37226.
// It will be fixed in a next PR that removes the wrong asserts.
[lifecycleExpectations
addObject:
[[XCTestExpectation alloc]
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
// These operations are synchronous so if they trigger any lifecycle events, they should trigger
// failures in the message handler immediately.
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationWillResignActiveNotification
object:nil];
[lifecycleExpectations
addObject:
[[XCTestExpectation alloc]
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidEnterBackgroundNotification
object:nil];
[lifecycleExpectations
addObject:
[[XCTestExpectation alloc]
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationWillEnterForegroundNotification
object:nil];
[lifecycleExpectations
addObject:
[[XCTestExpectation alloc]
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification
object:nil];

// There's no timing latch for our semi-fake background-foreground cycle so launch the
// FlutterViewController again to check the complete event list again.
[lifecycleExpectations addObject:[[XCTestExpectation alloc]
initWithDescription:@"A second FlutterViewController goes "
@"through AppLifecycleState.inactive"]];
[lifecycleExpectations
addObject:[[XCTestExpectation alloc]
initWithDescription:
@"A second FlutterViewController goes through AppLifecycleState.resumed"]];

// Expect only lifecycle events from showing the FlutterViewController again, not from any
// backgrounding/foregrounding.
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"showing a FlutterViewController a second time after backgrounding"],
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.resumed"
forStep:@"showing a FlutterViewController a second time after backgrounding"]
]];
flutterVC = [rootVC showFlutter];
[self waitForExpectations:lifecycleExpectations timeout:5 enforceOrder:YES];

// Dismantle.
[engine.lifecycleChannel setMessageHandler:nil];
[flutterVC dismissViewControllerAnimated:NO completion:nil];
[engine setViewController:nil];
}

- (void)testVisibleFlutterViewControllerRespondsToApplicationLifecycle {
XCTestExpectation* engineStartedExpectation = [self expectationWithDescription:@"Engine started"];

// Let the engine finish booting (at the end of which the channels are properly set-up) before
// moving onto the next step of showing the next view controller.
ScreenBeforeFlutter* rootVC = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:^void() {
[engineStartedExpectation fulfill];
}];

[self waitForExpectationsWithTimeout:5 handler:nil];

UIApplication* application = UIApplication.sharedApplication;
application.delegate.window.rootViewController = rootVC;
FlutterEngine* engine = rootVC.engine;

NSMutableArray* lifecycleExpectations = [NSMutableArray arrayWithCapacity:10];

// Expected sequence from showing the FlutterViewController is inactive and resumed.
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"showing a FlutterViewController"],
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.resumed"
forStep:@"showing a FlutterViewController"]
]];

FlutterViewController* flutterVC = [rootVC showFlutter];
[engine.lifecycleChannel setMessageHandler:^(id message, FlutterReply callback) {
if (lifecycleExpectations.count == 0) {
XCTFail(@"Unexpected lifecycle transition: %@", message);
return;
}
XCAppLifecycleTestExpectation* nextExpectation = [lifecycleExpectations objectAtIndex:0];
if (![[nextExpectation expectedLifecycle] isEqualToString:message]) {
XCTFail(@"Expected lifecycle %@ but instead received %@", [nextExpectation expectedLifecycle],
message);
return;
}

[nextExpectation fulfill];
[lifecycleExpectations removeObjectAtIndex:0];
}];

[self waitForExpectations:lifecycleExpectations timeout:5];

// Now put the FlutterViewController into background.
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"putting FlutterViewController to the background"],
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.paused"
forStep:@"putting FlutterViewController to the background"]
]];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidEnterBackgroundNotification
object:nil];
[self waitForExpectations:lifecycleExpectations timeout:5];

// Now restore to foreground
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"putting FlutterViewController back to foreground"],
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.resumed"
forStep:@"putting FlutterViewController back to foreground"]
]];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationWillEnterForegroundNotification
object:nil];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification
object:nil];
[self waitForExpectations:lifecycleExpectations timeout:5];
expectedStates = @[
@"AppLifecycleState.inactive", @"AppLifecycleState.resumed", @"AppLifecycleState.inactive",
@"AppLifecycleState.paused",

// The production code currently misbehaves. https://github.com/flutter/flutter/issues/37226.
// It will be fixed in a next PR that removes the wrong asserts.
@"AppLifecycleState.inactive", @"AppLifecycleState.paused", @"AppLifecycleState.inactive",
@"AppLifecycleState.resumed",

// We only added 2 from re-launching the FlutterViewController
// and none from the background-foreground cycle.
@"AppLifecycleState.inactive", @"AppLifecycleState.resumed"
];
XCTAssertEqualObjects(
lifecycleEvents, expectedStates,
@"AppLifecycleState transitions while presenting a second time not as expected");

// Dismantle.
[engine.lifecycleChannel setMessageHandler:nil];
[flutterVC dismissViewControllerAnimated:NO completion:nil];
flutterVC = nil;
[engine setViewController:nil];
[rootVC dismissViewControllerAnimated:NO completion:nil];
rootVC = nil;
}

@end