[go: nahoru, domu]

blob: b64773ff005019d09a1a4f00c31548d4248e7a31 [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/app/application_delegate/app_state.h"
#import <memory>
#import "base/apple/foundation_util.h"
#import "base/functional/bind.h"
#import "base/ios/block_types.h"
#import "base/ios/ios_util.h"
#import "base/test/task_environment.h"
#import "ios/chrome/app/app_startup_parameters.h"
#import "ios/chrome/app/application_delegate/app_state+private.h"
#import "ios/chrome/app/application_delegate/app_state_observer.h"
#import "ios/chrome/app/application_delegate/fake_startup_information.h"
#import "ios/chrome/app/application_delegate/memory_warning_helper.h"
#import "ios/chrome/app/application_delegate/metrics_mediator.h"
#import "ios/chrome/app/application_delegate/mock_tab_opener.h"
#import "ios/chrome/app/application_delegate/startup_information.h"
#import "ios/chrome/app/application_delegate/user_activity_handler.h"
#import "ios/chrome/app/enterprise_app_agent.h"
#import "ios/chrome/app/safe_mode_app_state_agent+private.h"
#import "ios/chrome/app/safe_mode_app_state_agent.h"
#import "ios/chrome/browser/crash_report/crash_helper.h"
#import "ios/chrome/browser/device_sharing/device_sharing_manager.h"
#import "ios/chrome/browser/shared/coordinator/scene/connection_information.h"
#import "ios/chrome/browser/shared/coordinator/scene/test/fake_scene_state.h"
#import "ios/chrome/browser/shared/coordinator/scene/test/stub_browser_provider.h"
#import "ios/chrome/browser/shared/coordinator/scene/test/stub_browser_provider_interface.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider_interface.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/browser_state/test_chrome_browser_state.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/ui/safe_mode/safe_mode_coordinator.h"
#import "ios/chrome/browser/ui/settings/settings_navigation_controller.h"
#import "ios/chrome/common/crash_report/crash_helper.h"
#import "ios/chrome/test/block_cleanup_test.h"
#import "ios/chrome/test/providers/app_distribution/test_app_distribution.h"
#import "ios/chrome/test/scoped_key_window.h"
#import "ios/public/provider/chrome/browser/app_distribution/app_distribution_api.h"
#import "ios/testing/ocmock_complex_type_helper.h"
#import "ios/testing/scoped_block_swizzler.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
// Subclass of AppState that allow returning a fake list of connected scenes.
@interface TestAppState : AppState
- (instancetype)
initWithStartupInformation:(id<StartupInformation>)startupInformation
connectedScenes:(NSArray<SceneState*>*)connectedScenes
NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithStartupInformation:
(id<StartupInformation>)startupInformation NS_UNAVAILABLE;
@end
@implementation TestAppState {
NSArray<SceneState*>* _connectedScenes;
}
- (instancetype)
initWithStartupInformation:(id<StartupInformation>)startupInformation
connectedScenes:(NSArray<SceneState*>*)connectedScenes {
if ((self = [super initWithStartupInformation:startupInformation])) {
_connectedScenes = connectedScenes ? [connectedScenes copy] : @[];
}
return self;
}
- (NSArray<SceneState*>*)connectedScenes {
return _connectedScenes;
}
@end
// App state observer that is used to replace the main controller to transition
// through stages.
@interface AppStateObserverToMockMainController : NSObject <AppStateObserver>
@end
@implementation AppStateObserverToMockMainController
- (void)appState:(AppState*)appState
didTransitionFromInitStage:(InitStage)previousInitStage {
switch (appState.initStage) {
case InitStageStart:
[appState queueTransitionToNextInitStage];
break;
case InitStageBrowserBasic:
break;
case InitStageSafeMode:
break;
case InitStageVariationsSeed:
[appState queueTransitionToNextInitStage];
break;
case InitStageBrowserObjectsForBackgroundHandlers:
[appState queueTransitionToNextInitStage];
break;
case InitStageEnterprise:
break;
case InitStageBrowserObjectsForUI:
[appState queueTransitionToNextInitStage];
break;
case InitStageNormalUI:
[appState queueTransitionToNextInitStage];
break;
case InitStageFirstRun:
[appState queueTransitionToNextInitStage];
break;
case InitStageFinal:
break;
}
}
@end
#pragma mark - Class definition.
namespace {
// A block that takes self as argument and return a BOOL.
typedef BOOL (^DecisionBlock)(id self);
// A block that takes the arguments of UserActivityHandler's
// +handleStartupParametersWithTabOpener.
typedef void (^HandleStartupParam)(
id self,
id<TabOpening> tabOpener,
id<ConnectionInformation> connectionInformation,
id<StartupInformation> startupInformation,
ChromeBrowserState* browserState);
// A block ths returns values of AppState connectedScenes.
typedef NSArray<SceneState*>* (^ScenesBlock)(id self);
} // namespace
// An app state observer that will call [AppState
// queueTransitionToNextInitStage] once (when a flag is set) from one of
// willTransitionToInitStage: and didTransitionFromInitStage: Defaults to
// willTransitioin.
@interface AppStateTransitioningObserver : NSObject <AppStateObserver>
// When set, will call queueTransitionToNextInitStage on
// didTransitionFromInitStage; otherwise, on willTransitionToInitStage
@property(nonatomic, assign) BOOL triggerOnDidTransition;
// Will do nothing when this is not set.
// Will call queueTransitionToNextInitStage on correct callback and reset this
// flag when it's set. The flag is init to YES when the object is created.
@property(nonatomic, assign) BOOL needsQueueTransition;
@end
@implementation AppStateTransitioningObserver
- (instancetype)init {
self = [super init];
if (self) {
_needsQueueTransition = YES;
}
return self;
}
- (void)appState:(AppState*)appState
willTransitionToInitStage:(InitStage)nextInitStage {
if (self.needsQueueTransition && !self.triggerOnDidTransition) {
[appState queueTransitionToNextInitStage];
self.needsQueueTransition = NO;
}
}
- (void)appState:(AppState*)appState
didTransitionFromInitStage:(InitStage)previousInitStage {
if (self.needsQueueTransition && self.triggerOnDidTransition) {
[appState queueTransitionToNextInitStage];
self.needsQueueTransition = NO;
}
}
@end
class AppStateTest : public BlockCleanupTest {
protected:
AppStateTest() {
// Init mocks.
startup_information_mock_ =
[OCMockObject mockForProtocol:@protocol(StartupInformation)];
connection_information_mock_ =
[OCMockObject mockForProtocol:@protocol(ConnectionInformation)];
window_ = [OCMockObject mockForClass:[UIWindow class]];
app_state_observer_mock_ =
[OCMockObject mockForProtocol:@protocol(AppStateObserver)];
provider_interface_ = [[StubBrowserProviderInterface alloc] init];
app_state_observer_to_mock_main_controller_ =
[AppStateObserverToMockMainController alloc];
}
void SetUp() override {
BlockCleanupTest::SetUp();
TestChromeBrowserState::Builder test_cbs_builder;
browser_state_ = test_cbs_builder.Build();
}
void TearDown() override {
main_scene_state_ = nil;
BlockCleanupTest::TearDown();
}
void SwizzleConnectedScenes(NSArray<SceneState*>* connectedScenes) {
connected_scenes_swizzle_block_ = ^NSArray<SceneState*>*(id self) {
return connectedScenes;
};
connected_scenes_swizzler_.reset(
new ScopedBlockSwizzler([AppState class], @selector(connectedScenes),
connected_scenes_swizzle_block_));
}
void SwizzleSafeModeShouldStart(BOOL shouldStart) {
safe_mode_swizzle_block_ = ^BOOL(id self) {
return shouldStart;
};
safe_mode_swizzler_.reset(new ScopedBlockSwizzler(
[SafeModeCoordinator class], @selector(shouldStart),
safe_mode_swizzle_block_));
}
void SwizzleHandleStartupParameters(
id<TabOpening> expectedTabOpener,
ChromeBrowserState* expectedBrowserState) {
handle_startup_swizzle_block_ =
^(id self, id<TabOpening> tabOpener,
id<ConnectionInformation> connectionInformation,
id<StartupInformation> startupInformation,
ChromeBrowserState* browserState) {
ASSERT_EQ(connection_information_mock_, connectionInformation);
ASSERT_EQ(startup_information_mock_, startupInformation);
ASSERT_EQ(expectedTabOpener, tabOpener);
ASSERT_EQ(expectedBrowserState, browserState);
};
handle_startup_swizzler_.reset(new ScopedBlockSwizzler(
[UserActivityHandler class],
@selector(handleStartupParametersWithTabOpener:
connectionInformation:startupInformation
:browserState:initStage:),
handle_startup_swizzle_block_));
}
AppState* GetAppStateWithOpenNTP(BOOL shouldOpenNTP, UIWindow* window) {
AppState* appState = GetAppStateWithRealWindow(window);
id application = [OCMockObject mockForClass:[UIApplication class]];
id metricsMediator = [OCMockObject mockForClass:[MetricsMediator class]];
id memoryHelper = [OCMockObject mockForClass:[MemoryWarningHelper class]];
id tabOpener = [OCMockObject mockForProtocol:@protocol(TabOpening)];
Browser* browser = provider_interface_.currentBrowserProvider.browser;
[[metricsMediator stub] updateMetricsStateBasedOnPrefsUserTriggered:NO];
[[memoryHelper stub] resetForegroundMemoryWarningCount];
[[[memoryHelper stub] andReturnValue:@0] foregroundMemoryWarningCount];
[[[tabOpener stub] andReturnValue:@(shouldOpenNTP)]
shouldOpenNTPTabOnActivationOfBrowser:browser];
void (^swizzleBlock)() = ^{
};
ScopedBlockSwizzler swizzler(
[MetricsMediator class],
@selector(logLaunchMetricsWithStartupInformation:connectedScenes:),
swizzleBlock);
[appState applicationWillEnterForeground:application
metricsMediator:metricsMediator
memoryHelper:memoryHelper];
return appState;
}
SafeModeAppAgent* GetSafeModeAppAgent() {
if (!safe_mode_app_agent_) {
safe_mode_app_agent_ = [[SafeModeAppAgent alloc] init];
}
return safe_mode_app_agent_;
}
EnterpriseAppAgent* GetEnterpriseAppAgent() {
if (!enterprise_app_agent_) {
enterprise_app_agent_ = [[EnterpriseAppAgent alloc] init];
}
return enterprise_app_agent_;
}
AppState* GetAppStateWithMock(bool with_safe_mode_agent) {
if (!app_state_) {
// The swizzle block needs the scene state before app_state is create, but
// the scene state needs the app state. So this alloc before swizzling
// and initiate after app state is created.
main_scene_state_ = [FakeSceneState alloc];
SwizzleConnectedScenes(@[ main_scene_state_ ]);
app_state_ = [[TestAppState alloc]
initWithStartupInformation:startup_information_mock_
connectedScenes:@[ main_scene_state_ ]];
main_scene_state_ =
[main_scene_state_ initWithAppState:app_state_
browserState:browser_state_.get()];
main_scene_state_.window = GetWindowMock();
if (with_safe_mode_agent) {
[app_state_ addAgent:GetSafeModeAppAgent()];
// Retrigger a sceneConnected event for the safe mode agent. This is
// needed because the sceneConnected event triggered by the app state is
// done before resetting the scene state with initWithAppState which
// clears the observers and agents.
[GetSafeModeAppAgent() appState:app_state_
sceneConnected:main_scene_state_];
}
// Add the enterprise agent for the app to boot past the enterprise init
// stage.
[app_state_ addAgent:GetEnterpriseAppAgent()];
[app_state_ addObserver:app_state_observer_to_mock_main_controller_];
}
return app_state_;
}
AppState* GetAppStateWithMock() {
return GetAppStateWithMock(/*with_safe_mode_agent=*/true);
}
AppState* GetAppStateWithRealWindow(UIWindow* window) {
if (!app_state_) {
// The swizzle block needs the scene state before app_state is create, but
// the scene state needs the app state. So this alloc before swizzling
// and initiate after app state is created.
main_scene_state_ = [FakeSceneState alloc];
SwizzleConnectedScenes(@[ main_scene_state_ ]);
app_state_ = [[TestAppState alloc]
initWithStartupInformation:startup_information_mock_
connectedScenes:@[ main_scene_state_ ]];
main_scene_state_ =
[main_scene_state_ initWithAppState:app_state_
browserState:browser_state_.get()];
main_scene_state_.window = window;
[window makeKeyAndVisible];
[app_state_ addAgent:GetSafeModeAppAgent()];
// Add the enterprise agent for the app to boot past the enterprise init
// stage.
[app_state_ addAgent:GetEnterpriseAppAgent()];
[app_state_ addObserver:app_state_observer_to_mock_main_controller_];
// Retrigger a sceneConnected event for the safe mode agent with the real
// scene state. This is needed because the sceneConnected event triggered
// by the app state is done before resetting the scene state with
// initWithAppState which clears the observers and agents.
[GetSafeModeAppAgent() appState:app_state_
sceneConnected:main_scene_state_];
}
return app_state_;
}
id GetStartupInformationMock() { return startup_information_mock_; }
id GetConnectionInformationMock() { return connection_information_mock_; }
id GetWindowMock() { return window_; }
id GetAppStateObserverMock() { return app_state_observer_mock_; }
ChromeBrowserState* GetBrowserState() { return browser_state_.get(); }
private:
web::WebTaskEnvironment task_environment_;
TestAppState* app_state_;
FakeSceneState* main_scene_state_;
SafeModeAppAgent* safe_mode_app_agent_;
EnterpriseAppAgent* enterprise_app_agent_;
AppStateObserverToMockMainController*
app_state_observer_to_mock_main_controller_;
id connection_information_mock_;
id startup_information_mock_;
id window_;
id app_state_observer_mock_;
StubBrowserProviderInterface* provider_interface_;
ScenesBlock connected_scenes_swizzle_block_;
DecisionBlock safe_mode_swizzle_block_;
HandleStartupParam handle_startup_swizzle_block_;
std::unique_ptr<ScopedBlockSwizzler> safe_mode_swizzler_;
std::unique_ptr<ScopedBlockSwizzler> connected_scenes_swizzler_;
std::unique_ptr<ScopedBlockSwizzler> handle_startup_swizzler_;
std::unique_ptr<TestChromeBrowserState> browser_state_;
};
// Used to have a thread handling the closing of the IO threads.
class AppStateWithThreadTest : public PlatformTest {
protected:
AppStateWithThreadTest()
: task_environment_(web::WebTaskEnvironment::REAL_IO_THREAD) {}
private:
web::WebTaskEnvironment task_environment_;
};
#pragma mark - Tests.
using AppStateNoFixtureTest = PlatformTest;
// Test that -willResignActive set cold start to NO and launch record.
TEST_F(AppStateNoFixtureTest, willResignActive) {
// Setup.
base::test::TaskEnvironment task_environment;
FakeStartupInformation* startupInformation =
[[FakeStartupInformation alloc] init];
[startupInformation setIsColdStart:YES];
AppState* appState =
[[AppState alloc] initWithStartupInformation:startupInformation];
[appState addAgent:[[SafeModeAppAgent alloc] init]];
AppStateObserverToMockMainController* observer =
[AppStateObserverToMockMainController alloc];
[appState addObserver:observer];
// Start init stages.
[appState startInitialization];
[appState queueTransitionToNextInitStage];
[appState queueTransitionToNextInitStage];
ASSERT_TRUE([startupInformation isColdStart]);
// Action.
[appState willResignActive];
// Test.
EXPECT_FALSE([startupInformation isColdStart]);
}
// Test that -applicationWillTerminate clears everything.
TEST_F(AppStateWithThreadTest, willTerminate) {
// Setup.
ios::provider::test::ResetAppDistributionNotificationsState();
ASSERT_FALSE(ios::provider::test::AreAppDistributionNotificationsCanceled());
id startupInformation =
[OCMockObject mockForProtocol:@protocol(StartupInformation)];
[[startupInformation expect] stopChromeMain];
AppState* appState =
[[AppState alloc] initWithStartupInformation:startupInformation];
id appStateMock = OCMPartialMock(appState);
[[appStateMock expect] completeUIInitialization];
[appState addAgent:[[SafeModeAppAgent alloc] init]];
AppStateObserverToMockMainController* observer =
[AppStateObserverToMockMainController alloc];
[appState addObserver:observer];
// Start init stages.
[appState startInitialization];
[appState queueTransitionToNextInitStage];
id application = [OCMockObject mockForClass:[UIApplication class]];
// Action.
[appState applicationWillTerminate:application];
// Test.
EXPECT_OCMOCK_VERIFY(startupInformation);
EXPECT_OCMOCK_VERIFY(application);
for (SceneState* connectedScene in appState.connectedScenes) {
EXPECT_FALSE(connectedScene.browserProviderInterface.mainBrowserProvider
.userInteractionEnabled);
}
EXPECT_TRUE(ios::provider::test::AreAppDistributionNotificationsCanceled());
}
// Tests that -applicationWillEnterForeground resets components as needed.
TEST_F(AppStateTest, applicationWillEnterForeground) {
SwizzleSafeModeShouldStart(NO);
[[GetStartupInformationMock() stub] setIsFirstRun:YES];
[[[GetStartupInformationMock() stub] andReturnValue:@YES] isFirstRun];
// Setup.
id application = [OCMockObject mockForClass:[UIApplication class]];
id metricsMediator = [OCMockObject mockForClass:[MetricsMediator class]];
id memoryHelper = [OCMockObject mockForClass:[MemoryWarningHelper class]];
id tabOpener = [OCMockObject mockForProtocol:@protocol(TabOpening)];
std::unique_ptr<Browser> browser =
std::make_unique<TestBrowser>(GetBrowserState());
[[metricsMediator expect] updateMetricsStateBasedOnPrefsUserTriggered:NO];
[[memoryHelper expect] resetForegroundMemoryWarningCount];
[[[memoryHelper stub] andReturnValue:@0] foregroundMemoryWarningCount];
[[[tabOpener stub] andReturnValue:@YES]
shouldOpenNTPTabOnActivationOfBrowser:browser.get()];
id appStateMock = OCMPartialMock(GetAppStateWithMock());
[[appStateMock expect] completeUIInitialization];
// Simulate finishing the initialization before going to background.
[GetAppStateWithMock() startInitialization];
[GetAppStateWithMock() queueTransitionToNextInitStage];
// Simulate background before going to foreground.
[[GetStartupInformationMock() expect] expireFirstUserActionRecorder];
[GetAppStateWithMock() applicationDidEnterBackground:application
memoryHelper:memoryHelper];
void (^swizzleBlock)() = ^{
};
ScopedBlockSwizzler swizzler(
[MetricsMediator class],
@selector(logLaunchMetricsWithStartupInformation:connectedScenes:),
swizzleBlock);
// Actions.
[GetAppStateWithMock() applicationWillEnterForeground:application
metricsMediator:metricsMediator
memoryHelper:memoryHelper];
// Tests.
EXPECT_OCMOCK_VERIFY(metricsMediator);
EXPECT_OCMOCK_VERIFY(memoryHelper);
EXPECT_OCMOCK_VERIFY(GetStartupInformationMock());
}
// Tests that -applicationWillEnterForeground starts the browser if the
// application is in background.
TEST_F(AppStateTest, applicationWillEnterForegroundFromBackground) {
// Setup.
id application = [OCMockObject mockForClass:[UIApplication class]];
id metricsMediator = [OCMockObject mockForClass:[MetricsMediator class]];
id memoryHelper = [OCMockObject mockForClass:[MemoryWarningHelper class]];
[[[GetWindowMock() stub] andReturn:nil] rootViewController];
SwizzleSafeModeShouldStart(NO);
[[[GetStartupInformationMock() stub] andReturnValue:@YES] isColdStart];
[[GetStartupInformationMock() stub] setIsFirstRun:YES];
[[[GetStartupInformationMock() stub] andReturnValue:@YES] isFirstRun];
// Simulate finishing the initialization before going to background.
[GetAppStateWithMock() startInitialization];
[GetAppStateWithMock() queueTransitionToNextInitStage];
// Actions.
[GetAppStateWithMock() applicationWillEnterForeground:application
metricsMediator:metricsMediator
memoryHelper:memoryHelper];
}
// Tests that -applicationDidEnterBackground do nothing if the application has
// never been in a Foreground stage.
TEST_F(AppStateTest, applicationDidEnterBackgroundStageBackground) {
SwizzleSafeModeShouldStart(NO);
[[GetStartupInformationMock() stub] setIsFirstRun:YES];
[[[GetStartupInformationMock() stub] andReturnValue:@YES] isFirstRun];
// Setup.
ScopedKeyWindow scopedKeyWindow;
id application = [OCMockObject mockForClass:[UIApplication class]];
id memoryHelper = [OCMockObject mockForClass:[MemoryWarningHelper class]];
ASSERT_EQ(NSUInteger(0), [scopedKeyWindow.Get() subviews].count);
// Action.
[GetAppStateWithRealWindow(scopedKeyWindow.Get())
applicationDidEnterBackground:application
memoryHelper:memoryHelper];
// Tests.
EXPECT_EQ(NSUInteger(0), [scopedKeyWindow.Get() subviews].count);
}
// Tests that -queueTransitionToNextInitStage transitions to the next stage.
TEST_F(AppStateTest, queueTransitionToNextInitStage) {
AppState* appState = GetAppStateWithMock();
ASSERT_EQ(appState.initStage, InitStageStart);
[appState queueTransitionToNextInitStage];
ASSERT_EQ(appState.initStage, static_cast<InitStage>(InitStageStart + 1));
}
// Tests that -queueTransitionToNextInitStage notifies observers.
TEST_F(AppStateTest, queueTransitionToNextInitStageNotifiesObservers) {
// Setup.
AppState* appState = GetAppStateWithMock();
id observer = [OCMockObject mockForProtocol:@protocol(AppStateObserver)];
InitStage secondStage = static_cast<InitStage>(InitStageStart + 1);
[appState addObserver:observer];
[[[observer expect] andDo:^(NSInvocation*) {
// Verify that the init stage isn't yet increased when calling
// #willTransitionToInitStage.
EXPECT_EQ(InitStageStart, appState.initStage);
}] appState:appState willTransitionToInitStage:secondStage];
[[[observer expect] andDo:^(NSInvocation*) {
// Verify that the init stage is increased when calling
// #didTransitionFromInitStage.
EXPECT_EQ(secondStage, appState.initStage);
}] appState:appState didTransitionFromInitStage:InitStageStart];
[appState queueTransitionToNextInitStage];
EXPECT_EQ(secondStage, appState.initStage);
[observer verify];
}
// Tests that -queueTransitionToNextInitStage, when called from an observer's
// call, first completes sending previous updates and doesn't change the init
// stage, then transitions to the next init stage and sends updates.
TEST_F(AppStateTest,
queueTransitionToNextInitStageReentrantFromWillTransitionToInitStage) {
// Setup.
AppState* appState = GetAppStateWithMock(/*with_safe_mode_agent=*/false);
id observer1 = [OCMockObject mockForProtocol:@protocol(AppStateObserver)];
AppStateTransitioningObserver* transitioningObserver =
[[AppStateTransitioningObserver alloc] init];
id observer2 = [OCMockObject mockForProtocol:@protocol(AppStateObserver)];
InitStage secondStage = static_cast<InitStage>(InitStageStart + 1);
InitStage thirdStage = static_cast<InitStage>(InitStageStart + 2);
// The order is important here.
[appState addObserver:observer1];
[appState addObserver:transitioningObserver];
[appState addObserver:observer2];
// The order is important here. We want to first receive all notifications for
// the second stage, then all the notifications for the third stage, despite
// transitioningObserver queueing a new transition from one of the callbacks.
[[observer1 expect] appState:appState willTransitionToInitStage:secondStage];
[[observer1 expect] appState:appState
didTransitionFromInitStage:InitStageStart];
[[observer2 expect] appState:appState willTransitionToInitStage:secondStage];
[[observer2 expect] appState:appState
didTransitionFromInitStage:InitStageStart];
[[observer1 expect] appState:appState willTransitionToInitStage:thirdStage];
[[observer1 expect] appState:appState didTransitionFromInitStage:secondStage];
[[observer2 expect] appState:appState willTransitionToInitStage:thirdStage];
[[observer2 expect] appState:appState didTransitionFromInitStage:secondStage];
[observer1 setExpectationOrderMatters:YES];
[observer2 setExpectationOrderMatters:YES];
[appState queueTransitionToNextInitStage];
[observer1 verify];
[observer2 verify];
}
// Tests that -queueTransitionToNextInitStage, when called from an observer's
// call, first completes sending previous updates and doesn't change the init
// stage, then transitions to the next init stage and sends updates.
TEST_F(AppStateTest,
queueTransitionToNextInitStageReentrantFromdidTransitionFromInitStage) {
// Setup.
AppState* appState = GetAppStateWithMock(/*with_safe_mode_agent=*/false);
id observer1 = [OCMockObject mockForProtocol:@protocol(AppStateObserver)];
AppStateTransitioningObserver* transitioningObserver =
[[AppStateTransitioningObserver alloc] init];
transitioningObserver.triggerOnDidTransition = YES;
id observer2 = [OCMockObject mockForProtocol:@protocol(AppStateObserver)];
InitStage secondStage = static_cast<InitStage>(InitStageStart + 1);
InitStage thirdStage = static_cast<InitStage>(InitStageStart + 2);
// The order is important here.
[appState addObserver:observer1];
[appState addObserver:transitioningObserver];
[appState addObserver:observer2];
// The order is important here. We want to first receive all notifications for
// the second stage, then all the notifications for the third stage, despite
// transitioningObserver queueing a new transition from one of the callbacks.
[[observer1 expect] appState:appState willTransitionToInitStage:secondStage];
[[observer1 expect] appState:appState
didTransitionFromInitStage:InitStageStart];
[[observer2 expect] appState:appState willTransitionToInitStage:secondStage];
[[observer2 expect] appState:appState
didTransitionFromInitStage:InitStageStart];
[[observer1 expect] appState:appState willTransitionToInitStage:thirdStage];
[[observer1 expect] appState:appState didTransitionFromInitStage:secondStage];
[[observer2 expect] appState:appState willTransitionToInitStage:thirdStage];
[[observer2 expect] appState:appState didTransitionFromInitStage:secondStage];
[observer1 setExpectationOrderMatters:YES];
[observer2 setExpectationOrderMatters:YES];
[appState queueTransitionToNextInitStage];
[observer1 verify];
[observer2 verify];
}