chromium/chrome/browser/ui/views/tabs/tab_drag_controller_interactive_uitest.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ui/views/tabs/tab_drag_controller_interactive_uitest.h"

#include <stddef.h>

#include <algorithm>
#include <limits>
#include <memory>
#include <optional>
#include <set>
#include <utility>

#include "base/command_line.h"
#include "base/dcheck_is_on.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/tabs/features.h"
#include "chrome/browser/ui/tabs/tab_group.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/native_browser_frame_factory.h"
#include "chrome/browser/ui/views/frame/tab_strip_region_view.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_drag_controller.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/tabs/window_finder.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/test/os_integration_test_override_impl.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/tab_groups/tab_group_color.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features.h"
#include "ui/aura/env.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/test/ui_controls.h"
#include "ui/compositor/layer.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/animation/animation_test_api.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/native/native_view_host.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"

#if defined(USE_AURA)
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/aura/test/test_windows.h"
#include "ui/aura/window_targeter.h"
#endif

#if defined(USE_AURA) && !BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ui/views/frame/desktop_browser_frame_aura.h"
#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h"
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "base/test/simple_test_tick_clock.h"
#include "ui/events/base_event_utils.h"
#endif

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/public/cpp/split_view_test_api.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/wm/window_state.h"
#include "chrome/browser/ash/system_web_apps/test_support/test_system_web_app_installation.h"
#include "chrome/browser/ui/views/frame/browser_view_layout.h"
#include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
#include "chrome/browser/ui/views/frame/immersive_mode_controller_chromeos.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller_test_api.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/client/cursor_shape_client.h"
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/base/cursor/cursor.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/test/display_manager_test_api.h"  // nogncheck
#include "ui/events/gesture_detection/gesture_configuration.h"
#endif

#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chrome/browser/ui/views/frame/desktop_browser_frame_lacros.h"
#include "chromeos/crosapi/mojom/test_controller.mojom-test-utils.h"
#include "chromeos/lacros/lacros_service.h"
#include "ui/aura/window_tree_host_platform.h"
#include "ui/platform_window/extensions/wayland_extension.h"
#define DESKTOP_BROWSER_FRAME_AURA
#elif BUILDFLAG(IS_LINUX)
#include "chrome/browser/ui/views/frame/desktop_browser_frame_aura_linux.h"
#include "ui/ozone/public/ozone_platform.h"
#define DESKTOP_BROWSER_FRAME_AURA
#else
#define DESKTOP_BROWSER_FRAME_AURA
#endif

#if BUILDFLAG(IS_WIN)
#include "ui/base/ui_base_features.h"
#endif

WebContents;
Display;
GetDisplays;

namespace test {

namespace {

const char kTabDragControllerInteractiveUITestUserDataKey[] =;

class TabDragControllerInteractiveUITestUserData
    : public base::SupportsUserData::Data {};

#if BUILDFLAG(IS_CHROMEOS_ASH)
aura::Window* GetWindowForTabStrip(TabStrip* tab_strip) {
  return tab_strip ? tab_strip->GetWidget()->GetNativeWindow() : nullptr;
}
#endif

gfx::Point GetLeftCenterInScreenCoordinates(const views::View* view) {}

gfx::Point GetRightCenterInScreenCoordinates(const views::View* view) {}

}  // namespace

class QuitDraggingObserver {};

void SetID(WebContents* web_contents, int id) {}

void ResetIDs(TabStripModel* model, int start) {}

std::string IDString(TabStripModel* model) {}

TabStrip* GetTabStripForBrowser(Browser* browser) {}

TabDragController* GetTabDragController(TabStrip* tab_strip) {}

// Resizes the given browser to the specified bounds by using ui_test_utils to
// generate mouse movements, similar to how a regular user would resize the
// window. This is used instead of BrowserWindow::SetBounds() on platforms where
// clients don't have complete control over window bounds (i.e., Wayland).
void ResizeUsingMouseEmulation(Browser* browser,
                               const gfx::Rect& target_bounds) {}

// If this returns false, we must use ResizeUsingMouseEmulation() instead of
// BrowserWindow::SetBounds().
bool CanUseSetBounds() {}

}  // namespace test

GetTabDragController;
GetTabStripForBrowser;
IDString;
ResetIDs;
SetID;
GetCenterInScreenCoordinates;

TabDragControllerTest::TabDragControllerTest()
    :{}

TabDragControllerTest::~TabDragControllerTest() = default;

void TabDragControllerTest::StopAnimating(TabStrip* tab_strip) {}

void TabDragControllerTest::AddTabsAndResetBrowser(Browser* browser,
                                                   int additional_tabs,
                                                   const GURL& url) {}

Browser* TabDragControllerTest::CreateAnotherBrowserAndResize() {}

void TabDragControllerTest::SetWindowFinderForTabStrip(
    TabStrip* tab_strip,
    std::unique_ptr<WindowFinder> window_finder) {}

void TabDragControllerTest::HandleGestureEvent(TabStrip* tab_strip,
                                               ui::GestureEvent* event) {}

bool TabDragControllerTest::HasDragStarted(TabStrip* tab_strip) const {}

void TabDragControllerTest::SetUp() {}

namespace {

enum InputSource {};

int GetDetachY(TabStrip* tab_strip) {}

bool GetIsDragged(Browser* browser) {}

}  // namespace

#if !BUILDFLAG(IS_CHROMEOS_ASH) && defined(USE_AURA)

// Following classes verify a crash scenario. Specifically on Windows when focus
// changes it can trigger capture being lost. This was causing a crash in tab
// dragging as it wasn't set up to handle this scenario. These classes
// synthesize this scenario.

// Allows making ClearNativeFocus() invoke ReleaseCapture().
class TestDesktopBrowserFrameAura : public DESKTOP_BROWSER_FRAME_AURA {};

// Factory for creating a TestDesktopBrowserFrameAura.
class TestNativeBrowserFrameFactory : public NativeBrowserFrameFactory {};

class TabDragCaptureLostTest : public TabDragControllerTest {};

// See description above for details.
IN_PROC_BROWSER_TEST_F(TabDragCaptureLostTest, ReleaseCaptureOnDrag) {}

#endif

IN_PROC_BROWSER_TEST_F(TabDragControllerTest, GestureEndShouldEndDragTest) {}

class DetachToBrowserTabDragControllerTest
    : public TabDragControllerTest,
      public ::testing::WithParamInterface<
#if !BUILDFLAG(IS_CHROMEOS)
          testing::tuple<bool, bool, const char*, bool>> {};

// Creates a browser with four tabs. The first three belong in the same Tab
// Group. Dragging the third tab to after the fourth tab will result in a
// removal of the dragged tab from its group. Then dragging the second tab to
// after the third tab will also result in a removal of that dragged tab from
// its group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragRightToUngroupTab) {}

// Creates a browser with four tabs. The last three belong in the same Tab
// Group. Dragging the second tab to before the first tab will result in a
// removal of the dragged tab from its group. Then dragging the third tab to
// before the second tab will also result in a removal of that dragged tab from
// its group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragLeftToUngroupTab) {}

// Creates a browser with four tabs. The first three belong in the same Tab
// Group. Dragging tabs in a tab group within the defined threshold does not
// modify the group of the dragged tab.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragTabWithinGroupDoesNotModifyGroup) {}

// Creates a browser with four tabs. The first tab is in a Tab Group. Dragging
// the only tab in that group will remove the group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragOnlyTabInGroupRemovesGroup) {}

// Creates a browser with four tabs. The first tab is in Tab Group 1. The
// third tab is in Tab Group 2. Dragging the second tab over one to the left
// will result in the second tab joining Tab Group 1. Then dragging the third
// tab over one to the left will result in the tab joining Tab Group 1. Then
// dragging the fourth tab over one to the left will result in the tab joining
// Tab Group 1 as well.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragSingleTabLeftIntoGroup) {}

// Creates a browser with five tabs. The fourth tab is in Tab Group 1. The
// second tab is in Tab Group 2. Dragging the third tab over one to the right
// will result in the tab joining Tab Group 1. Then dragging the second tab
// over one to the right will result in the tab joining Tab Group 1. Then
// dragging the first tab over one to the right will result in the tab joining
// Tab Group 1 as well.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragSingleTabRightIntoGroup) {}

// Creates a browser with five tabs. The last two tabs are in a Tab Group.
// Dragging the first tab past the last slot should allow it to exit the group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragSingleTabRightOfRightmostGroup) {}

// Creates a browser with four tabs each in its own group. Selecting and
// dragging the first and third tabs right at the first tab will result in the
// tabs joining the same group as the tab in the second position. Then dragging
// the tabs over two to the right will result in the tabs joining the same group
// as the last tab.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragMultipleTabsRightIntoGroup) {}

// Creates a browser with four tabs each in its own group. Selecting and
// dragging the second and fourth tabs left at the fourth tab will result in the
// tabs joining the same group as the tab in the third position.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragMultipleTabsLeftIntoGroup) {}

// Creates a browser with four tabs with third and fourth in a group.
// Selecting and  dragging the second and third tabs towards left out
// of the group will result in the tabs being ungrouped.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragUngroupedTabGroupedTabOutsideGroup) {}

// Creates a browser with four tabs. The first two tabs are in Tab Group 1.
// Dragging the third tab over one to the left will result in the tab joining
// Tab Group 1. While this drag is still in session, pressing escape will revert
// group of the tab to before the drag session started.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertDragSingleTabIntoGroup) {}

// Creates a browser with four tabs. The last two tabs are in Tab Group 1. The
// second tab is in Tab Group 2. Dragging the second tab over one to the right
// will result in the tab joining Tab Group 1. While this drag is still in
// session, pressing escape will revert group of the tab to before the drag
// session started.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertDragSingleTabGroupIntoGroup) {}

// Creates a browser with four tabs. The middle two belong in the same Tab
// Group. Dragging the group header will result in both the grouped tabs moving
// together.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragGroupHeaderDragsGroup) {}

// Creates a browser with four tabs. The first two belong in Tab Group 1, and
// the last two belong in Tab Group 2. Dragging the group header of Tab Group 1
// right will result in Tab Group 1 moving but avoiding Tab Group 2.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragGroupHeaderRightAvoidsOtherGroups) {}

// Creates a browser with four tabs. The first two belong in Tab Group 1, and
// the last two belong in Tab Group 2. Dragging the group header of Tab Group 2
// left will result in Tab Group 2 moving but avoiding Tab Group 1.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragGroupHeaderLeftAvoidsOtherGroups) {}

// Creates a browser with four tabs. The first tab is pinned, and the last
// three belong in the same Tab Group. Dragging the pinned tab to the middle
// of the group will not result in the pinned tab getting grouped or moving
// into the group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragPinnedTabDoesNotGroup) {}

// Drags a tab within the window (without dragging the whole window) then
// pressing a key ends the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       KeyPressShouldEndDragTest) {}

// Flaky. https://crbug.com/343188577
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_StartDragWhileEndingPreviousDragDoesNothingTest
#else
#define MAYBE_StartDragWhileEndingPreviousDragDoesNothingTest
#endif

// Can't start another drag session while the previous one is still ending.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_StartDragWhileEndingPreviousDragDoesNothingTest) {}

#if defined(USE_AURA)
bool SubtreeShouldBeExplored(aura::Window* window,
                             const gfx::Point& local_point) {}

// The logic to find the target tabstrip should take the window mask into
// account. This test hangs without the fix. crbug.com/473080.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragWithMaskedWindows) {}
#endif  // USE_AURA

namespace {

// Invoked from the nested run loop.
void DragToSeparateWindowStep2(DetachToBrowserTabDragControllerTest* test,
                               TabStrip* not_attached_tab_strip,
                               TabStrip* target_tab_strip) {}

}  // namespace

// Flaky. https://crbug.com/1176998
#if (BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64)) || BUILDFLAG(IS_LINUX)
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/1154345
#define MAYBE_DragToSeparateWindow
#else
#define MAYBE_DragToSeparateWindow
#endif

// Creates two browsers, drags from first into second.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragToSeparateWindow) {}

// Test is based on DragToSeparateWindow. https://crbug.com/1176998
#if (BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64)) || BUILDFLAG(IS_LINUX) || \
    BUILDFLAG(IS_CHROMEOS)
#define MAYBE_DragToSeparateWindowDuringDragEnd
#else
#define MAYBE_DragToSeparateWindowDuringDragEnd
#endif

// Creates two browsers, starts and end a drag in the target browser, then drags
// from source to target before the target browser finishes ending drags.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragToSeparateWindowDuringDragEnd) {}

#if BUILDFLAG(IS_WIN)

// Create two browsers, with the second one occluded, and drag from first over
// second. This should create a third browser, w/o bringing forward the second
// browser, because it's occluded.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragToOccludedWindow) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);

  // Create another browser.
  Browser* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);

  // Mark the second browser as occluded. NativeWindow occlusion calculation has
  // been disabled in test constructor, so we don't need an actual occluding
  // window.
  browser2->window()
      ->GetNativeWindow()
      ->GetHost()
      ->SetNativeWindowOcclusionState(aura::Window::OcclusionState::OCCLUDED,
                                      {});

  // Drag a tab from first browser to middle of first tab of the second,
  // occluded browser, and drop. This should create a third browser window.
  ASSERT_TRUE(PressInput(GetCenterInScreenCoordinates(tab_strip->tab_at(1))));
  ASSERT_TRUE(
      DragInputToAsync(GetCenterInScreenCoordinates(tab_strip2->tab_at(0))));
  ASSERT_TRUE(ReleaseInput());

  EXPECT_EQ(3u, browser_list()->size());
}

#endif  // BUILDFLAG(IS_WIN)

namespace {

// WindowFinder that calls OnMouseCaptureLost() from
// GetLocalProcessWindowAtPoint().
class CaptureLoseWindowFinder : public WindowFinder {};

}  // namespace

// Calls OnMouseCaptureLost() from WindowFinder::GetLocalProcessWindowAtPoint()
// and verifies we don't crash. This simulates a crash seen on windows.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       CaptureLostDuringDrag) {}

namespace {

#if BUILDFLAG(IS_CHROMEOS_ASH)
bool IsWindowPositionManaged(aura::Window* window) {
  return window->GetProperty(ash::kWindowPositionManagedTypeKey);
}
bool HasUserChangedWindowPositionOrSize(aura::Window* window) {
  return ash::WindowState::Get(window)->bounds_changed_by_user();
}
#else
bool IsWindowPositionManaged(gfx::NativeWindow window) {}
bool HasUserChangedWindowPositionOrSize(gfx::NativeWindow window) {}
#endif

}  // namespace

// Drags from browser to separate window and releases mouse.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachToOwnWindow) {}

class TestDialog : public views::DialogDelegateView {};

// Drags from browser that has a web dialog to separate window.
// The dialog should follow the new browser window.
// TODO(crbug.com/40934892): Expectations are sometimes off by one pixel on
// Windows. Reenable once deflaked.
#if BUILDFLAG(IS_WIN)
#define MAYBE_DetachToOwnWindowWithDialog
#else
#define MAYBE_DetachToOwnWindowWithDialog
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DetachToOwnWindowWithDialog) {}

#if BUILDFLAG(IS_CHROMEOS_ASH)
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachToOwnWindowWithNonVisibleOnAllWorkspaceState) {
  // Set the source browser to be visible on all workspace.
  ASSERT_EQ(1u, browser_list()->size());
  Browser* source_browser = browser_list()->get(0);
  auto* source_window = source_browser->window()->GetNativeWindow();
  source_window->SetProperty(
      aura::client::kWindowWorkspaceKey,
      aura::client::kWindowWorkspaceVisibleOnAllWorkspaces);

  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  int tab_0_width = tab_strip->tab_at(0)->width();
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                      ReleaseInputAfterWindowDetached,
                                  base::Unretained(this), tab_0_width));

  // Should no longer be dragging.
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());

  // There should now be another browser.
  ASSERT_EQ(2u, browser_list()->size());
  Browser* new_browser = browser_list()->get(1);

  EXPECT_TRUE(new_browser->window()->IsActive());
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  EXPECT_FALSE(tab_strip2->GetDragContext()->IsDragSessionActive());

  EXPECT_EQ("0", IDString(new_browser->tab_strip_model()));
  EXPECT_EQ("1", IDString(browser()->tab_strip_model()));

  EXPECT_FALSE(GetIsDragged(browser()));
  EXPECT_FALSE(GetIsDragged(new_browser));
  // After this both windows should still be manageable.
  EXPECT_TRUE(IsWindowPositionManaged(browser()->window()->GetNativeWindow()));
  EXPECT_TRUE(
      IsWindowPositionManaged(new_browser->window()->GetNativeWindow()));

  auto* new_window = new_browser->window()->GetNativeWindow();
  // The new window should not be visible on all workspace.
  ASSERT_FALSE(new_window->GetProperty(aura::client::kWindowWorkspaceKey) ==
               aura::client::kWindowWorkspaceVisibleOnAllWorkspaces);
}
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

// Tests that a tab can be dragged from a browser window that is resized to full
// screen.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachFromFullsizeWindow) {}

// This test doesn't make sense on Mac, since it has no concept of "maximized".
#if !BUILDFLAG(IS_MAC)

// Drags from browser to a separate window and releases mouse.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachToOwnWindowFromMaximizedWindow) {}
#endif  // !BUILDFLAG(IS_MAC)

#if BUILDFLAG(IS_CHROMEOS_ASH)

// This test makes sense only on Chrome OS where we have the immersive
// fullscreen mode. The detached tab to a new browser window should remain in
// immersive fullscreen mode.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachToOwnWindowWhileInImmersiveFullscreenMode) {
  // Toggle the immersive fullscreen mode for the initial browser.
  chrome::ToggleFullscreenMode(browser());
  ImmersiveModeController* controller =
      BrowserView::GetBrowserViewForBrowser(browser())
          ->immersive_mode_controller();
  ASSERT_TRUE(controller->IsEnabled());

  // Forcively reveal the tabstrip immediately.
  std::unique_ptr<ImmersiveRevealedLock> lock =
      controller->GetRevealedLock(ImmersiveModeController::ANIMATE_REVEAL_NO);

  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  int tab_0_width = tab_strip->tab_at(0)->width();
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                      ReleaseInputAfterWindowDetached,
                                  base::Unretained(this), tab_0_width));

  // Should no longer be dragging.
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());

  // There should now be another browser.
  ASSERT_EQ(2u, browser_list()->size());
  Browser* new_browser = browser_list()->get(1);
  ASSERT_TRUE(new_browser->window()->IsActive());
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  ASSERT_FALSE(tab_strip2->GetDragContext()->IsDragSessionActive());

  EXPECT_EQ("0", IDString(new_browser->tab_strip_model()));
  EXPECT_EQ("1", IDString(browser()->tab_strip_model()));

  // The bounds of the initial window should not have changed.
  EXPECT_TRUE(browser()->window()->IsFullscreen());
  ASSERT_TRUE(BrowserView::GetBrowserViewForBrowser(browser())
                  ->immersive_mode_controller()
                  ->IsEnabled());

  EXPECT_FALSE(GetIsDragged(browser()));
  EXPECT_FALSE(GetIsDragged(new_browser));
  // After this both windows should still be manageable.
  EXPECT_TRUE(IsWindowPositionManaged(browser()->window()->GetNativeWindow()));
  EXPECT_TRUE(
      IsWindowPositionManaged(new_browser->window()->GetNativeWindow()));

  // The new browser should be in immersive fullscreen mode.
  ASSERT_TRUE(BrowserView::GetBrowserViewForBrowser(new_browser)
                  ->immersive_mode_controller()
                  ->IsEnabled());
  EXPECT_TRUE(new_browser->window()->IsFullscreen());
}

#endif

// Deletes a tab being dragged before the user moved enough to start a drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DeleteBeforeStartedDragging) {}

// Replaces a tab being dragged before the user moved enough to start a drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       ReplaceBeforeStartedDragging) {}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragDoesntStartFromClick) {}

// Deletes a tab being dragged while still attached.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DeleteTabWhileAttached) {}

// Selects 1 tab out of 4, drags it out and closes the new browser window while
// dragging.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DeleteTabsWhileDetached) {}

namespace {

void PressEscapeWhileDetachedStep2(DetachToBrowserTabDragControllerTest* test) {}

}  // namespace

// Detaches a tab and while detached presses escape to revert the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertDragWhileDetached) {}

// Creates a browser with two tabs, drags the second to the first.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest, DragInSameWindow) {}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       TabDragContextOwnsDraggedTabs) {}

#if BUILDFLAG(IS_LINUX)
#define MAYBE_TabDragContextOwnsClosingDraggedTabs
#else
#define MAYBE_TabDragContextOwnsClosingDraggedTabs
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_TabDragContextOwnsClosingDraggedTabs) {}

namespace {

void DragAllStep2(DetachToBrowserTabDragControllerTest* test) {}

}  // namespace

// Selects multiple tabs and starts dragging the window.
// TODO(crbug.com/40934892): Expectations are sometimes off by one pixel on
// Windows. Reenable once deflaked.
#if BUILDFLAG(IS_WIN)
#define MAYBE_DragAll
#else
#define MAYBE_DragAll
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest, MAYBE_DragAll) {}

namespace {

// Invoked from the nested run loop.
void DragAllToSeparateWindowStep2(DetachToBrowserTabDragControllerTest* test,
                                  TabStrip* attached_tab_strip,
                                  TabStrip* target_tab_strip) {}

}  // namespace

// Flaky. http://crbug.com/1128774 and http://crbug.com/1176998
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/1154345
// These were flaking on all macs, so commented out ARCH_ above for
// crbug.com/1160917 too.
#define MAYBE_DragAllToSeparateWindow
#else
#define MAYBE_DragAllToSeparateWindow
#endif

// Creates two browsers, selects all tabs in first and drags into second.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragAllToSeparateWindow) {}

namespace {

// Invoked from the nested run loop.
void DoubleNestedRunLoopStep2(DetachToBrowserTabDragControllerTest* test,
                              TabStrip* attached_tab_strip,
                              TabStrip* target_tab_strip) {}

}  // namespace

// TODO(crbug.com/326021146): flaky test.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#define MAYBE_DragToSeparateWindowAttemptToSpawnDoubleNestedRunLoop
#else
#define MAYBE_DragToSeparateWindowAttemptToSpawnDoubleNestedRunLoop
#endif
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserTabDragControllerTest,
    MAYBE_DragToSeparateWindowAttemptToSpawnDoubleNestedRunLoop) {}

#if BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64)
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/1154345
#define MAYBE_DragWindowIntoGroup
#else
#define MAYBE_DragWindowIntoGroup
#endif

// Creates two browser with two tabs each. The first browser has one tab in a
// group and the second tab not in a group. The second browser {browser2} has
// two tabs in another group {group1}. Dragging the two tabs in the first
// browser into the middle of the second browser will insert the two dragged
// tabs into the {group1}} after the first tab.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragWindowIntoGroup) {}

#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/1154345
// Flaky on Mac10.14 and Linux: https://crbug.com/1213345
#define MAYBE_DragGroupHeaderToSeparateWindow
#else
#define MAYBE_DragGroupHeaderToSeparateWindow
#endif

// Creates two browsers, then drags a group from one to the other.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragGroupHeaderToSeparateWindow) {}

// Drags a tab group by the header to a new position toward the right and
// presses escape to revert the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertHeaderDragRight) {}

// Drags a tab group by the header to a new position toward the left and presses
// escape to revert the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertHeaderDragLeft) {}

namespace {

void PressEscapeWhileDetachedHeaderStep2(
    DetachToBrowserTabDragControllerTest* test) {}

}  // namespace

// Drags a tab group by the header and while detached presses escape to revert
// the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertHeaderDragWhileDetached) {}

// Creates a browser with four tabs where the second and third tab is in a
// collapsed group. Drag the fourth tab to the left past the group header. The
// fourth tab should swap places with the collapsed group header.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragTabLeftPastCollapsedGroupHeader) {}

// Creates a browser with four tabs where the second and third tab is in a
// collapsed group. Drag the first tab to the right past the group header. The
// first tab should swap places with the collapsed group header.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragTabRightPastCollapsedGroupHeader) {}

// Drags a tab group by the header and while detached presses escape to revert
// the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertCollapsedHeaderDragWhileDetached) {}

// Creates a browser with four tabs. The first two tabs belong in Tab Group 1.
// Dragging the collapsed group header of Tab Group 1 will result in Tab Group 1
// expanding.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragCollapsedGroupHeaderExpandsGroup) {}

// TODO(crbug.com/40118868): Revisit once build flag switch of lacros-chrome is
// complete.
#if BUILDFLAG(IS_MAC) || (BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS))
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/1154345
// Test is flaky on Mac and Linux: https://crbug.com/1167249
#define MAYBE_DragCollapsedGroupHeaderToSeparateWindow
#else
#define MAYBE_DragCollapsedGroupHeaderToSeparateWindow
#endif

// Creates two browsers, then drags a collapsed group from one to the other.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragCollapsedGroupHeaderToSeparateWindow) {}

DetachTabWithUrlControlledByWebApp;

// Test tearing off a tab displaying a url controlled by a web app.
// The kTearOffWebAppTabOpensWebAppWindow experiment determines whether the new
// browser window will be a normal browser window or an app window.
IN_PROC_BROWSER_TEST_P(DetachTabWithUrlControlledByWebApp, TearOffWebApp) {}

// Detachable tabs are not supported for PWAs on Mac so these tests don't apply.
#if !BUILDFLAG(IS_MAC)
class DetachToBrowserTabDragControllerTestWithTabbedWebApp
    : public DetachToBrowserTabDragControllerTest {};

// Tabbed web apps with the home tab cannot have detachable tabs.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithTabbedWebApp,
                       HomeTabAddedToEveryWindow) {}

// Home tab can't be detached.
// TODO(crbug.com/40245163): Enable this test for Linux and Lacros.
#if (BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS))
#define MAYBE_CantDragHomeTab
#else
#define MAYBE_CantDragHomeTab
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithTabbedWebApp,
                       MAYBE_CantDragHomeTab) {}

// Tabbed web apps without a home tab do not have home tab added.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithTabbedWebApp,
                       NoHomeTab) {}
#endif  // !BUILDFLAG(IS_MAC)

class DetachToBrowserTabDragControllerTestWithScrollableTabStripEnabled
    : public DetachToBrowserTabDragControllerTest {};

// TODO(crbug.com/40118868): Revisit once build flag switch of lacros-chrome is
// complete.
// Disabling on macOS due to DCHECK crashes; see https://crbug.com/1183043.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS) || \
    (BUILDFLAG(IS_MAC) && DCHECK_IS_ON())
#define MAYBE_DraggingRightExpandsTabStripSize
#else
#define MAYBE_DraggingRightExpandsTabStripSize
#endif
// Creates a browser with two tabs and drags the rightmost tab. Given the
// browser window is large enough, the tabstrip should expand to accommodate
// this tab. Note: There must be at least two tabs because dragging a singular
// tab will drag the window.
// Disabled for Linux due to test dragging flakiness.
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserTabDragControllerTestWithScrollableTabStripEnabled,
    MAYBE_DraggingRightExpandsTabStripSize) {}

namespace {

// Invoked from the nested run loop.
void DragAllToSeparateWindowAndCancelStep2(
    DetachToBrowserTabDragControllerTest* test,
    TabStrip* attached_tab_strip,
    TabStrip* target_tab_strip) {}

}  // namespace

#if BUILDFLAG(IS_MAC) /* && defined(ARCH_CPU_ARM64) */
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/1154345
// These were flaking on all macs, so commented out ARCH_ above for
// crbug.com/1160917 too.
#define MAYBE_DragAllToSeparateWindowAndCancel
#else
// TODO(crbug.com/40740354): Flaky on Windows and Linux.
#define MAYBE_DragAllToSeparateWindowAndCancel
#endif

// Creates two browsers, selects all tabs in first, drags into second, then hits
// escape.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragAllToSeparateWindowAndCancel) {}

#if BUILDFLAG(IS_MAC) /* && defined(ARCH_CPU_ARM64) */
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/1154345
// These were flaking on all macs, so commented out ARCH_ above for
// crbug.com/1160917 too.
#define MAYBE_DragAllToSeparateWindowWithPinnedTabs
#else
// TODO(crbug.com/40740354): Flaky on Windows and Linux.
#define MAYBE_DragAllToSeparateWindowWithPinnedTabs
#endif

// Creates two browsers, selects all tabs in first, drags into second, then hits
// escape.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragAllToSeparateWindowWithPinnedTabs) {}

// Creates two browsers, drags from first into the second in such a way that
// no detaching should happen.
// TODO(crbug.com/41482323): Reenable flaky test.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DISABLED_DragDirectlyToSecondWindow) {}

// Flaky. https://crbug.com/1176998
#if (BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64)) || BUILDFLAG(IS_LINUX)
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/1154345
#define MAYBE_DragSingleTabToSeparateWindow
#else
#define MAYBE_DragSingleTabToSeparateWindow
#endif

// Creates two browsers, the first browser has a single tab and drags into the
// second browser.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragSingleTabToSeparateWindow) {}

#if !BUILDFLAG(IS_MAC)
namespace {

// Invoked from the nested run loop.
void CancelOnNewTabWhenDraggingStep2(DetachToBrowserTabDragControllerTest* test,
                                     const Browser* default_browser,
                                     base::OnceClosure quit_closure,
                                     WebContents** contents_out) {}

}  // namespace

// Adds another tab, detaches into separate window, adds another tab and
// verifies the run loop ends.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       CancelOnNewTabWhenDragging) {}
#endif  // !BUILDFLAG(IS_MAC)

namespace {

TabStrip* GetAttachedTabstrip() {}

void DragWindowAndVerifyOffset(DetachToBrowserTabDragControllerTest* test,
                               TabStrip* tab_strip,
                               int tab_index) {}

}  // namespace

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC)
// TODO(mukai): enable this test on Windows and Linux.
// TODO(crbug.com/41468034): flaky on Mac
#define MAYBE_OffsetForDraggingTab
#else
#define MAYBE_OffsetForDraggingTab
#endif

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_OffsetForDraggingTab) {}

// TODO(crbug.com/41457552): fix flakiness and re-enable this test on mac/linux.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DISABLED_OffsetForDraggingDetachedTab) {}

#if BUILDFLAG(IS_CHROMEOS)
namespace {

void DragInMaximizedWindowStep2(DetachToBrowserTabDragControllerTest* test,
                                Browser* browser,
                                TabStrip* tab_strip) {
  // There should be another browser.
  size_t num_browsers = test->browser_list()->size();
  EXPECT_EQ(2u, num_browsers);
  Browser* new_browser = test->browser_list()->get(num_browsers - 1);
  EXPECT_NE(browser, new_browser);
  ui_test_utils::BrowserActivationWaiter activation_waiter(new_browser);
  activation_waiter.WaitForActivation();
  EXPECT_TRUE(new_browser->window()->IsActive());
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);

  EXPECT_TRUE(tab_strip2->GetDragContext()->IsDragSessionActive());
  EXPECT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());

  // Both windows should be visible.
  EXPECT_TRUE(tab_strip->GetWidget()->IsVisible());
  EXPECT_TRUE(tab_strip2->GetWidget()->IsVisible());

  // Stops dragging.
  EXPECT_TRUE(test->ReleaseInput());
}

}  // namespace

// Creates a browser with two tabs, maximizes it, drags the tab out.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragInMaximizedWindow) {
  {
    auto waiter = ui_test_utils::CreateAsyncWidgetRequestWaiter(*browser());
    browser()->window()->Maximize();
    waiter.Wait();
  }
  ASSERT_TRUE(browser()->window()->IsMaximized());
  AddTabsAndResetBrowser(browser(), 1);

  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  DragTabAndNotify(tab_strip, base::BindOnce(&DragInMaximizedWindowStep2, this,
                                             browser(), tab_strip));

  ASSERT_FALSE(TabDragController::IsActive());

  // Should be two browsers.
  ASSERT_EQ(2u, browser_list()->size());
  Browser* new_browser = browser_list()->get(1);
  ASSERT_TRUE(new_browser->window()->IsActive());

  EXPECT_TRUE(browser()->window()->GetNativeWindow()->IsVisible());
  EXPECT_TRUE(new_browser->window()->GetNativeWindow()->IsVisible());

  EXPECT_FALSE(GetIsDragged(browser()));
  EXPECT_FALSE(GetIsDragged(new_browser));

  // The source window should be maximized.
  EXPECT_TRUE(browser()->window()->IsMaximized());
  // The new window should be maximized.
  EXPECT_TRUE(new_browser->window()->IsMaximized());
}
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS)
namespace {

void NewBrowserWindowStateStep2(DetachToBrowserTabDragControllerTest* test,
                                TabStrip* tab_strip) {
  // There should be two browser windows, including the newly created one for
  // the dragged tab.
  EXPECT_EQ(3u, test->browser_list()->size());

  // Get this new created window for the dragged tab.
  Browser* new_browser = test->browser_list()->get(2);
  aura::Window* window = new_browser->window()->GetNativeWindow();
  EXPECT_NE(window->GetProperty(aura::client::kShowStateKey),
            ui::SHOW_STATE_MAXIMIZED);
  EXPECT_EQ(window->GetProperty(aura::client::kShowStateKey),
            ui::SHOW_STATE_DEFAULT);

  EXPECT_TRUE(test->ReleaseInput());
}

}  // namespace

// Test that tab dragging can work on a browser window with its initial show
// state is MAXIMIZED.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       NewBrowserWindowState) {
  // Create a browser window whose initial show state is MAXIMIZED.
  Browser::CreateParams params(browser()->profile(), /*user_gesture=*/false);
  params.initial_show_state = ui::SHOW_STATE_MAXIMIZED;
  Browser* browser = Browser::Create(params);
  AddBlankTabAndShow(browser);
  TabStrip* tab_strip = GetTabStripForBrowser(browser);
  AddTabsAndResetBrowser(browser, 1);

  // Maximize the browser window.
  browser->window()->Maximize();
  EXPECT_EQ(browser->window()->GetNativeWindow()->GetProperty(
                aura::client::kShowStateKey),
            ui::SHOW_STATE_MAXIMIZED);

  // Drag it far enough that the first tab detaches.
  DragTabAndNotify(
      tab_strip, base::BindOnce(&NewBrowserWindowStateStep2, this, tab_strip));
}
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS)
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       OffsetForDraggingInMaximizedWindow) {
  AddTabsAndResetBrowser(browser(), 1);
  // Moves the browser window slightly to ensure that the browser's restored
  // bounds are different from the maximized bound's origin.
  browser()->window()->SetBounds(browser()->window()->GetBounds() +
                                 gfx::Vector2d(100, 50));
  browser()->window()->Maximize();

  DragWindowAndVerifyOffset(this, GetTabStripForBrowser(browser()), 0);
  ASSERT_FALSE(TabDragController::IsActive());
}
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS_ASH)
namespace {

// A window observer that observes the dragged window's property
// ash::kIsDraggingTabsKey.
class DraggedWindowObserver : public aura::WindowObserver {
 public:
  DraggedWindowObserver(DetachToBrowserTabDragControllerTest* test,
                        aura::Window* window,
                        const gfx::Rect& bounds,
                        const gfx::Point& end_point)
      : test_(test), end_bounds_(bounds), end_point_(end_point) {}
  DraggedWindowObserver(const DraggedWindowObserver&) = delete;
  DraggedWindowObserver& operator=(const DraggedWindowObserver&) = delete;
  ~DraggedWindowObserver() override {
    if (window_)
      window_->RemoveObserver(this);
  }

  void StartObserving(aura::Window* window) {
    DCHECK(!window_);
    window_ = window;
    window_->AddObserver(this);
  }

  // aura::WindowObserver:
  void OnWindowDestroying(aura::Window* window) override {
    DCHECK_EQ(window_, window);
    window_->RemoveObserver(this);
    window_ = nullptr;
  }

  void OnWindowPropertyChanged(aura::Window* window,
                               const void* key,
                               intptr_t old) override {
    DCHECK_EQ(window_, window);
    if (key == ash::kIsDraggingTabsKey) {
      if (!window_->GetProperty(ash::kIsDraggingTabsKey)) {
        // It should be triggered by TabDragController::ClearTabDraggingInfo()
        // from TabDragController::EndDragImpl(). Theoretically at this point
        // TabDragController should have removed itself as an observer of the
        // dragged tabstrip's widget. So changing its bounds should do nothing.

        // It's to ensure the current cursor location is within the bounds of
        // another browser's tabstrip.
        test_->MoveInputTo(end_point_);

        // Change window's bounds to simulate what might happen in ash. If
        // TabDragController is still an observer of the dragged tabstrip's
        // widget, OnWidgetBoundsChanged() will calls into ContinueDragging()
        // to attach the dragged tabstrip into another browser, which might
        // cause chrome crash.
        window_->SetBounds(end_bounds_);
      }
    }
  }

 private:
  raw_ptr<DetachToBrowserTabDragControllerTest> test_;
  // The dragged window.
  raw_ptr<aura::Window> window_ = nullptr;
  // The bounds that |window_| will change to when the drag ends.
  gfx::Rect end_bounds_;
  // The position that the mouse/touch event will move to when the drag ends.
  gfx::Point end_point_;
};

void DoNotObserveDraggedWidgetAfterDragEndsStep2(
    DetachToBrowserTabDragControllerTest* test,
    DraggedWindowObserver* observer,
    TabStrip* attached_tab_strip) {
  EXPECT_TRUE(attached_tab_strip->GetDragContext()->IsDragSessionActive());
  EXPECT_TRUE(TabDragController::IsActive());

  // Start observe the dragged window.
  observer->StartObserving(attached_tab_strip->GetWidget()->GetNativeWindow());

  EXPECT_TRUE(test->ReleaseInput());
}

}  // namespace

// Test that after the drag ends, TabDragController is no longer an observer of
// the dragged widget, so that if the bounds of the dragged widget change,
// TabDragController won't be put into dragging process again.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DoNotObserveDraggedWidgetAfterDragEnds) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create another browser.
  Browser* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  EXPECT_EQ(2u, browser_list()->size());

  // Create an window observer to observe the dragged window.
  std::unique_ptr<DraggedWindowObserver> observer(new DraggedWindowObserver(
      this, test::GetWindowForTabStrip(tab_strip),
      tab_strip2->GetWidget()->GetNativeWindow()->bounds(),
      GetCenterInScreenCoordinates(tab_strip2)));

  // Drag the tab long enough so that it moves.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DoNotObserveDraggedWidgetAfterDragEndsStep2,
                                  this, observer.get(), tab_strip));

  // There should be still two browsers at this moment. |tab_strip| should not
  // be merged into |tab_strip2|.
  EXPECT_EQ(2u, browser_list()->size());

  ASSERT_FALSE(TabDragController::IsActive());
}
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

#if BUILDFLAG(IS_CHROMEOS)
namespace {

// Returns true if the web contents that's associated with |browser| is using
// fast resize.
bool WebContentsIsFastResized(Browser* browser) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
  ContentsWebView* contents_web_view =
      static_cast<ContentsWebView*>(browser_view->GetContentsView());
  return contents_web_view->holder()->fast_resize();
}

void FastResizeDuringDraggingStep2(DetachToBrowserTabDragControllerTest* test,
                                   TabStrip* not_attached_tab_strip,
                                   TabStrip* target_tab_strip) {
  // There should be three browser windows, including the newly created one for
  // the dragged tab.
  size_t num_browsers = test->browser_list()->size();
  EXPECT_EQ(3u, num_browsers);

  // TODO(crbug.com/40142064): Remove explicit OS_CHROMEOS check once OS_LINUX
  // CrOS cleanup is done.
// TODO(crbug.com/40118868): Revisit the macro expression once build flag switch
// of lacros-chrome is complete.
#if !(BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS))
  // Get this new created window for the drag. It should have fast resize set.
  Browser* new_browser = test->browser_list()->get(num_browsers - 1);
  EXPECT_TRUE(WebContentsIsFastResized(new_browser));
  // The source window should also have fast resize set.
  EXPECT_TRUE(WebContentsIsFastResized(test->browser()));
#endif

  // Now drag to target_tab_strip.
  EXPECT_TRUE(
      test->DragInputTo(GetCenterInScreenCoordinates(target_tab_strip)));

  EXPECT_TRUE(test->ReleaseInput());
}

}  // namespace

// Tests that we use fast resize to resize the web contents of the dragged
// window and the source window during tab dragging process, and don't use fast
// resize after tab dragging ends.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       FastResizeDuringDragging) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  AddTabsAndResetBrowser(browser(), 1);

  // Create another browser.
  Browser* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  EXPECT_EQ(2u, browser_list()->size());

  EXPECT_FALSE(WebContentsIsFastResized(browser()));
  EXPECT_FALSE(WebContentsIsFastResized(browser2));

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip, base::BindOnce(&FastResizeDuringDraggingStep2,
                                             this, tab_strip, tab_strip2));

  EXPECT_FALSE(WebContentsIsFastResized(browser()));
  EXPECT_FALSE(WebContentsIsFastResized(browser2));
}
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS_ASH)
class DetachToBrowserTabDragControllerTestWithTabbedSystemApp
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserTabDragControllerTestWithTabbedSystemApp()
      : test_system_web_app_installation_(
            ash::TestSystemWebAppInstallation::SetUpTabbedMultiWindowApp()) {}

  webapps::AppId InstallMockApp() {
    test_system_web_app_installation_->WaitForAppInstall();
    return test_system_web_app_installation_->GetAppId();
  }

  Browser* LaunchWebAppBrowser(webapps::AppId app_id) {
    return web_app::LaunchWebAppBrowser(browser()->profile(), app_id);
  }

  const GURL& GetAppUrl() {
    return test_system_web_app_installation_->GetAppUrl();
  }

 private:
  std::unique_ptr<ash::TestSystemWebAppInstallation>
      test_system_web_app_installation_;
};

// Move tab from TYPE_APP Browser to create new TYPE_APP Browser.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithTabbedSystemApp,
                       DragAppToOwnWindow) {
  // Install and get a tabbed system app.
  webapps::AppId tabbed_app_id = InstallMockApp();
  Browser* app_browser = LaunchWebAppBrowser(tabbed_app_id);
  ASSERT_EQ(2u, browser_list()->size());

  // Close normal browser since other code expects only 1 browser to start.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(1u, browser_list()->size());
  SelectFirstBrowser();
  ASSERT_EQ(app_browser, browser());
  EXPECT_EQ(Browser::Type::TYPE_APP, browser_list()->get(0)->type());
  AddTabsAndResetBrowser(browser(), 1, GetAppUrl());
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  int tab_0_width = tab_strip->tab_at(0)->width();
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                      ReleaseInputAfterWindowDetached,
                                  base::Unretained(this), tab_0_width));

  // New browser should be TYPE_APP.
  ASSERT_EQ(2u, browser_list()->size());
  EXPECT_EQ(Browser::Type::TYPE_APP, browser_list()->get(1)->type());
}

// TODO (crbug.com/1521327): Test fails after migrating to ChromeRefresh2023.
// Move tab from TYPE_APP Browser to another TYPE_APP Browser.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithTabbedSystemApp,
                       DISABLED_DragAppToAppWindow) {
  // Install and get 2 browsers with tabbed system app.
  webapps::AppId tabbed_app_id = InstallMockApp();
  Browser* app_browser1 = LaunchWebAppBrowser(tabbed_app_id);
  Browser* app_browser2 = LaunchWebAppBrowser(tabbed_app_id);
  ASSERT_EQ(3u, browser_list()->size());
  ResetIDs(app_browser2->tab_strip_model(), 100);

  gfx::Rect work_area =
      display::Screen::GetScreen()
          ->GetDisplayNearestWindow(app_browser1->window()->GetNativeWindow())
          .work_area();
  const gfx::Size size(work_area.width() / 3, work_area.height() / 2);
  gfx::Rect browser_rect(work_area.origin(), size);
  app_browser1->window()->SetBounds(browser_rect);
  browser_rect.set_x(browser_rect.right());
  app_browser2->window()->SetBounds(browser_rect);

  // Close normal browser since other code expects only 1 browser to start.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(2u, browser_list()->size());
  SelectFirstBrowser();
  ASSERT_EQ(app_browser1, browser());

  AddTabsAndResetBrowser(browser(), 1, GetAppUrl());
  TabStrip* tab_strip1 = GetTabStripForBrowser(app_browser1);
  TabStrip* tab_strip2 = GetTabStripForBrowser(app_browser2);

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip1, base::BindOnce(&DragToSeparateWindowStep2, this,
                                              tab_strip1, tab_strip2));

  // Should now be attached to tab_strip2.
  // Release mouse or touch, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  EXPECT_EQ("100 0", IDString(app_browser2->tab_strip_model()));
  EXPECT_EQ("1", IDString(app_browser1->tab_strip_model()));
}
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

#if BUILDFLAG(IS_CHROMEOS)
// Subclass of DetachToBrowserTabDragControllerTest that
// creates multiple displays.
class DetachToBrowserInSeparateDisplayTabDragControllerTest
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserInSeparateDisplayTabDragControllerTest() {}
  DetachToBrowserInSeparateDisplayTabDragControllerTest(
      const DetachToBrowserInSeparateDisplayTabDragControllerTest&) = delete;
  DetachToBrowserInSeparateDisplayTabDragControllerTest& operator=(
      const DetachToBrowserInSeparateDisplayTabDragControllerTest&) = delete;
  virtual ~DetachToBrowserInSeparateDisplayTabDragControllerTest() {}

  void SetUpOnMainThread() override {
    DetachToBrowserTabDragControllerTest::SetUpOnMainThread();
    // Make screens sufficiently wide to host 2 browsers side by side.
    // 1280x800 is the default resolution for the main display in tests.
    // We stick to it, as opposed to a smaller one, to avoid the browser
    // window being shrunk and maximized when calling UpdateDisplay.
    const std::string display_specs = "1280x800,1280x800";
#if BUILDFLAG(IS_CHROMEOS_LACROS)
    ui_controls::UpdateDisplaySync(display_specs);
#else
    display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
        .UpdateDisplay(display_specs);
#endif
  }
};

namespace {

void DragSingleTabToSeparateWindowInSecondDisplayStep3(
    DetachToBrowserTabDragControllerTest* test) {
  EXPECT_TRUE(test->ReleaseInput());
}

void DragSingleTabToSeparateWindowInSecondDisplayStep2(
    DetachToBrowserTabDragControllerTest* test,
    const gfx::Point& target_point) {
  EXPECT_TRUE(test->DragInputToNotifyWhenDone(
      target_point,
      base::BindOnce(&DragSingleTabToSeparateWindowInSecondDisplayStep3,
                     test)));
}

}  // namespace

#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS_ASH)

// Drags from browser to a second display and releases input.
// TODO(crbug.com/40940016): Test is flaky on multiple bots.
IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
                       DISABLED_DragSingleTabToSeparateWindowInSecondDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  // Then drag it to the final destination on the second screen.
  display::Screen* const screen = display::Screen::GetScreen();
  display::Display second_display = ui_test_utils::GetSecondaryDisplay(screen);
  const gfx::Point start = GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  ASSERT_FALSE(second_display.bounds().Contains(start));
  const gfx::Point target(second_display.bounds().x() + 1,
                          start.y() + GetDetachY(tab_strip));
  ASSERT_TRUE(second_display.bounds().Contains(target));

  // TODO(crbug.com/40638870): Unit tests should be able to simulate mouse input
  // without having to call |CursorManager::SetDisplay|.
  ash::Shell::Get()->cursor_manager()->SetDisplay(second_display);
  DragTabAndNotify(
      tab_strip,
      base::BindOnce(&DragSingleTabToSeparateWindowInSecondDisplayStep2, this,
                     target));

  // Should no longer be dragging.
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());

  // There should now be another browser.
  ASSERT_EQ(2u, browser_list()->size());
  Browser* new_browser = browser_list()->get(1);
  ASSERT_TRUE(new_browser->window()->IsActive());
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  ASSERT_FALSE(tab_strip2->GetDragContext()->IsDragSessionActive());

  // This other browser should be on the second screen (with mouse drag)
  // With the touch input the browser cannot be dragged from one screen
  // to another and the window stays on the first screen.
  if (input_source() == INPUT_SOURCE_MOUSE) {
    EXPECT_EQ(
        ui_test_utils::GetSecondaryDisplay(screen).id(),
        screen
            ->GetDisplayNearestWindow(new_browser->window()->GetNativeWindow())
            .id());
  }

  EXPECT_EQ("0", IDString(new_browser->tab_strip_model()));
  EXPECT_EQ("1", IDString(browser()->tab_strip_model()));

  // Both windows should not be maximized
  EXPECT_FALSE(browser()->window()->IsMaximized());
  EXPECT_FALSE(new_browser->window()->IsMaximized());
}

#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

#if BUILDFLAG(IS_CHROMEOS)

namespace {

// Invoked from the nested run loop.
void DragTabToWindowInSeparateDisplayStep2(
    DetachToBrowserTabDragControllerTest* test,
    TabStrip* not_attached_tab_strip,
    TabStrip* target_tab_strip) {
  EXPECT_FALSE(not_attached_tab_strip->GetDragContext()->IsDragSessionActive());
  EXPECT_FALSE(target_tab_strip->GetDragContext()->IsDragSessionActive());
  EXPECT_TRUE(TabDragController::IsActive());

  // Drag to target_tab_strip. This should stop the nested loop from dragging
  // the window.
  gfx::Point target_point(
      GetCenterInScreenCoordinates(target_tab_strip->tab_at(0)));

  // Move it closer to the beginning of the tab so it will drop before that tab.
  target_point.Offset(-20, 0);
  EXPECT_TRUE(test->DragInputToAsync(target_point));
}

}  // namespace

// Drags from browser to another browser on a second display and releases input.
// TODO(crbug.com/329747667): Test is flaky on "Linux ChromiumOS MSan Tests"
#if BUILDFLAG(IS_CHROMEOS_ASH) && defined(MEMORY_SANITIZER)
#define MAYBE_DragTabToWindowInSeparateDisplay
#else
#define MAYBE_DragTabToWindowInSeparateDisplay
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
                       MAYBE_DragTabToWindowInSeparateDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create another browser.
  Browser* browser2 = CreateBrowser(browser()->profile());
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  ResetIDs(browser2->tab_strip_model(), 100);

  // Move the second browser to the second display.
  display::Screen* screen = display::Screen::GetScreen();
  Display second_display = ui_test_utils::GetSecondaryDisplay(screen);
  ui_test_utils::SetAndWaitForBounds(*browser2, second_display.work_area());
  EXPECT_EQ(
      second_display.id(),
      screen->GetDisplayNearestWindow(browser2->window()->GetNativeWindow())
          .id());

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DragTabToWindowInSeparateDisplayStep2, this,
                                  tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(tab_strip2->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_TRUE(TabDragController::IsActive());

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  ASSERT_FALSE(tab_strip2->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 100", IDString(browser2->tab_strip_model()));
  EXPECT_EQ("1", IDString(browser()->tab_strip_model()));

  // Both windows should not be maximized
  EXPECT_FALSE(browser()->window()->IsMaximized());
  EXPECT_FALSE(browser2->window()->IsMaximized());
}

// Crashes on ChromeOS. crbug.com/1003288
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserInSeparateDisplayTabDragControllerTest,
    DISABLED_DragBrowserWindowWhenMajorityOfBoundsInSecondDisplay) {
  // Set the browser's window bounds such that the majority of its bounds
  // resides in the second display.
  const std::pair<Display, Display> displays =
      GetDisplays(display::Screen::GetScreen());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  {
    // Moves the browser window through dragging so that the majority of its
    // bounds are in the secondary display but it's still be in the primary
    // display. Do not use SetBounds() or related, it may move the browser
    // window to the secondary display in some configurations like Mash.
    int target_x = displays.first.bounds().right() -
                   browser()->window()->GetBounds().width() / 2 + 20;
    const gfx::Point target_point =
        GetCenterInScreenCoordinates(tab_strip->tab_at(0)) +
        gfx::Vector2d(target_x - browser()->window()->GetBounds().x(),
                      GetDetachY(tab_strip));
    DragTabAndNotify(
        tab_strip,
        base::BindOnce(&DragSingleTabToSeparateWindowInSecondDisplayStep2, this,
                       target_point));
    StopAnimating(tab_strip);
  }
  EXPECT_EQ(displays.first.id(),
            browser()->window()->GetNativeWindow()->GetHost()->GetDisplayId());

  // Start dragging the window by the tab strip, and move it only to the edge
  // of the first display. Expect at that point mouse would warp and the window
  // will therefore reside in the second display when mouse is released.
  const gfx::Point tab_0_center =
      GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  const int offset_x = tab_0_center.x() - browser()->window()->GetBounds().x();
  const int detach_y = tab_0_center.y() + GetDetachY(tab_strip);
  const int first_display_warp_edge_x = displays.first.bounds().right() - 1;
  const gfx::Point warped_point(displays.second.bounds().x() + 1, detach_y);

  DragTabAndNotify(
      tab_strip, base::BindLambdaForTesting([&]() {
        // This makes another event on the warped location because the test
        // system does not create it automatically as the result of pointer
        // warp.
        ASSERT_TRUE(DragInputToNotifyWhenDone(
            gfx::Point(first_display_warp_edge_x, detach_y),
            base::BindOnce(&DragSingleTabToSeparateWindowInSecondDisplayStep2,
                           this, warped_point)));
      }));

  // Should no longer be dragging.
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());

  // There should only be a single browser.
  ASSERT_EQ(1u, browser_list()->size());
  ASSERT_EQ(browser(), browser_list()->get(0));
  ASSERT_TRUE(browser()->window()->IsActive());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());

  // Browser now resides in display 2.
  EXPECT_EQ(warped_point.x() - offset_x, browser()->window()->GetBounds().x());
  EXPECT_EQ(displays.second.id(),
            browser()->window()->GetNativeWindow()->GetHost()->GetDisplayId());
}

// Drags from browser to another browser on a second display and releases input.
#if BUILDFLAG(IS_CHROMEOS) && defined(MEMORY_SANITIZER)
// TODO(crbug.com/333006829):
#define MAYBE_DragTabToWindowOnSecondDisplay
#else
#define MAYBE_DragTabToWindowOnSecondDisplay
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
                       MAYBE_DragTabToWindowOnSecondDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create another browser.
  Browser* browser2 = CreateBrowser(browser()->profile());
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  ResetIDs(browser2->tab_strip_model(), 100);

  // Move both browsers to be side by side on the second display.
  display::Screen* screen = display::Screen::GetScreen();
  Display second_display = ui_test_utils::GetSecondaryDisplay(screen);
  gfx::Rect work_area = second_display.work_area();
  work_area.set_width(work_area.width() / 2);
  ui_test_utils::SetAndWaitForBounds(*browser(), work_area);
  // It's possible the window will not fit in half the screen, in which case we
  // will position the windows as well as we can.
  work_area.set_x(browser()->window()->GetBounds().right());
  // Sanity check: second browser should still be on the second display.
  ASSERT_LT(work_area.x(), second_display.work_area().right());
  ui_test_utils::SetAndWaitForBounds(*browser2, work_area);
  EXPECT_EQ(
      second_display.id(),
      screen->GetDisplayNearestWindow(browser()->window()->GetNativeWindow())
          .id());
  EXPECT_EQ(
      second_display.id(),
      screen->GetDisplayNearestWindow(browser2->window()->GetNativeWindow())
          .id());

  // Sanity check: make sure the target position is also within in the screen
  // bounds:
  ASSERT_LT(GetCenterInScreenCoordinates(tab_strip2->tab_at(0)).x(),
            second_display.work_area().right());

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DragTabToWindowInSeparateDisplayStep2, this,
                                  tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(tab_strip2->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_TRUE(TabDragController::IsActive());

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  ASSERT_FALSE(tab_strip2->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 100", IDString(browser2->tab_strip_model()));
  EXPECT_EQ("1", IDString(browser()->tab_strip_model()));

  // Both windows should not be maximized
  EXPECT_FALSE(browser()->window()->IsMaximized());
  EXPECT_FALSE(browser2->window()->IsMaximized());
}

// Drags from a maximized browser to another non-maximized browser on a second
// display and releases input.
#if BUILDFLAG(IS_CHROMEOS) && defined(MEMORY_SANITIZER)
// TODO(crbug.com/333006829):
#define MAYBE_DragMaxTabToNonMaxWindowInSeparateDisplay
#else
#define MAYBE_DragMaxTabToNonMaxWindowInSeparateDisplay
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
                       MAYBE_DragMaxTabToNonMaxWindowInSeparateDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  ASSERT_TRUE(ui_test_utils::MaximizeAndWaitUntilUIUpdateDone(*browser()));
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create another browser on the second display.
  display::Screen* screen = display::Screen::GetScreen();
  ASSERT_EQ(2, screen->GetNumDisplays());
  const std::pair<Display, Display> displays = GetDisplays(screen);
  gfx::Rect work_area = displays.second.work_area();
  work_area.Inset(gfx::Insets::TLBR(20, 20, 60, 20));
  Browser::CreateParams params(browser()->profile(), true);
  params.initial_show_state = ui::SHOW_STATE_NORMAL;
  params.initial_bounds = work_area;
  Browser* browser2 = Browser::Create(params);
  AddBlankTabAndShow(browser2);

  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  ResetIDs(browser2->tab_strip_model(), 100);

  EXPECT_EQ(
      displays.second.id(),
      screen->GetDisplayNearestWindow(browser2->window()->GetNativeWindow())
          .id());
  EXPECT_EQ(
      displays.first.id(),
      screen->GetDisplayNearestWindow(browser()->window()->GetNativeWindow())
          .id());
  EXPECT_EQ(2, tab_strip->GetTabCount());
  EXPECT_EQ(1, tab_strip2->GetTabCount());

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DragTabToWindowInSeparateDisplayStep2, this,
                                  tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(tab_strip2->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_TRUE(TabDragController::IsActive());

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());

  // tab should have moved
  EXPECT_EQ(1, tab_strip->GetTabCount());
  EXPECT_EQ(2, tab_strip2->GetTabCount());

  ASSERT_FALSE(tab_strip2->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 100", IDString(browser2->tab_strip_model()));
  EXPECT_EQ("1", IDString(browser()->tab_strip_model()));

  // Source browser should still be maximized, target should not
  EXPECT_TRUE(browser()->window()->IsMaximized());
  EXPECT_FALSE(browser2->window()->IsMaximized());
}

#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS_ASH)

// Drags from a restored browser to an immersive fullscreen browser on a
// second display and releases input.
// TODO(pkasting) https://crbug.com/910782 Hangs.
IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
                       DISABLED_DragTabToImmersiveBrowserOnSeparateDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create another browser.
  Browser* browser2 = CreateBrowser(browser()->profile());
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  ResetIDs(browser2->tab_strip_model(), 100);

  // Move the second browser to the second display.
  display::Screen* screen = display::Screen::GetScreen();
  const std::pair<Display, Display> displays = GetDisplays(screen);
  ui_test_utils::SetAndWaitForBounds(*browser2, displays.second.work_area());
  EXPECT_EQ(
      displays.second.id(),
      screen->GetDisplayNearestWindow(browser2->window()->GetNativeWindow())
          .id());

  // Put the second browser into immersive fullscreen.
  BrowserView* browser_view2 = BrowserView::GetBrowserViewForBrowser(browser2);
  ImmersiveModeController* immersive_controller2 =
      browser_view2->immersive_mode_controller();
  chromeos::ImmersiveFullscreenControllerTestApi(
      static_cast<ImmersiveModeControllerChromeos*>(immersive_controller2)
          ->controller())
      .SetupForTest();
  chrome::ToggleFullscreenMode(browser2);
  // For MD, the browser's top chrome is completely offscreen, with tabstrip
  // visible.
  ASSERT_TRUE(immersive_controller2->IsEnabled());
  ASSERT_FALSE(immersive_controller2->IsRevealed());
  ASSERT_TRUE(tab_strip2->GetVisible());

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DragTabToWindowInSeparateDisplayStep2, this,
                                  tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(tab_strip2->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_TRUE(TabDragController::IsActive());

  // browser2's top chrome should be revealed and the tab strip should be
  // at normal height while user is dragging tabs_strip2's tabs.
  ASSERT_TRUE(immersive_controller2->IsRevealed());
  ASSERT_TRUE(tab_strip2->GetVisible());

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  ASSERT_FALSE(tab_strip2->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 100", IDString(browser2->tab_strip_model()));
  EXPECT_EQ("1", IDString(browser()->tab_strip_model()));

  // Move the mouse off of browser2's top chrome.
  ASSERT_TRUE(
      ui_test_utils::SendMouseMoveSync(displays.first.bounds().CenterPoint()));

  // The first browser window should not be in immersive fullscreen.
  // browser2 should still be in immersive fullscreen, but the top chrome should
  // no longer be revealed.
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  EXPECT_FALSE(browser_view->immersive_mode_controller()->IsEnabled());

  EXPECT_TRUE(immersive_controller2->IsEnabled());
  EXPECT_FALSE(immersive_controller2->IsRevealed());
  EXPECT_TRUE(tab_strip2->GetVisible());
}

// Subclass of DetachToBrowserTabDragControllerTest that
// creates multiple displays with different device scale factors.
class DifferentDeviceScaleFactorDisplayTabDragControllerTest
    : public DetachToBrowserTabDragControllerTest {
 public:
  DifferentDeviceScaleFactorDisplayTabDragControllerTest() {}
  DifferentDeviceScaleFactorDisplayTabDragControllerTest(
      const DifferentDeviceScaleFactorDisplayTabDragControllerTest&) = delete;
  DifferentDeviceScaleFactorDisplayTabDragControllerTest& operator=(
      const DifferentDeviceScaleFactorDisplayTabDragControllerTest&) = delete;
  virtual ~DifferentDeviceScaleFactorDisplayTabDragControllerTest() {}

  void SetUpOnMainThread() override {
    DetachToBrowserTabDragControllerTest::SetUpOnMainThread();
    display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
        .UpdateDisplay("1280x800,1280x800*2");
  }

  float GetCursorDeviceScaleFactor() const {
    auto* cursor_client = aura::client::GetCursorClient(
        browser()->window()->GetNativeWindow()->GetRootWindow());
    const auto& cursor_shape_client = aura::client::GetCursorShapeClient();
    return cursor_shape_client.GetCursorData(cursor_client->GetCursor())
        ->scale_factor;
  }
};

namespace {

// The points where a tab is dragged in CursorDeviceScaleFactorStep.
constexpr gfx::Point kDragPoints[] = {
    {300, 200}, {399, 200}, {500, 200}, {400, 200}, {300, 200},
};

// The expected device scale factors after the cursor is moved to the
// corresponding kDragPoints in CursorDeviceScaleFactorStep.
constexpr float kDeviceScaleFactorExpectations[] = {
    1.0f, 1.0f, 2.0f, 2.0f, 1.0f,
};

static_assert(
    std::size(kDragPoints) == std::size(kDeviceScaleFactorExpectations),
    "kDragPoints and kDeviceScaleFactorExpectations must have the same "
    "number of elements");

// Drags tab to |kDragPoints[index]|, then calls the next step function.
void CursorDeviceScaleFactorStep(
    DifferentDeviceScaleFactorDisplayTabDragControllerTest* test,
    TabStrip* not_attached_tab_strip,
    size_t index) {
  SCOPED_TRACE(index);
  ASSERT_FALSE(not_attached_tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_TRUE(TabDragController::IsActive());

  if (index > 0) {
    EXPECT_EQ(kDragPoints[index - 1],
              aura::Env::GetInstance()->last_mouse_location());
    EXPECT_EQ(kDeviceScaleFactorExpectations[index - 1],
              test->GetCursorDeviceScaleFactor());
  }

  if (index < std::size(kDragPoints)) {
    ASSERT_TRUE(test->DragInputToNotifyWhenDone(
        kDragPoints[index], base::BindOnce(&CursorDeviceScaleFactorStep, test,
                                           not_attached_tab_strip, index + 1)));
  } else {
    // Finishes a series of CursorDeviceScaleFactorStep calls and ends drag.
    ASSERT_TRUE(
        ui_test_utils::SendMouseEventsSync(ui_controls::LEFT, ui_controls::UP));
  }
}

}  // namespace

// Verifies cursor's device scale factor is updated when a tab is moved across
// displays with different device scale factors (http://crbug.com/154183).
// TODO(pkasting): In interactive_ui_tests, scale factor never changes to 2.
// https://crbug.com/918731
// TODO(pkasting): In non_single_process_mash_interactive_ui_tests, pointer is
// warped during the drag (which results in changing to scale factor 2 early),
// and scale factor doesn't change back to 1 at the end.
// https://crbug.com/918732
IN_PROC_BROWSER_TEST_P(DifferentDeviceScaleFactorDisplayTabDragControllerTest,
                       DISABLED_CursorDeviceScaleFactor) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move the second browser to the second display.
  ASSERT_EQ(2, display::Screen::GetScreen()->GetNumDisplays());

  // Move to the first tab and drag it enough so that it detaches.
  DragTabAndNotify(tab_strip, base::BindOnce(&CursorDeviceScaleFactorStep, this,
                                             tab_strip, 0));
}

class DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest() {}
  DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest(
      const DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest&) =
      delete;
  DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest& operator=(
      const DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest&) =
      delete;

  void SetUpOnMainThread() override {
    DetachToBrowserTabDragControllerTest::SetUpOnMainThread();
    display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
        .UpdateDisplay("1280x800,1280x800");
  }
};

namespace {

// Invoked from the nested run loop.
void CancelDragTabToWindowInSeparateDisplayStep3(
    DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest* test,
    TabStrip* tab_strip) {
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_TRUE(TabDragController::IsActive());
  ASSERT_EQ(2u, test->browser_list()->size());

  // Switching display mode should cancel the drag operation.
  ash::ShellTestApi().AddRemoveDisplay();
}

// Invoked from the nested run loop.
void CancelDragTabToWindowInSeparateDisplayStep2(
    DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest* test,
    TabStrip* tab_strip,
    Display current_display,
    gfx::Point final_destination) {
  EXPECT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  EXPECT_TRUE(TabDragController::IsActive());
  size_t num_browsers = test->browser_list()->size();
  EXPECT_EQ(2u, num_browsers);

  Browser* new_browser = test->browser_list()->get(num_browsers - 1);
  EXPECT_EQ(
      current_display.id(),
      display::Screen::GetScreen()
          ->GetDisplayNearestWindow(new_browser->window()->GetNativeWindow())
          .id());

  EXPECT_TRUE(test->DragInputToNotifyWhenDone(
      final_destination,
      base::BindOnce(&CancelDragTabToWindowInSeparateDisplayStep3, test,
                     tab_strip)));
}

}  // namespace

// TODO(crbug.com/333085989): Re-enable flaky tests
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_CancelDragTabToWindowIn2ndDisplay
#else
#define MAYBE_CancelDragTabToWindowIn2ndDisplay
#endif

// Drags from browser to a second display and releases input.
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest,
    MAYBE_CancelDragTabToWindowIn2ndDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  EXPECT_EQ("0 1", IDString(browser()->tab_strip_model()));

  // Move the second browser to the second display.
  const std::pair<Display, Display> displays =
      GetDisplays(display::Screen::GetScreen());
  gfx::Point final_destination = displays.second.work_area().CenterPoint();

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough to move to another display.
  DragTabAndNotify(
      tab_strip,
      base::BindOnce(&CancelDragTabToWindowInSeparateDisplayStep2, this,
                     tab_strip, displays.first, final_destination));

  ASSERT_EQ(1u, browser_list()->size());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 1", IDString(browser()->tab_strip_model()));

  // Release the mouse
  ASSERT_TRUE(
      ui_test_utils::SendMouseEventsSync(ui_controls::LEFT, ui_controls::UP));
}

// TODO(crbug.com/333085989): Re-enable flaky tests on ChromeOS
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_CancelDragTabToWindowIn1stDisplay
#else
#define MAYBE_CancelDragTabToWindowIn1stDisplay
#endif

// Drags from browser from a second display to primary and releases input.
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest,
    MAYBE_CancelDragTabToWindowIn1stDisplay) {
  display::Screen* screen = display::Screen::GetScreen();
  const std::pair<Display, Display> displays = GetDisplays(screen);

  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  EXPECT_EQ("0 1", IDString(browser()->tab_strip_model()));
  EXPECT_EQ(
      displays.first.id(),
      screen->GetDisplayNearestWindow(browser()->window()->GetNativeWindow())
          .id());

  browser()->window()->SetBounds(displays.second.work_area());
  EXPECT_EQ(
      displays.second.id(),
      screen->GetDisplayNearestWindow(browser()->window()->GetNativeWindow())
          .id());

  // Move the second browser to the display.
  gfx::Point final_destination = displays.first.work_area().CenterPoint();

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough to move to another display.
  DragTabAndNotify(
      tab_strip,
      base::BindOnce(&CancelDragTabToWindowInSeparateDisplayStep2, this,
                     tab_strip, displays.second, final_destination));

  ASSERT_EQ(1u, browser_list()->size());
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 1", IDString(browser()->tab_strip_model()));

  // Release the mouse
  ASSERT_TRUE(
      ui_test_utils::SendMouseEventsSync(ui_controls::LEFT, ui_controls::UP));
}

#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

#if BUILDFLAG(IS_CHROMEOS)

// Subclass of DetachToBrowserTabDragControllerTest that runs tests only with
// touch input.
class DetachToBrowserTabDragControllerTestTouch
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserTabDragControllerTestTouch() {}
  DetachToBrowserTabDragControllerTestTouch(
      const DetachToBrowserTabDragControllerTestTouch&) = delete;
  DetachToBrowserTabDragControllerTestTouch& operator=(
      const DetachToBrowserTabDragControllerTestTouch&) = delete;
  virtual ~DetachToBrowserTabDragControllerTestTouch() {}

  void TearDown() override {
    ui::SetEventTickClockForTesting(nullptr);
    clock_.reset();
  }

 protected:
  std::unique_ptr<base::SimpleTestTickClock> clock_;
};

#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS_ASH)

namespace {
void PressSecondFingerWhileDetachedStep3(
    DetachToBrowserTabDragControllerTest* test) {
  EXPECT_TRUE(TabDragController::IsActive());
  EXPECT_EQ(2u, test->browser_list()->size());
  EXPECT_TRUE(test->browser_list()->get(1)->window()->IsActive());

  EXPECT_TRUE(test->ReleaseInput());
  EXPECT_TRUE(test->ReleaseInput(1));
}

void PressSecondFingerWhileDetachedStep2(
    DetachToBrowserTabDragControllerTest* test,
    const gfx::Point& target_point) {
  EXPECT_TRUE(TabDragController::IsActive());
  size_t num_browsers = test->browser_list()->size();
  EXPECT_EQ(2u, num_browsers);
  EXPECT_TRUE(
      test->browser_list()->get(num_browsers - 1)->window()->IsActive());

  // Continue dragging after adding a second finger.
  EXPECT_TRUE(test->PressInput(gfx::Point(), 1));
  EXPECT_TRUE(test->DragInputToNotifyWhenDone(
      target_point,
      base::BindOnce(&PressSecondFingerWhileDetachedStep3, test)));
}

}  // namespace

// Detaches a tab and while detached presses a second finger.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestTouch,
                       PressSecondFingerWhileDetached) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  EXPECT_EQ("0 1", IDString(browser()->tab_strip_model()));

  // Move to the first tab and drag it enough so that it detaches. Drag it
  // slightly more horizontally so that it does not generate a swipe down
  // gesture that minimizes the detached browser window.
  const int touch_move_delta = GetDetachY(tab_strip);
  const gfx::Point target = GetCenterInScreenCoordinates(tab_strip->tab_at(0)) +
                            gfx::Vector2d(0, 2 * touch_move_delta);
  DragTabAndNotify(
      tab_strip,
      base::BindOnce(&PressSecondFingerWhileDetachedStep2, this, target), 0,
      touch_move_delta + 5);

  // Should no longer be dragging.
  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());

  // There should now be another browser.
  ASSERT_EQ(2u, browser_list()->size());
  Browser* new_browser = browser_list()->get(1);
  ASSERT_TRUE(new_browser->window()->IsActive());
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  ASSERT_FALSE(tab_strip2->GetDragContext()->IsDragSessionActive());

  EXPECT_EQ("0", IDString(new_browser->tab_strip_model()));
  EXPECT_EQ("1", IDString(browser()->tab_strip_model()));
}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestTouch,
                       LeftSnapShouldntCauseMergeAtEnd) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  AddTabsAndResetBrowser(browser(), 1);

  // Set the last mouse location at the center of tab 0. This shouldn't affect
  // the touch behavior below. See https://crbug.com/914527#c1 for the details
  // of how this can affect the result.
  gfx::Point tab_0_center(GetCenterInScreenCoordinates(tab_strip->tab_at(0)));
  base::RunLoop run_loop;
  ui_controls::SendMouseMoveNotifyWhenDone(tab_0_center.x(), tab_0_center.y(),
                                           run_loop.QuitClosure());
  run_loop.Run();

  // Drag the tab 1 to left-snapping.
  DragTabAndNotify(
      tab_strip, base::BindLambdaForTesting([&]() {
        const gfx::Rect display_bounds =
            display::Screen::GetScreen()->GetPrimaryDisplay().bounds();
        const gfx::Point target(display_bounds.x(),
                                display_bounds.CenterPoint().y());
        ASSERT_TRUE(
            DragInputToNotifyWhenDone(target, base::BindLambdaForTesting([&]() {
                                        ASSERT_TRUE(ReleaseInput());
                                      })));
      }),
      1);

  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ(2u, browser_list()->size());
}

#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

#if BUILDFLAG(IS_CHROMEOS)

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestTouch,
                       FlingDownAtEndOfDrag) {
  // Reduce the minimum fling velocity for this specific test case to cause the
  // fling-down gesture in the middle of tab-dragging. This should end up with
  // minimizing the window. See https://crbug.com/902897 for the details.
  SetMinFlingVelocity(1);

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  test::QuitDraggingObserver observer(tab_strip);
  const gfx::Point tab_0_center =
      GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  const gfx::Vector2d detach(0, GetDetachY(tab_strip));
  clock_ = std::make_unique<base::SimpleTestTickClock>();
  clock_->SetNowTicks(base::TimeTicks::Now());
  ui::SetEventTickClockForTesting(clock_.get());
  ASSERT_TRUE(PressInput(tab_0_center));
  clock_->Advance(base::Milliseconds(5));
  ASSERT_TRUE(DragInputToNotifyWhenDone(
      tab_0_center + detach, base::BindLambdaForTesting([&]() {
        // Drag down again; this should cause a fling-down event.
        clock_->Advance(base::Milliseconds(5));
        ASSERT_TRUE(DragInputToNotifyWhenDone(
            tab_0_center + detach + detach, base::BindLambdaForTesting([&]() {
              clock_->Advance(base::Milliseconds(5));
              ASSERT_TRUE(ReleaseInput());
            })));
      })));
  observer.Wait();

  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_TRUE(browser()->window()->IsMinimized());
  EXPECT_FALSE(browser()->window()->IsVisible());
}

// TODO(http://crbug/343503164) This test seems to be attempting to induce a
// fling gesture event, but it currently fails to do so. If a fling gesture
// event was produced, the detached window should end up minimized.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestTouch,
                       DISABLED_FlingOnStartingDrag) {
  SetMinFlingVelocity(1);
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  const gfx::Point tab_0_center =
      GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  const gfx::Vector2d detach(0, GetDetachY(tab_strip));

  // Sends events to the server without waiting for its reply, which will cause
  // extra touch events before PerformWindowMove starts handling events.
  test::QuitDraggingObserver observer(tab_strip);
  clock_ = std::make_unique<base::SimpleTestTickClock>();
  clock_->SetNowTicks(base::TimeTicks::Now());
  ui::SetEventTickClockForTesting(clock_.get());
  ASSERT_TRUE(PressInput(tab_0_center));
  clock_->Advance(base::Milliseconds(5));
  ASSERT_TRUE(DragInputToAsync(tab_0_center + detach));
  clock_->Advance(base::Milliseconds(5));
  ASSERT_TRUE(DragInputToAsync(tab_0_center + detach + detach));
  clock_->Advance(base::Milliseconds(2));
  ASSERT_TRUE(ReleaseInput());
  observer.Wait();

  ASSERT_FALSE(tab_strip->GetDragContext()->IsDragSessionActive());
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ(2u, browser_list()->size());
  auto* browser2 = browser_list()->get(1);
  EXPECT_TRUE(browser2->window()->IsMinimized());
  EXPECT_FALSE(browser2->window()->IsVisible());
}

#endif  // BUILDFLAG(IS_CHROMEOS)

namespace {

class SelectTabDuringDragObserver : public TabStripModelObserver {};

}  // namespace

// Bug fix for crbug.com/1196309. Don't change tab selection while dragging.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       SelectTabDuringDrag) {}

#if BUILDFLAG(IS_CHROMEOS_ASH)
// Runs tests with a tabbed system web app that is locked for OnTask. This is
// not related to normal web browsers.
using DetachToBrowserTabDragControllerTestWithOnTaskLocked =
    DetachToBrowserTabDragControllerTestWithTabbedSystemApp;

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithOnTaskLocked,
                       MoveTabOnDrag) {
  // Install and launch mock app that can be locked for OnTask.
  const webapps::AppId tabbed_app_id = InstallMockApp();
  Browser* const app_browser = LaunchWebAppBrowser(tabbed_app_id);
  ASSERT_EQ(2u, browser_list()->size());

  // Close normal browser.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(1u, browser_list()->size());
  SelectFirstBrowser();
  ASSERT_EQ(app_browser, browser());
  EXPECT_EQ(Browser::Type::TYPE_APP, browser_list()->get(0)->type());

  // Lock the app for OnTask and set up app for testing drag behavior.
  browser()->SetLockedForOnTask(true);
  AddTabsAndResetBrowser(browser(), /*additional_tabs=*/3, GetAppUrl());
  TabStripModel* const tab_strip_model = browser()->tab_strip_model();
  ASSERT_EQ("0 1 2 3", IDString(tab_strip_model));

  // Drag tab in the second index to the tab in the third index to switch tab
  // positioning.
  TabStrip* const tab_strip = GetTabStripForBrowser(browser());
  ASSERT_TRUE(PressInput(GetCenterInScreenCoordinates(tab_strip->tab_at(1))));
  ASSERT_TRUE(DragInputTo(GetCenterInScreenCoordinates(tab_strip->tab_at(2))));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Verify tab is not detached and its position is updated.
  ASSERT_EQ(1u, browser_list()->size());
  EXPECT_EQ("0 2 1 3", IDString(tab_strip_model));
}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithOnTaskLocked,
                       TabDoesNotDetachOnDrag) {
  // Install and launch mock app that can be locked for OnTask.
  const webapps::AppId tabbed_app_id = InstallMockApp();
  Browser* const app_browser = LaunchWebAppBrowser(tabbed_app_id);
  ASSERT_EQ(2u, browser_list()->size());

  // Close normal browser.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(1u, browser_list()->size());
  SelectFirstBrowser();
  ASSERT_EQ(app_browser, browser());
  EXPECT_EQ(Browser::Type::TYPE_APP, browser_list()->get(0)->type());

  // Lock the app for OnTask and set up app for testing drag behavior.
  browser()->SetLockedForOnTask(true);
  AddTabsAndResetBrowser(browser(), /*additional_tabs=*/3, GetAppUrl());

  // Drag tab away from tab strip and verify it is not detached.
  TabStrip* const tab_strip = GetTabStripForBrowser(browser());
  const gfx::Point initial_drag_position =
      GetCenterInScreenCoordinates(tab_strip->tab_at(1));
  ASSERT_TRUE(PressInput(initial_drag_position));
  ASSERT_TRUE(DragInputTo(initial_drag_position +
                          gfx::Vector2d(0, GetDetachY(tab_strip) + 1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);
  EXPECT_EQ(1u, browser_list()->size());
}
#endif

#if BUILDFLAG(IS_CHROMEOS)
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTest,
    ::testing::Combine(
        /*kSplitTabStrip=*/::testing::Bool(),
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestWithScrollableTabStripEnabled,
    ::testing::Combine(
        /*kSplitTabStrip=*/::testing::Bool(),
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
#else
INSTANTIATE_TEST_SUITE_P();
INSTANTIATE_TEST_SUITE_P();
#endif

#if BUILDFLAG(IS_CHROMEOS)
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserInSeparateDisplayTabDragControllerTest,
    ::testing::Combine(
        /*kSplitTabStrip=*/::testing::Bool(),
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestTouch,
    ::testing::Combine(
        /*kSplitTabStrip=*/::testing::Bool(),
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("touch")));
#endif  // BUILDFLAG(IS_CHROMEOS)
#if BUILDFLAG(IS_CHROMEOS_ASH)
// TODO(crbug.com/40283516): Enable Multi Display Test on lacros
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DifferentDeviceScaleFactorDisplayTabDragControllerTest,
    ::testing::Combine(
        /*kSplitTabStrip=*/::testing::Bool(),
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest,
    ::testing::Combine(
        /*kSplitTabStrip=*/::testing::Bool(),
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestWithTabbedSystemApp,
    ::testing::Combine(
        /*kSplitTabStrip=*/::testing::Bool(),
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestWithOnTaskLocked,
    ::testing::Combine(
        /*kSplitTabStrip=*/::testing::Bool(),
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

#if BUILDFLAG(IS_CHROMEOS)
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestWithTabbedWebApp,
    ::testing::Combine(
        /*kSplitTabStrip=*/::testing::Bool(),
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
#elif !BUILDFLAG(IS_MAC)
INSTANTIATE_TEST_SUITE_P();
#endif

#if BUILDFLAG(IS_CHROMEOS)
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachTabWithUrlControlledByWebApp,
    ::testing::Combine(
        /*kSplitTabStrip=*/::testing::Bool(),
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Bool(),
        /*input_source=*/::testing::Values("mouse")));

#else
INSTANTIATE_TEST_SUITE_P();
#endif