chromium/ios/chrome/browser/shared/model/web_state_list/test/web_state_list_builder_from_description_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/web_state_list/test/web_state_list_builder_from_description.h"

#import "components/tab_groups/tab_group_id.h"
#import "components/tab_groups/tab_group_visual_data.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/browser/shared/model/web_state_list/web_state_list_delegate.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "testing/platform_test.h"

using tab_groups::TabGroupId;

// Tests for `WebStateListBuilderFromDescription`.
class WebStateListBuilderFromDescriptionTest : public PlatformTest,
                                               public WebStateListDelegate {
 public:
  // WebStateListDelegate implementation
  void WillAddWebState(web::WebState* web_state) final {}
  void WillActivateWebState(web::WebState* web_state) final {}

 protected:
  // Shorthand for `builder_.BuildWebStateListFromDescription(...)`.
  bool BuildWebStateList(std::string_view description) {
    return builder_.BuildWebStateListFromDescription(description);
  }

  // Shorthand for `builder_.GetWebStateListDescription()`.
  std::string GetDescription() const {
    return builder_.GetWebStateListDescription();
  }

  using InsertionParams = WebStateList::InsertionParams;
  // Shorthand for
  // `web_state_list_.InsertWebState(std::make_unique<FakeWebState>(), ...)`.
  void InsertWebState(const InsertionParams& params) {
    web_state_list_.InsertWebState(std::make_unique<web::FakeWebState>(),
                                   params);
  }

  // Resets `web_state_list_`.
  void Reset() {
    CloseAllWebStates(web_state_list_, WebStateList::CLOSE_NO_FLAGS);
  }

  WebStateList web_state_list_{this};
  WebStateListBuilderFromDescription builder_{&web_state_list_};
};

// Test that `WebStateListBuilderFromDescription` builds the expected
// WebStateList from the valid description "a b | [ 0 c d* ] e f [ 1 g h ]".
TEST_F(WebStateListBuilderFromDescriptionTest,
       CanBuildWebStateListFromDescription) {
  constexpr std::string_view valid_description =
      "a b | [ 0 c d* ] e f [ 1 g h ]";
  EXPECT_TRUE(BuildWebStateList(valid_description));
  EXPECT_EQ(valid_description, GetDescription());

  EXPECT_EQ(2, web_state_list_.pinned_tabs_count());
  EXPECT_EQ(6, web_state_list_.regular_tabs_count());
  EXPECT_EQ(3, web_state_list_.active_index());

  EXPECT_EQ(web_state_list_.GetWebStateAt(0),
            builder_.GetWebStateForIdentifier('a'));
  EXPECT_EQ(web_state_list_.GetWebStateAt(1),
            builder_.GetWebStateForIdentifier('b'));
  EXPECT_EQ(web_state_list_.GetWebStateAt(2),
            builder_.GetWebStateForIdentifier('c'));
  EXPECT_EQ(web_state_list_.GetWebStateAt(3),
            builder_.GetWebStateForIdentifier('d'));
  EXPECT_EQ(web_state_list_.GetWebStateAt(4),
            builder_.GetWebStateForIdentifier('e'));
  EXPECT_EQ(web_state_list_.GetWebStateAt(5),
            builder_.GetWebStateForIdentifier('f'));
  EXPECT_EQ(web_state_list_.GetWebStateAt(6),
            builder_.GetWebStateForIdentifier('g'));
  EXPECT_EQ(web_state_list_.GetWebStateAt(7),
            builder_.GetWebStateForIdentifier('h'));

  EXPECT_EQ(web_state_list_.GetGroupOfWebStateAt(2),
            builder_.GetTabGroupForIdentifier('0'));
  EXPECT_EQ(web_state_list_.GetGroupOfWebStateAt(3),
            builder_.GetTabGroupForIdentifier('0'));
  EXPECT_EQ(web_state_list_.GetGroupOfWebStateAt(4), nullptr);
  EXPECT_EQ(web_state_list_.GetGroupOfWebStateAt(5), nullptr);
  EXPECT_EQ(web_state_list_.GetGroupOfWebStateAt(6),
            builder_.GetTabGroupForIdentifier('1'));
  EXPECT_EQ(web_state_list_.GetGroupOfWebStateAt(7),
            builder_.GetTabGroupForIdentifier('1'));
}

// Tests that the description returned by `GetWebStateListDescription(...)` is
// as expected when a WebStateList is built manually.
TEST_F(WebStateListBuilderFromDescriptionTest, CanGetWebStateListDescription) {
  // Manually populate the WebStateList.
  InsertWebState(InsertionParams::AtIndex(0).Pinned(true));
  InsertWebState(InsertionParams::AtIndex(1).Pinned(true));
  InsertWebState(InsertionParams::AtIndex(2));
  InsertWebState(InsertionParams::AtIndex(3).Activate(true));
  InsertWebState(InsertionParams::AtIndex(4));
  InsertWebState(InsertionParams::AtIndex(5));
  InsertWebState(InsertionParams::AtIndex(6));
  InsertWebState(InsertionParams::AtIndex(7));
  web_state_list_.CreateGroup({2, 3}, {}, TabGroupId::GenerateNew());
  web_state_list_.CreateGroup({6, 7}, {}, TabGroupId::GenerateNew());
  EXPECT_EQ("_ _ | [ _ _ _* ] _ _ [ _ _ _ ]", GetDescription());
  builder_.GenerateIdentifiersForWebStateList();
  EXPECT_EQ("a b | [ 0 c d* ] e f [ 1 g h ]", GetDescription());
}

// Test that `BuildWebStateListFromDescription(description)`
// returns true when given a valid `description` and that the description
// returned by `GetWebStateListDescription()` is consistent with the description
// from which the WebStateList was built.
TEST_F(WebStateListBuilderFromDescriptionTest, ManyValidDescriptions) {
  constexpr std::string_view valid_descriptions[] = {
      "|",
      "a b |",
      "a b* |",
      "a | b*",
      "| a b*",
      "c | a b*",
      "c | a b* d",
      "c | a b* [ 0 d ]",
      "c | a b* [ 0 d e ]",
      "c | [ 1 a b* ] [ 0 d e ]",
      "c | [ 1 a ] b* [ 0 d e ]",
      // Three identical descriptions with different formatting, all valid.
      "| [ 1 a ] t u v [ 2 b* ] q r s [ 0 d e ]",
      "|[1a]tuv[2b*]qrs[0de]",
      "        |[  1a]    tu v [ 2  b* ]q  rs[0 d e]       ",
  };
  for (const auto& valid_description : valid_descriptions) {
    Reset();
    ASSERT_TRUE(BuildWebStateList(valid_description));
    EXPECT_EQ(builder_.FormatWebStateListDescription(valid_description),
              GetDescription());
  }
}

// Test that `BuildWebStateListFromDescription(description)`
// returns false when given an invalid `description`.
TEST_F(WebStateListBuilderFromDescriptionTest, ManyInvalidDescriptions) {
  constexpr std::string_view invalid_descriptions[] = {
      "a b c",  // The pinned WebStates separator '|' must appear exactly once.
      "[ 1 a b ] | c",      // Pinned WebStates cannot be in a group.
      "| [ 1 a [ 0 b ] ]",  // TabGroups cannot be nested.
      "| a a",              // Two WebStates cannot share the same identifier.
      "| [ 0 a ] [ 0 b ]",  // Two TabGroups cannot share the same identifier.
      "| a* b*",    // The description cannot contain several active WebStates.
      "| *a b c",   // The token '*' must be placed after a WebState identifier.
      "a | b | c",  // The pinned WebStates separator '|' can only occur once.
      "| [ 0 ]",    // TabGroups cannot be empty.
      "| 0 a ]",    // TabGroups need an opening bracket.
      "| [ 0 a",    // TabGroups need a closing bracket.
      "| [ a ]",    // TabGroups need an identifier.
      "| [ a 0 ]",  // The identifier for a TabGroup must immediately follow the
                    // opening bracket.
      "| 0 [ a ]",  // The identifier for a TabGroup must immediately follow the
                    // opening bracket.
      "():/+=",     // Invalid tokens.
      "| _",        // Cannot use placeholders to build a WebStateList.
  };
  for (const auto& invalid_description : invalid_descriptions) {
    Reset();
    EXPECT_FALSE(BuildWebStateList(invalid_description))
        << "\nBuildWebStateListFromDescription(...) unexpectedly returned true "
           "for an invalid description."
        << "\nInvalid description: " << invalid_description;
  }
}

// Test that WebStates and TabGroups can be given identifiers using
// `SetWebStateIdentifier(...)`, `SetTabGroupIdentifier(...)` and
// `GenerateIdentifiersForWebStateList(...)`.
TEST_F(WebStateListBuilderFromDescriptionTest, CanSetIdentifiers) {
  auto web_state_1 = std::make_unique<web::FakeWebState>();
  web::WebState* web_state_1_ptr = web_state_1.get();
  web_state_list_.InsertWebState(std::move(web_state_1));
  EXPECT_EQ("| _", GetDescription());
  builder_.SetWebStateIdentifier(web_state_1_ptr, 'a');
  EXPECT_EQ("| a", GetDescription());
  builder_.SetWebStateIdentifier(web_state_1_ptr, 'b');
  EXPECT_EQ("| b", GetDescription());

  auto web_state_2 = std::make_unique<web::FakeWebState>();
  web::WebState* web_state_2_ptr = web_state_2.get();
  web_state_list_.InsertWebState(std::move(web_state_2));
  EXPECT_EQ("| b _", GetDescription());
  builder_.SetWebStateIdentifier(web_state_2_ptr, 'a');
  EXPECT_EQ("| b a", GetDescription());
  builder_.SetWebStateIdentifier(web_state_2_ptr, 'c');
  EXPECT_EQ("| b c", GetDescription());

  const TabGroup* tab_group =
      web_state_list_.CreateGroup({0, 1}, {}, TabGroupId::GenerateNew());
  EXPECT_EQ("| [ _ b c ]", GetDescription());
  builder_.SetTabGroupIdentifier(tab_group, '0');
  EXPECT_EQ("| [ 0 b c ]", GetDescription());
  builder_.SetTabGroupIdentifier(tab_group, '1');
  EXPECT_EQ("| [ 1 b c ]", GetDescription());

  builder_.SetTabGroupIdentifier(tab_group, '9');
  builder_.SetWebStateIdentifier(web_state_1_ptr, 'z');
  builder_.SetWebStateIdentifier(web_state_2_ptr, 'y');
  EXPECT_EQ("| [ 9 z y ]", GetDescription());

  InsertWebState(InsertionParams::Automatic());
  InsertWebState(InsertionParams::Automatic());
  InsertWebState(InsertionParams::Automatic());
  InsertWebState(InsertionParams::Automatic());
  InsertWebState(InsertionParams::Automatic());
  EXPECT_EQ("| [ 9 z y ] _ _ _ _ _", GetDescription());

  builder_.SetWebStateIdentifier(web_state_list_.GetWebStateAt(2), 'x');
  builder_.SetWebStateIdentifier(web_state_list_.GetWebStateAt(4), 'w');
  builder_.SetWebStateIdentifier(web_state_list_.GetWebStateAt(6), 'v');
  EXPECT_EQ("| [ 9 z y ] x _ w _ v", GetDescription());

  builder_.GenerateIdentifiersForWebStateList();
  EXPECT_EQ("| [ 9 z y ] x a w b v", GetDescription());

  Reset();
  EXPECT_EQ("|", GetDescription());
  ASSERT_TRUE(BuildWebStateList("| [ 0 a b ] c d e f g"));
  EXPECT_EQ("| [ 0 a b ] c d e f g", GetDescription());
}

// Tests that modifications on a WebStateList are reflected in the description
// returned by `GetWebStateListDescription()`
TEST_F(WebStateListBuilderFromDescriptionTest,
       CanCheckWebStateListModifications) {
  InsertWebState(InsertionParams::AtIndex(0));
  InsertWebState(InsertionParams::AtIndex(1));
  InsertWebState(InsertionParams::AtIndex(2));

  EXPECT_EQ("| _ _ _", GetDescription());
  builder_.GenerateIdentifiersForWebStateList();
  EXPECT_EQ("| a b c", GetDescription());

  web_state_list_.SetWebStatePinnedAt(2, true);
  web_state_list_.SetWebStatePinnedAt(2, true);
  web_state_list_.SetWebStatePinnedAt(2, true);
  EXPECT_EQ("c b a |", GetDescription());

  web_state_list_.SetWebStatePinnedAt(0, false);
  web_state_list_.SetWebStatePinnedAt(0, false);
  web_state_list_.SetWebStatePinnedAt(0, false);
  EXPECT_EQ("| c b a", GetDescription());

  web_state_list_.MoveWebStateAt(0, 2);
  web_state_list_.MoveWebStateAt(1, 0);
  EXPECT_EQ("| a b c", GetDescription());

  {
    // Check that there is no sharing with another builder.
    WebStateList other_web_state_list{this};
    WebStateListBuilderFromDescription other_builder(&other_web_state_list);
    EXPECT_EQ("| a b c", builder_.GetWebStateListDescription());
    EXPECT_EQ("|", other_builder.GetWebStateListDescription());
    other_web_state_list.InsertWebState(web_state_list_.DetachWebStateAt(2));
    other_web_state_list.InsertWebState(web_state_list_.DetachWebStateAt(1));
    other_web_state_list.InsertWebState(web_state_list_.DetachWebStateAt(0));
    EXPECT_EQ("|", builder_.GetWebStateListDescription());
    // WebStates have no identifiers that could have leaked from the initial
    // builder.
    EXPECT_EQ("| _ _ _", other_builder.GetWebStateListDescription());
    CloseAllWebStates(other_web_state_list, WebStateList::CLOSE_NO_FLAGS);
    EXPECT_EQ("|", other_builder.GetWebStateListDescription());
  }

  Reset();

  InsertWebState(InsertionParams::Automatic().Pinned(true));
  InsertWebState(InsertionParams::Automatic().Pinned(true));
  InsertWebState(InsertionParams::Automatic());
  InsertWebState(InsertionParams::Automatic().Activate(true));
  InsertWebState(InsertionParams::Automatic());
  EXPECT_EQ("_ _ | _ _* _", GetDescription());
  builder_.GenerateIdentifiersForWebStateList();
  EXPECT_EQ("a b | c d* e", GetDescription());

  web_state_list_.ActivateWebStateAt(WebStateList::kInvalidIndex);
  EXPECT_EQ("a b | c d e", GetDescription());
  web_state_list_.ActivateWebStateAt(0);
  EXPECT_EQ("a* b | c d e", GetDescription());
  web_state_list_.ActivateWebStateAt(1);
  EXPECT_EQ("a b* | c d e", GetDescription());
  web_state_list_.ActivateWebStateAt(2);
  EXPECT_EQ("a b | c* d e", GetDescription());
  web_state_list_.ActivateWebStateAt(3);
  EXPECT_EQ("a b | c d* e", GetDescription());
  web_state_list_.ActivateWebStateAt(4);
  EXPECT_EQ("a b | c d e*", GetDescription());

  web_state_list_.CreateGroup({2}, {}, TabGroupId::GenerateNew());
  EXPECT_EQ("a b | [ _ c ] d e*", GetDescription());
  web_state_list_.CreateGroup({3}, {}, TabGroupId::GenerateNew());
  EXPECT_EQ("a b | [ _ c ] [ _ d ] e*", GetDescription());
  web_state_list_.CreateGroup({4}, {}, TabGroupId::GenerateNew());
  EXPECT_EQ("a b | [ _ c ] [ _ d ] [ _ e* ]", GetDescription());
  builder_.GenerateIdentifiersForWebStateList();
  EXPECT_EQ("a b | [ 0 c ] [ 1 d ] [ 2 e* ]", GetDescription());

  web_state_list_.CloseWebStatesAtIndices(WebStateList::CLOSE_NO_FLAGS,
                                          {0, 2, 4});
  EXPECT_EQ("b | [ 1 d* ]", GetDescription());
  CloseAllNonPinnedWebStates(web_state_list_, WebStateList::CLOSE_NO_FLAGS);
  EXPECT_EQ("b* |", GetDescription());
  CloseAllWebStates(web_state_list_, WebStateList::CLOSE_NO_FLAGS);
  EXPECT_EQ("|", GetDescription());
}

// Tests that the identifier of a closed tab doesn’t get reused for a new tab.
TEST_F(WebStateListBuilderFromDescriptionTest, ResetsTabIdentifier) {
  ASSERT_TRUE(BuildWebStateList("| a"));
  std::unique_ptr<web::WebState> detached_web_state =
      web_state_list_.DetachWebStateAt(0);
  EXPECT_EQ("|", GetDescription());

  web_state_list_.InsertWebState(std::move(detached_web_state),
                                 InsertionParams::Automatic());

  EXPECT_EQ("| _", GetDescription());
}

// Tests that the identifier of a closed group doesn’t get reused for a new
// group.
TEST_F(WebStateListBuilderFromDescriptionTest, ResetsGroupIdentifier) {
  ASSERT_TRUE(BuildWebStateList("| [ 0 a ]"));
  const TabGroup* group = builder_.GetTabGroupForIdentifier('0');
  web_state_list_.DeleteGroup(group);
  EXPECT_EQ("| a", GetDescription());

  const auto visual_data = tab_groups::TabGroupVisualData(
      u"New Group", tab_groups::TabGroupColorId::kGrey);
  web_state_list_.CreateGroup({0}, visual_data, TabGroupId::GenerateNew());

  EXPECT_EQ("| [ _ a ]", GetDescription());
}

// Tests that replacing a WebState doesn’t change the identifier.
TEST_F(WebStateListBuilderFromDescriptionTest, ReplaceKeepsIdentifier) {
  ASSERT_TRUE(BuildWebStateList("| a"));
  web::WebState* initial_web_state = web_state_list_.GetWebStateAt(0);
  ASSERT_EQ(initial_web_state, web_state_list_.GetWebStateAt(0));

  web_state_list_.ReplaceWebStateAt(0, std::make_unique<web::FakeWebState>());

  // Check that the WebState actually changed.
  EXPECT_NE(initial_web_state, web_state_list_.GetWebStateAt(0));
  // Check that the replacing WebState inherited the identifier.
  EXPECT_EQ("| a", GetDescription());
}

// Tests that detaching a WebState removes its identifier.
TEST_F(WebStateListBuilderFromDescriptionTest, DetachRemovesIdentifier) {
  ASSERT_TRUE(BuildWebStateList("| a"));
  web::WebState* web_state = web_state_list_.GetWebStateAt(0);
  EXPECT_EQ('a', builder_.GetWebStateIdentifier(web_state));

  std::unique_ptr<web::WebState> detached_web_state =
      web_state_list_.DetachWebStateAt(0);

  EXPECT_EQ(detached_web_state.get(), web_state);
  EXPECT_EQ('_', builder_.GetWebStateIdentifier(web_state));
  EXPECT_EQ("|", GetDescription());
}