chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_mediator_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/ui/tab_switcher/tab_grid/tab_groups/tab_group_mediator.h"

#import <memory>

#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "ios/chrome/browser/drag_and_drop/model/drag_item_util.h"
#import "ios/chrome/browser/main/model/browser_web_state_list_delegate.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.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/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/features/features.h"
#import "ios/chrome/browser/snapshots/model/snapshot_browser_agent.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_drag_drop_metrics.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_mediator_test.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_mode_holder.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/test/fake_tab_collection_consumer.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"

class TabGroupMediatorTest : public GridMediatorTestClass {
 public:
  void SetUp() override {
    scoped_feature_list_.InitWithFeatures(
        {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip}, {});

    GridMediatorTestClass::SetUp();
    WebStateList* web_state_list = browser_->GetWebStateList();
    CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);
    builder_ =
        std::make_unique<WebStateListBuilderFromDescription>(web_state_list);
    ASSERT_TRUE(builder_->BuildWebStateListFromDescription(
        "| f [ 1 a* b c ] d e ", browser_->GetBrowserState()));

    mode_holder_ = [[TabGridModeHolder alloc] init];

    tab_group_ = web_state_list->GetGroupOfWebStateAt(1);

    tab_group_consumer_ = OCMProtocolMock(@protocol(TabGroupConsumer));

    mediator_ = [[TabGroupMediator alloc]
        initWithWebStateList:browser_->GetWebStateList()
                    tabGroup:tab_group_->GetWeakPtr()
                    consumer:tab_group_consumer_
                gridConsumer:consumer_
                  modeHolder:mode_holder_];
    mediator_.browser = browser_.get();
  }

  void TearDown() override {
    // Forces the mediator to removes its Observer from WebStateList before the
    // Browser is destroyed.
    mediator_.browser = nullptr;
    mediator_ = nil;
    GridMediatorTestClass::TearDown();
  }

  // Checks that the drag item origin metric is logged in UMA.
  void ExpectThatDragItemOriginMetricLogged(DragItemOrigin origin,
                                            int count = 1) {
    histogram_tester_.ExpectUniqueSample(kUmaGroupViewDragOrigin, origin,
                                         count);
  }

 protected:
  TabGroupMediator* mediator_;
  id<TabGroupConsumer> tab_group_consumer_;
  const TabGroup* tab_group_;
  std::unique_ptr<WebStateListBuilderFromDescription> builder_;
  base::test::ScopedFeatureList scoped_feature_list_;
  base::HistogramTester histogram_tester_;
  TabGridModeHolder* mode_holder_;
};

// Tests dropping a local tab (e.g. drag from same window) in the grid.
TEST_F(TabGroupMediatorTest, DropLocalTab) {
  WebStateList* web_state_list = browser_->GetWebStateList();

  web::WebStateID web_state_id =
      web_state_list->GetWebStateAt(4)->GetUniqueIdentifier();

  id local_object = [[TabInfo alloc] initWithTabID:web_state_id
                                      browserState:browser_->GetBrowserState()];
  NSItemProvider* item_provider = [[NSItemProvider alloc] init];
  UIDragItem* drag_item =
      [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;

  // Drop item.
  [mediator_ dropItem:drag_item toIndex:1 fromSameCollection:YES];

  EXPECT_EQ("| f [ 1 a* d b c ] e", builder_->GetWebStateListDescription());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameCollection);
}

// Tests dropping tabs from the grid to a tab group.
TEST_F(TabGroupMediatorTest, DropFromTabGrid) {
  WebStateList* web_state_list = browser_->GetWebStateList();

  // Drop "F" before "A".
  web::WebStateID web_state_id =
      web_state_list->GetWebStateAt(0)->GetUniqueIdentifier();
  id local_object = [[TabInfo alloc] initWithTabID:web_state_id
                                      browserState:browser_->GetBrowserState()];
  NSItemProvider* item_provider = [[NSItemProvider alloc] init];
  UIDragItem* drag_item =
      [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;
  [mediator_ dropItem:drag_item toIndex:0 fromSameCollection:NO];
  EXPECT_EQ("| [ 1 f a* b c ] d e", builder_->GetWebStateListDescription());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameBrowser, 1);

  // Drop "D" before "B".
  web_state_id = web_state_list->GetWebStateAt(4)->GetUniqueIdentifier();
  local_object = [[TabInfo alloc] initWithTabID:web_state_id
                                   browserState:browser_->GetBrowserState()];
  item_provider = [[NSItemProvider alloc] init];
  drag_item = [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;
  [mediator_ dropItem:drag_item toIndex:2 fromSameCollection:NO];
  EXPECT_EQ("| [ 1 f a* d b c ] e", builder_->GetWebStateListDescription());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameBrowser, 2);
}

// Tests dropping a tab from another browser (e.g. drag from another window) in
// the grid.
TEST_F(TabGroupMediatorTest, DropCrossWindowTab) {
  auto other_browser = std::make_unique<TestBrowser>(
      browser_state_.get(), scene_state_,
      std::make_unique<BrowserWebStateListDelegate>());
  SnapshotBrowserAgent::CreateForBrowser(other_browser.get());

  browser_list_->AddBrowser(other_browser.get());

  GURL url_to_load = GURL("https://dragged_url.com");
  std::unique_ptr<web::FakeWebState> other_web_state =
      CreateFakeWebStateWithURL(url_to_load);
  web::WebStateID other_id = other_web_state->GetUniqueIdentifier();
  other_browser->GetWebStateList()->InsertWebState(std::move(other_web_state));

  WebStateList* web_state_list = browser_->GetWebStateList();
  ASSERT_EQ(6, web_state_list->count());

  id local_object = [[TabInfo alloc] initWithTabID:other_id
                                      browserState:browser_->GetBrowserState()];
  NSItemProvider* item_provider = [[NSItemProvider alloc] init];
  UIDragItem* drag_item =
      [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;

  // Drop item.
  [mediator_ dropItem:drag_item toIndex:3 fromSameCollection:NO];

  EXPECT_EQ(7, web_state_list->count());
  EXPECT_EQ(0, other_browser->GetWebStateList()->count());
  EXPECT_EQ(url_to_load, web_state_list->GetWebStateAt(4)->GetVisibleURL());
  EXPECT_EQ(tab_group_, web_state_list->GetGroupOfWebStateAt(4));
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kOtherBrowser);
}

// Tests dropping an interal URL (e.g. drag from omnibox) in the grid.
TEST_F(TabGroupMediatorTest, DropInternalURL) {
  WebStateList* web_state_list = browser_->GetWebStateList();
  ASSERT_EQ(6, web_state_list->count());

  GURL url_to_load = GURL("https://dragged_url.com");
  id local_object = [[URLInfo alloc] initWithURL:url_to_load title:@"My title"];
  NSItemProvider* item_provider = [[NSItemProvider alloc] init];
  UIDragItem* drag_item =
      [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;

  // Drop item.
  [mediator_ dropItem:drag_item toIndex:1 fromSameCollection:NO];

  EXPECT_EQ(7, web_state_list->count());
  web::WebState* web_state = web_state_list->GetWebStateAt(2);
  EXPECT_EQ(url_to_load,
            web_state->GetNavigationManager()->GetPendingItem()->GetURL());
  EXPECT_EQ(tab_group_, web_state_list->GetGroupOfWebStateAt(2));
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kOther);
}

// Tests dropping an external URL in the grid.
TEST_F(TabGroupMediatorTest, DropExternalURL) {
  WebStateList* web_state_list = browser_->GetWebStateList();
  ASSERT_EQ(6, web_state_list->count());

  NSItemProvider* item_provider = [[NSItemProvider alloc]
      initWithContentsOfURL:[NSURL URLWithString:@"https://dragged_url.com"]];

  // Drop item.
  [mediator_ dropItemFromProvider:item_provider
                          toIndex:0
               placeholderContext:nil];

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::Seconds(1), ^bool(void) {
        return web_state_list->count() == 7;
      }));
  web::WebState* web_state = web_state_list->GetWebStateAt(1);
  EXPECT_EQ(GURL("https://dragged_url.com"),
            web_state->GetNavigationManager()->GetPendingItem()->GetURL());
  EXPECT_EQ(tab_group_, web_state_list->GetGroupOfWebStateAt(2));
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kOther);
}

// Tests that deleting a group works.
TEST_F(TabGroupMediatorTest, DeleteGroup) {
  WebStateList* web_state_list = browser_->GetWebStateList();
  ASSERT_EQ(6, web_state_list->count());
  EXPECT_EQ(1u, web_state_list->GetGroups().size());

  [mediator_ deleteGroup];
  ASSERT_EQ(3, web_state_list->count());
  EXPECT_EQ(0u, web_state_list->GetGroups().size());
}

// Tests that ungrouping a group works.
TEST_F(TabGroupMediatorTest, Ungroup) {
  WebStateList* web_state_list = browser_->GetWebStateList();
  ASSERT_EQ(6, web_state_list->count());
  EXPECT_EQ(1u, web_state_list->GetGroups().size());

  [mediator_ ungroup];
  ASSERT_EQ(6, web_state_list->count());
  EXPECT_EQ(0u, web_state_list->GetGroups().size());
}