chromium/ios/chrome/browser/shared/model/browser_state/incognito_session_tracker_unittest.mm

// Copyright 2024 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/browser/shared/model/browser_state/incognito_session_tracker.h"

#import "base/test/task_environment.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_manager_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/removing_indexes.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"

namespace {

// Names of the created ChromeBrowserState.
constexpr char kProfile1[] = "Profile1";
constexpr char kProfile2[] = "Profile2";
constexpr char kProfile3[] = "Profile3";

// Wrapper over a Browser that registers it with the ChromeBrowserState's
// BrowserList on construction and unregister it on destruction. It must
// not outlive the ChromeBrowserState.
class ScopedBrowser {
 public:
  explicit ScopedBrowser(std::unique_ptr<Browser> browser);

  ScopedBrowser(const ScopedBrowser&) = delete;
  ScopedBrowser& operator=(const ScopedBrowser&) = delete;

  ~ScopedBrowser();

  Browser* get() { return browser_.get(); }

  Browser* operator->() { return browser_.get(); }

  Browser& operator*() { return *browser_; }

 private:
  std::unique_ptr<Browser> browser_;
};

ScopedBrowser::ScopedBrowser(std::unique_ptr<Browser> browser)
    : browser_(std::move(browser)) {
  DCHECK(browser_);
  BrowserListFactory::GetForBrowserState(
      browser_->GetBrowserState()->GetOriginalChromeBrowserState())
      ->AddBrowser(browser_.get());
}

ScopedBrowser::~ScopedBrowser() {
  BrowserListFactory::GetForBrowserState(
      browser_->GetBrowserState()->GetOriginalChromeBrowserState())
      ->RemoveBrowser(browser_.get());
}

// Helper object used to count how many times a callback is invoked and
// that stores the last value passed to the callback.
class SessionStateChangedCallbackHelper {
 public:
  SessionStateChangedCallbackHelper() = default;

  SessionStateChangedCallbackHelper(const SessionStateChangedCallbackHelper&) =
      delete;
  SessionStateChangedCallbackHelper& operator=(
      const SessionStateChangedCallbackHelper&) = delete;

  ~SessionStateChangedCallbackHelper() = default;

  // Register a callback with `tracker`.
  void RegisterCallback(IncognitoSessionTracker* tracker) {
    // It is safe to pass base::Unretained(this) since the object will
    // destroy the subscription in its destructor, and invalidate the
    // callback.
    subscription_ = tracker->RegisterCallback(base::BindRepeating(
        &SessionStateChangedCallbackHelper::SessionStateChanged,
        base::Unretained(this)));
  }

  // Check that the count and last value of the parameter are as expected.
  bool ExpectCallCountAndParameter(int count, int value) const {
    return callback_call_count_ == count && value == callback_parameter_;
  }

 private:
  void SessionStateChanged(bool has_incognito_tabs) {
    ++callback_call_count_;
    callback_parameter_ = has_incognito_tabs;
  }

  base::CallbackListSubscription subscription_;
  int callback_call_count_ = 0;
  bool callback_parameter_ = false;
};

// Creates a new WebState and insert it into `browser`'s WebStateList.
void InsertNewTab(Browser* browser) {
  browser->GetWebStateList()->InsertWebState(
      std::make_unique<web::FakeWebState>(web::WebStateID::NewUnique()));
}

}  // anonymous namespace

class IncognitoSessionTrackerTest : public PlatformTest {
 public:
  IncognitoSessionTrackerTest() {
    AddBrowserStateWithName(kProfile1);
    AddBrowserStateWithName(kProfile2);
  }

  // Adds a new ChromeBrowserState with the given name.
  void AddBrowserStateWithName(const char* name) {
    profile_manager_.AddProfileWithBuilder(
        std::move(TestChromeBrowserState::Builder().SetName(name)));
  }

  ProfileManagerIOS* profile_manager() { return &profile_manager_; }

 private:
  base::test::TaskEnvironment task_environment_;
  IOSChromeScopedTestingLocalState scoped_local_state_;
  TestProfileManagerIOS profile_manager_;
};

// Tests that `HasIncognitoSessionTabs()` value changes when off-the-record
// tabs are opened or closed after the object is created.
TEST_F(IncognitoSessionTrackerTest, HasIncognitoSessionTabs) {
  IncognitoSessionTracker tracker(profile_manager());

  // Install a callback that count how many times it is invoked and store the
  // last value it was invoked with.
  SessionStateChangedCallbackHelper callback_helper;
  callback_helper.RegisterCallback(&tracker);

  // As there are no tabs, HasIncognitoSessionTabs() should return false
  // and the callbacks must not have been notified since nothing changed.
  EXPECT_FALSE(tracker.HasIncognitoSessionTabs());
  EXPECT_TRUE(callback_helper.ExpectCallCountAndParameter(0, false));

  // Install a regular Browser for kProfile1 ...
  ScopedBrowser scoped_browser_for_profile1(std::make_unique<TestBrowser>(
      profile_manager()->GetProfileWithName(kProfile1)));

  // ... and insert a few tabs. Then check that HasIncognitoSessionTabs()
  // still returns false and the callbacks must not have been notified.
  InsertNewTab(scoped_browser_for_profile1.get());
  InsertNewTab(scoped_browser_for_profile1.get());
  InsertNewTab(scoped_browser_for_profile1.get());
  EXPECT_FALSE(tracker.HasIncognitoSessionTabs());
  EXPECT_TRUE(callback_helper.ExpectCallCountAndParameter(0, false));

  // Install a new off-the-record Browser for kProfile1 ...
  ScopedBrowser otr_scoped_browser_for_profile1(
      std::make_unique<TestBrowser>(profile_manager()
                                        ->GetProfileWithName(kProfile1)
                                        ->GetOffTheRecordChromeBrowserState()));

  // ... and HasIncognitoSessionTabs() should still return false and the
  // callbacks must not have been notified.
  EXPECT_FALSE(tracker.HasIncognitoSessionTabs());
  EXPECT_TRUE(callback_helper.ExpectCallCountAndParameter(0, false));

  // Insert a few tabs in the off-the-record Browser however should cause
  // HasIncognitoSessionTabs() to return true and the callbacks to be
  // notified once.
  InsertNewTab(otr_scoped_browser_for_profile1.get());
  InsertNewTab(otr_scoped_browser_for_profile1.get());
  InsertNewTab(otr_scoped_browser_for_profile1.get());
  EXPECT_TRUE(tracker.HasIncognitoSessionTabs());
  EXPECT_TRUE(callback_helper.ExpectCallCountAndParameter(1, true));

  // Closing some of the off-the-record Browser should not change the value
  // returned by HasIncognitoSessionTabs() and the callbacks must not have
  // been notified.
  otr_scoped_browser_for_profile1->GetWebStateList()->CloseWebStateAt(
      0, WebStateList::CLOSE_NO_FLAGS);
  EXPECT_TRUE(tracker.HasIncognitoSessionTabs());
  EXPECT_TRUE(callback_helper.ExpectCallCountAndParameter(1, true));

  // Closing all the off-the-record tabs will however change the value
  // returned by HasIncognitoSessionTabs(), but only at the end of the
  // batch operation.
  {
    WebStateList* const web_state_list =
        otr_scoped_browser_for_profile1->GetWebStateList();

    WebStateList::ScopedBatchOperation batch_operation =
        web_state_list->StartBatchOperation();

    web_state_list->CloseWebStatesAtIndices(
        WebStateList::CLOSE_NO_FLAGS, RemovingIndexes({
                                          .start = 0,
                                          .count = web_state_list->count(),
                                      }));

    EXPECT_TRUE(tracker.HasIncognitoSessionTabs());
    EXPECT_TRUE(callback_helper.ExpectCallCountAndParameter(1, true));
  }

  // Once the batch operation has completed, the value should have changed.
  EXPECT_FALSE(tracker.HasIncognitoSessionTabs());
  EXPECT_TRUE(callback_helper.ExpectCallCountAndParameter(2, false));

  // Install a new off-the-record Browser for kProfile2 ...
  ScopedBrowser otr_scoped_browser_for_profile2(
      std::make_unique<TestBrowser>(profile_manager()
                                        ->GetProfileWithName(kProfile2)
                                        ->GetOffTheRecordChromeBrowserState()));

  // ... and create a few tabs and check that this change the value of
  // HasIncognitoSessionTabs() and has notified the callbacks.
  InsertNewTab(otr_scoped_browser_for_profile2.get());
  InsertNewTab(otr_scoped_browser_for_profile2.get());
  InsertNewTab(otr_scoped_browser_for_profile2.get());
  EXPECT_TRUE(tracker.HasIncognitoSessionTabs());
  EXPECT_TRUE(callback_helper.ExpectCallCountAndParameter(3, true));

  // Dynamically create a new ChromeBrowserState kProfile3, insert some
  // off-the-record tabs there, and check that even after closing all
  // the tabs in otr_scoped_browser_for_profile2, there value returned
  // by HasIncognitoSessionTabs().
  AddBrowserStateWithName(kProfile3);

  {
    ScopedBrowser otr_scoped_browser_for_profile3_1(
        std::make_unique<TestBrowser>(
            profile_manager()
                ->GetProfileWithName(kProfile3)
                ->GetOffTheRecordChromeBrowserState()));

    ScopedBrowser otr_scoped_browser_for_profile3_2(
        std::make_unique<TestBrowser>(
            profile_manager()
                ->GetProfileWithName(kProfile3)
                ->GetOffTheRecordChromeBrowserState()));

    InsertNewTab(otr_scoped_browser_for_profile3_1.get());
    InsertNewTab(otr_scoped_browser_for_profile3_1.get());
    InsertNewTab(otr_scoped_browser_for_profile3_2.get());

    CloseAllWebStates(*otr_scoped_browser_for_profile2->GetWebStateList(),
                      WebStateList::CLOSE_NO_FLAGS);

    EXPECT_TRUE(tracker.HasIncognitoSessionTabs());
    EXPECT_TRUE(callback_helper.ExpectCallCountAndParameter(3, true));
  }

  // But destroying those Browser, should consider the tabs as closed and
  // thus HasIncognitoSessionTabs() should return false and notify the
  // callbacks one time.
  EXPECT_FALSE(tracker.HasIncognitoSessionTabs());
  EXPECT_TRUE(callback_helper.ExpectCallCountAndParameter(4, false));
}