| // Copyright 2025 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/change_profile_animator.h" |
| |
| #import "base/functional/bind.h" |
| #import "base/functional/callback.h" |
| #import "base/run_loop.h" |
| #import "base/test/task_environment.h" |
| #import "ios/chrome/app/profile/profile_init_stage.h" |
| #import "ios/chrome/app/profile/profile_state.h" |
| #import "ios/chrome/app/profile/profile_state_test_utils.h" |
| #import "ios/chrome/browser/shared/coordinator/scene/scene_activation_level.h" |
| #import "ios/chrome/browser/shared/coordinator/scene/scene_state.h" |
| #import "testing/gtest/include/gtest/gtest.h" |
| #import "testing/platform_test.h" |
| |
| namespace { |
| |
| // Returns a closure that will set `called` to `true` when invoked. |
| base::OnceClosure RecordWhenCalled(bool* called) { |
| return base::BindOnce([](bool* called) { *called = true; }, called); |
| } |
| |
| // Returns a continuation that call a closure when invoked. |
| ChangeProfileContinuation ContinuationFromClosure(base::OnceClosure closure) { |
| return base::BindOnce( |
| [](base::OnceClosure done, SceneState*, base::OnceClosure next) { |
| std::move(done).Run(); |
| std::move(next).Run(); |
| }, |
| std::move(closure)); |
| } |
| |
| } // namespace |
| |
| // NOTE for the whole file: |
| // |
| // The code wants to test that the ChangeProfileAnimator extends its lifetime |
| // to stay alive as long as the wait for the initialisation is not over, then |
| // ensure it is deallocated before calling the continuation. |
| // |
| // To detect this, the test cases create a weak pointer to the animator, and |
| // wrap all calls to method that could result in deallocation of the object |
| // in @autoreleasepool { ... }. |
| // |
| // The goal is to ensure that even if the compiler inserts retain/autorelease |
| // to manage the lifetime of the object, those would not extend it past the |
| // @autoreleasepool { ... } blocks. This allow to detect if the animator is |
| // correctly deallocated when expected and only when expected (if only the |
| // calls where the deallocation is expected were to be wrapped, the bug could |
| // be missed if the compiler used retain/autorelease). |
| // |
| // So please keep those @autoreleasepool { ... } in the test case around the |
| // method calls that may cause the wait to complete. |
| |
| class ChangeProfileAnimatorTest : public PlatformTest { |
| public: |
| void SetUp() override { |
| PlatformTest::SetUp(); |
| profile_state_ = [[ProfileState alloc] initWithAppState:nil]; |
| scene_state_ = [[SceneState alloc] initWithAppState:nil]; |
| scene_state_.profileState = profile_state_; |
| [profile_state_ sceneStateConnected:scene_state_]; |
| } |
| |
| void TearDown() override { |
| DisconnectSceneState(); |
| profile_state_ = nil; |
| PlatformTest::TearDown(); |
| } |
| |
| ProfileState* profile_state() { return profile_state_; } |
| |
| SceneState* scene_state() { return scene_state_; } |
| |
| void DisconnectSceneState() { |
| scene_state_.activationLevel = SceneActivationLevelDisconnected; |
| scene_state_.profileState = nil; |
| scene_state_ = nil; |
| } |
| |
| private: |
| base::test::TaskEnvironment task_env_; |
| ProfileState* profile_state_ = nil; |
| SceneState* scene_state_ = nil; |
| }; |
| |
| // Check that ChangeProfileAnimator is strongly retained by the SceneState |
| // until the continuation is called, and that it calls the continuation as |
| // soon as the SceneState and ProfileState are both ready. |
| TEST_F(ChangeProfileAnimatorTest, waitForSceneState_ProfileReadyBeforeScene) { |
| base::RunLoop run_loop; |
| bool continuation_called = false; |
| __weak ChangeProfileAnimator* weak_animator = nil; |
| @autoreleasepool { |
| ChangeProfileAnimator* animator = |
| [[ChangeProfileAnimator alloc] initWithWindow:nil]; |
| |
| [animator waitForSceneState:scene_state() |
| toReachInitStage:ProfileInitStage::kUIReady |
| continuation:ContinuationFromClosure( |
| RecordWhenCalled(&continuation_called) |
| .Then(run_loop.QuitClosure()))]; |
| |
| weak_animator = animator; |
| animator = nil; |
| } |
| |
| // The animator should be kept alive by the SceneState, the continuation |
| // should not yet have been called. |
| ASSERT_TRUE(weak_animator); |
| ASSERT_FALSE(continuation_called); |
| |
| // Simulate the ProfileState initialisation progressing to kUIReady. The |
| // continuation is not called yet. |
| @autoreleasepool { |
| SetProfileStateInitStage(profile_state(), ProfileInitStage::kUIReady); |
| } |
| |
| EXPECT_TRUE(weak_animator); |
| EXPECT_FALSE(continuation_called); |
| |
| // Simulate the SceneState reaches the foreground. The animator posts the |
| // continuation to be invoked soon, and then is deallocated. |
| @autoreleasepool { |
| scene_state().activationLevel = SceneActivationLevelForegroundInactive; |
| scene_state().UIEnabled = YES; |
| } |
| |
| EXPECT_FALSE(weak_animator); |
| EXPECT_FALSE(continuation_called); |
| |
| // Wait until the posted continuation is invoked. |
| run_loop.Run(); |
| |
| EXPECT_FALSE(weak_animator); |
| EXPECT_TRUE(continuation_called); |
| } |
| |
| // Check that ChangeProfileAnimator is strongly retained by the SceneState |
| // until the continuation is called, and that it calls the continuation as |
| // soon as the SceneState and ProfileState are both ready. |
| TEST_F(ChangeProfileAnimatorTest, waitForSceneState_SceneReadyBeforeProfile) { |
| base::RunLoop run_loop; |
| bool continuation_called = false; |
| __weak ChangeProfileAnimator* weak_animator = nil; |
| @autoreleasepool { |
| ChangeProfileAnimator* animator = |
| [[ChangeProfileAnimator alloc] initWithWindow:nil]; |
| |
| [animator waitForSceneState:scene_state() |
| toReachInitStage:ProfileInitStage::kUIReady |
| continuation:ContinuationFromClosure( |
| RecordWhenCalled(&continuation_called) |
| .Then(run_loop.QuitClosure()))]; |
| |
| weak_animator = animator; |
| animator = nil; |
| } |
| |
| // The animator should be kept alive by the SceneState, the continuation |
| // should not yet have been called. |
| ASSERT_TRUE(weak_animator); |
| ASSERT_FALSE(continuation_called); |
| |
| // Simulate the ProfileState initialisation progressing to kPrepareUI. The |
| // continuation is not called yet. |
| @autoreleasepool { |
| SetProfileStateInitStage(profile_state(), ProfileInitStage::kPrepareUI); |
| } |
| |
| EXPECT_TRUE(weak_animator); |
| EXPECT_FALSE(continuation_called); |
| |
| // Simulate the SceneState reaches the foreground. The continuation is not |
| // called yet. |
| @autoreleasepool { |
| scene_state().activationLevel = SceneActivationLevelForegroundInactive; |
| scene_state().UIEnabled = YES; |
| } |
| |
| EXPECT_TRUE(weak_animator); |
| EXPECT_FALSE(continuation_called); |
| |
| // Simulate the ProfileState initialisation progressing to kUIReady. The |
| // animator post the continuation to be invoked soon, and then is |
| // deallocated. |
| @autoreleasepool { |
| SetProfileStateInitStage(profile_state(), ProfileInitStage::kUIReady); |
| } |
| |
| EXPECT_FALSE(weak_animator); |
| EXPECT_FALSE(continuation_called); |
| |
| // Wait until the posted continuation is invoked. |
| run_loop.Run(); |
| |
| EXPECT_FALSE(weak_animator); |
| EXPECT_TRUE(continuation_called); |
| } |
| |
| // Check that ChangeProfileAnimator is strongly retained by the SceneState |
| // until the continuation is called, and that it calls the continuation as |
| // soon as the SceneState and ProfileState are both ready. |
| TEST_F(ChangeProfileAnimatorTest, waitForSceneState_ProfileAlreadyReady) { |
| // Simulate the ProfileState initialisation progressing to kFinal. This |
| // correspond to the case when switching to a profile that is already |
| // used for another Scene. |
| @autoreleasepool { |
| SetProfileStateInitStage(profile_state(), ProfileInitStage::kFinal); |
| } |
| |
| base::RunLoop run_loop; |
| bool continuation_called = false; |
| __weak ChangeProfileAnimator* weak_animator = nil; |
| @autoreleasepool { |
| ChangeProfileAnimator* animator = |
| [[ChangeProfileAnimator alloc] initWithWindow:nil]; |
| |
| [animator waitForSceneState:scene_state() |
| toReachInitStage:ProfileInitStage::kUIReady |
| continuation:ContinuationFromClosure( |
| RecordWhenCalled(&continuation_called) |
| .Then(run_loop.QuitClosure()))]; |
| |
| weak_animator = animator; |
| animator = nil; |
| } |
| |
| // The animator should be kept alive by the SceneState, the continuation |
| // should not yet have been called. |
| ASSERT_TRUE(weak_animator); |
| ASSERT_FALSE(continuation_called); |
| |
| // Simulate the SceneState reaches the foreground. The animator post the |
| // continuation to be invoked soon, and then is deallocated. |
| @autoreleasepool { |
| scene_state().activationLevel = SceneActivationLevelForegroundInactive; |
| scene_state().UIEnabled = YES; |
| } |
| |
| EXPECT_FALSE(weak_animator); |
| EXPECT_FALSE(continuation_called); |
| |
| // Wait until the posted continuation is invoked. |
| run_loop.Run(); |
| |
| EXPECT_FALSE(weak_animator); |
| EXPECT_TRUE(continuation_called); |
| } |
| |
| // Check that ChangeProfileAnimator is dropped and the continuation is not |
| // called if the Scene is deallocated. |
| TEST_F(ChangeProfileAnimatorTest, waitForSceneState_SceneDeallocated) { |
| // Simulate the ProfileState initialisation progressing to kFinal. This |
| // correspond to the case when switching to a profile that is already |
| // used for another Scene. |
| @autoreleasepool { |
| SetProfileStateInitStage(profile_state(), ProfileInitStage::kFinal); |
| } |
| |
| base::RunLoop run_loop; |
| bool continuation_called = false; |
| __weak ChangeProfileAnimator* weak_animator = nil; |
| @autoreleasepool { |
| ChangeProfileAnimator* animator = |
| [[ChangeProfileAnimator alloc] initWithWindow:nil]; |
| |
| [animator waitForSceneState:scene_state() |
| toReachInitStage:ProfileInitStage::kUIReady |
| continuation:ContinuationFromClosure( |
| RecordWhenCalled(&continuation_called) |
| .Then(run_loop.QuitClosure()))]; |
| |
| weak_animator = animator; |
| animator = nil; |
| } |
| |
| // The animator should be kept alive by the SceneState, the continuation |
| // should not yet have been called. |
| ASSERT_TRUE(weak_animator); |
| ASSERT_FALSE(continuation_called); |
| |
| // Simulate the SceneState is deallocated while waiting for initialisation. |
| @autoreleasepool { |
| DisconnectSceneState(); |
| } |
| |
| EXPECT_FALSE(weak_animator); |
| EXPECT_FALSE(continuation_called); |
| |
| // Ensure the continuation is not invoked. Post a callback with a timeout |
| // to avoid failing the test due to a timeout. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(100)); |
| |
| run_loop.Run(); |
| |
| EXPECT_FALSE(weak_animator); |
| EXPECT_FALSE(continuation_called); |
| } |