// 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/saved_tab_groups/model/ios_tab_group_sync_delegate.h"
#import <memory>
#import <optional>
#import <string>
#import "base/memory/raw_ptr.h"
#import "base/test/ios/wait_util.h"
#import "base/uuid.h"
#import "components/saved_tab_groups/mock_tab_group_sync_service.h"
#import "components/saved_tab_groups/saved_tab_group.h"
#import "components/saved_tab_groups/saved_tab_group_tab.h"
#import "components/saved_tab_groups/types.h"
#import "components/tab_groups/tab_group_color.h"
#import "components/tab_groups/tab_group_id.h"
#import "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/browser/saved_tab_groups/model/ios_tab_group_action_context.h"
#import "ios/chrome/browser/saved_tab_groups/model/ios_tab_group_sync_util.h"
#import "ios/chrome/browser/saved_tab_groups/model/tab_group_local_update_observer.h"
#import "ios/chrome/browser/saved_tab_groups/model/tab_group_sync_service_factory.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_factory.h"
#import "ios/chrome/browser/sessions/model/test_session_restoration_service.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/coordinator/scene/test/fake_scene_state.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/browser_provider.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/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.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/test/web_state_list_builder_from_description.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.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/tab_grid_commands.h"
#import "ios/chrome/browser/shared/public/commands/tab_groups_commands.h"
#import "ios/chrome/browser/tab_insertion/model/tab_insertion_browser_agent.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_id.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
using ::testing::_;
using ::testing::Property;
using ::testing::Return;
namespace tab_groups {
namespace {
std::unique_ptr<KeyedService> CreateMockSyncService(
web::BrowserState* context) {
return std::make_unique<::testing::NiceMock<MockTabGroupSyncService>>();
}
// Updates the association of the local tab id.
void FakeUpdateLocalTabId(web::WebState* web_state,
SavedTabGroupTab& saved_tab,
SavedTabGroup& saved_group) {
saved_tab.SetLocalTabID(web_state->GetUniqueIdentifier().identifier());
saved_group.UpdateTab(saved_tab);
}
} // namespace
class IOSTabGroupSyncDelegateTest : public PlatformTest {
public:
IOSTabGroupSyncDelegateTest() {
app_state_ = OCMClassMock([AppState class]);
TestChromeBrowserState::Builder builder;
builder.AddTestingFactory(TabGroupSyncServiceFactory::GetInstance(),
base::BindRepeating(&CreateMockSyncService));
builder.AddTestingFactory(
SessionRestorationServiceFactory::GetInstance(),
TestSessionRestorationService::GetTestingFactory());
browser_state_ = std::move(builder).Build();
mock_service_ = static_cast<MockTabGroupSyncService*>(
TabGroupSyncServiceFactory::GetForBrowserState(browser_state_.get()));
scene_state_ =
[[FakeSceneState alloc] initWithAppState:app_state_
browserState:browser_state_.get()];
browser_ =
scene_state_.browserProviderInterface.mainBrowserProvider.browser;
TabInsertionBrowserAgent::CreateForBrowser(browser_);
scene_state_same_browser_state_ =
[[FakeSceneState alloc] initWithAppState:app_state_
browserState:browser_state_.get()];
browser_same_browser_state_ =
scene_state_same_browser_state_.browserProviderInterface
.mainBrowserProvider.browser;
TabInsertionBrowserAgent::CreateForBrowser(browser_same_browser_state_);
other_browser_state_ = TestChromeBrowserState::Builder().Build();
other_scene_state_ =
[[FakeSceneState alloc] initWithAppState:app_state_
browserState:other_browser_state_.get()];
other_browser_ =
other_scene_state_.browserProviderInterface.mainBrowserProvider.browser;
TabInsertionBrowserAgent::CreateForBrowser(other_browser_);
other_inactive_browser_ = other_browser_->CreateInactiveBrowser();
browser_list_ =
BrowserListFactory::GetForBrowserState(browser_state_.get());
auto local_observer = std::make_unique<TabGroupLocalUpdateObserver>(
browser_list_.get(), mock_service_);
browser_list_->AddBrowser(browser_);
browser_list_->AddBrowser(browser_same_browser_state_);
delegate_ = std::make_unique<IOSTabGroupSyncDelegate>(
browser_list_, mock_service_, std::move(local_observer));
mock_application_handler_ =
OCMStrictProtocolMock(@protocol(ApplicationCommands));
mock_tab_groups_handler_ =
OCMStrictProtocolMock(@protocol(TabGroupsCommands));
mock_tab_grid_handler_ = OCMStrictProtocolMock(@protocol(TabGridCommands));
CommandDispatcher* dispatcher = browser_->GetCommandDispatcher();
[dispatcher startDispatchingToTarget:mock_application_handler_
forProtocol:@protocol(ApplicationCommands)];
[dispatcher startDispatchingToTarget:mock_tab_groups_handler_
forProtocol:@protocol(TabGroupsCommands)];
[dispatcher startDispatchingToTarget:mock_tab_grid_handler_
forProtocol:@protocol(TabGridCommands)];
}
// Returns a vector containing the 3 distant tabs.
std::vector<SavedTabGroupTab> CreateSavedTabs(base::Uuid saved_tab_group_id) {
std::vector<SavedTabGroupTab> tabs;
tabs.push_back(FirstTab(saved_tab_group_id));
tabs.push_back(SecondTab(saved_tab_group_id));
tabs.push_back(ThirdTab(saved_tab_group_id));
return tabs;
}
// Verify that the `web_state_list` contains the 3 local tabs.
// `web_state_offset` is used to provide the number of web_states that were
// added before the tabs.
void VerifyLocalTabGroup(WebStateList* web_state_list, int web_state_offset) {
web::WebState* first_web_state =
web_state_list->GetWebStateAt(0 + web_state_offset);
EXPECT_EQ(kFirstTabURL, first_web_state->GetVisibleURL());
EXPECT_EQ(kFirstTabTitle, first_web_state->GetTitle());
web::WebState* second_web_state =
web_state_list->GetWebStateAt(1 + web_state_offset);
EXPECT_EQ(kSecondTabURL, second_web_state->GetVisibleURL());
EXPECT_EQ(kSecondTabTitle, second_web_state->GetTitle());
web::WebState* third_web_state =
web_state_list->GetWebStateAt(2 + web_state_offset);
EXPECT_EQ(kThirdTabURL, third_web_state->GetVisibleURL());
EXPECT_EQ(kThirdTabTitle, third_web_state->GetTitle());
}
// Return a distant tab at position 0 with the "first" ids.
SavedTabGroupTab FirstTab(base::Uuid group_guid) {
return SavedTabGroupTab(kFirstTabURL, kFirstTabTitle, group_guid,
std::make_optional(0), kFirstTabId);
}
// Return a distant tab at position 1 with the "second" ids.
SavedTabGroupTab SecondTab(base::Uuid group_guid) {
return SavedTabGroupTab(kSecondTabURL, kSecondTabTitle, group_guid,
std::make_optional(1), kSecondTabId);
}
// Return a distant tab at position 2 with the "third" ids.
SavedTabGroupTab ThirdTab(base::Uuid group_guid) {
return SavedTabGroupTab(kThirdTabURL, kThirdTabTitle, group_guid,
std::make_optional(2), kThirdTabId);
}
// Returns the sync tab group prediction for the given `saved_group`.
auto SyncTabGroupPrediction(SavedTabGroup saved_group) {
return AllOf(
Property(&SavedTabGroup::local_group_id, saved_group.local_group_id()),
Property(&SavedTabGroup::title, saved_group.title()),
Property(&SavedTabGroup::color, saved_group.color()));
}
// Creates a vector of `saved_tabs` based on the given `range`.
std::vector<SavedTabGroupTab> SavedTabGroupTabsFromTabs(
std::vector<int> indexes,
WebStateList* web_state_list,
base::Uuid saved_tab_group_id) {
std::vector<SavedTabGroupTab> saved_tabs;
for (int index : indexes) {
web::WebState* web_state = web_state_list->GetWebStateAt(index);
SavedTabGroupTab saved_tab(web_state->GetVisibleURL(),
web_state->GetTitle(), saved_tab_group_id,
std::make_optional(index), std::nullopt,
web_state->GetUniqueIdentifier().identifier());
saved_tabs.push_back(saved_tab);
}
return saved_tabs;
}
protected:
web::WebTaskEnvironment task_environment_;
id app_state_;
FakeSceneState* scene_state_;
FakeSceneState* scene_state_same_browser_state_;
FakeSceneState* other_scene_state_;
std::unique_ptr<TestChromeBrowserState> browser_state_;
Browser* browser_;
Browser* browser_same_browser_state_;
std::unique_ptr<TestChromeBrowserState> other_browser_state_;
Browser* other_browser_;
Browser* other_inactive_browser_;
raw_ptr<BrowserList> browser_list_;
std::unique_ptr<IOSTabGroupSyncDelegate> delegate_;
raw_ptr<MockTabGroupSyncService> mock_service_;
const base::Uuid kFirstTabId = base::Uuid::GenerateRandomV4();
const base::Uuid kSecondTabId = base::Uuid::GenerateRandomV4();
const base::Uuid kThirdTabId = base::Uuid::GenerateRandomV4();
const GURL kFirstTabURL = GURL("https://first_tab.com");
const GURL kSecondTabURL = GURL("https://second_tab.com");
const GURL kThirdTabURL = GURL("https://third_tab.com");
const std::u16string kFirstTabTitle = u"first tab";
const std::u16string kSecondTabTitle = u"second tab";
const std::u16string kThirdTabTitle = u"third tab";
const std::u16string kGroupTitle = u"my group title";
const TabGroupColorId kGroupColor = TabGroupColorId::kPurple;
id mock_application_handler_;
id mock_tab_groups_handler_;
id mock_tab_grid_handler_;
};
// Tests adding a tab group when the currently foregrounded active scene is with
// the same browser state.
TEST_F(IOSTabGroupSyncDelegateTest, CreateTabGroupSameBrowserStateForeground) {
scene_state_same_browser_state_.activationLevel =
SceneActivationLevelForegroundActive;
scene_state_.activationLevel = SceneActivationLevelForegroundInactive;
other_scene_state_.activationLevel = SceneActivationLevelForegroundInactive;
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_service_,
UpdateLocalTabGroupMapping(saved_tab_group_id, _));
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kFirstTabId, _));
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kSecondTabId, _));
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kThirdTabId, _));
const GURL kFakeUrl = GURL("https://fakeWebState.com");
std::unique_ptr<web::FakeWebState> fake_web_state =
std::make_unique<web::FakeWebState>(web::WebStateID::NewUnique());
fake_web_state->SetCurrentURL(kFakeUrl);
WebStateList* target_web_state_list =
browser_same_browser_state_->GetWebStateList();
target_web_state_list->InsertWebState(std::move(fake_web_state));
target_web_state_list->ActivateWebStateAt(0);
SavedTabGroup saved_group(kGroupTitle, kGroupColor,
CreateSavedTabs(saved_tab_group_id),
std::make_optional(0), saved_tab_group_id);
delegate_->CreateLocalTabGroup(saved_group);
ASSERT_EQ(4, target_web_state_list->count());
EXPECT_EQ(kFakeUrl, target_web_state_list->GetWebStateAt(0)->GetVisibleURL());
EXPECT_EQ(0, target_web_state_list->active_index());
EXPECT_FALSE(target_web_state_list->GetGroupOfWebStateAt(0));
const TabGroup* tab_group = target_web_state_list->GetGroupOfWebStateAt(1);
ASSERT_TRUE(tab_group);
VerifyLocalTabGroup(target_web_state_list, 1);
}
// Tests adding a tab group when the currently foreground active scene is from
// another browser state.
TEST_F(IOSTabGroupSyncDelegateTest, CreateTabGroupOtherBrowserStateForeground) {
other_scene_state_.activationLevel = SceneActivationLevelForegroundActive;
scene_state_.activationLevel = SceneActivationLevelForegroundInactive;
scene_state_same_browser_state_.activationLevel =
SceneActivationLevelBackground;
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_service_,
UpdateLocalTabGroupMapping(saved_tab_group_id, _));
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kFirstTabId, _));
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kSecondTabId, _));
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kThirdTabId, _));
const GURL kFakeUrl = GURL("https://fakeWebState.com");
std::unique_ptr<web::FakeWebState> fake_web_state =
std::make_unique<web::FakeWebState>(web::WebStateID::NewUnique());
fake_web_state->SetCurrentURL(kFakeUrl);
WebStateList* target_web_state_list = browser_->GetWebStateList();
target_web_state_list->InsertWebState(std::move(fake_web_state));
target_web_state_list->ActivateWebStateAt(0);
SavedTabGroup saved_group(kGroupTitle, kGroupColor,
CreateSavedTabs(saved_tab_group_id),
std::make_optional(0), saved_tab_group_id);
delegate_->CreateLocalTabGroup(saved_group);
ASSERT_EQ(4, target_web_state_list->count());
EXPECT_EQ(kFakeUrl, target_web_state_list->GetWebStateAt(0)->GetVisibleURL());
EXPECT_EQ(0, target_web_state_list->active_index());
EXPECT_FALSE(target_web_state_list->GetGroupOfWebStateAt(0));
const TabGroup* tab_group = target_web_state_list->GetGroupOfWebStateAt(1);
ASSERT_TRUE(tab_group);
VerifyLocalTabGroup(target_web_state_list, 1);
}
// Tests adding a tab group when there is no currently foreground active scene,
// the only foreground scene is from another browser state and there is one
// scene in background.
TEST_F(IOSTabGroupSyncDelegateTest, CreateTabGroupBackgroundScene) {
other_scene_state_.activationLevel = SceneActivationLevelForegroundInactive;
scene_state_.activationLevel = SceneActivationLevelBackground;
scene_state_same_browser_state_.activationLevel =
SceneActivationLevelDisconnected;
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_service_,
UpdateLocalTabGroupMapping(saved_tab_group_id, _));
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kFirstTabId, _));
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kSecondTabId, _));
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kThirdTabId, _));
const GURL kFakeUrl = GURL("https://fakeWebState.com");
std::unique_ptr<web::FakeWebState> fake_web_state =
std::make_unique<web::FakeWebState>(web::WebStateID::NewUnique());
fake_web_state->SetCurrentURL(kFakeUrl);
WebStateList* target_web_state_list = browser_->GetWebStateList();
target_web_state_list->InsertWebState(std::move(fake_web_state));
target_web_state_list->ActivateWebStateAt(0);
SavedTabGroup saved_group(kGroupTitle, kGroupColor,
CreateSavedTabs(saved_tab_group_id),
std::make_optional(0), saved_tab_group_id);
delegate_->CreateLocalTabGroup(saved_group);
ASSERT_EQ(4, target_web_state_list->count());
EXPECT_EQ(kFakeUrl, target_web_state_list->GetWebStateAt(0)->GetVisibleURL());
EXPECT_EQ(0, target_web_state_list->active_index());
EXPECT_FALSE(target_web_state_list->GetGroupOfWebStateAt(0));
const TabGroup* tab_group = target_web_state_list->GetGroupOfWebStateAt(1);
ASSERT_TRUE(tab_group);
VerifyLocalTabGroup(target_web_state_list, 1);
}
// Tests `CloseLocalTabGroup`.
TEST_F(IOSTabGroupSyncDelegateTest, CloseLocalTabGroup) {
WebStateList* web_state_list = browser_->GetWebStateList();
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"| a [0 b* c ] d", browser_->GetBrowserState()));
WebStateList* web_state_list_same_browser_state =
browser_same_browser_state_->GetWebStateList();
WebStateListBuilderFromDescription builder_same_browser_state(
web_state_list_same_browser_state);
ASSERT_TRUE(builder_same_browser_state.BuildWebStateListFromDescription(
"| [1 e* f ] g h ", browser_->GetBrowserState()));
LocalTabGroupID local_id_group_0 =
builder.GetTabGroupForIdentifier('0')->tab_group_id();
delegate_->CloseLocalTabGroup(local_id_group_0);
EXPECT_EQ("| a d*", builder.GetWebStateListDescription());
LocalTabGroupID local_id_group_1 =
builder_same_browser_state.GetTabGroupForIdentifier('1')->tab_group_id();
delegate_->CloseLocalTabGroup(local_id_group_1);
EXPECT_EQ("| g* h", builder_same_browser_state.GetWebStateListDescription());
}
// Tests `CloseLocalTabGroup` correctly updates all local tabs.
TEST_F(IOSTabGroupSyncDelegateTest, UpdateLocalTabGroup) {
WebStateList* web_state_list = browser_->GetWebStateList();
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"| a* [0 b c ] d", browser_->GetBrowserState()));
WebStateList* web_state_list_same_browser_state =
browser_same_browser_state_->GetWebStateList();
WebStateListBuilderFromDescription builder_same_browser_state(
web_state_list_same_browser_state);
ASSERT_TRUE(builder_same_browser_state.BuildWebStateListFromDescription(
"| [1 e* f ] g h ", browser_->GetBrowserState()));
const TabGroup* tab_group = builder.GetTabGroupForIdentifier('0');
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
std::vector<SavedTabGroupTab> tabs;
SavedTabGroupTab first_tab = FirstTab(saved_tab_group_id);
SavedTabGroupTab second_tab = SecondTab(saved_tab_group_id);
SavedTabGroupTab third_tab = ThirdTab(saved_tab_group_id);
SavedTabGroup saved_group(u"my group", TabGroupColorId::kPink, tabs,
std::make_optional(0), saved_tab_group_id);
saved_group.SetLocalGroupId(tab_group->tab_group_id());
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kFirstTabId, _)).Times(1);
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kSecondTabId, _)).Times(2);
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kThirdTabId, _)).Times(2);
// Update the local group with only one tab.
saved_group.AddTabFromSync(first_tab);
delegate_->UpdateLocalTabGroup(saved_group);
web::WebState* first_web_state = web_state_list->GetWebStateAt(1);
FakeUpdateLocalTabId(first_web_state, first_tab, saved_group);
ASSERT_EQ(3, web_state_list->count());
EXPECT_FALSE(first_web_state->IsRealized());
EXPECT_EQ(kFirstTabURL, first_web_state->GetVisibleURL());
EXPECT_EQ(kFirstTabTitle, first_web_state->GetTitle());
EXPECT_EQ(1, tab_group->range().count());
EXPECT_TRUE([tab_group->GetTitle() isEqual:@"my group"]);
EXPECT_TRUE([tab_group->GetColor()
isEqual:TabGroup::ColorForTabGroupColorId(TabGroupColorId::kPink)]);
// Update the local group by adding 2 new tabs.
saved_group.AddTabFromSync(second_tab);
saved_group.AddTabFromSync(third_tab);
delegate_->UpdateLocalTabGroup(saved_group);
first_web_state = web_state_list->GetWebStateAt(1);
web::WebState* second_web_state = web_state_list->GetWebStateAt(2);
web::WebState* third_web_state = web_state_list->GetWebStateAt(3);
FakeUpdateLocalTabId(second_web_state, second_tab, saved_group);
FakeUpdateLocalTabId(third_web_state, third_tab, saved_group);
ASSERT_EQ(5, web_state_list->count());
EXPECT_FALSE(first_web_state->IsRealized());
EXPECT_FALSE(second_web_state->IsRealized());
EXPECT_FALSE(third_web_state->IsRealized());
EXPECT_EQ(kFirstTabURL, first_web_state->GetVisibleURL());
EXPECT_EQ(kFirstTabTitle, first_web_state->GetTitle());
EXPECT_EQ(kSecondTabURL, second_web_state->GetVisibleURL());
EXPECT_EQ(kSecondTabTitle, second_web_state->GetTitle());
EXPECT_EQ(kThirdTabURL, third_web_state->GetVisibleURL());
EXPECT_EQ(kThirdTabTitle, third_web_state->GetTitle());
EXPECT_EQ(3, tab_group->range().count());
// Move the second tab at the end and remove the first tab.
saved_group.MoveTabLocally(kSecondTabId, 2);
saved_group.RemoveTabFromSync(kFirstTabId);
delegate_->UpdateLocalTabGroup(saved_group);
second_web_state = web_state_list->GetWebStateAt(2);
third_web_state = web_state_list->GetWebStateAt(1);
ASSERT_EQ(4, web_state_list->count());
EXPECT_FALSE(second_web_state->IsRealized());
EXPECT_FALSE(third_web_state->IsRealized());
EXPECT_EQ(kSecondTabURL, second_web_state->GetVisibleURL());
EXPECT_EQ(kSecondTabTitle, second_web_state->GetTitle());
EXPECT_EQ(kThirdTabURL, third_web_state->GetVisibleURL());
EXPECT_EQ(kThirdTabTitle, third_web_state->GetTitle());
EXPECT_EQ(2, tab_group->range().count());
// Update the URL of both tabs.
GURL second_tab_updated_url = GURL("https://second_tab_updated.com");
GURL third_tab_updated_url = GURL("https://third_tab_updated.com");
second_tab.SetURL(second_tab_updated_url);
third_tab.SetURL(third_tab_updated_url);
saved_group.UpdateTab(second_tab);
saved_group.UpdateTab(third_tab);
delegate_->UpdateLocalTabGroup(saved_group);
second_web_state = web_state_list->GetWebStateAt(2);
third_web_state = web_state_list->GetWebStateAt(1);
FakeUpdateLocalTabId(second_web_state, second_tab, saved_group);
FakeUpdateLocalTabId(third_web_state, third_tab, saved_group);
ASSERT_EQ(4, web_state_list->count());
EXPECT_FALSE(second_web_state->IsRealized());
EXPECT_FALSE(third_web_state->IsRealized());
EXPECT_EQ(second_tab_updated_url, second_web_state->GetVisibleURL());
EXPECT_EQ(kSecondTabTitle, second_web_state->GetTitle());
EXPECT_EQ(third_tab_updated_url, third_web_state->GetVisibleURL());
EXPECT_EQ(kThirdTabTitle, third_web_state->GetTitle());
EXPECT_EQ(2, tab_group->range().count());
}
// Tests `CloseLocalTabGroup` correctly updates local group with one tab.
TEST_F(IOSTabGroupSyncDelegateTest, UpdateLocalTabGroupOneTab) {
WebStateList* web_state_list = browser_->GetWebStateList();
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"| a* [0 b ] c", browser_->GetBrowserState()));
WebStateList* web_state_list_same_browser_state =
browser_same_browser_state_->GetWebStateList();
WebStateListBuilderFromDescription builder_same_browser_state(
web_state_list_same_browser_state);
ASSERT_TRUE(builder_same_browser_state.BuildWebStateListFromDescription(
"| [1 e* f ] g h ", browser_->GetBrowserState()));
const TabGroup* tab_group = builder.GetTabGroupForIdentifier('0');
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
std::vector<SavedTabGroupTab> tabs;
SavedTabGroupTab first_tab = FirstTab(saved_tab_group_id);
SavedTabGroup saved_group(u"my group", TabGroupColorId::kPink, tabs,
std::make_optional(0), saved_tab_group_id);
saved_group.SetLocalGroupId(tab_group->tab_group_id());
EXPECT_CALL(*mock_service_, UpdateLocalTabId(_, kFirstTabId, _)).Times(1);
// Update the local group with only one tab.
saved_group.AddTabFromSync(first_tab);
delegate_->UpdateLocalTabGroup(saved_group);
web::WebState* first_web_state = web_state_list->GetWebStateAt(1);
FakeUpdateLocalTabId(first_web_state, first_tab, saved_group);
ASSERT_EQ(3, web_state_list->count());
EXPECT_FALSE(first_web_state->IsRealized());
EXPECT_EQ(kFirstTabURL, first_web_state->GetVisibleURL());
EXPECT_EQ(kFirstTabTitle, first_web_state->GetTitle());
EXPECT_EQ(1, tab_group->range().count());
EXPECT_TRUE([tab_group->GetTitle() isEqual:@"my group"]);
EXPECT_TRUE([tab_group->GetColor()
isEqual:TabGroup::ColorForTabGroupColorId(TabGroupColorId::kPink)]);
}
// Tests that the service correctly returns local ids.
TEST_F(IOSTabGroupSyncDelegateTest, GetLocalTabGroupIds) {
WebStateList* web_state_list = browser_->GetWebStateList();
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"| a [0 b* c ] d [1 e ]", browser_->GetBrowserState()));
auto local_group_ids = delegate_->GetLocalTabGroupIds();
EXPECT_EQ(2u, local_group_ids.size());
}
// Tests that the service correctly returns local ids.
TEST_F(IOSTabGroupSyncDelegateTest, GetLocalTabIdsForTabGroup) {
WebStateList* web_state_list = browser_->GetWebStateList();
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"| a [0 b* c ] d [1 e ]", browser_->GetBrowserState()));
LocalTabGroupID local_id_group_0 =
builder.GetTabGroupForIdentifier('0')->tab_group_id();
auto local_tab_ids = delegate_->GetLocalTabIdsForTabGroup(local_id_group_0);
EXPECT_EQ(2u, local_tab_ids.size());
LocalTabGroupID local_id_group_2 = TabGroupId::GenerateNew();
local_tab_ids = delegate_->GetLocalTabIdsForTabGroup(local_id_group_2);
EXPECT_EQ(0u, local_tab_ids.size());
}
// Tests that the service is correctly updated when creating a remote tab group.
TEST_F(IOSTabGroupSyncDelegateTest, CreateRemoteTabGroup) {
WebStateList* web_state_list = browser_same_browser_state_->GetWebStateList();
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription("| a b c* d e f"));
TabGroupId tab_group_id = TabGroupId::GenerateNew();
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
tab_groups::TabGroupVisualData visual_data(
kGroupTitle, tab_groups::TabGroupColorId::kBlue);
web_state_list->CreateGroup({0, 1}, visual_data, tab_group_id);
std::vector<SavedTabGroupTab> saved_tabs =
SavedTabGroupTabsFromTabs({0, 1}, web_state_list, saved_tab_group_id);
SavedTabGroup saved_group(kGroupTitle, visual_data.color(), saved_tabs,
std::nullopt, saved_tab_group_id, tab_group_id);
EXPECT_CALL(*mock_service_, GetGroup(tab_group_id));
EXPECT_CALL(*mock_service_, AddGroup(SyncTabGroupPrediction(saved_group)));
delegate_->CreateRemoteTabGroup(tab_group_id);
}
// Tests opening an unknown tab group ID doesn't do anything.
TEST_F(IOSTabGroupSyncDelegateTest,
HandleOpenTabGroupRequest_UnknownSavedTabGroupID) {
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
EXPECT_CALL(*mock_service_, GetGroup(saved_tab_group_id))
.WillOnce(Return(std::nullopt));
delegate_->HandleOpenTabGroupRequest(
saved_tab_group_id, std::make_unique<IOSTabGroupActionContext>(browser_));
// Check that no tab group was opened locally.
auto local_group_ids = delegate_->GetLocalTabGroupIds();
EXPECT_EQ(0u, local_group_ids.size());
}
// Tests opening a tab group from sync that isn't already open locally.
TEST_F(IOSTabGroupSyncDelegateTest,
HandleOpenTabGroupRequest_UnopenedSavedTabGroup) {
// Have another scene as the active one to make sure that the `browser` passed
// is correctly used.
scene_state_same_browser_state_.activationLevel =
SceneActivationLevelForegroundActive;
scene_state_.activationLevel = SceneActivationLevelForegroundInactive;
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
SavedTabGroup saved_group(kGroupTitle, kGroupColor,
CreateSavedTabs(saved_tab_group_id),
std::make_optional(0), saved_tab_group_id);
__block const TabGroup* tab_group_shown;
__block const TabGroup* tab_group_for_grid;
__block BOOL grid_updated = NO;
OCMStub([mock_application_handler_
displayTabGridInMode:TabGridOpeningMode::kRegular]);
OCMStub([mock_tab_groups_handler_
showTabGroup:(const TabGroup*)[OCMArg anyPointer]])
.andDo(^(NSInvocation* invocation) {
[invocation getArgument:&tab_group_shown atIndex:2];
});
OCMStub([mock_tab_grid_handler_
bringGroupIntoView:(const TabGroup*)[OCMArg anyPointer]
animated:NO])
.andDo(^(NSInvocation* invocation) {
grid_updated = YES;
[invocation getArgument:&tab_group_for_grid atIndex:2];
});
EXPECT_CALL(*mock_service_, GetGroup(saved_tab_group_id))
.WillOnce(Return(saved_group));
delegate_->HandleOpenTabGroupRequest(
saved_tab_group_id, std::make_unique<IOSTabGroupActionContext>(browser_));
// Check that a tab group was opened locally.
auto local_group_ids = delegate_->GetLocalTabGroupIds();
EXPECT_EQ(1u, local_group_ids.size());
const auto local_group_id = local_group_ids[0];
const auto local_tab_group_info =
tab_groups::utils::GetLocalTabGroupInfo(browser_list_, local_group_id);
EXPECT_EQ(browser_->GetWebStateList(), local_tab_group_info.web_state_list);
const auto groups = local_tab_group_info.web_state_list->GetGroups();
EXPECT_EQ(1u, groups.size());
EXPECT_OCMOCK_VERIFY(mock_application_handler_);
EXPECT_OCMOCK_VERIFY(mock_tab_groups_handler_);
// The grid operation is happening with a delay.
EXPECT_TRUE(
base::test::ios::WaitUntilConditionOrTimeout(base::Milliseconds(300), ^{
return grid_updated;
}));
EXPECT_OCMOCK_VERIFY(mock_tab_grid_handler_);
EXPECT_EQ(tab_group_shown, tab_group_for_grid);
EXPECT_TRUE(groups.contains(tab_group_shown));
EXPECT_TRUE(groups.contains(tab_group_for_grid));
}
// Tests opening a tab group from sync that is already open locally in this
// window doesn't open a new local group.
TEST_F(IOSTabGroupSyncDelegateTest,
HandleOpenTabGroupRequest_OpenedSavedTabGroupSameWindow) {
// Create a local group.
WebStateList* web_state_list = browser_->GetWebStateList();
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"| a [0 b* c ] d", browser_->GetBrowserState()));
const TabGroup* local_group = builder.GetTabGroupForIdentifier('0');
LocalTabGroupID local_id_group_0 = local_group->tab_group_id();
ASSERT_EQ(1u, delegate_->GetLocalTabGroupIds().size());
ASSERT_EQ(1u, web_state_list->GetGroups().size());
// Create the associated distant group.
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
SavedTabGroup saved_group(kGroupTitle, kGroupColor,
CreateSavedTabs(saved_tab_group_id),
std::make_optional(0), saved_tab_group_id);
saved_group.SetLocalGroupId(local_id_group_0);
OCMStub([mock_application_handler_
displayTabGridInMode:TabGridOpeningMode::kRegular]);
OCMStub([mock_tab_groups_handler_ showTabGroup:local_group]);
__block BOOL grid_updated = NO;
OCMStub([mock_tab_grid_handler_ bringGroupIntoView:local_group animated:NO])
.andDo(^(NSInvocation* invocation) {
grid_updated = YES;
});
EXPECT_CALL(*mock_service_, GetGroup(saved_tab_group_id))
.WillOnce(Return(saved_group));
delegate_->HandleOpenTabGroupRequest(
saved_tab_group_id, std::make_unique<IOSTabGroupActionContext>(browser_));
// Check that there is still only one tab group opened locally.
auto local_group_ids = delegate_->GetLocalTabGroupIds();
EXPECT_EQ(1u, local_group_ids.size());
EXPECT_EQ(1u, web_state_list->GetGroups().size());
EXPECT_OCMOCK_VERIFY(mock_application_handler_);
EXPECT_OCMOCK_VERIFY(mock_tab_groups_handler_);
// The grid operation is happening with a delay.
EXPECT_TRUE(
base::test::ios::WaitUntilConditionOrTimeout(base::Milliseconds(300), ^{
return grid_updated;
}));
EXPECT_OCMOCK_VERIFY(mock_tab_grid_handler_);
}
// Tests opening a tab group from sync that is already open locally on another
// window doesn't open a new local group.
TEST_F(IOSTabGroupSyncDelegateTest,
HandleOpenTabGroupRequest_OpenedSavedTabGroupDifferentWindow) {
// Create a local group.
WebStateList* web_state_list = browser_->GetWebStateList();
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"| a [0 b* c ] d", browser_->GetBrowserState()));
const TabGroup* local_tab_group = builder.GetTabGroupForIdentifier('0');
LocalTabGroupID local_id_group_0 = local_tab_group->tab_group_id();
ASSERT_EQ(1u, delegate_->GetLocalTabGroupIds().size());
ASSERT_EQ(1u, web_state_list->GetGroups().size());
// Create the associated distant group.
base::Uuid saved_tab_group_id = base::Uuid::GenerateRandomV4();
SavedTabGroup saved_group(kGroupTitle, kGroupColor,
CreateSavedTabs(saved_tab_group_id),
std::make_optional(0), saved_tab_group_id);
saved_group.SetLocalGroupId(local_id_group_0);
SceneState* scene_state = browser_->GetSceneState();
scene_state.UIEnabled = NO;
UIApplication* app = [UIApplication sharedApplication];
id app_mock = OCMPartialMock(app);
if (@available(iOS 17, *)) {
id request_arg =
[OCMArg checkWithBlock:^BOOL(UISceneSessionActivationRequest* request) {
return request.session == scene_state.scene.session &&
request.options.requestingScene ==
browser_same_browser_state_->GetSceneState().scene;
}];
OCMStub([app_mock activateSceneSessionForRequest:request_arg
errorHandler:[OCMArg any]])
.andDo(^(NSInvocation* invocation) {
scene_state.UIEnabled = YES;
});
} else {
OCMStub([app_mock requestSceneSessionActivation:scene_state.scene.session
userActivity:nil
options:[OCMArg any]
errorHandler:[OCMArg any]])
.andDo(^(NSInvocation* invocation) {
scene_state.UIEnabled = YES;
});
}
OCMStub([mock_application_handler_
displayTabGridInMode:TabGridOpeningMode::kRegular]);
OCMStub([mock_tab_groups_handler_ showTabGroup:local_tab_group]);
EXPECT_CALL(*mock_service_, GetGroup(saved_tab_group_id))
.WillOnce(Return(saved_group));
delegate_->HandleOpenTabGroupRequest(
saved_tab_group_id,
std::make_unique<IOSTabGroupActionContext>(browser_same_browser_state_));
// Check that there is still only one tab group opened locally.
auto local_group_ids = delegate_->GetLocalTabGroupIds();
EXPECT_EQ(1u, local_group_ids.size());
EXPECT_EQ(1u, web_state_list->GetGroups().size());
EXPECT_OCMOCK_VERIFY(mock_application_handler_);
EXPECT_OCMOCK_VERIFY(mock_tab_groups_handler_);
}
} // namespace tab_groups