chromium/ios/chrome/browser/sessions/model/session_restoration_web_state_list_observer_unittest.mm

// Copyright 2023 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/sessions/model/session_restoration_web_state_list_observer.h"

#import "base/containers/contains.h"
#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "ios/chrome/browser/shared/model/web_state_list/test/fake_web_state_list_delegate.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "testing/platform_test.h"

namespace {

// Increments count when called.
void IncrementCounter(size_t* counter) {
  ++*counter;
}

// State in which the WebState is created.
enum class CreateWebStateAs {
  kUnrealized,
  kSerializable,
  kRestoreInProgress,
};

}  // anonymous namespace

class SessionRestorationWebStateListObserverTest : public PlatformTest {
 public:
  WebStateList* web_state_list() { return &web_state_list_; }

  std::unique_ptr<web::FakeWebState> CreateWebState(CreateWebStateAs state) {
    auto web_state = std::make_unique<web::FakeWebState>();
    web_state->SetWebFramesManager(
        std::make_unique<web::FakeWebFramesManager>());
    web_state->SetNavigationManager(
        std::make_unique<web::FakeNavigationManager>());

    // Initialize the state of the WebState.
    switch (state) {
      case CreateWebStateAs::kUnrealized:
        web_state->SetIsRealized(false);
        break;

      case CreateWebStateAs::kSerializable:
        web_state->SetIsRealized(true);
        static_cast<web::FakeNavigationManager*>(
            web_state->GetNavigationManager())
            ->SetIsRestoreSessionInProgress(false);
        break;

      case CreateWebStateAs::kRestoreInProgress:
        web_state->SetIsRealized(true);
        static_cast<web::FakeNavigationManager*>(
            web_state->GetNavigationManager())
            ->SetIsRestoreSessionInProgress(true);
        break;
    }

    return web_state;
  }

  web::FakeWebState* InsertWebState(
      std::unique_ptr<web::FakeWebState> web_state) {
    const int insertion_index =
        web_state_list_.InsertWebState(std::move(web_state));
    return static_cast<web::FakeWebState*>(
        web_state_list_.GetWebStateAt(insertion_index));
  }

 private:
  FakeWebStateListDelegate web_state_list_delegate_;
  WebStateList web_state_list_{&web_state_list_delegate_};
};

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as clean on creation.
TEST_F(SessionRestorationWebStateListObserverTest, Creation) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 0u);
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList and the inserted WebState as dirty when inserting a
// WebState that can be serialized.
TEST_F(SessionRestorationWebStateListObserverTest, Insert) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(base::Contains(observer.dirty_web_states(), web_state));
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when inserting an unrealized WebState.
TEST_F(SessionRestorationWebStateListObserverTest, Insert_Unrealized) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kUnrealized));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.inserted_web_states(), web_state_id));
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when inserting a WebState whose navigation
// history is still being restored.
TEST_F(SessionRestorationWebStateListObserverTest, Insert_Unserializable) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kRestoreInProgress));

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(base::Contains(observer.dirty_web_states(), web_state));
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList and the inserted WebStates as dirty when inserting
// multiple WebStates that can be serialized.
TEST_F(SessionRestorationWebStateListObserverTest, Insert_MultipleWebStates) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state_1 =
      InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));
  web::WebState* const web_state_2 =
      InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(base::Contains(observer.dirty_web_states(), web_state_1));
  EXPECT_TRUE(base::Contains(observer.dirty_web_states(), web_state_2));
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);  // The callback is only called once!

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as clean after calling ClearDirty().
TEST_F(SessionRestorationWebStateListObserverTest, ClearDirty) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  // Insert a WebState to force the observer to be marked as dirty.
  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(base::Contains(observer.dirty_web_states(), web_state));
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Clear the dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);  // The callback is not invoked by ClearDirty()!
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when detaching a serializable WebState. The
// WebState is still listed as up for adoption.
TEST_F(SessionRestorationWebStateListObserverTest, Detach) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->DetachWebStateAt(/*index*/ 0);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.detached_web_states(), web_state_id));
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when detaching an unrealized WebState. The
// WebState is listed as up for adoption.
TEST_F(SessionRestorationWebStateListObserverTest, Detach_Unrealized) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kUnrealized));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->DetachWebStateAt(/*index*/ 0);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.detached_web_states(), web_state_id));
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when detaching a WebState whose navigation
// history is still being restored. The WebState is listed as up for
// adoption.
TEST_F(SessionRestorationWebStateListObserverTest, Detach_Unserializable) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kRestoreInProgress));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->DetachWebStateAt(/*index*/ 0);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.detached_web_states(), web_state_id));
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when detaching a serializable WebState that
// has just been inserted. The WebState is not listed as up for
// adoption, and is no longer listed as dirty.
TEST_F(SessionRestorationWebStateListObserverTest, Detach_Dirty) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->DetachWebStateAt(/*index*/ 0);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.detached_web_states(), web_state_id));
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);  // The callback is only called once!

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when detaching an unrealized WebState that
// has just been inserted. The WebState is no longer listed as inserted
// and is not listed for adoption either.
TEST_F(SessionRestorationWebStateListObserverTest, Detach_DirtyUnrealized) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  InsertWebState(CreateWebState(CreateWebStateAs::kUnrealized));

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->DetachWebStateAt(/*index*/ 0);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);  // The callback is only called once!

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when detaching a WebState  that has just been
// inserted whose navigation history is still being restored. The WebState
// is no longer listed as inserted and is not listed for adoption either.
TEST_F(SessionRestorationWebStateListObserverTest, Detach_DirtyUnserializable) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kRestoreInProgress));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->DetachWebStateAt(/*index*/ 0);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.detached_web_states(), web_state_id));
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);  // The callback is only called once!

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when closing a serializable WebState. The
// WebState is not listed as up for adoption.
TEST_F(SessionRestorationWebStateListObserverTest, Close) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->CloseWebStateAt(/*index*/ 0, WebStateList::CLOSE_NO_FLAGS);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.closed_web_states(), web_state_id));
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when closing an unrealized WebState. The
// WebState is not listed as up for adoption.
TEST_F(SessionRestorationWebStateListObserverTest, Close_Unrealized) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kUnrealized));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->CloseWebStateAt(/*index*/ 0, WebStateList::CLOSE_NO_FLAGS);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.closed_web_states(), web_state_id));
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when closing a WebState whose navigation
// history is still being restored. The WebState is listed as up for
// adoption.
TEST_F(SessionRestorationWebStateListObserverTest, Close_Unserializable) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kRestoreInProgress));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->CloseWebStateAt(/*index*/ 0, WebStateList::CLOSE_NO_FLAGS);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.closed_web_states(), web_state_id));
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when closing a serializable WebState that
// has just been inserted. The WebState is not listed as up for adoption,
// and is no longer listed as dirty.
TEST_F(SessionRestorationWebStateListObserverTest, Close_Dirty) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->CloseWebStateAt(/*index*/ 0, WebStateList::CLOSE_NO_FLAGS);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.closed_web_states(), web_state_id));
  EXPECT_EQ(call_count, 1u);  // The callback is only called once!

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when closing an unrealized WebState that
// has just been inserted. The WebState is no longer listed as inserted
// and is not listed for adoption either.
TEST_F(SessionRestorationWebStateListObserverTest, Close_DirtyUnrealized) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kUnrealized));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->CloseWebStateAt(/*index*/ 0, WebStateList::CLOSE_NO_FLAGS);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.closed_web_states(), web_state_id));
  EXPECT_EQ(call_count, 1u);  // The callback is only called once!

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when closing a WebState that has just been
// inserted whose navigation history is still being restored. The WebState
// is no longer listed as inserted and is not listed for adoption either.
TEST_F(SessionRestorationWebStateListObserverTest, Close_DirtyUnserializable) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kRestoreInProgress));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->CloseWebStateAt(/*index*/ 0, WebStateList::CLOSE_NO_FLAGS);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.closed_web_states(), web_state_id));
  EXPECT_EQ(call_count, 1u);  // The callback is only called once!

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when moving WebState, but no WebStates are
// considered dirty.
TEST_F(SessionRestorationWebStateListObserverTest, Move) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));
  InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  ASSERT_GE(web_state_list()->count(), 2);
  web_state_list()->MoveWebStateAt(/*from_index*/ 0, /*to_index*/ 1);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty when changing the active WebState, but no
// WebStates are considered dirty.
TEST_F(SessionRestorationWebStateListObserverTest, Activate) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));
  InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  ASSERT_GE(web_state_list()->count(), 2);
  ASSERT_EQ(web_state_list()->active_index(), WebStateList::kInvalidIndex);
  web_state_list()->ActivateWebStateAt(/*index*/ 0);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList and the inserted WebState as dirty when replacing a
// WebState.
TEST_F(SessionRestorationWebStateListObserverTest, Replace) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::WebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  ASSERT_GT(web_state_list()->count(), 0);
  web_state_list()->ReplaceWebStateAt(
      /*index*/ 0, CreateWebState(CreateWebStateAs::kSerializable));
  web::WebState* const new_web_state =
      web_state_list()->GetWebStateAt(/*index*/ 0);

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(base::Contains(observer.dirty_web_states(), new_web_state));
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(base::Contains(observer.closed_web_states(), web_state_id));
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest consider the
// WebStateList as dirty after a batch operation (even if no change
// occurred).
TEST_F(SessionRestorationWebStateListObserverTest, BatchOperation) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  // Perform a batch operation to does nothing. The WebStateList should
  // be considered as dirty anyway.
  {
    WebStateList::ScopedBatchOperation lock =
        web_state_list()->StartBatchOperation();
  }

  EXPECT_TRUE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest calls the callback
// when a WebState becomes dirty, even if the WebStateList itself has not
// changed.
TEST_F(SessionRestorationWebStateListObserverTest, WebStateDirty) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::FakeWebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kSerializable));

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  web::FakeNavigationContext context;
  web_state->OnNavigationFinished(&context);

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(base::Contains(observer.dirty_web_states(), web_state));
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest call the callback
// when an WebState whose navigation restoration is still in progress becomes
// dirty.
TEST_F(SessionRestorationWebStateListObserverTest,
       WebStateDirty_Unserializable) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::FakeWebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kRestoreInProgress));

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  web::FakeNavigationContext context;
  web_state->OnNavigationFinished(&context);

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(base::Contains(observer.dirty_web_states(), web_state));
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 1u);

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}

// Tests that SessionRestorationWebStateListObserverTest does not call the
// callback when an unrealized WebState becomes realized.
TEST_F(SessionRestorationWebStateListObserverTest, WebStateRealized) {
  size_t call_count = 0;
  SessionRestorationWebStateListObserver observer(
      web_state_list(), base::IgnoreArgs<WebStateList*>(base::BindRepeating(
                            &IncrementCounter, &call_count)));

  web::FakeWebState* const web_state =
      InsertWebState(CreateWebState(CreateWebStateAs::kUnrealized));

  // Clear the dirty state and reset the call counter.
  observer.ClearDirty();
  call_count = 0;

  web_state->ForceRealized();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
  EXPECT_EQ(call_count, 0u);  // Callback is not called!

  // Check that calling ClearDirty() leaves the observer in a non-dirty state.
  observer.ClearDirty();

  EXPECT_FALSE(observer.is_web_state_list_dirty());
  EXPECT_TRUE(observer.dirty_web_states().empty());
  EXPECT_TRUE(observer.inserted_web_states().empty());
  EXPECT_TRUE(observer.detached_web_states().empty());
  EXPECT_TRUE(observer.closed_web_states().empty());
}