chromium/ash/drag_drop/tab_drag_drop_delegate_unittest.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/drag_drop/tab_drag_drop_delegate.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/test/test_new_window_delegate.h"
#include "ash/screen_util.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "ash/test_shell_delegate.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_test_util.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
#include "ash/wm/window_util.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/memory/raw_ptr.h"
#include "base/pickle.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/base/clipboard/clipboard_format_type.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/test/test_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/wm/core/scoped_animation_disabler.h"
#include "ui/wm/core/window_util.h"

using ::base::test::RunOnceCallback;
using ::testing::_;
using ::testing::NiceMock;
using ::testing::Return;

namespace ash {

namespace {

constexpr int kWebUITabStripHeight = 100;

class MockShellDelegate : public TestShellDelegate {
 public:
  MockShellDelegate() = default;
  ~MockShellDelegate() override = default;

  MOCK_METHOD(bool, IsTabDrag, (const ui::OSExchangeData&), (override));

  int GetBrowserWebUITabStripHeight() override { return kWebUITabStripHeight; }
};

class MockNewWindowDelegate : public TestNewWindowDelegate {
 public:
  MockNewWindowDelegate() = default;
  ~MockNewWindowDelegate() override = default;

  MOCK_METHOD(void,
              NewWindowForDetachingTab,
              (aura::Window*,
               const ui::OSExchangeData&,
               NewWindowForDetachingTabCallback),
              (override));
};

}  // namespace

class TabDragDropDelegateTest : public AshTestBase {
 public:
  TabDragDropDelegateTest() = default;

  // AshTestBase:
  void SetUp() override {
    auto mock_new_window_delegate =
        std::make_unique<NiceMock<MockNewWindowDelegate>>();
    mock_new_window_delegate_ptr_ = mock_new_window_delegate.get();
    test_new_window_delegate_provider_ =
        std::make_unique<TestNewWindowDelegateProvider>(
            std::move(mock_new_window_delegate));

    auto mock_shell_delegate = std::make_unique<NiceMock<MockShellDelegate>>();
    mock_shell_delegate_ = mock_shell_delegate.get();
    AshTestBase::SetUp(std::move(mock_shell_delegate));
    ash::TabletModeControllerTestApi().EnterTabletMode();

    // Create a dummy window and exit overview mode since drags can't be
    // initiated from overview mode.
    dummy_window_ = CreateToplevelTestWindow();
    ASSERT_TRUE(ExitOverview());
  }

  void TearDown() override {
    // Must be deleted before AshTestBase's tear down.
    dummy_window_.reset();

    // Clear our pointer before the object is destroyed.
    mock_shell_delegate_ = nullptr;
    test_new_window_delegate_provider_.reset();
    AshTestBase::TearDown();
  }

  MockShellDelegate* mock_shell_delegate() { return mock_shell_delegate_; }

  MockNewWindowDelegate* mock_new_window_delegate() {
    return static_cast<MockNewWindowDelegate*>(
        NewWindowDelegate::GetInstance());
  }

 private:
  raw_ptr<NiceMock<MockShellDelegate>> mock_shell_delegate_ = nullptr;

  std::unique_ptr<TestNewWindowDelegateProvider>
      test_new_window_delegate_provider_;
  raw_ptr<NiceMock<MockNewWindowDelegate>, DanglingUntriaged>
      mock_new_window_delegate_ptr_ = nullptr;

  std::unique_ptr<aura::Window> dummy_window_;
};

TEST_F(TabDragDropDelegateTest, ForwardsDragCheckToShellDelegate) {
  ON_CALL(*mock_shell_delegate(), IsTabDrag(_)).WillByDefault(Return(false));
  EXPECT_FALSE(TabDragDropDelegate::IsChromeTabDrag(ui::OSExchangeData()));

  ON_CALL(*mock_shell_delegate(), IsTabDrag(_)).WillByDefault(Return(true));
  EXPECT_TRUE(TabDragDropDelegate::IsChromeTabDrag(ui::OSExchangeData()));
}

TEST_F(TabDragDropDelegateTest, DragToExistingTabStrip) {
  // Create a fake source window. Its details don't matter.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();

  // A new window shouldn't be created in this case.
  EXPECT_CALL(*mock_new_window_delegate(), NewWindowForDetachingTab(_, _, _))
      .Times(0);

  // Emulate a drag session whose drop target accepts the drop. In this
  // case, TabDragDropDelegate::Drop() is not called.
  TabDragDropDelegate delegate(Shell::GetPrimaryRootWindow(),
                               source_window.get(), gfx::Point(0, 0));
  delegate.DragUpdate(gfx::Point(1, 0));
  delegate.DragUpdate(gfx::Point(2, 0));

  // Let |delegate| be destroyed without a Drop() call.
}

TEST_F(TabDragDropDelegateTest, DragToNewWindow) {
  // Create the source window. This should automatically fill the work area
  // since we're in tablet mode.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();

  EXPECT_FALSE(
      SplitViewController::Get(source_window.get())->InTabletSplitViewMode());

  const gfx::Point drag_start_location = source_window->bounds().CenterPoint();
  // Emulate a drag session ending in a drop to a new window.
  auto delegate = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(), drag_start_location);
  delegate->DragUpdate(drag_start_location);
  delegate->DragUpdate(drag_start_location + gfx::Vector2d(1, 0));
  delegate->DragUpdate(drag_start_location + gfx::Vector2d(2, 0));

  // Check that a new window is requested. Assume the correct drop data
  // is passed. Return the new window.
  std::unique_ptr<aura::Window> new_window = CreateToplevelTestWindow();
  EXPECT_CALL(*mock_new_window_delegate(),
              NewWindowForDetachingTab(source_window.get(), _, _))
      .Times(1)
      .WillOnce(RunOnceCallback<2>(new_window.get()));

  delegate.release()->DropAndDeleteSelf(
      drag_start_location + gfx::Vector2d(2, 0), ui::OSExchangeData());

  EXPECT_FALSE(
      SplitViewController::Get(source_window.get())->InTabletSplitViewMode());
}

// When a tab is dragged to the left/right side of the Web Contents. It should
// enter split view.
TEST_F(TabDragDropDelegateTest, DropOnEdgeEntersSplitView) {
  // Create the source window. This should automatically fill the work area
  // since we're in tablet mode.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();

  // We want to avoid entering overview mode between the delegate.Drop()
  // call and |new_window|'s destruction. So we define it here before
  // creating it.
  std::unique_ptr<aura::Window> new_window;

  // Emulate a drag to the right edge of the screen.
  const gfx::Point drag_start_location = source_window->bounds().CenterPoint();
  const gfx::Point drag_end_location =
      screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
          source_window.get())
          .right_center();

  auto delegate = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(), drag_start_location);
  delegate->DragUpdate(drag_start_location);
  delegate->DragUpdate(drag_end_location);

  new_window = CreateToplevelTestWindow();
  EXPECT_CALL(*mock_new_window_delegate(),
              NewWindowForDetachingTab(source_window.get(), _, _))
      .Times(1)
      .WillOnce(RunOnceCallback<2>(new_window.get()));

  delegate.release()->DropAndDeleteSelf(drag_end_location,
                                        ui::OSExchangeData());

  SplitViewController* const split_view_controller =
      SplitViewController::Get(source_window.get());
  EXPECT_TRUE(split_view_controller->InTabletSplitViewMode());
  EXPECT_EQ(new_window.get(),
            split_view_controller->GetSnappedWindow(SnapPosition::kSecondary));
  EXPECT_EQ(source_window.get(),
            split_view_controller->GetSnappedWindow(SnapPosition::kPrimary));
}

// When a tab is dragged to the left/right edge of the tab strip. It should not
// enter split view.
// https://crbug.com/1316070
TEST_F(TabDragDropDelegateTest, DropOnEdgeShouldNotEnterSplitView) {
  // Create the source window. This should automatically fill the work area
  // since we're in tablet mode.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();

  // We want to avoid entering overview mode between the delegate.Drop()
  // call and |new_window|'s destruction. So we define it here before
  // creating it.
  std::unique_ptr<aura::Window> new_window;

  // Emulate a drag to the right edge of the tab strip. It should not enter
  // split view.
  const gfx::Point drag_start_location = source_window->bounds().CenterPoint();
  const gfx::Point drag_end_location =
      gfx::Point(source_window->bounds().right(), kWebUITabStripHeight * 0.5);

  auto delegate = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(), drag_start_location);
  delegate->DragUpdate(drag_start_location);
  delegate->DragUpdate(drag_end_location);

  new_window = CreateToplevelTestWindow();
  EXPECT_CALL(*mock_new_window_delegate(),
              NewWindowForDetachingTab(source_window.get(), _, _))
      .Times(1)
      .WillOnce(RunOnceCallback<2>(new_window.get()));

  delegate.release()->DropAndDeleteSelf(drag_end_location,
                                        ui::OSExchangeData());

  SplitViewController* const split_view_controller =
      SplitViewController::Get(source_window.get());
  EXPECT_FALSE(split_view_controller->InTabletSplitViewMode());
}

TEST_F(TabDragDropDelegateTest, DropTabInSplitViewMode) {
  // Enter tablet split view mode by snap the source window to the left.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();
  SplitViewController* const split_view_controller =
      SplitViewController::Get(source_window.get());
  split_view_controller->SnapWindow(source_window.get(),
                                    SnapPosition::kPrimary);
  EXPECT_TRUE(split_view_controller->InTabletSplitViewMode());
  // Snap another window to the right to make sure right split screen is not in
  // overview mode.
  std::unique_ptr<aura::Window> right_window = CreateToplevelTestWindow();
  split_view_controller->SnapWindow(right_window.get(),
                                    SnapPosition::kSecondary);

  const gfx::Point drag_start_location = source_window->bounds().CenterPoint();
  auto area =
      screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
          source_window.get());

  // Emulate a drag to the right side of the screen.
  // |new_window1| should snap to the right split view.
  gfx::Point drag_end_location_right(area.width() * 0.8, area.height() * 0.5);
  auto delegate1 = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(), drag_start_location);
  delegate1->DragUpdate(drag_start_location);
  delegate1->DragUpdate(drag_end_location_right);
  std::unique_ptr<aura::Window> new_window1 = CreateToplevelTestWindow();
  EXPECT_CALL(*mock_new_window_delegate(),
              NewWindowForDetachingTab(source_window.get(), _, _))
      .Times(1)
      .WillOnce(RunOnceCallback<2>(new_window1.get()));
  delegate1.release()->DropAndDeleteSelf(drag_end_location_right,
                                         ui::OSExchangeData());

  EXPECT_TRUE(split_view_controller->InTabletSplitViewMode());
  EXPECT_EQ(new_window1.get(),
            split_view_controller->GetSnappedWindow(SnapPosition::kSecondary));
  EXPECT_EQ(source_window.get(),
            split_view_controller->GetSnappedWindow(SnapPosition::kPrimary));
  new_window1.reset();  // Close |new_window1|.

  // Emulate a drag to the left side of the screen.
  // |new_window2| should snap to the left split view.
  // |source_window| should go into overview mode.
  gfx::Point drag_end_location_left(area.width() * 0.2, area.height() * 0.5);
  auto delegate2 = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(), drag_start_location);
  delegate2->DragUpdate(drag_start_location);
  delegate2->DragUpdate(drag_end_location_left);
  std::unique_ptr<aura::Window> new_window2 = CreateToplevelTestWindow();
  EXPECT_CALL(*mock_new_window_delegate(),
              NewWindowForDetachingTab(source_window.get(), _, _))
      .Times(1)
      .WillOnce(RunOnceCallback<2>(new_window2.get()));
  delegate2.release()->DropAndDeleteSelf(drag_end_location_left,
                                         ui::OSExchangeData());

  EXPECT_TRUE(split_view_controller->InTabletSplitViewMode());
  EXPECT_EQ(nullptr,
            split_view_controller->GetSnappedWindow(SnapPosition::kSecondary));
  EXPECT_EQ(new_window2.get(),
            split_view_controller->GetSnappedWindow(SnapPosition::kPrimary));
  ASSERT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());
  EXPECT_TRUE(
      base::Contains(GetWindowsListInOverviewGrids(), source_window.get()));
}

TEST_F(TabDragDropDelegateTest, DropTabToOverviewMode) {
  // Enter tablet split view mode by snap the source window to the left.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();
  SplitViewController* const split_view_controller =
      SplitViewController::Get(source_window.get());
  split_view_controller->SnapWindow(source_window.get(),
                                    SnapPosition::kPrimary);
  EXPECT_TRUE(split_view_controller->InTabletSplitViewMode());
  ASSERT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());

  const gfx::Point drag_start_location = source_window->bounds().CenterPoint();
  auto area =
      screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
          source_window.get());

  // Emulate a drag to the right side of the screen.
  // |new_window1| should snap to overview mode.
  gfx::Point drag_end_location_right(area.width() * 0.8, area.height() * 0.5);
  auto delegate1 = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(), drag_start_location);
  delegate1->DragUpdate(drag_start_location);
  delegate1->DragUpdate(drag_end_location_right);
  std::unique_ptr<aura::Window> new_window = CreateToplevelTestWindow();
  EXPECT_CALL(*mock_new_window_delegate(),
              NewWindowForDetachingTab(source_window.get(), _, _))
      .Times(1)
      .WillOnce(RunOnceCallback<2>(new_window.get()));
  delegate1.release()->DropAndDeleteSelf(drag_end_location_right,
                                         ui::OSExchangeData());

  EXPECT_EQ(nullptr,
            split_view_controller->GetSnappedWindow(SnapPosition::kSecondary));
  EXPECT_TRUE(
      base::Contains(GetWindowsListInOverviewGrids(), new_window.get()));
}

TEST_F(TabDragDropDelegateTest, WillNotDropTabToOverviewModeInSnappingZone) {
  // Enter tablet split view mode by snap the source window to the left.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();
  SplitViewController* const split_view_controller =
      SplitViewController::Get(source_window.get());
  split_view_controller->SnapWindow(source_window.get(),
                                    SnapPosition::kPrimary);
  EXPECT_TRUE(split_view_controller->InTabletSplitViewMode());
  ASSERT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());

  const gfx::Point drag_start_location = source_window->bounds().CenterPoint();
  auto area =
      screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
          source_window.get());

  // Emulate a drag to the right snapping zone of the screen.
  // |new_window1| should not snap to overview mode.
  gfx::Point drag_end_location_right(area.width() * 0.95, area.height() * 0.5);
  auto delegate1 = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(), drag_start_location);
  delegate1->DragUpdate(drag_start_location);
  delegate1->DragUpdate(drag_end_location_right);
  std::unique_ptr<aura::Window> new_window = CreateToplevelTestWindow();
  EXPECT_CALL(*mock_new_window_delegate(),
              NewWindowForDetachingTab(source_window.get(), _, _))
      .Times(1)
      .WillOnce(RunOnceCallback<2>(new_window.get()));
  delegate1.release()->DropAndDeleteSelf(drag_end_location_right,
                                         ui::OSExchangeData());

  EXPECT_EQ(new_window.get(),
            split_view_controller->GetSnappedWindow(SnapPosition::kSecondary));
  ASSERT_FALSE(Shell::Get()->overview_controller()->InOverviewSession());
}

TEST_F(TabDragDropDelegateTest, WillNotDropTabToOverviewMode) {
  // Enter tablet split view mode by snap the source window to the left.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();
  SplitViewController* const split_view_controller =
      SplitViewController::Get(source_window.get());
  split_view_controller->SnapWindow(source_window.get(),
                                    SnapPosition::kPrimary);
  EXPECT_TRUE(split_view_controller->InTabletSplitViewMode());
  ASSERT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());

  const gfx::Point drag_start_location = source_window->bounds().CenterPoint();
  auto area =
      screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
          source_window.get());

  // Emulate a drag to the left side of the screen.
  // |new_window1| should not snap to overview mode.
  gfx::Point drag_end_location_right(area.width() * 0.2, area.height() * 0.5);
  auto delegate1 = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(), drag_start_location);
  delegate1->DragUpdate(drag_start_location);
  delegate1->DragUpdate(drag_end_location_right);
  std::unique_ptr<aura::Window> new_window = CreateToplevelTestWindow();
  EXPECT_CALL(*mock_new_window_delegate(),
              NewWindowForDetachingTab(source_window.get(), _, _))
      .Times(1)
      .WillOnce(RunOnceCallback<2>(new_window.get()));
  delegate1.release()->DropAndDeleteSelf(drag_end_location_right,
                                         ui::OSExchangeData());

  EXPECT_EQ(new_window.get(),
            split_view_controller->GetSnappedWindow(SnapPosition::kPrimary));
  EXPECT_FALSE(
      base::Contains(GetWindowsListInOverviewGrids(), new_window.get()));
}

TEST_F(TabDragDropDelegateTest, SourceWindowBoundsUpdatedWhileDragging) {
  // Create the source window. This should automatically fill the work area
  // since we're in tablet mode.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();
  const gfx::Rect original_bounds = source_window->bounds();

  // Drag a few pixels away to trigger window scaling, then to the
  // screen edge to visually snap the source window.
  const gfx::Point drag_start_location = source_window->bounds().CenterPoint();
  const gfx::Point drag_mid_location =
      drag_start_location + gfx::Vector2d(10, 0);
  const gfx::Point drag_end_location =
      screen_util::GetDisplayWorkAreaBoundsInScreenForActiveDeskContainer(
          source_window.get())
          .left_center();

  {
    TabDragDropDelegate delegate(Shell::GetPrimaryRootWindow(),
                                 source_window.get(), drag_start_location);
    delegate.DragUpdate(drag_start_location);
    delegate.DragUpdate(drag_mid_location);

    // |source_window| should be shrunk in all directions
    EXPECT_GT(source_window->bounds().x(), original_bounds.x());
    EXPECT_GT(source_window->bounds().y(), original_bounds.y());
    EXPECT_LT(source_window->bounds().right(), original_bounds.right());
    EXPECT_LT(source_window->bounds().bottom(), original_bounds.bottom());

    delegate.DragUpdate(drag_end_location);

    // |source_window| should appear in the snapped position, but not
    // actually be snapped.
    SplitViewController* const split_view_controller =
        SplitViewController::Get(source_window.get());
    EXPECT_EQ(source_window->bounds(),
              split_view_controller->GetSnappedWindowBoundsInParent(
                  SnapPosition::kSecondary, source_window.get(),
                  chromeos::kDefaultSnapRatio));
    EXPECT_FALSE(split_view_controller->InSplitViewMode());
  }

  // The original bounds should be restored.
  EXPECT_EQ(source_window->bounds(), original_bounds);
}

TEST_F(TabDragDropDelegateTest, SnappedSourceWindowNotMoved) {
  // Create the source window. This should automatically fill the work area
  // since we're in tablet mode.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();

  SplitViewController* const split_view_controller =
      SplitViewController::Get(source_window.get());
  SnapPosition const snap_position = SnapPosition::kPrimary;
  split_view_controller->SnapWindow(source_window.get(), snap_position);
  const gfx::Rect original_bounds = source_window->bounds();

  const gfx::Point drag_start_location = source_window->bounds().CenterPoint();
  const gfx::Point drag_end_location =
      drag_start_location + gfx::Vector2d(10, 0);

  {
    TabDragDropDelegate delegate(Shell::GetPrimaryRootWindow(),
                                 source_window.get(), drag_start_location);
    delegate.DragUpdate(drag_start_location);
    delegate.DragUpdate(drag_end_location);

    // |source_window| should remain snapped and it's bounds should not change.
    EXPECT_EQ(source_window.get(),
              split_view_controller->GetSnappedWindow(snap_position));
    EXPECT_EQ(original_bounds, source_window->bounds());
  }

  // Everything should still be the same after the drag ends.
  EXPECT_EQ(source_window.get(),
            split_view_controller->GetSnappedWindow(snap_position));
  EXPECT_EQ(original_bounds, source_window->bounds());
}

// Make sure metrics is recorded during tab dragging in tablet mode with
// webui tab strip enable.
TEST_F(TabDragDropDelegateTest, TabDraggingHistogram) {
  base::HistogramTester histogram_tester;

  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();
  EXPECT_FALSE(
      SplitViewController::Get(source_window.get())->InTabletSplitViewMode());

  const gfx::Point drag_start_location = source_window->bounds().CenterPoint();

  // Emulate a drag session ending in a drop to a new window. This should
  // generate a histogram.
  auto delegate = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(), drag_start_location);
  delegate->DragUpdate(drag_start_location + gfx::Vector2d(1, 0));
  EXPECT_TRUE(ui::WaitForNextFrameToBePresented(
      source_window->layer()->GetCompositor()));

  // Check that a new window is requested. Assume the correct drop data
  // is passed. Return the new window.
  std::unique_ptr<aura::Window> new_window = CreateToplevelTestWindow();
  EXPECT_CALL(*mock_new_window_delegate(),
              NewWindowForDetachingTab(source_window.get(), _, _))
      .Times(1)
      .WillOnce(RunOnceCallback<2>(new_window.get()));
  delegate.release()->DropAndDeleteSelf(
      drag_start_location + gfx::Vector2d(1, 0), ui::OSExchangeData());
  EXPECT_TRUE(ui::WaitForNextFrameToBePresented(
      source_window->layer()->GetCompositor()));

  EXPECT_FALSE(
      SplitViewController::Get(source_window.get())->InTabletSplitViewMode());
  histogram_tester.ExpectTotalCount("Ash.TabDrag.PresentationTime.TabletMode",
                                    1);
  histogram_tester.ExpectTotalCount(
      "Ash.TabDrag.PresentationTime.MaxLatency.TabletMode", 1);
}

// There are edge cases where a dragging tab closes itself before being dropped.
// In these cases new window will be nullptr and it
// should be handled gracefully. https://crbug.com/1286203
TEST_F(TabDragDropDelegateTest, DropWithoutNewWindow) {
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();
  const gfx::Point drag_location = source_window->bounds().CenterPoint();
  auto delegate = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(), drag_location);
  delegate->OnNewBrowserWindowCreated(drag_location, /*new_window=*/nullptr);
}

// Tests that if tab dragging is started on a floated window and then canceled,
// the float window returns to its original bounds.
TEST_F(TabDragDropDelegateTest, CancelTabDragWithFloatedWindow) {
  // Create a floated window.
  std::unique_ptr<aura::Window> source_window = CreateToplevelTestWindow();
  source_window->SetProperty(chromeos::kAppTypeKey, chromeos::AppType::BROWSER);
  wm::ActivateWindow(source_window.get());
  PressAndReleaseKey(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(WindowState::Get(source_window.get())->IsFloated());
  const gfx::Rect original_bounds = source_window->GetBoundsInScreen();

  // Simulate tab dragging from the floated source window.
  auto delegate = std::make_unique<TabDragDropDelegate>(
      Shell::GetPrimaryRootWindow(), source_window.get(),
      source_window->bounds().CenterPoint());
  delegate.reset();
  EXPECT_EQ(original_bounds, source_window->GetBoundsInScreen());
}

TEST_F(TabDragDropDelegateTest, CaptureShouldBeReleasedAfterDrop) {
  std::unique_ptr<aura::Window> source_window =
      CreateToplevelTestWindow(gfx::Rect(0, 0, 10, 10));

  constexpr gfx::Point kDragStartLocation(5, 5);

  // Emulate a drag session ending in a drop to a new window.
  auto* delegate = new TabDragDropDelegate(
      Shell::GetPrimaryRootWindow(), source_window.get(), kDragStartLocation);

  delegate->TakeCapture(Shell::GetPrimaryRootWindow(), source_window.get(),
                        base::BindLambdaForTesting([]() {}),
                        ui::TransferTouchesBehavior::kCancel);

  delegate->DragUpdate(kDragStartLocation);
  delegate->DragUpdate(kDragStartLocation + gfx::Vector2d(10, 0));

  // Input capture should still be active.
  EXPECT_TRUE(ash::window_util::GetCaptureWindow());

  NewWindowDelegate::NewWindowForDetachingTabCallback new_window_callback;
  EXPECT_CALL(*mock_new_window_delegate(),
              NewWindowForDetachingTab(source_window.get(), _, _))
      .Times(1)
      .WillOnce(
          [&](aura::Window* source_window, const ui::OSExchangeData& drop_data,
              NewWindowDelegate::NewWindowForDetachingTabCallback callback) {
            new_window_callback = std::move(callback);
          });

  delegate->DropAndDeleteSelf(kDragStartLocation + gfx::Vector2d(10, 0),
                              ui::OSExchangeData());

  // Input capture should have been released.
  EXPECT_FALSE(ash::window_util::GetCaptureWindow());

  std::move(new_window_callback).Run(source_window.get());
}

}  // namespace ash